Skip to content

perf(cursor): long command output streams without copying its tail per chunk - #26934

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
hieu-lee:perf/prov-adapters
Nov 14, 2026
Merged

juliusmarminge merged 3 commits into
pingdotgg:mainfrom
hieu-lee:perf/prov-adapters

Conversation

@hieu-lee

@hieu-lee hieu-lee commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Cursor's shell-output-delta handler kept the streamed command output with streamedOutput = (streamedOutput + chunk).slice(-256 KiB). Once a command has printed more than 256 KiB, every chunk flattens and copies the whole 256 KiB tail, so a command that prints a few MB blocks the event loop and allocates gigabytes.

Fix

packages/provider-cursor/src/server/adapter.ts: append each chunk with += and trim back to 256 KiB only once the string passes 512 KiB (amortised O(1) per chunk). The only reader (outputText in the tool-update path) now applies the same slice(-256 KiB), so the visible output is unchanged and retained memory stays at most 512 KiB plus one chunk.

Measurements

Real makeCursorAdapterV2 with a fake SDK; deltas are fired after the turn context exists. Fresh process per round, 5 interleaved rounds (base origin/main vs this branch), median. Allocated bytes are summed heap growth between GCs (v8.GCProfiler).

Input Base New Ratio
25,000 deltas x 200 B (5 MB) 707 ms, 6,300 MB 44 ms, 91 MB 16x faster, 69x fewer bytes
100,000 deltas x 80 B (8 MB, 2 rounds) ~2,850 ms, 25.7 GB ~130 ms, ~350 MB ~22x faster, ~74x fewer bytes

The sha1 of the final command output is identical before and after. What limits the new number: about 1.7 us and 3.6 KB per delta in emitProviderEvent, id derivation, the event object and the tools scan for the active shell.

Scope and approval

No triaged issue or maintainer-approved discussion exists for this; it is a focused, behaviour-preserving performance fix of one measured hot path (one problem, one PR, output identical to main as shown under Validation, no new setting, wire change or UI). Dedupe: I searched all open, merged and closed PRs by file and symbol and found none covering this function.

Validation

  • New test in adapter.test.ts streams 3,000 x 256 B chunks (past the trim point) and asserts the output is the last 256 KiB of the concatenation. It passes on base too, since behaviour is unchanged; no existing test covered shell-output-delta.
  • vp run check:changed: format, lint, knip, typecheck, tests pass.
  • No other open or merged PR changes these lines (searched all PR states by file and by streamedOutput / shell-output-delta); perf(cursor): coalesce streamed reply text into 50 ms snapshots #24663 only coalesces text deltas.

Not checked: Windows/Linux, live Cursor sessions.

Authored with Claude Sonnet 5.5 via the Claude Code harness in T3 Code.

🤖 Generated with Claude Code

…r chunk

Trim the retained shell output only past 512 KiB instead of re-slicing the 256 KiB tail on every delta; the read site still applies the 256 KiB cap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched Author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Oct 29, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 0d41fa5

Macroscope's review found this PR approvable — The change reduces repeated copying while buffering Cursor command output, preserving the existing fallback output limit and live streaming behavior. The bounded increase in transient buffering presents no obvious material risk.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The Cursor adapter now batches shell-output trimming and uses the final bounded segment of streamed output when completed tool output is empty. A test streams 3,000 chunks and checks that the completed command output contains the final 256 KiB.

Changes: Cursor shell output

Layer / File(s) Summary
Bounded shell output
packages/provider-cursor/src/server/adapter.ts, packages/provider-cursor/src/server/adapter.test.ts
The adapter trims accumulated shell output after it exceeds twice the configured limit. If completed tool output is empty, the adapter uses the final limited segment of streamed output. The test checks the retained output after 3,000 chunks.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Suggested reviewers: juliusmarminge

Merge Risk: Minimal · up to 53c5f

This change reduces memory and CPU use when Cursor streams large shell output, while the final output stays limited to the last 256 KiB. No merge-blocking risk was identified.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: long Cursor command output now streams without copying its tail on every chunk. It uses the conventional commit format and matches the adapter.ts change that appends c…
Description check Passed The description follows the repository template. It has Problem, Change (labelled Fix), Scope, and Verification sections, and the Problem and Fix sections give the root cause and mechanism. It include…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: long Cursor command output now streams without copying its tail on every chunk. It uses the conventional commit format and matches the adapter.ts change that appends chunks and trims only after 512 KiB. The 'perf(cursor)' scope and wording are specific and accurate.

