Add ResourceUsage to report subprocess CPU time and memory consumption - #226
Open
jakepetroules wants to merge 1 commit into
Open
Add ResourceUsage to report subprocess CPU time and memory consumption#226jakepetroules wants to merge 1 commit into
jakepetroules wants to merge 1 commit into
Conversation
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
2 times, most recently
from
March 17, 2026 17:39
a8f9745 to
66ae4c3
Compare
compnerd
reviewed
Mar 18, 2026
jakepetroules
commented
Mar 22, 2026
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
2 times, most recently
from
April 13, 2026 19:02
a357701 to
ed3aa46
Compare
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
from
May 13, 2026 17:05
ed3aa46 to
9630762
Compare
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
from
May 21, 2026 07:19
9630762 to
d173b7d
Compare
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
from
June 3, 2026 23:15
d173b7d to
006121c
Compare
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
from
June 11, 2026 22:17
006121c to
578e47a
Compare
jakepetroules
force-pushed
the
eng/PR-resource-usage
branch
3 times, most recently
from
August 9, 2026 02:33
470fb4e to
abd8399
Compare
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
force-pushed
the
eng/PR-resource-usage
branch
from
August 9, 2026 02:49
abd8399 to
0211f39
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.