04 / EXECUTION

Sequential target runs

Profile scripts and commands one at a time, with timeout handling and process-tree cleanup.

← Back to overview

What it is

A target runner is the boundary between a measurement tool and the program being measured. It starts one script or command, captures its output and exit status, and measures the interval around that target.

DoL8 runs requested targets sequentially. A directory is invoked through pytest, a Python file uses the active Python interpreter, and other targets are launched as commands. The default timeout is 600 seconds.

Why it matters

Concurrent targets compete for CPU, caches, and power limits. Their energy counters overlap, so the result cannot be attributed cleanly to either target. One-at-a-time execution gives each measurement a distinct start and end.

Runaway children are another measurement hazard: a timed-out parent can leave descendants consuming energy after the tool has moved on. Cleanup must cover the process tree, not only the original PID.

How it works

TargetRunner starts the child with captured stdout and stderr. On POSIX it creates a new session, giving the target an isolated process group. If the timeout expires, DoL8 sends SIGTERM to that group, waits up to two seconds, then escalates to SIGKILL for stragglers and checks for survivors.

The runner records return code, captured output, wall duration, timeout state, and surviving process IDs. DoL8 labels interrupted reports as partial. A normal non-zero exit still has a counter interval and is surfaced through the CLI exit status.

python -m dol8.cli run workload.py --timeout 120 --runs 3

# One target at a time; never overlap counter intervals.
for target in targets:
    result = measure_and_run(target, timeout_seconds=120)

On POSIX, process-group signaling is used; other platforms follow a separate termination path. Energy profiling itself is supported only on Linux bare metal with readable RAPL counters.

What to build

  1. Resolve the command. In dol8/cli.py, map Python scripts to the selected interpreter and preserve explicit executable commands.
  2. Isolate each run. In dol8/runner.py, start a fresh POSIX session/process group, capture both streams concurrently, and use monotonic time for wall duration.
  3. Bound execution. Validate that timeout is positive. On expiry, terminate the whole group, wait a defined grace period, escalate, and report any survivors.
  4. Keep status with metrics. Store exit code, timed-out/interrupted state, duration, and whether the sample is complete. Decide explicitly whether failed or partial work belongs in aggregates.
  5. Test process trees. Use tests/fixtures/forever.py and spawn_tree.py to assert timeout handling, cleanup, output capture, and survivor reporting without relying on real RAPL.

Current behavior: a timed-out target is stopped and returns a non-zero run status, but its counter measurement is still present in the aggregation path. Excluding aborted samples requires an explicit validity policy.

How to use it

Set a timeout appropriate for the workload. Multiple targets in one invocation are processed in order; example durations and values below are illustrative.

python -m dol8.cli run slow_job.py quick_check.py --timeout 90 --runs 3
[1/2] slow_job.py .... OK  12.40s | 310.0J +- 4.2 | 25.0W | 71.0C
[2/2] quick_check.py .... OK  1.10s | 18.5J +- 0.8 | 16.8W | 69.0C

Edge cases & gotchas

  • Process-group support differs by operating system. Test Windows cleanup separately; do not claim POSIX signals are portable.
  • A child can spawn descendants that outlive the parent. Snapshot and clean up descendants, then report survivors instead of hiding them.
  • Targets may emit large output streams. Drain stdout and stderr concurrently to avoid pipe-buffer deadlocks.
  • Timeouts, Ctrl+C, target crashes, and tool failures are distinct outcomes and should map to documented exit codes.
  • A failed process can still consume measurable energy. Preserve that fact in raw output while keeping incomplete samples out of any clean comparison by policy.