Full details: Description check

Explanation

The description follows the repository template. It has Problem, Change (labelled Fix), Scope, and Verification sections, and the Problem and Fix sections give the root cause and mechanism. It includes measurements, a new test, and the validation commands run. It also states what was not checked (Windows/Linux, live Cursor sessions) and names the model and harness used, as the template requests. Minor gaps: the Scope section argues against linking an issue instead of linking one, and the PR objectives mention tracker #9661 without the description linking it. The template's requirement to link an issue or explain why none exists is met by the author's explanation.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 0d41fa5 and 53c5f7e.

Files selected for processing: 2

  • packages/provider-cursor/src/server/adapter.test.ts
  • packages/provider-cursor/src/server/adapter.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@hieu-lee

Copy link
Copy Markdown
Contributor Author

@juliusmarminge ready for review. Cursor's shell-output handler re-copied its 256 KiB tail on every chunk once a command passed 256 KiB; it now trims in batches. 5 MB of output: 707 ms and 6.3 GB allocated down to 44 ms and 91 MB (16x faster, 69x fewer bytes), with an identical final output. Related tracker: #9661. The CI workflows on this fork PR show action_required, so they are waiting on a maintainer to approve the run. CodeRabbit and Macroscope are done with no open findings (Macroscope approved).

@hieu-lee

Copy link
Copy Markdown
Contributor Author

@juliusmarminge could you review this when you have a moment?

Written by Claude Sonnet 5.5 via Claude Code in T3 Code.

@juliusmarminge juliusmarminge added the takeover:in-progress A takeover lead is working on this PR label Nov 14, 2026
@juliusmarminge

Copy link
Copy Markdown
Member

Evidence

Ran real Cursor Grok 4.7 turns against independent fresh T3 servers on Linux / Node 26.12.1 / Cursor SDK 1.2.0. On each commit: run python3 emit.py 16 foreground without output redirection, then python3 paced.py 30000 (30,000 flushed 64-byte lines, 1 ms apart). Repeat the paced command for separate CPU-only and allocation captures.

Before After
13f89ae0a7 53c5f7e36a
Before: measured CPU and allocations After: measured CPU and allocations
Handler sampled self CPU: 380.195 ms; inclusive: 469.285 ms Handler sampled self CPU: 43.744 ms; inclusive: 137.670 ms
Sampled slice allocations beneath handler: 3,203,398,480 bytes Sampled slice allocations beneath handler: 1,048,736 bytes
Before live-stream recording After live-stream recording

CPU-only captures had allocation sampling disabled. Base source-position samples identify the concat+slice statement at adapter.ts:2115 in 624 of 664 handler self ticks. Allocation figures are statistical samples including collected objects. Handler self CPU decreased about 88%; slice allocations decreased over 99.9%.

Both sides streamed exactly the same 1,048,576 characters, retained byte-identical 262,144-character command output, exited 0, and returned the actual final line line 00029999 …. The SDK's approximately 1 MiB streaming ceiling remains on both sides. The burst control had little copying CPU cost, consistent with triage.

The charts derive from raw profiles. Videos are explicitly labeled rendered playbacks of timestamped live RPC frames at original timing, not browser screen captures. Start, middle and completion frames of both encoded videos were inspected.

Raw profiles, recordings data, transcripts and analysis scripts · Driver · Paced workload · Burst workload · Measurements and hashes · Methodology

Not shown:

  • Web, Electron and mobile rendering were not exercised; this proof targets the real server adapter and its live output RPC.
  • Output streaming beyond the SDK's observed 1 MiB ceiling was not demonstrated.
  • No end-to-end latency or garbage-collection reduction is claimed; model latency varied between turns.

@juliusmarminge juliusmarminge added takeover:ready Takeover done; auto-lands when gates pass and removed takeover:in-progress A takeover lead is working on this PR labels Nov 14, 2026
@juliusmarminge
juliusmarminge merged commit 6be1bd6 into pingdotgg:main Nov 14, 2026
49 checks passed
@juliusmarminge juliusmarminge removed the takeover:ready Takeover done; auto-lands when gates pass label Nov 14, 2026
Pipyakas pushed a commit to Pipyakas/t3code that referenced this pull request Nov 15, 2026
…r chunk (pingdotgg#26934)

Co-authored-by: Julius Marminge <julius0216@outlook.com>
(cherry picked from commit 6be1bd6)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched Author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants