Skip to content

Measure API client allocations and client/server memory in separate passes - #164

Draft
Andrew Branch (andrewbranch) wants to merge 3 commits into
mainfrom
api-memory-benchmarks
Draft

Andrew Branch (andrewbranch) wants to merge 3 commits into
mainfrom
api-memory-benchmarks

Conversation

@andrewbranch

@andrewbranch Andrew Branch (andrewbranch) commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Copilot slop:

Important

Source prerequisite is not published. --apiMemory requires accompanying TypeScript compiler/API changes implementing TS_API_MEMORY_DIR, collected-allocation V8 sampling, the native getServerMemory request, and the reviewed Linux client VmHWM peak-RSS correction. Those changes remain local, not included in this PR or an upstream prerequisite PR. Timing-only API benchmarks continue working without them; requesting memory on an older candidate fails explicitly. Integration used a patched source tree based on 28534d7609062699a68b1b66e2367f8d95dbf290, not an unmodified released compiler.

Adds client allocations and client/server heap memory using the existing scalar benchmark/report model. Server peak RSS has been removed at the user's request, including its producer wire field and OS-specific Go helpers. There is no replacement server RSS mechanism.

  • ts-perf benchmark api --apiMemory, or CI preset api apiMemory, completes ordinary timing iterations with memory profiling disabled, then repeats the identical workload/candidate/installed Node/bundled npm in separate memory-only iterations. Memory runs have no additional discarded warmups: each starts fresh processes and the timing pass has already warmed OS caches. Both commits receive the same opt-in; asymmetric memory/timing-only report comparisons fail.
  • CI has one opt-in path: an always-present true/false TSPERF_API_MEMORY, required at the sandbox boundary and forwarded only for saved API runs, not load-only comparisons. The runner consumes the producer's byte estimate without requiring a particular sampling interval.
  • Client JS allocations are a sampled estimate, not exact totals: producer-owned V8 inspector sampling at 32 KiB with both includeObjectsCollectedByMajorGC and includeObjectsCollectedByMinorGC. Both heap snapshots include uncollected garbage. Go TotalAlloc is exact cumulative Go heap allocated bytes. Client RSS is lifetime high-water, not live heap.
  • Process-wide snapshots deduplicate by PID; overlapping client allocation windows deduplicate by sampling ID and disjoint windows sum. Heap metrics explicitly say "sum of process maxima"; client RSS reports the maximum single-client-process peak, never a simultaneous total or sum of peaks. On Linux, the JS client uses /proc/self/status VmHWM for the current executable image, not getrusage's inherited pre-exec launcher high-water mark. macOS retains its existing client rusage measurement.
  • The revised server contract is exactly {pid, heapAllocBytes, totalAllocBytes}. Retained memory metrics are client sampled JS allocations, client heap, client peak RSS, server Go TotalAlloc, and server heap, plus client/server process counts. Missing/unsupported retained counters fail the requested memory pass. No runner preload, compiler source rewriting, forced GC, new report format, or availability/provenance protocol.

JS allocation/heap metrics cover the main V8 isolate, not worker isolates/native allocations; client RSS covers the whole client process. Profiling windows cover overlapping API-client lifetimes, not all command startup/shutdown. Profiling overhead and sampling noise remain. Worker-thread collection fails explicitly. Requested heap snapshots and process counts are retained; the comparison guard also protects local CLI and saved-report comparisons outside CI.

Validation

  • Build, changed-file ESLint/dprint, and 123 focused tests pass: parser/aggregation, exact retained metric keys, serialization/merge/comparison and console/Markdown/HTML rendering with no server RSS, opt-in/off and inherited-env isolation, timing warmups without memory warmups, missing retained counters, variable sampling intervals, exact pipeline cost accounting, and opt-in forwarding only to saved baseline/candidate commands.
  • All three real upstream workloads passed against the revised no-server-RSS producer packages on macOS arm64 with checksum-verified Node24.21.0 and bundled npm: npm run generate:metaModel, floating-rule npm run lint, and recommended-rule npm run lint. Each ran as separate timing/memory passes twice (same patched compiler, labeled baseline/candidate to exercise report plumbing, not a performance comparison between revisions). Every memory run reported exactly one API client and one server despite npm/wrappers. Production packages came from the producer's standard packaging task and existing preserveApiArtifact; telemetry JS and native binary bytes were unchanged during staging.
