Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Per-task execution timing in chain mode (sequential + parallel) #58

Description

@kjanat

Summary

When running task chains (runner run -s …, runner run -p …, or runner install [tasks]), users have no visibility into how long each task took. Add per-task duration reporting across sequential and parallel execution paths (and optionally single-task runs for consistency).

Motivation

Chain mode is increasingly used for local dev and CI (runner run -p lint test build). Failures and slow tasks are easier to diagnose when each item reports wall-clock duration on completion — similar to turbo run / nx run-many task summaries, or time appended to each step.

Current architecture (where to hook)

All chain dispatch funnels through chain::exec::run_chain (src/chain/exec.rs):

Mode Entry Per-item dispatch Completion detection
Sequential run_sequential dispatch_itemcmd::run::run / cmd::install::install_pms (inherited stdio) synchronous return of exit code
Parallel (live) run_parallel_streaming cmd::run::dispatch_task_piped (piped stdio + prefix muxer) poll child.try_wait() loop
Parallel (grouped) run_parallel_grouped same piped dispatch poll loop → flush_task_group replays buffered output

Other relevant pieces:

  • Single-task path (dispatch_run without -s/-p) calls cmd::run directly — no chain wrapper today.
  • runner install <tasks> builds a synthetic sequential chain with an Install head (dispatch_install_chain in src/lib.rs).
  • Output grouping: single-task and sequential chains use task_group (src/cmd/mod.rs) for GHA ::group:: wrappers; grouped parallel uses flush_task_group with runner: <name> headers.
  • No timing exists todayInstant/Duration only appear in reader-drain grace logic and unrelated schema code.

Proposed implementation

1. Measurement

Wrap each per-item execution with std::time::Instant:

struct TaskTiming {
    name: String,
    started: Instant,
    finished: Option<Instant>,
    exit_code: i32,
}
  • Sequential / dispatch_item: let start = Instant::now(); let code = dispatch_item(...)?; emit_timing(name, start.elapsed(), code);
  • Parallel: record started at spawn; on try_wait() == Some(_) compute elapsed before/after reader join.
  • Deno self-exec (Dispatch::DenoSelfExec): still measurable around self_exec.run() — parallel chain currently bails on self-exec, sequential can hit it.

Use wall-clock elapsed (not CPU); format human-readably (e.g. 1.2s, 342ms, 1m 04s).

2. Emission site (by output mode)

Mode Suggested placement Rationale
Sequential stderr after child exits, before next task inherited stdio is clean; mirrors dispatch arrow on stderr
Parallel live one line per task on stderr after child reaped (prefix optional: [build] done in 1.2s (exit 0)) stdout/stderr already prefixed by muxer; timing is meta-output
Parallel grouped footer line inside/at end of each runner: <name> block in flush_task_group keeps block self-contained; GHA group title could include duration
Single-task (optional) stderr after cmd::run returns consistency when not chaining

Respect --quiet: timing is diagnostic meta-output — either suppress under quiet (like the arrow) or treat as orthogonal (needs product decision; I'd default to show unless --quiet since timing is the point).

3. CLI / config API (sketch)

Opt-in vs default-on is a product call. Suggested:

  • --timing / -T global flag
  • RUNNER_TIMING env (truthy, same pattern as RUNNER_EXPLAIN)
  • Optional [chain].timing = true in runner.toml

Could later add a chain-end summary table (--timing=verbose?) listing all tasks sorted by duration.

4. Code changes (expected touch points)

  1. src/chain/exec.rs — primary logic in run_sequential, run_parallel_streaming, run_parallel_grouped / flush_task_group
  2. src/cmd/mod.rs — small emit_task_timing(...) helper (stderr, colored/dimmed, GHA-aware)
  3. src/resolver/types.rs + src/cli.rs + src/resolver/overrides.rs — wire timing flag through ResolutionOverrides (same pattern as quiet / explain)
  4. src/lib.rs — optionally wrap single-task cmd::run if we want parity
  5. tests/chain_integration.rs — fixture with sleep recipes; assert timing line appears and ordering

5. Edge cases

  • --kill-on-fail: killed siblings still emit timing for work completed before SIGKILL
  • --keep-going: emit timing for every item, including failures
  • Install head in runner install build test: time install separately from following tasks
  • Parallel + install: still rejected (install items cannot run in parallel chains) — no change
  • Grouped vs live parallel: both paths must report (don't only implement one)
  • GHA: duration in group footer avoids breaking ::group:: title parsing; optional ::notice for slow tasks is a follow-up
  • JSON/machine output: out of scope for v1 unless --json chain summary is planned

Example output (sketch)

Sequential:

→ justfile build
… build output …
· build finished in 1.8s (exit 0)
→ justfile test
…
· test finished in 4.2s (exit 1)

Parallel grouped:

runner: lint
… lint output …
finished in 0.9s (exit 0)

runner: test
…
finished in 6.1s (exit 0)

Testing

  • Unit: duration formatting helper
  • Integration: tests/fixtures/chain-sequential (or new fixture) with a slow recipe (sleep 0.2); runner run -s a slow asserts two timing lines in order
  • Parallel: assert each task gets a line even when completion order ≠ spawn order (grouped mode)
  • --quiet / flag-off → no timing lines

Non-goals (v1)

  • Per-line streaming latency
  • CPU/time profiling
  • Persisting timing history
  • Changing JSON schema for doctor/why

References

  • Chain executor: src/chain/exec.rs
  • Prefix muxer: src/chain/mux.rs
  • Chain types: src/chain/mod.rs
  • Existing chain integration tests: tests/chain_integration.rs

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions