Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add per-task metrics #56320

Merged
merged 56 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
85b89f4
basic task cpu time tracking
vilterp Jan 26, 2023
dad31b0
initialize to zero
vilterp Jan 26, 2023
3802448
make new task field visible from Julia
vilterp Jan 26, 2023
bbe8b29
add start time
vilterp Jan 26, 2023
480b916
fix typo
vilterp Jan 26, 2023
be8c1a2
add test
vilterp Jan 26, 2023
ad8c184
add a wait
vilterp Jan 26, 2023
e37004a
import `LinearAlgebra` for `peakflops`
vilterp Jan 27, 2023
6e4fd27
update last scheduled on task finish as well
vilterp Jan 31, 2023
8721245
no need to update last scheduled in finish
vilterp Feb 3, 2023
a2c98fa
add test
vilterp Jun 23, 2023
c5562d7
fix test
vilterp Jun 23, 2023
9685032
Add per-task wall-time
nickrobinson251 Oct 16, 2024
fa4f0e8
Test individual task cpu_time less than wall_time
nickrobinson251 Oct 17, 2024
89f8239
More tests
nickrobinson251 Oct 17, 2024
6b0855e
Move task timings mostly to julia
nickrobinson251 Oct 21, 2024
5e7e385
Record cpu time on entrance to `wait/yield`
nickrobinson251 Oct 23, 2024
182c1a5
Rename 'scheduled_at' -> '[en/de]queued_at'
nickrobinson251 Oct 24, 2024
b60f3c4
Ability to enable/disable task timings globally
nickrobinson251 Oct 24, 2024
7e871e2
fixup whitespace
nickrobinson251 Oct 24, 2024
523d936
Enable task timing in test
nickrobinson251 Oct 24, 2024
2c7aa36
Enable task timing for root task
nickrobinson251 Oct 28, 2024
70c56b5
Prevent `task_timing(false)` decrementing below zero
nickrobinson251 Oct 28, 2024
1b60b9f
Change task-timing flag to require yes/no arg
nickrobinson251 Oct 28, 2024
81cd488
Rename `timings`->`metrics`, `dequeued`->`started_running`
nickrobinson251 Oct 28, 2024
a42d524
Update task-state-transition comments
nickrobinson251 Oct 28, 2024
9b35821
Remove unused debug function
nickrobinson251 Oct 28, 2024
8fcc685
Add NEWS
nickrobinson251 Oct 28, 2024
6e248ef
Add fallback recording of cpu time in `wait()`
nickrobinson251 Oct 28, 2024
8b0d493
Instrument async io
nickrobinson251 Oct 28, 2024
81edb59
Record task cpu time in `wait()`
nickrobinson251 Oct 28, 2024
0863ab0
fixup! Record task cpu time in `wait()`
nickrobinson251 Oct 30, 2024
ea1a6f4
fixup! Change task-timing flag to require yes/no arg
nickrobinson251 Oct 30, 2024
a6724d3
Make task metrics flag const and counters atomic
nickrobinson251 Oct 30, 2024
e9e1e65
Mark the new APIs experimental
nickrobinson251 Oct 30, 2024
6c57fd7
Return task metrics as Int or nothing if disabled
nickrobinson251 Oct 30, 2024
7167d87
Test metrics updating as expected
nickrobinson251 Oct 30, 2024
6bdb7a8
fixup! Make task metrics flag const and counters atomic
nickrobinson251 Oct 31, 2024
dae7627
More tests
nickrobinson251 Oct 31, 2024
f5b55f2
Update test/core.jl
nickrobinson251 Oct 31, 2024
2532e24
Introduce `current_task_*_time_ns` for non-racy continuous metrics
nickrobinson251 Oct 31, 2024
3be2365
Consolidate names again
nickrobinson251 Oct 31, 2024
f8a0185
Update CPU time on finish before setting task done
nickrobinson251 Oct 31, 2024
8475671
Remove TODO comment
nickrobinson251 Oct 31, 2024
bdfff79
Add new functions to docs
nickrobinson251 Oct 31, 2024
51ca0cb
fixup! Add new functions to docs
nickrobinson251 Oct 31, 2024
6bf9aa9
Return `UInt64` instead of `Int`
kpamnany Nov 8, 2024
8fbe1ff
Always return a `UInt64` type (even when returning 0)
kpamnany Nov 8, 2024
4be17af
Convert task timestamps to UInt32 delta-times since system boot.
NHDaly Nov 20, 2024
8d1cde7
Update base/task.jl
NHDaly Nov 20, 2024
f817369
Revert "Convert task timestamps to UInt32 delta-times since system bo…
NHDaly Nov 20, 2024
36f85f3
Try adding `alignas(8)` to force correct alignment and work around GC…
NHDaly Nov 21, 2024
6871b73
Revert "Try adding `alignas(8)` to force correct alignment and work a…
kpamnany Dec 3, 2024
788f46f
Reorder task structure and add padding
kpamnany Dec 4, 2024
e02c830
More changes of `task_cpu_time_ns` to `task_running_time_ns`
kpamnany Dec 4, 2024
18ee519
Move calls to `record_running_time!`
kpamnany Dec 4, 2024
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
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ New language features
- atomic set once (`@atomiconce v[3] = 2`),
- atomic swap (`x = @atomicswap v[3] = 2`), and
- atomic replace (`x = @atomicreplace v[3] 2=>5`).
- New option `--task-metrics=yes` to enable the collection of per-task timing information,
which can also be enabled/disabled at runtime with `Base.Experimental.task_metrics(::Bool)`. ([#56320])
The available metrics are:
- actual running time for the task (`Base.Experimental.task_running_time_ns`), and
- wall-time for the task (`Base.Experimental.task_wall_time_ns`).

Language changes
----------------
Expand Down
26 changes: 22 additions & 4 deletions base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,33 @@
#end

#mutable struct Task
# parent::Task
# next::Any
# queue::Any
# storage::Any
# state::Symbol
# donenotify::Any
# result::Any
# exception::Any
# backtrace::Any
# scope::Any
# code::Any
# @atomic _state::UInt8
# sticky::UInt8
# priority::UInt16
# @atomic _isexception::UInt8
# pad00::UInt8
# pad01::UInt8
# pad02::UInt8
# rngState0::UInt64
# rngState1::UInt64
# rngState2::UInt64
# rngState3::UInt64
# rngState4::UInt64
# const metrics_enabled::Bool
# pad10::UInt8
# pad11::UInt8
# pad12::UInt8
# @atomic first_enqueued_at::UInt64
# @atomic last_started_running_at::UInt64
# @atomic running_time_ns::UInt64
# @atomic finished_at::UInt64
#end

export
Expand Down
74 changes: 74 additions & 0 deletions base/experimental.jl
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,78 @@ usage, by eliminating the tracking of those possible invalidation.
"""
disable_new_worlds() = ccall(:jl_disable_new_worlds, Cvoid, ())

### Task metrics

"""
Base.Experimental.task_metrics(::Bool)

Enable or disable the collection of per-task metrics.
A `Task` created when `Base.Experimental.task_metrics(true)` is in effect will have
[`Base.Experimental.task_running_time_ns`](@ref) and [`Base.Experimental.task_wall_time_ns`](@ref)
timing information available.

!!! note
Task metrics can be enabled at start-up via the `--task-metrics=yes` command line option.
"""
function task_metrics(b::Bool)
if b
ccall(:jl_task_metrics_enable, Cvoid, ())
else
ccall(:jl_task_metrics_disable, Cvoid, ())
end
return nothing
end

"""
Base.Experimental.task_running_time_ns(t::Task) -> Union{UInt64, Nothing}

Return the total nanoseconds that the task `t` has spent running.
This metric is only updated when `t` yields or completes unless `t` is the current task, in
which it will be updated continuously.
See also [`Base.Experimental.task_wall_time_ns`](@ref).

Returns `nothing` if task timings are not enabled.
See [`Base.Experimental.task_metrics`](@ref).

!!! note "This metric is from the Julia scheduler"
A task may be running on an OS thread that is descheduled by the OS
scheduler, this time still counts towards the metric.

!!! compat "Julia 1.12"
This method was added in Julia 1.12.
"""
function task_running_time_ns(t::Task=current_task())
t.metrics_enabled || return nothing
if t == current_task()
# These metrics fields can't update while we're running.
# But since we're running we need to include the time since we last started running!
return t.running_time_ns + (time_ns() - t.last_started_running_at)
else
return t.running_time_ns
end
end

"""
Base.Experimental.task_wall_time_ns(t::Task) -> Union{UInt64, Nothing}

Return the total nanoseconds that the task `t` was runnable.
This is the time since the task first entered the run queue until the time at which it
completed, or until the current time if the task has not yet completed.
See also [`Base.Experimental.task_running_time_ns`](@ref).

Returns `nothing` if task timings are not enabled.
See [`Base.Experimental.task_metrics`](@ref).

!!! compat "Julia 1.12"
This method was added in Julia 1.12.
"""
function task_wall_time_ns(t::Task=current_task())
t.metrics_enabled || return nothing
start_at = t.first_enqueued_at
start_at == 0 && return UInt64(0)
end_at = t.finished_at
end_at == 0 && return time_ns() - start_at
return end_at - start_at
end

end # module
1 change: 1 addition & 0 deletions base/options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ struct JLOptions
heap_size_hint::UInt64
trace_compile_timing::Int8
trim::Int8
task_metrics::Int8
end

# This runs early in the sysimage != is not defined yet
Expand Down
53 changes: 49 additions & 4 deletions base/task.jl
NHDaly marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,11 @@ function enq_work(t::Task)
return t
end

schedule(t::Task) = enq_work(t)
function schedule(t::Task)
# [task] created -scheduled-> wait_time
maybe_record_enqueued!(t)
enq_work(t)
end

"""
schedule(t::Task, [val]; error=false)
Expand Down Expand Up @@ -1031,6 +1035,8 @@ function schedule(t::Task, @nospecialize(arg); error=false)
t.queue === nothing || Base.error("schedule: Task not runnable")
setfield!(t, :result, arg)
end
# [task] created -scheduled-> wait_time
maybe_record_enqueued!(t)
enq_work(t)
return t
end
Expand Down Expand Up @@ -1064,11 +1070,15 @@ immediately yields to `t` before calling the scheduler.
Throws a `ConcurrencyViolationError` if `t` is the currently running task.
"""
function yield(t::Task, @nospecialize(x=nothing))
current = current_task()
t === current && throw(ConcurrencyViolationError("Cannot yield to currently running task!"))
ct = current_task()
t === ct && throw(ConcurrencyViolationError("Cannot yield to currently running task!"))
(t._state === task_state_runnable && t.queue === nothing) || throw(ConcurrencyViolationError("yield: Task not runnable"))
# [task] user_time -yield-> wait_time
record_running_time!(ct)
# [task] created -scheduled-> wait_time
maybe_record_enqueued!(t)
t.result = x
enq_work(current)
enq_work(ct)
set_next_task(t)
return try_yieldto(ensure_rescheduled)
end
Expand All @@ -1082,13 +1092,18 @@ call to `yieldto`. This is a low-level call that only switches tasks, not consid
or scheduling in any way. Its use is discouraged.
"""
function yieldto(t::Task, @nospecialize(x=nothing))
ct = current_task()
# TODO: these are legacy behaviors; these should perhaps be a scheduler
# state error instead.
if t._state === task_state_done
return x
elseif t._state === task_state_failed
throw(t.result)
end
# [task] user_time -yield-> wait_time
record_running_time!(ct)
# [task] created -scheduled-unfairly-> wait_time
maybe_record_enqueued!(t)
t.result = x
set_next_task(t)
return try_yieldto(identity)
Expand All @@ -1102,6 +1117,10 @@ function try_yieldto(undo)
rethrow()
end
ct = current_task()
# [task] wait_time -(re)started-> user_time
if ct.metrics_enabled
@atomic :monotonic ct.last_started_running_at = time_ns()
end
if ct._isexception
exc = ct.result
ct.result = nothing
Expand All @@ -1115,6 +1134,11 @@ end

# yield to a task, throwing an exception in it
function throwto(t::Task, @nospecialize exc)
ct = current_task()
# [task] user_time -yield-> wait_time
record_running_time!(ct)
# [task] created -scheduled-unfairly-> wait_time
maybe_record_enqueued!(t)
t.result = exc
t._isexception = true
set_next_task(t)
Expand Down Expand Up @@ -1167,6 +1191,9 @@ checktaskempty = Partr.multiq_check_empty
end

function wait()
ct = current_task()
# [task] user_time -yield-or-done-> wait_time
record_running_time!(ct)
GC.safepoint()
W = workqueue_for(Threads.threadid())
poptask(W)
Expand All @@ -1181,3 +1208,21 @@ if Sys.iswindows()
else
pause() = ccall(:pause, Cvoid, ())
end

# update the `running_time_ns` field of `t` to include the time since it last started running.
function record_running_time!(t::Task)
if t.metrics_enabled && !istaskdone(t)
@atomic :monotonic t.running_time_ns += time_ns() - t.last_started_running_at
end
return t
end
NHDaly marked this conversation as resolved.
Show resolved Hide resolved

# if this is the first time `t` has been added to the run queue
# (or the first time it has been unfairly yielded to without being added to the run queue)
# then set the `first_enqueued_at` field to the current time.
function maybe_record_enqueued!(t::Task)
if t.metrics_enabled && t.first_enqueued_at == 0
@atomic :monotonic t.first_enqueued_at = time_ns()
end
return t
end
4 changes: 4 additions & 0 deletions doc/man/julia.1
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ If --trace-compile is enabled show how long each took to compile in ms
--trace-dispatch={stderr|name}
Print precompile statements for methods dispatched during execution or save to stderr or a path.

.TP
--task-metrics={yes|no*}
Enable the collection of per-task metrics.

.TP
-image-codegen
Force generate code in imaging mode
Expand Down
8 changes: 8 additions & 0 deletions doc/src/base/multi-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ These building blocks are used to create the regular synchronization objects.
```@docs
Base.Threads.SpinLock
```

## Task metrics (Experimental)

```@docs
Base.Experimental.task_metrics
Base.Experimental.task_running_time_ns
Base.Experimental.task_wall_time_ns
```
1 change: 1 addition & 0 deletions doc/src/manual/command-line-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ The following is a complete list of command-line switches available when launchi
|`--code-coverage=tracefile.info` |Append coverage information to the LCOV tracefile (filename supports format tokens).|
|`--track-allocation[={none*\|user\|all}]` |Count bytes allocated by each source line (omitting setting is equivalent to "user")|
|`--track-allocation=@<path>` |Count bytes but only in files that fall under the given file path/directory. The `@` prefix is required to select this option. A `@` with no path will track the current directory.|
|`--task-metrics={yes\|no*}` |Enable the collection of per-task metrics|
|`--bug-report=KIND` |Launch a bug report session. It can be used to start a REPL, run a script, or evaluate expressions. It first tries to use BugReporting.jl installed in current environment and falls back to the latest compatible BugReporting.jl if not. For more information, see `--bug-report=help`.|
|`--heap-size-hint=<size>` |Forces garbage collection if memory usage is higher than the given value. The value may be specified as a number of bytes, optionally in units of KB, MB, GB, or TB, or as a percentage of physical memory with %.|
|`--compile={yes*\|no\|all\|min}` |Enable or disable JIT compiler, or request exhaustive or minimal compilation|
Expand Down
4 changes: 4 additions & 0 deletions src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,10 @@ JL_DLLEXPORT void julia_init(JL_IMAGE_SEARCH rel)
#if defined(_COMPILER_GCC_) && __GNUC__ >= 12
#pragma GCC diagnostic ignored "-Wdangling-pointer"
#endif
if (jl_options.task_metrics == JL_OPTIONS_TASK_METRICS_ON) {
// enable before creating the root task so it gets timings too.
jl_atomic_fetch_add(&jl_task_metrics_enabled, 1);
}
// warning: this changes `jl_current_task`, so be careful not to call that from this function
jl_task_t *ct = jl_init_root_task(ptls, stack_lo, stack_hi);
#pragma GCC diagnostic pop
Expand Down
22 changes: 22 additions & 0 deletions src/jlapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,28 @@ JL_DLLEXPORT uint64_t jl_cumulative_recompile_time_ns(void)
return jl_atomic_load_relaxed(&jl_cumulative_recompile_time);
}

/**
* @brief Enable per-task timing.
*/
JL_DLLEXPORT void jl_task_metrics_enable(void)
{
// Increment the flag to allow reentrant callers.
jl_atomic_fetch_add(&jl_task_metrics_enabled, 1);
NHDaly marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @brief Disable per-task timing.
*/
JL_DLLEXPORT void jl_task_metrics_disable(void)
{
// Prevent decrementing the counter below zero
uint8_t enabled = jl_atomic_load_relaxed(&jl_task_metrics_enabled);
while (enabled > 0) {
if (jl_atomic_cmpswap(&jl_task_metrics_enabled, &enabled, enabled-1))
break;
}
}

/**
* @brief Retrieve floating-point environment constants.
*
Expand Down
12 changes: 12 additions & 0 deletions src/jloptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ JL_DLLEXPORT void jl_init_options(void)
0, // heap-size-hint
0, // trace_compile_timing
JL_TRIM_NO, // trim
0, // task_metrics
};
jl_options_initialized = 1;
}
Expand Down Expand Up @@ -316,6 +317,7 @@ static const char opts_hidden[] =
" comment if color is not supported\n"
" --trace-compile-timing If --trace-compile is enabled show how long each took to\n"
" compile in ms\n"
" --task-metrics={yes|no*} Enable collection of per-task timing data.\n"
" --image-codegen Force generate code in imaging mode\n"
" --permalloc-pkgimg={yes|no*} Copy the data section of package images into memory\n"
" --trim={no*|safe|unsafe|unsafe-warn}\n"
Expand Down Expand Up @@ -347,6 +349,7 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp)
opt_trace_compile,
opt_trace_compile_timing,
opt_trace_dispatch,
opt_task_metrics,
opt_math_mode,
opt_worker,
opt_bind_to,
Expand Down Expand Up @@ -427,6 +430,7 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp)
{ "trace-compile", required_argument, 0, opt_trace_compile },
{ "trace-compile-timing", no_argument, 0, opt_trace_compile_timing },
{ "trace-dispatch", required_argument, 0, opt_trace_dispatch },
{ "task-metrics", required_argument, 0, opt_task_metrics },
{ "math-mode", required_argument, 0, opt_math_mode },
{ "handle-signals", required_argument, 0, opt_handle_signals },
// hidden command line options
Expand Down Expand Up @@ -978,6 +982,14 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp)
else
jl_errorf("julia: invalid argument to --trim={safe|no|unsafe|unsafe-warn} (%s)", optarg);
break;
case opt_task_metrics:
if (!strcmp(optarg, "no"))
jl_options.task_metrics = JL_OPTIONS_TASK_METRICS_OFF;
else if (!strcmp(optarg, "yes"))
jl_options.task_metrics = JL_OPTIONS_TASK_METRICS_ON;
else
jl_errorf("julia: invalid argument to --task-metrics={yes|no} (%s)", optarg);
break;
default:
jl_errorf("julia: unhandled option -- %c\n"
"This is a bug, please report it.", c);
Expand Down
1 change: 1 addition & 0 deletions src/jloptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ typedef struct {
uint64_t heap_size_hint;
int8_t trace_compile_timing;
int8_t trim;
int8_t task_metrics;
} jl_options_t;

#endif
Loading
Loading