Workload JS allocated (sampled bytes) Client heap snapshot bytes Client peak RSS bytes Go TotalAlloc bytes Server heap snapshot bytes
Metamodel 33,514,688 31,398,992 109,887,488 40,741,304 13,456,072
ESLint floating 3,318,092,432 683,542,760 1,080,000,512 216,672,568 121,909,840
ESLint recommended 4,895,228,832 1,038,460,792 1,329,119,232 852,707,208 265,486,272
  • Revised-package lifecycle/churn harness passed on pinned Node24.21.0: exact three-field server wire shape, profiling off, real synchronous/asynchronous servers, independent timing, overlapping/disjoint window IDs, and idempotent close. A 32 MiB old-space / 1 MiB semispace stress run observed 21 natural major and 302 minor GC, with approximately 283 MB sampled allocations versus 15–27 MB close heap, demonstrating collected churn rather than retained-only profiling. No global.gc, --expose-gc, or forced-GC inspector request; heap limits were used only for this stress test, not benchmark workloads.
  • Real reports were serialized/reloaded and rendered in console, Markdown and HTML. Explicit assertions confirm server RSS is absent and client peak RSS remains present.
  • Initial full local suite: 242 passed, 4 failed. All four also reproduce on untouched main: macOS chmod --, Bash3.2 test -v (two tests), and /private temporary-directory mount normalization. No unrelated portability fixes included.
  • The producer reports full validation (983 API tests passing, one Linux-only test skipped), native race tests, build/lint/format and standard packaging passing after server RSS removal. Its Linux-only inherited-RSS regression now covers client peak RSS only. That regression and actual Linux memory-profile/bubblewrap benchmark execution remain unverified without a Linux runtime. macOS integration cannot establish Linux client peak-RSS correctness. The API preset's Node24.21.0 pin and existing installed-host/npm selection are preserved.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2e98ee2-8ada-4ded-92fb-05508196941e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2e98ee2-8ada-4ded-92fb-05508196941e

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The unpublished compiler prerequisite and unverified Linux sandbox execution and peak-RSS behavior require human validation.

No open issues 🎉

What changed in this PR

Adds opt-in API allocation and memory measurements through separate profiling passes, using the existing benchmark and reporting model.

Changes:

  • Separates timing and memory runs, validates telemetry, and aggregates process/window measurements.
  • Adds memory metrics and rejects comparisons with inconsistent memory coverage.
  • Wires the opt-in through CI and the sandbox, with tests and documentation.
File Description
ts-perf/​packages/​commands/​src/​benchmark/​runApiScenario.ts Runs isolated memory passes after timing.
ts-perf/​packages/​commands/​src/​benchmark/​runApiScenario.test.ts Tests pass isolation and failures.
ts-perf/​packages/​commands/​src/​benchmark/​index.ts Adds the memory option type.
ts-perf/​packages/​commands/​src/​benchmark/​apiMemory.ts Validates and aggregates memory dumps.
ts-perf/​packages/​commands/​src/​benchmark/​apiMemory.test.ts Tests aggregation and report integration.
ts-perf/​packages/​commands/​src/​benchmark/​api.ts Exposes --apiMemory.
ts-perf/​packages/​api/​src/​model/​sample.ts Defines memory metrics.
ts-perf/​packages/​api/​src/​model/​measurementComparison.ts Guards against asymmetric memory comparisons.
scripts/​src/​setupPipeline.ts Parses opt-in and accounts for extra runs.
scripts/​src/​runTsPerf.ts Forwards opt-in to saved API runs.
scripts/​src/​__tests__/​utils.test.ts Tests conditional option forwarding.
scripts/​src/​__tests__/​setupPipeline.test.ts Tests defaults, restrictions, and cost accounting.
scripts/​src/​__tests__/​scenarioSetup.test.ts Supplies the sandbox memory variable.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​vscode/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​vscode_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​tsserver-only/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​tsserver-only_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​tsc-only/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​tsc-only_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​this is not a preset/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​this is not a preset_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​startup-only/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​startup-only_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​startup-only hosts=node@22.8.0/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​startup-only hosts=node@22.8.0_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​regular/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​regular_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​public/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​public_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​predictable=true/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​predictable=true_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​hosts=bun@1.1.3,vscode@1.88.1/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​hosts=bun@1.1.3,vscode@1.88.1_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​full/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​full_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster predictable host=node@18.5.1/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster predictable host=node@18.5.1_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster commits=release-5.3...release-5.4/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​faster commits=release-5.3...release-5.4_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​default/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​default_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​bun/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​bun_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​baseline/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​baseline_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​src/​__tests__/​__file_snapshots__/​setupPipeline/​api_tsgo/​outputVariables.snap Records memory-disabled default.
scripts/​runBenchmarkSandbox.sh Requires and forwards the memory variable.
README.md Documents usage, metrics, and limitations.
build/​benchmark.yml Propagates memory configuration between jobs.

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2e98ee2-8ada-4ded-92fb-05508196941e
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.

2 participants