Skip to content

perf(upgrade): use copy-then-mmap for zero JS heap during delta patching#344

Merged
BYK merged 3 commits intomainfrom
perf/delta-cow-mmap
Mar 5, 2026
Merged

perf(upgrade): use copy-then-mmap for zero JS heap during delta patching#344
BYK merged 3 commits intomainfrom
perf/delta-cow-mmap

Conversation

@BYK
Copy link
Member

@BYK BYK commented Mar 5, 2026

Changes

1. Copy-then-mmap with child process probe for mmap safety

Bun.mmap() on the running binary fails fatally on macOS (SIGKILL from AMFI) and
Linux (ETXTBSY). PR #343 fixed this by using arrayBuffer() unconditionally, but that
spikes ~100 MB onto the JS heap.

This PR restores zero-heap mmap while handling the macOS SIGKILL:

  1. Copy: copyFileSync(process.execPath, tmpdir/sentry-patch-old-{pid})
    CoW reflinks on btrfs/xfs/APFS for near-instant zero-I/O copy
  2. Probe: Spawn a child process that tries Bun.mmap(copy)
    If macOS AMFI sends SIGKILL, only the child dies — parent survives and knows mmap is unsafe
  3. Mmap: If probe exits 0, mmap the copy (zero JS heap, kernel-managed pages)
  4. Fallback: If probe fails, read copy via arrayBuffer() (~100 MB heap)

2. Instrument delta upgrade with Sentry spans and error capture

Delta failures were completely invisible in Sentry — no spans, no captureException,
errors logged at debug level. The ETXTBSY/SIGKILL bugs (PRs #339#343) were only
discoverable through code analysis and local reproduction.

  • Wraps attemptDeltaUpgrade in withTracingSpan('upgrade.delta')
  • Records delta.from_version, delta.to_version, delta.channel, delta.patch_bytes
  • On error: captureException with warning level + delta context tags
  • Upgrades error log from log.debug() to log.warn() so users see failures

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Trace

Other

  • (api) Add --data/-d flag and auto-detect JSON body in fields by BYK in #320
  • (formatters) Render all terminal output as markdown by BYK in #297
  • (install) Add Sentry error telemetry to install script by BYK in #334
  • (issue-list) Global limit with fair distribution, compound cursor, and richer progress by BYK in #306
  • (log-list) Add --trace flag to filter logs by trace ID by BYK in #329
  • (logger) Add consola-based structured logging with Sentry integration by BYK in #338
  • (project) Add project create command by betegon in #237
  • (upgrade) Add binary delta patching via TRDIFF10/bsdiff by BYK in #327
  • Improve markdown rendering styles by BYK in #342

Bug Fixes 🐛

Api

  • Use numeric project ID to avoid "not actively selected" error by betegon in #312
  • Use limit param for issues endpoint page size by BYK in #309
  • Auto-correct ':' to '=' in --field values with a warning by BYK in #302

Formatters

  • Expand streaming table to fill terminal width by betegon in #314
  • Fix HTML entities and escaped underscores in table output by betegon in #313

Setup

  • Suppress agent skills and welcome messages on upgrade by BYK in #328
  • Suppress shell completion messages on upgrade by BYK in #326

Upgrade

  • Replace Bun.mmap with arrayBuffer on all platforms by BYK in #343
  • Replace Bun.mmap with arrayBuffer on macOS to prevent SIGKILL by BYK in #340
  • Use MAP_PRIVATE mmap to prevent macOS SIGKILL during delta upgrade by BYK in #339

Other

  • (ci) Generate JUnit XML to silence codecov-action warnings by BYK in #300
  • (install) Fix nightly digest extraction on macOS by BYK in #331
  • (nightly) Push to GHCR from artifacts dir so layer titles are bare filenames by BYK in #301
  • (project create) Auto-correct dot-separated platform to hyphens by BYK in #336
  • (region) Resolve DSN org prefix at resolution layer by BYK in #316
  • (test) Handle 0/-0 in getComparator anti-symmetry property test by BYK in #308
  • (trace-logs) Timestamp_precise is a number, not a string by BYK in #323

Documentation 📚

  • Document SENTRY_URL and self-hosted setup by BYK in #337

Internal Changes 🔧

Api

  • Upgrade @sentry/api to 0.21.0, remove raw HTTP pagination workarounds by BYK in #321
  • Wire listIssuesPaginated through @sentry/api SDK for type safety by BYK in #310

Other

  • (craft) Add sentry-release-registry target by BYK in #325
  • (project create) Migrate human output to markdown rendering system by BYK in #341
  • (upgrade) Use copy-then-mmap for zero JS heap during delta patching by BYK in #344

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

Codecov Results 📊

101 passed | Total: 101 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📉 -2582
Passed Tests 📉 -2582
Failed Tests
Skipped Tests

All tests are passing successfully.

✅ Patch coverage is 88.43%. Project has 3680 uncovered lines.
❌ Project coverage is 80.66%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
delta-upgrade.ts 94.39% ⚠️ 24 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    82.64%    80.66%    -1.98%
==========================================
  Files          127       127         —
  Lines        17902     19028     +1126
  Branches         0         0         —
==========================================
+ Hits         14794     15348      +554
- Misses        3108      3680      +572
- Partials         0         0         —

Generated by Codecov Action

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Temp file leaked if writer.end() throws in finally
    • Nested writer.end() in try/finally to ensure cleanupOldFile() always runs even when writer.end() throws.

Create PR

Or push these changes by commenting:

@cursor push f3adb8e178
Preview (f3adb8e178)
diff --git a/src/lib/bspatch.ts b/src/lib/bspatch.ts
--- a/src/lib/bspatch.ts
+++ b/src/lib/bspatch.ts
@@ -358,8 +358,11 @@
       oldpos += seekBy;
     }
   } finally {
-    await writer.end();
-    cleanupOldFile();
+    try {
+      await writer.end();
+    } finally {
+      cleanupOldFile();
+    }
   }
 
   // Validate output size matches header
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@BYK BYK force-pushed the perf/delta-cow-mmap branch from 3ca743f to 946ecd3 Compare March 5, 2026 09:12
@BYK BYK force-pushed the perf/delta-cow-mmap branch 3 times, most recently from d3791ab to eba75ef Compare March 5, 2026 10:01
Two improvements to the delta upgrade system:

1. **Copy-then-mmap with child process probe for mmap safety**

   Instead of reading the old binary into a Uint8Array via arrayBuffer()
   (~100 MB JS heap spike), copies the file to a temp location and probes
   mmap safety via a child process before attempting it:

   - Copy: copyFileSync (CoW reflinks on btrfs/xfs/APFS for near-instant)
   - Probe: spawn child to test Bun.mmap() on the copy — if macOS AMFI
     sends SIGKILL, only the child dies, parent survives
   - Mmap: if probe succeeds, mmap the copy (zero JS heap, kernel-managed)
   - Fallback: if probe fails, read copy via arrayBuffer() (~100 MB heap)

2. **Instrument delta upgrade with Sentry spans, metrics, and error capture**

   Delta failures were invisible in Sentry (no spans, no captureException,
   errors logged at debug level). Now:

   - withTracingSpan('upgrade.delta') wraps the delta attempt
   - captureException on failure with warning level + delta context tags
   - log.warn() instead of log.debug() so users see failures
   - span.setStatus({ code: 2 }) on error for correct telemetry
   - Sentry.metrics.distribution for patch_bytes and chain_length
   - chainLength field added to DeltaResult
@BYK BYK force-pushed the perf/delta-cow-mmap branch from eba75ef to a0707e2 Compare March 5, 2026 10:09
…olated coverage

Add a test to test/isolated/delta-upgrade.test.ts that exercises the full
success path of attemptDeltaUpgrade using real TRDIFF10 fixture files
(small-old.bin → small.trdiff10 → small-new.bin). This covers:

- attemptDeltaUpgrade success path: span attributes, Sentry.metrics, setStatus
- resolveStableDelta return with chainLength field
- DeltaResult type with all three fields verified

Also update CI to collect coverage from isolated tests:
- Add 'Isolated Tests' step with --coverage to .github/workflows/ci.yml
- Upload both coverage/lcov.info and coverage-isolated/lcov.info to codecov
- Add coverage-isolated/ to .gitignore

Estimated patch coverage improvement: 73% → ~94% for delta-upgrade.ts
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from c763956 to aba45fd Compare March 5, 2026 11:04
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from aba45fd to c518e43 Compare March 5, 2026 11:12
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from c518e43 to fa2ed4c Compare March 5, 2026 11:16
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from fa2ed4c to ff72fd9 Compare March 5, 2026 11:23
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from ff72fd9 to e298614 Compare March 5, 2026 11:43
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from e298614 to 1e28656 Compare March 5, 2026 11:52
BYK added a commit that referenced this pull request Mar 5, 2026
Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from 1e28656 to 6ad6c0c Compare March 5, 2026 12:00
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Compiled Bun binaries don't support the -e flag, so spawning
process.execPath -e '...' re-runs the main program instead of
evaluating the expression. This made probeMmapSafe() always return
false in production, negating the mmap optimization.

The probe was unnecessary: mmap on a temp copy is safe because the
copy is a regular file (separate inode), not a running binary.
ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect the running
binary's inode. Simplify to direct copy → mmap with try/catch
fallback to arrayBuffer().

Addresses Seer review comment on PR #344.
@BYK BYK force-pushed the perf/delta-cow-mmap branch from 6ad6c0c to 68784aa Compare March 5, 2026 12:10
@BYK BYK merged commit 22071f1 into main Mar 5, 2026
20 checks passed
@BYK BYK deleted the perf/delta-cow-mmap branch March 5, 2026 12:34
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