Skip to content

perf: smaller release archives, less memory in long sessions - #72

Merged
Kushalkhemka merged 5 commits into
pi-boltfrom
perf/lighter-096
Oct 20, 2026
Merged

Kushalkhemka merged 5 commits into
pi-boltfrom
perf/lighter-096

Conversation

@Kushalkhemka

@Kushalkhemka Kushalkhemka commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Update after merging origin/pi-bolt (#69, #73): #69 fixed item 1 at the call site (interactive-mode.ts, tool_execution_end passes only the drawn fields). The component-level copy of the same fix and its test are removed from this branch; #69's stays. Items 2 and 3 are unchanged. Re-measured on the merged branch (macOS, 0.9.5 runtime, live heap after a full collection): bash long session at 30 prompts 17.8 MB (heap 14.4 + strings 3.4), standard long session at 60 prompts 13.6 MB (heap 11.6 + strings 2.0); 0.9.5: 91.4 MB and 40.6 MB. The CI numbers below are from before the merge (this branch alone).

Smaller downloads and less memory in long sessions, without changing the executable or the engine. Measured against 0.9.5's release files.

1. Tool results held twice in the TUI (packages/coding-agent): now #69's fix

Problem. ToolExecutionComponent.updateResult() kept the whole result object it got from tool_execution_end, structuredContent included. For bash that is the command's output up to 1 MB (STRUCTURED_OUTPUT_MAX_BYTES), while the content shows 50 KB. The component lives for the whole session, so every command with large output (builds, tests, logs) stayed in memory to the end. The component draws only content, details, isError and durationMs, and renderers receive only { content, details }.

Example: 30 prompts that each run seq N 200000 (1.3 MB of output) plus three file reads. Live heap after a full collection (macOS, heapStats() from an extension): heap 51.2 MB + external strings 40.2 MB.

Fix. The component keeps only the four fields it draws. Result: heap 19.5 MB + external strings 8.4 MB (91.4 MB -> 27.9 MB).

2. The same file read again is held once (packages/coding-agent)

Problem. Each read returns a new string, even when the file did not change. A session that reads the same file again holds one copy per read. In bench/long_session.py (the same four files every prompt), 7.3 MB of the 8.5 MB of strings at 30 prompts were such copies.

Fix. detachSharedString() returns the copy it gave out before for equal content. It keeps the latest outputs of 4K+ characters, up to 2M characters, and is used by read in place of detachString(). Only identity changes; the text is the same. Live heap after a full collection, 60 prompts: 40.6 MB -> 13.6 MB. In real sessions the gain is smaller: it applies only to re-reads of unchanged files.

3. Smaller archives (scripts)

  • Docs images. docs/images (2.5 MB of PNG) is no longer shipped. The model reads the text and a terminal shows no pictures. The only picture the docs embed has alt text. build-pi.sh and build-pi.ps1 remove it. All .md docs stay, including the ones doctor points at (Release tarballs omit docs/PLUGINS.md, which pi-bolt doctor and the docs point at #63).
  • x86 filter. On x86-64 the .tar.xz goes through xz's x86 BCJ filter first (xz -T0 --x86 --lzma2=preset=9e), which makes 2% of the remaining size go away. Every xz and liblzma since 5.0 (2010) unpacks it, and install.sh runs xz -dc or tar. ARM64 does not use the ARM64 filter, because xz and libarchive older than 5.4 cannot unpack it (Ubuntu 22.04, macOS's tar).
  • Windows. The .zip is written with 7-Zip's deflate (-mx=7) where 7-Zip is installed (GitHub's Windows runners), otherwise with .NET's as before. It is the same format: deflate, no Zip64. npm/install.cjs's zip reader unpacks it byte-identical (checked locally on a 7-Zip zip of 0.9.5's tree). The release workflow unpacks each archive with Expand-Archive and runs its tests on that.

Numbers (0.9.5 release -> this branch)

CI run 39328730913 (bench/lighter-096): this branch's Pi on 0.9.5's runtimes, packaged as package-release.sh does, against gh release download bolt-v0.9.5.

linux-x64 linux-arm64 darwin-arm64 win32-x64
.tar.xz (MB) 58.68 -> 55.10 (-6.1%) 54.35 -> 52.08 (-4.2%) 46.54 -> 44.32 (-4.8%)
.tar.gz (MB) 97.76 -> 95.46 (-2.3%) 94.64 -> 92.34 (-2.4%) 81.94 -> 79.64 (-2.8%)
.zip (MB) 98.25 -> ~92.6 (-5.7%, estimate)
installed (MB, du) 261.9 -> 259.5 247.5 -> 245.2 213.5 -> 211.1 (local, apparent size) -2.5
peak RSS start / headless / interactive 46 / 112 / 142, unchanged 39 / 103 / 132, unchanged 25 / 69 / 93, unchanged (footprint 8 / 26 / 41 -> 40) not measured
long session, 40 prompts, own MB at 30 / 40 79.7 / 86.7 -> 58.3 / 82.8 97.3 / 88.1 -> 72.8 / 83.2 37.9 / 37.4 -> 34.8 / 34.4
long session with 1.3 MB bash output, 30 prompts: own MB (RSS) 116.7 (209.3) -> 66.8 (159.5) 179.6 (259.1) -> 73.4 (153.2) 42.8 (235.4) -> 30.3 (174.0)

"Own" is Private_Dirty on Linux and the physical footprint on macOS. Long-session figures are single runs, sampled every 10 prompts, so they show GC timing too. The live-heap figures above, taken after a full collection, are the steady measure.

  • Windows estimate. pi-bolt.exe from 93.91 to 90.69 MB with 7-Zip -mx=7 (local, 0.9.5's exe), plus 2.4 MB of compressed PNGs. Not yet measured from a release run.
  • Executable. The executable is unchanged apart from the Pi code above, so start-up and turns cost the same. CI CPU and wall times are within noise. On macOS, instructions per prompt in the bash long session (2 interleaved rounds) were 231 / 295 / 361M (base) and 236 / 282 / 342M (cand) at prompts 10 / 20 / 30. benchmark.py instructions: startup 90 -> 88M, headless 307 -> 306M, interactive 814 -> 779M.

Checks

  • install.sh installs this branch's linux-x64 archive (x86 filter) and its linux-arm64 archive from a served folder. The installed tree is identical to the archive's, and pi --version runs (CI, both Linux runners).
    • The macOS job's server was not up in time (download failed: .../SHA256SUMS). The macOS archive format is unchanged apart from the missing PNGs.
  • Unpacked with xz 5.8, macOS bsdtar and Python's lzma: identical trees.
  • pi-bolt doctor checks only that SHA256SUMS lists the archive names, which are unchanged. pi-bolt update runs install.sh.
  • bench/e2e_tools.py against 0.9.5's code: PASS. bench/e2e_screen.py: PASS at 120 and 80 columns (the TUI scrollback is the same). bench/ui_check.py: PASS.
  • New tests:
    • test/detach-shared-string.test.ts: equal content held once, the 2M-character bound, and a changed file read again.
  • npm run check is clean.
  • ./test.sh: coding-agent 337 files pass. The failures are elsewhere and do not involve this change:
    • chord state-fuzz is a 5 s timeout at load average 130;
    • codemode worker-lifetime passes when re-run;
    • packages/env needs the daemon, as before.

Not done (data in the notes)

  • Bytecode (24 MB on disk, 8.3 MB of each .tar.xz). The bytecode payload and its string table are cold when the prebuilt heap maps: a session reads 7 of 1,494 pages. Dropping them is the largest saving left. The node builtins' bytecode is linked into the same payload, though: zeroing the payload made --version take 1,142M instructions instead of 93M. Dropping it needs the builtins linked separately in Bun's bundler. That is engine format work, too large for tonight.
  • ICU on Linux (33 MB, 7.9 MB xz). Trimming it changes Intl and TextDecoder behaviour.
  • .bss stored as zeros (1.9 MB) and zero pages in the prebuilt heap (4 MB). These cost disk only (0.7% and 1.5%), compress away, and the heap layout belongs to the start-up work.

- The TUI's tool component kept the whole result it was given, structuredContent too: for bash, the
  command's output up to 1 MB (the content shows 50 KB), for the rest of the session. It now keeps
  content, details, isError and durationMs, which is all it draws.
- A file read again unchanged gives the string the earlier read gave (detachSharedString: the latest
  2M characters of read outputs, by content), so the session holds it once.

Live heap after a full collection (macOS, 0.9.5 runtime), long_session.py with every prompt also
running a command with 1.3 MB of output, 30 prompts: 91.4 MB -> 27.9 MB. Standard long_session.py
(the same four files read every prompt), 60 prompts: 40.6 MB -> 13.6 MB.
…for x86-64

- The docs' PNG files (2.5 MB; the model reads the text, and a terminal shows no picture) are not
  shipped.
- The x86-64 .tar.xz archives go through xz's x86 filter first (call targets made relative):
  2% smaller, and every xz since 5.0 unpacks it. Not the ARM64 filter, which xz and libarchive
  before 5.4 cannot unpack.

linux-x64 .tar.xz: 58.68 MB -> 55.17 MB (-6.0%).
The same kind of .zip (deflate, no Zip64), which install.ps1, npm/install.cjs and Expand-Archive
unpack: pi-bolt.exe 93.9 MB -> 90.7 MB (-3.4%) at -mx=7, about a minute more per archive. Without
7-Zip, .NET's ZipFile as before.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 01:05

@claude claude Bot 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.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T01:07:33.962877Z 4baa966 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Kushalkhemka
Kushalkhemka merged commit 97ba0a1 into pi-bolt Oct 20, 2026
12 checks passed
@Kushalkhemka
Kushalkhemka deleted the perf/lighter-096 branch October 20, 2026 07:37
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.

3 participants