Add opt-in memory shims for attachment payloads and test output - #3051
Open
erneestoc wants to merge 2 commits into
Open
Add opt-in memory shims for attachment payloads and test output#3051erneestoc wants to merge 2 commits into
erneestoc wants to merge 2 commits into
Conversation
erneestoc
requested review from
aaronsky,
adincebic,
brentleyjones,
keith and
luispadron
as code owners
August 21, 2026 00:42
erneestoc
force-pushed
the
test-memory-shims
branch
from
August 21, 2026 00:52
f0808cf to
5a2ec73
Compare
adincebic
reviewed
Aug 21, 2026
adincebic
left a comment
Contributor
There was a problem hiding this comment.
Can you prefix those feature flag names with 'test' or something along those lines?
`xcodebuild test-without-building` buffers two classes of test payload in the client's memory, and no xcodebuild flag avoids either (measured against -resultBundlePath, -resultStreamPath, -collect-test-diagnostics, and attachment lifetimes on Xcode 26.2): - XCTAttachment payloads transit xcodebuild at ~1.4x their size - even under an attachment lifetime of "keepNever", which discards them only at finalization, after the full transfer. Measured: a hosted test adding 500MB of attachments drives xcodebuild from its 231 MiB baseline to 936 MiB. - The test process's console output is captured into xcodebuild's structured session log at ~26x per byte, independent of line length. Measured: 300MB of stdout drives xcodebuild to 7.9 GiB and the test to 27.3s mean. For attachment- or log-heavy suites this dominates harness memory and limits how many simulators a machine can run in parallel. Three opt-in features, following the apple.swizzle_absolute_ xcttestsourcelocation precedent (feature-gated force_load into the test bundle, plus the activating variable in the simulator test environment): - apple.test_drop_attachment_payloads: payloads over 4KB are replaced with a short note before reaching XCTest. For keepNever suites, where the payloads were going to be discarded after transfer anyway. Measured: 936 -> 232 MiB. - apple.test_spill_attachment_payloads: payloads are written to $TEST_UNDECLARED_OUTPUTS_DIR/spilled_attachments/ (delivered in Bazel's test outputs zip) and the attachment carries a note naming the file. Measured: 936 -> 232 MiB. - apple.test_redirect_stdout: the test process's standard output is dup2()d at load time to $TEST_UNDECLARED_OUTPUTS_DIR/ test_process_output.log. Only stdout: XCTest reports results on stderr, and the runner's tests-ran detection depends on those lines (verified both ways). Measured: 7.9 GiB -> 232 MiB, and the 300MB-stdout test drops from 27.3s mean to 2.5s mean over 3 trials - identical to an empty test - while trivial tests are unaffected. The attachment shim intercepts XCTAttachment's public designated initializer (unchanged since Xcode 9); both shims fail open with the install or degradation logged (linked-but-inactive measured identical to control, 935 vs 936 MiB). Runner-suite canary tests exercise both features end-to-end - linking and environment - so an Xcode release that restructures either seam turns a named test red instead of silently restoring the memory cost.
erneestoc
force-pushed
the
test-memory-shims
branch
from
August 21, 2026 06:34
5a2ec73 to
c0b94c1
Compare
Contributor
Author
|
Done — renamed with a consistent |
aaronsky
approved these changes
Aug 21, 2026
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.
Summary
xcodebuild test-without-buildingbuffers two classes of test payload in the client process's memory, and no xcodebuild flag avoids either — measured against-resultBundlePath,-resultStreamPath(requires a bundle path; changes nothing),-collect-test-diagnostics never, and every attachment-lifetime setting on Xcode 26.2:XCTAttachmentpayloads transit xcodebuild at ~1.4× their size — even underattachment_lifetime = "keepNever", which discards them only at finalization, after the full transfer (final bundle 208K from a 936 MiB run).For attachment- or log-heavy suites this dominates harness memory and is what limits how many simulators a machine can run in parallel.
This PR adds three opt-in features, following the
apple.swizzle_absolute_xcttestsourcelocationprecedent — the feature force-loads a small shim into the test bundle and sets its activating variable in the simulator test environment:--features=apple.test_drop_attachment_payloads— payloads >4KB replaced with a short note before reaching XCTest (forkeepNeversuites, whose payloads were doomed after transfer anyway)--features=apple.test_spill_attachment_payloads— payloads written to$TEST_UNDECLARED_OUTPUTS_DIR/spilled_attachments/(delivered in Bazel's outputs zip), attachment carries a note naming the file--features=apple.test_redirect_stdout— test process stdoutdup2()d to$TEST_UNDECLARED_OUTPUTS_DIR/test_process_output.log. stdout only: XCTest reports results on stderr, and the runner's tests-ran detection depends on those lines (verified both ways)Features disabled = byte-for-byte today's behavior (linked-but-inactive measured identical to control).
Measurements
Hosted
ios_unit_test, iPhone 16 / iOS 26.2 simulator, Xcode 26.2, RSS sampled at 2Hz across the process family; "baseline" is the identical test with no attachments/output:RULES_APPLE_ATTACHMENT_PAYLOADS=drop=spill(payloads delivered inoutputs.zip)apple.test_redirect_stdoutoutputs.zip)Wall-time, 3 trials each (bazel-reported test time):
apple.test_redirect_stdoutThe redirected log-heavy test runs as fast as an empty test — xcodebuild's log-capture pipeline processes ~11MB/s, so heavy output also serializes the run; a local file write does not. Trivial tests are unaffected in both dimensions.
Design notes
apple.swizzle_absolute_xcttestsourcelocationexactly: private attrs on the test bundle rule, feature-gated-force_loadinapple_test_bundle_support, and the env pair from_get_test_memory_shim_environmentalongside the Main Thread Checker precedent inapple_test_rule_support.attachment_payload_shiminterceptsXCTAttachment's public designated initializer (initWithUniformTypeIdentifier:name:payload:userInfo:, unchanged since Xcode 9) viamethod_setImplementation, so it covers data-based attachments from any library (e.g. swift-snapshot-testing's failure diffs, which are PNGData).dropis intended forkeepNeversuites — those payloads were going to be discarded after transfer anyway;spillkeeps the bytes, in Bazel's test outputs instead of the result bundle.test_output_redirect_shimis pure POSIX (dup2at load time), no swizzling.stdoutis the supported mode with this runner: XCTest emits its result lines on stderr, and redirecting stderr breaks the runner's tests-ran detection — verified in both directions and documented in the source.test_ios_unit_test_attachment_payload_shim_drops,test_ios_unit_test_output_redirect_shim_stdout), so an Xcode release that restructures either seam turns a named test red instead of silently restoring the memory cost.Steps to reproduce the measurements
ios_unit_testwith atest_host) with a method adding 5×100MBXCTAttachment(data:)and a method writing 300MB to stdout.while :; do ps -axo rss=,comm= | grep -E "xcodebuild|testmanagerd"; sleep 0.5; done--nocache_test_results, toggling--features=apple.test_drop_attachment_payloads/apple.test_spill_attachment_payloads/apple.test_redirect_stdout, and compare xcodebuild's peak RSS and the reported test time.Test plan
//test:ios_xctestrun_runner_unit_testsuite passes locally with the two new canaries active (45/45); canaries exercise the features end-to-end (feature-gated linking + environment), with no explicit shim dep or env on the canary targets.spillfiles and the redirected log verified present inoutputs.zip(byte-exact 314,572,830 for the 300MB run).buildifierclean.