Skip to content

Document the test-binary-size memory multiplier and the strip mitigation - #3053

Closed
erneestoc wants to merge 2 commits into
bazelbuild:mainfrom
erneestoc:test-memory-docs
Closed

Document the test-binary-size memory multiplier and the strip mitigation#3053
erneestoc wants to merge 2 commits into
bazelbuild:mainfrom
erneestoc:test-memory-docs

Conversation

@erneestoc

Copy link
Copy Markdown
Contributor

Summary

Stacked on #3051 (contains its commit; only the last commit is new). Documents a third — and for large apps, dominant — test-harness memory class, plus the mitigation that works.

At session start, xcodebuild test-without-building eagerly builds an out-of-process symbolication service (XCTOutOfProcessSymbolicationService) that reads the test bundle and every session binary into memory — before any test runs, green runs included. Profiled on a large modular app (500+ deps), the two top allocation sites were the symbolication service's whole-binary/dSYM reads (~1.39GB) and test-session construction (~705MB).

Measurements

Size-controlled experiment (Xcode 26.2): a hosted test whose host binary carries an inert __TEXT blob, so binary size is the only variable.

Host binary xcodebuild peak delta / size
~0MB 231 MiB
100MB 1,134 MiB 9.0×
300MB 2,908 MiB 8.9×

Linear at ~9× host size. Harness peak model, combining with #3051's measurements:

~230MB fixed + 9× host binary + 1.4× attachment bytes + 26× console-output bytes

No off-switch exists. Each of these was measured against the 300MB fixture with no effect: -collect-test-diagnostics never, attachment lifetimes, XCTDisableAggressiveSymbolication, XCT_IMAGE_NAMES_FOR_SYMBOLICATION (the latter two found by string-mining XCTestCore/XCTHarness; the in-process gates don't govern the harness-side eager pass).

The working lever is shrinking what gets read: --linkopt=-Wl,-x --linkopt=-Wl,-S on CI configs (relink-only). Field-measured on the 500+ dep app: host 673MB → 500MB (__LINKEDIT 318MB → 146MB), xcodebuild peak −26%, 8s relink. Trade-offs documented in the section: crash-report backtraces degrade to exported symbols; XCTest assertion locations are unaffected (compiled into test code).

Steps to reproduce the 9× multiplier

  1. Add to any hosted test's app: an assembly file with .section __TEXT,__const / .globl _blob / .no_dead_strip _blob / .space 314572800, in an alwayslink objc_library.
  2. Sample: while :; do ps -axo rss=,comm= | grep xcodebuild; sleep 0.5; done
  3. Run a trivial hosted test with --nocache_test_results; compare peak against the same test without the blob; vary .space to see the slope.

`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.
At session start xcodebuild eagerly builds an out-of-process symbolication
service that reads the test bundle and every session binary into memory,
plus other whole-binary reads while constructing the session. Measured on
Xcode 26.2 with a size-controlled test host, xcodebuild's peak grows
linearly at ~9x the host binary size (100MB host -> +903MB, 300MB host ->
+2677MB, against a 231MB no-op baseline), before any test runs and on
green runs included.

No switch disables the eager pass - `-collect-test-diagnostics never`,
attachment lifetimes, `XCTDisableAggressiveSymbolication`, and
`XCT_IMAGE_NAMES_FOR_SYMBOLICATION` were each measured to have no effect
on it - so document the lever that works: stripping local symbols and the
debug map at link time (`--linkopt=-Wl,-x --linkopt=-Wl,-S`) on CI
configurations, which shrinks what the harness reads. Field-measured on a
500+ dep app: host 673MB -> 500MB, xcodebuild peak -26%, an 8s relink
with no recompile. Trade-offs documented (crash-report symbolication
degrades to exported symbols; XCTest failure locations unaffected).

Extends the "Reducing test harness memory" section added with the
apple.test_* features, completing the harness peak-memory model:
~230MB fixed + 9x host binary + 1.4x attachments + 26x console output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant