Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/swift/ABI/MetadataValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -2897,6 +2897,9 @@ enum class TaskStatusRecordKind : uint8_t {
/// A human-readable task name.
TaskName = 6,

// The total time the task has spent running.
TimeSpentRunning = 7,

// Kinds >= 192 are private to the implementation.
First_Reserved = 192,
Private_RecordLock = 192
Expand Down
42 changes: 41 additions & 1 deletion include/swift/ABI/Task.h
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,14 @@ class AsyncTask : public Job {
/// runInExecutorContext.
SWIFT_CC(swiftasync)
void runInFullyEstablishedContext() {
return ResumeTask(ResumeContext); // 'return' forces tail call
if (SWIFT_UNLIKELY(isTimeSpentRunningTracked())) {
auto begin = getNanosecondsOnSuspendingClock();
ResumeTask(ResumeContext);
auto end = getNanosecondsOnSuspendingClock();
ranForNanoseconds(end - begin);
} else {
return ResumeTask(ResumeContext); // 'return' forces tail call
}
}

/// A task can have the following states:
Expand Down Expand Up @@ -498,6 +505,28 @@ class AsyncTask : public Job {
/// `swift_task_popTaskExecutorPreference(record)` method pair.
void dropInitialTaskExecutorPreferenceRecord();

// ==== Tracking Time Spent Running ------------------------------------------

/// Whether or not the concurrency library is tracking the time spent running
/// tasks.
static inline bool isTimeSpentRunningTracked(void) {
return _isTimeSpentRunningTracked.load(std::memory_order_relaxed);
}

/// Set whether or not the concurrency library is tracking the time spent
/// running tasks. Returns the old value.
static inline bool setTimeSpentRunningTracked(bool isTracked) {
return _isTimeSpentRunningTracked.exchange(isTracked,
std::memory_order_relaxed);
}

/// Get the number of nanoseconds spent running this task so far, or `0` if
/// task duration tracking isn't enabled.
__attribute__((cold)) uint64_t getTimeSpentRunning(void);
Comment on lines +523 to +525
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it return a sentinel value if tracking isn't enabled, rather than 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because the caller already does that. We could conceivably return a std::optional but at this time it isn't worth it.


void pushTimeSpentRunningRecord(void);
void popTimeSpentRunningRecord(void);

// ==== Task Local Values ----------------------------------------------------

void localValuePush(const HeapObject *key,
Expand Down Expand Up @@ -779,6 +808,17 @@ class AsyncTask : public Job {
return reinterpret_cast<AsyncTask *&>(
SchedulerPrivate[NextWaitingTaskIndex]);
}

/// Whether or not the concurrency library is tracking the time spent running
/// tasks.
static std::atomic<bool> _isTimeSpentRunningTracked;

/// Record that the task spent an additional `ns` nanoseconds running.
void ranForNanoseconds(uint64_t ns);

/// Get the current instant on the system's suspending clock to use when
/// tracking the time spent running tasks.
static uint64_t getNanosecondsOnSuspendingClock(void);
};

// The compiler will eventually assume these.
Expand Down
12 changes: 12 additions & 0 deletions include/swift/ABI/TaskStatus.h
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,18 @@ class TaskDependencyStatusRecord : public TaskStatusRecord {
JobPriority oldPriority, JobPriority newPriority);
};

class TimeSpentRunningStatusRecord : public TaskStatusRecord {
public:
TimeSpentRunningStatusRecord()
: TaskStatusRecord(TaskStatusRecordKind::TimeSpentRunning) {}

uint64_t TimeSpentRunning = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut tells me we will want to lower the storage for this value into AsyncTask::PrivateStorage for the sake of performance.


static bool classof(const TaskStatusRecord *record) {
return record->getKind() == TaskStatusRecordKind::TimeSpentRunning;
}
};

} // end namespace swift

#endif
14 changes: 14 additions & 0 deletions include/swift/Runtime/Concurrency.h
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,20 @@ SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
void swift_task_donateThreadToGlobalExecutorUntil(bool (*condition)(void*),
void *context);

/// Set whether or not the concurrency library is tracking the time spent
/// running tasks. Returns the old value.
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
__attribute__((__cold__)) bool
_swift_task_setTimeSpentRunningTracked(bool isTracked);

/// Get the duration spent running the given task (so far) in nanoseconds.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VS Code is not my friend, it seems.

///
/// If `AsyncTask::isTimeSpentRunningTracked()` is `false` (the common case),
/// task duration isn't tracked and this function returns `false`.
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
__attribute__((__cold__)) bool
_swift_task_getTimeSpentRunning(AsyncTask *task, uint64_t *outNanoseconds);

enum swift_clock_id : int {
swift_clock_id_continuous = 1,
swift_clock_id_suspending = 2,
Expand Down
6 changes: 6 additions & 0 deletions include/swift/Runtime/ConcurrencyHooks.def
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ SWIFT_CONCURRENCY_HOOK(bool, swift_task_isMainExecutor, SerialExecutorRef);
SWIFT_CONCURRENCY_HOOK(void, swift_task_donateThreadToGlobalExecutorUntil,
bool (*condition)(void *), void *context);

SWIFT_CONCURRENCY_HOOK(bool, _swift_task_setTimeSpentRunningTracked,
bool isTracked);

SWIFT_CONCURRENCY_HOOK(bool, _swift_task_getTimeSpentRunning,
AsyncTask *task, uint64_t *outNanoseconds);

// .............................................................................

#undef SWIFT_CONCURRENCY_HOOK
Expand Down
14 changes: 14 additions & 0 deletions stdlib/public/Concurrency/ConcurrencyHooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,17 @@ swift_task_donateThreadToGlobalExecutorUntil(bool (*condition)(void *),
else
return swift_task_donateThreadToGlobalExecutorUntilOrig(condition, context);
}

SWIFT_CC(swift)
__attribute__((__cold__)) bool
swift::_swift_task_setTimeSpentRunningTracked(bool isTracked) {
return AsyncTask::setTimeSpentRunningTracked(isTracked);
}

SWIFT_CC(swift)
__attribute__((__cold__)) bool
swift::_swift_task_getTimeSpentRunning(AsyncTask *task,
uint64_t *outNanoseconds) {
*outNanoseconds = task->getTimeSpentRunning();
return AsyncTask::isTimeSpentRunningTracked();
}
16 changes: 16 additions & 0 deletions stdlib/public/Concurrency/Task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,18 @@ const void *AsyncTask::getResumeFunctionForLogging(bool isStarting) {
return __ptrauth_swift_runtime_function_entry_strip(result);
}

std::atomic<bool> AsyncTask::_isTimeSpentRunningTracked { false };

uint64_t AsyncTask::getNanosecondsOnSuspendingClock(void) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fundamentally the same as swift_time_now(), will want to reconcile that.

long long seconds = 0;
long long nanoseconds = 0;
swift_get_time(&seconds, &nanoseconds, swift_clock_id_suspending);

uint64_t result = static_cast<uint64_t>(seconds) * UINT64_C(1'000'000'000);
result += static_cast<uint64_t>(nanoseconds);
return result;
}

JobPriority swift::swift_task_currentPriority(AsyncTask *task) {
// This is racey but this is to be used in an API is inherently racey anyways.
auto oldStatus = task->_private()._status().load(std::memory_order_relaxed);
Expand Down Expand Up @@ -1191,6 +1203,10 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
if (jobFlags.task_hasInitialTaskName()) {
task->pushInitialTaskName(taskName);
}

if (SWIFT_UNLIKELY(AsyncTask::isTimeSpentRunningTracked())) {
task->pushTimeSpentRunningRecord();
}
}

// If we're supposed to enqueue the task, do so now.
Expand Down
40 changes: 40 additions & 0 deletions stdlib/public/Concurrency/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,46 @@ internal func _getCurrentTaskNameString() -> String? {
}
}

// MARK: - Time spent running (experimental)
Copy link
Contributor Author

@grynspan grynspan Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all is for the POC only. I wouldn't anticipate exposing it as API (although for Swift Testing to adopt, we may still need some underscored function since it can't directly access the _task property of UnsafeCurrentTask nor Task.)


