Skip to content

Add ResourceUsage to report subprocess CPU time and memory consumption - #226

Open
jakepetroules wants to merge 1 commit into
swiftlang:mainfrom
jakepetroules:eng/PR-resource-usage
Open

Add ResourceUsage to report subprocess CPU time and memory consumption#226
jakepetroules wants to merge 1 commit into
swiftlang:mainfrom
jakepetroules:eng/PR-resource-usage

Conversation

@jakepetroules

Copy link
Copy Markdown
Contributor

Introduce a public ResourceUsage struct that exposes userTime, systemTime (as Duration), and maxRSS (in bytes) for every terminated subprocess. An ExecutionResult protocol provides common access to terminationStatus and resourceUsage across both ExecutionOutcome and ExecutionRecord.

On Unix, resource data is collected via wait4 (BSD) or the Linux kernel's 5-argument waitid syscall, which populates a rusage struct alongside the termination status. A linux_waitid C shim is added because glibc and musl only expose the 4-parameter POSIX waitid that omits rusage. The raw rusage is available as a public property on non-Windows platforms, with maxRSS normalized from KiB to bytes on Linux, FreeBSD, and OpenBSD.

On Windows, GetProcessTimes provides CPU time (converted from FILETIME 100ns units) and GetProcessMemoryInfo provides PeakWorkingSetSize (maxRSS).

This functionality is particularly necessary as part of SwiftSubprocess because collecting rusage information from a terminated subprocess requires the ability to run code when a process has changed state from executing to zombie, but before its pid has been reaped - something not possible with the current Subprocess API. Further, it is notoriously difficult to collect this information across all OSes for an arbitrary PID anyways, at least in a way that doesn't also simultaneously reap the pid. User time, system time, and maxRSS are some of the most common metrics typically extracted from getrusage. Exposing the raw rusage struct provides access to the rest, and on Windows, callers can use DuplicateHandle to get a process descriptor that can outlive Subprocess's control of the process and collect any additional metrics from the process when it is known to be in a terminated state.

@jakepetroules jakepetroules added enhancement New feature or request API Change labels Mar 17, 2026
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch 2 times, most recently from a8f9745 to 66ae4c3 Compare March 17, 2026 17:39
Comment thread Sources/Subprocess/Result.swift Outdated
@iCharlesHu iCharlesHu added this to the Post 1.0 - Feature Release milestone Mar 19, 2026
Comment thread Sources/Subprocess/Result.swift Outdated
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch 2 times, most recently from a357701 to ed3aa46 Compare April 13, 2026 19:02
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch from ed3aa46 to 9630762 Compare May 13, 2026 17:05
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch from 9630762 to d173b7d Compare May 21, 2026 07:19
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch from d173b7d to 006121c Compare June 3, 2026 23:15
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch from 006121c to 578e47a Compare June 11, 2026 22:17
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch 3 times, most recently from 470fb4e to abd8399 Compare August 9, 2026 02:33
Introduce a public ResourceUsage struct that exposes userTime, systemTime
(as Duration), and maxRSS (in bytes) for every terminated subprocess.
An ExecutionResult protocol provides common access to terminationStatus
and resourceUsage across both ExecutionOutcome and ExecutionRecord.

On Unix, resource data is collected via wait4 (BSD) or the Linux kernel's
5-argument waitid syscall, which populates a rusage struct alongside the
termination status. A linux_waitid C shim is added because glibc and musl
only expose the 4-parameter POSIX waitid that omits rusage. The raw
rusage is available as a public property on non-Windows platforms, with
maxRSS normalized from KiB to bytes on Linux, FreeBSD, and OpenBSD.

On Windows, GetProcessTimes provides CPU time (converted from FILETIME
100ns units) and GetProcessMemoryInfo provides PeakWorkingSetSize (maxRSS).

This functionality is particularly necessary as part of SwiftSubprocess
because collecting rusage information from a terminated subprocess requires
the ability to run code when a process has changed state from executing
to zombie, but before its pid has been reaped - something not possible
with the current Subprocess API. Further, it is notoriously difficult
to collect this information across all OSes for an arbitrary PID anyways,
at least in a way that doesn't also simultaneously reap the pid.
User time, system time, and maxRSS are some of the most common metrics
typically extracted from getrusage. Exposing the raw rusage struct
provides access to the rest, and on Windows, callers can use DuplicateHandle
to get a process descriptor that can outlive Subprocess's control of the
process and collect any additional metrics from the process when it is
known to be in a terminated state.