@available(SwiftStdlib 6.3, *)
@_silgen_name("_swift_task_getTimeSpentRunning")
internal func _getTimeSpentRunning(
_ task: Builtin.NativeObject,
_ outNanoseconds: UnsafeMutablePointer<UInt64>
) -> Bool

@available(SwiftStdlib 6.3, *)
@unsafe
private func _getTimeSpentRunning(_ task: Builtin.NativeObject) -> Duration? {
var result = UInt64(0)
guard unsafe _getTimeSpentRunning(task, &result) else {
return nil
}
return .nanoseconds(result)
}

@available(SwiftStdlib 6.3, *)
extension Task {
/// Get the time this task has spent scheduled and running.
///
/// This interface is experimental. It may be removed in a future release.
public var _timeSpentRunning: Duration? {
unsafe _getTimeSpentRunning(_task)
}
}

@available(SwiftStdlib 6.3, *)
extension UnsafeCurrentTask {
/// Get the time this task has spent scheduled and running.
///
/// This interface is experimental. It may be removed in a future release.
public var _timeSpentRunning: Duration? {
unsafe _getTimeSpentRunning(_task)
}
}

// MARK: -

#if SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
@available(SwiftStdlib 5.8, *)
Expand Down
5 changes: 5 additions & 0 deletions stdlib/public/Concurrency/TaskPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,11 @@ struct AsyncTask::PrivateStorage {
}
}

// If we were tracking time spent running, clear that now too.
if (SWIFT_UNLIKELY(isTimeSpentRunningTracked())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could theoretically leak if somebody enables tracking before a task starts, then disables it before the task finishes. For this POC, I don't care. :)

task->popTimeSpentRunningRecord();
}

// Drain unlock the task and remove any overrides on thread as a
// result of the task
auto oldStatus = task->_private()._status().load(std::memory_order_relaxed);
Expand Down
60 changes: 60 additions & 0 deletions stdlib/public/Concurrency/TaskStatus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,10 @@ static void performCancellationAction(TaskStatusRecord *record) {
case TaskStatusRecordKind::TaskName:
break;

// Cancellation has no impact on the time a task spends running.
case TaskStatusRecordKind::TimeSpentRunning:
break;

// This should never be found, but the compiler complains if we don't check.
case TaskStatusRecordKind::First_Reserved:
break;
Expand Down Expand Up @@ -972,6 +976,9 @@ static void performEscalationAction(TaskStatusRecord *record,
/// Task names don't matter to priority escalation.
case TaskStatusRecordKind::TaskName:
return;
// Time spent running doesn't affect priority (outside the scheduler, anyway.)
case TaskStatusRecordKind::TimeSpentRunning:
return;
// This should never be found, but the compiler complains if we don't check.
case TaskStatusRecordKind::First_Reserved:
break;
Expand Down Expand Up @@ -1100,5 +1107,58 @@ void TaskDependencyStatusRecord::performEscalationAction(
}
}

/**************************************************************************/
/*************************** TIME SPENT RUNNING ***************************/
/**************************************************************************/

void AsyncTask::pushTimeSpentRunningRecord(void) {
void *allocation = std::malloc(sizeof(class TimeSpentRunningStatusRecord));
auto record = ::new (allocation) TimeSpentRunningStatusRecord();

addStatusRecord(this, record,
[&](ActiveTaskStatus oldStatus, ActiveTaskStatus &newStatus) {
return true; // always add the record
});
}

void AsyncTask::popTimeSpentRunningRecord(void) {
if (auto record = popStatusRecordOfType<TimeSpentRunningStatusRecord>(this)) {
record->~TimeSpentRunningStatusRecord();
std::free(record);
}
}

__attribute__((cold)) uint64_t AsyncTask::getTimeSpentRunning(void) {
uint64_t result = 0;

withStatusRecordLock(this, [&](ActiveTaskStatus status) {
for (auto record : status.records()) {
if (auto timeRecord = dyn_cast<TimeSpentRunningStatusRecord>(record)) {
result = timeRecord->TimeSpentRunning;
break;
}
}
});

return result;
}

void AsyncTask::ranForNanoseconds(uint64_t ns) {
withStatusRecordLock(this, [&](ActiveTaskStatus status) {
for (auto record : status.records()) {
if (auto timeRecord = dyn_cast<TimeSpentRunningStatusRecord>(record)) {
timeRecord->TimeSpentRunning += ns;
break;
}
}
});

if (hasChildFragment()) {
if (auto parent = childFragment()->getParent()) {
parent->ranForNanoseconds(ns);
}
}
}

#define OVERRIDE_TASK_STATUS COMPATIBILITY_OVERRIDE
#include "../CompatibilityOverride/CompatibilityOverrideIncludePath.h"