Fix Linux build and Windows test assertion for ResourceUsage

Two CI failures from the preceding commit.

Linux: import _SubprocessCShims in Result.swift. `timeval`'s members are
attributed to that module rather than to the platform libc overlay, so
reading ru_utime/ru_stime needs it in scope under InternalImportsByDefault.
Subprocess+Unix.swift already carries this import, including the OpenBSD
public-import variant, which is mirrored here.

Windows: stop asserting that a sleeping child accrues no CPU time. The
assertion was wrong, not the implementation. `longRunningProcess` has to use
powershell.exe on Windows, and its interpreter startup costs 2.75s of CPU
against a 1s sleep, so the quantity the test was checking isn't observable
through that process. The claim still holds and is still asserted wherever
the sleeper is a trivial binary; Windows CPU accounting remains covered by
testResourceUsageReportsCPUTime.

Fix glibc public-import error and Android signal test

Linux: import _SubprocessCShims publicly. On glibc, `rusage` itself -- not
just `timeval`'s members -- is attributed to that module rather than to the
libc overlay, so an internal import leaves ResourceUsage.rusage as a public
property of an internal type. This is the same root cause as the "why is
this only necessary on OpenBSD" FIXME in Subprocess+Unix.swift. Which
platforms need it depends on how each overlay claims the declaring header,
so it's applied to every non-Windows platform rather than enumerated:
Android built fine with an internal import and musl doesn't need the import
at all, but guessing wrong costs a CI round trip per platform.

Worth noting this is the cost of `public let rusage: rusage`: exposing a C
struct means publicly re-exporting an underscored shims module from a
package that is mostly a transitive dependency. Exposing the handful of
additional rusage fields as typed properties instead would let the import go
back to internal everywhere.

Android: send SIGTERM from the test instead of via `sh -c 'kill -TERM $$'`.
Whether a shell dies from the signal or traps it and exits 128+signum is
shell-specific, and Android's mksh reports exited(143). The test now mirrors
testExitViaSignal, which sends the signal itself and already passes on
Android; monitorProcessTermination returns the ResourceUsage directly, so
nothing is lost.

Keep the public _SubprocessCShims import OpenBSD-only

The previous commit fixed the glibc error by importing _SubprocessCShims
publicly on every non-Windows platform. That's the wrong trade: it puts an
underscored implementation module into the public surface of a package that
is mostly a transitive dependency, everywhere, to satisfy one platform.

Address the cause instead. `rusage` is only attributed to _SubprocessCShims
in files that import it -- the original commit declared `public let rusage:
rusage` with no such import and glibc accepted it, complaining only about
`timeval`'s members. So the module is kept out of Result.swift entirely and
the conversions that read those members move to the platform files that
already import it, next to the reaping code that is their only caller:
ResourceUsage.init(_ rusage:) to Subprocess+Unix.swift and
init(processHandle:) plus the FILETIME conversion to Subprocess+Windows.swift.
ResourceUsage gains internal memberwise initializers so both can build one
without touching a C struct through the public type.

The only public import of _SubprocessCShims is once again the OpenBSD branch
in Subprocess+Unix.swift.

Result.swift is now free of platform libc member access, which is also why
its WinSDK import is gone.

Keep implementation rationale out of public API docs

The doc comment on `ResourceUsage` was a `///` on a public type, so DocC
publishes it, but its second paragraph was written for maintainers: source
file names, module attribution mechanics, and a reference to "`rusage` below"
that means nothing in rendered documentation. That rationale is still worth
recording, so it becomes a `//` note in the import block, where someone would
actually be tempted to add the import back.

What the public doc says instead is the thing a caller can observe and would
not otherwise expect: equality and hashing ignore `rusage`, even though it's
a public stored property. That behaviour was previously explained only in an
implementation comment, which is backwards -- the rationale was documented
and the semantics weren't. `rusage` is referenced with plain backticks rather
than as a symbol link because it isn't compiled on Windows.

Trims the now-duplicated half of the comment above `==`, and the equivalent
paragraph on ResourceUsage.init(_:), which is internal and can just point at
the reason rather than restate it.
@jakepetroules
jakepetroules force-pushed the eng/PR-resource-usage branch from abd8399 to 0211f39 Compare August 9, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API Change enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants