Skip to content

Defer background-tab scrollback restoration on startup (APP-5257) - #15174

Draft
warp-agent-staging[bot] wants to merge 5 commits into
masterfrom
factory/app-5257-lazy-pane-restore
Draft

Defer background-tab scrollback restoration on startup (APP-5257)#15174
warp-agent-staging[bot] wants to merge 5 commits into
masterfrom
factory/app-5257-lazy-pane-restore

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

⚠️ Do not enable this feature flag — anywhere, including dogfood

FeatureFlag::LazyBackgroundTabScrollbackRestore must stay off in every environment. With it enabled, a background tab's restored (older) scrollback can render below live output the tab produced before it was ever activated — inverted, incorrect chronological order, visible to the user on first activation. This is not a bug awaiting a small patch: it is a structural property of this PR's approach (explained below), and closing it requires a different design, not a follow-up fix to this code. See "Known limitation" below before touching the flag or building on this code.

Description

APP-5257 / Sentry issue 7259255054: on macOS, restoring a window's tabs at startup fed every tab's persisted scrollback blocks and AI conversation history into its terminal view eagerly — including tabs the window doesn't display at launch.

The premise behind this PR does not survive measurement — read this before judging the change. The heap profiles for this signature attribute 11.6 GB of a 15.9 GB sample below PaneGroup::restore_pane_tree, fanning into AppKit/CoreGraphics/Metal frames; that was read first as per-pane block/text layout, then as per-pane view/scene construction. A direct measurement of the same restore path on macOS 26.3.1, on a stable-equivalent build (release + jemalloc), contradicts both readings:

  • Startup footprint grows ~7.8 MB per restored pane, linear across 1→32 panes (least squares, intercept 166.5 MB). Real, but 4 GB of it would require ~510 restored panes.
  • Graphics allocation is invariant to pane count. IOSurface is byte-identical at 40.7 MB with exactly 6 regions at 1, 4, 8, 16, 24 and 32 panes; IOAccelerator, CoreAnimation, CoreGraphics and IOKit are likewise identical. Surfaces are allocated per window, not per pane, so there is no per-pane surface to defer.
  • Restored content saturates. Per-pane persisted payload caps at ~2.80 MB: emitting 5× more scrollback (200k→1M lines) changed restored bytes by 72 bytes and footprint not at all. Marginal cost per restored block collapses from 91 KB to 0.83 KB.

All of the per-pane growth is jemalloc heap plus ~0.33 MB of per-pane thread stack. Since this PR defers only the scrollback/conversation payload — the component measured to saturate — its expected benefit is small independently of the ordering defect below. Caveat on the negative result: the measurement ran on an Apple Paravirtual GPU (hardware-backed Metal, but supportsFamily(.apple7)/.metal3 false), and never entered the memory-pressure regime the field reports show, so a real M-series driver behaving differently is not excluded.

This PR's approach: for background tabs (any tab other than the window's initially-active one), defer feeding persisted scrollback blocks and AI conversation history into the terminal view until the tab is first activated, while still starting every tab's terminal session/shell eagerly at launch exactly as before. Keeping the shell eager was deliberate — it avoids the product decision that deferring the whole pane (session included) would raise. All of it is gated behind a new, default-off FeatureFlag::LazyBackgroundTabScrollbackRestore.

CHANGELOG-IMPROVEMENT: Reduced memory usage on startup for sessions with many restored background tabs, by deferring restoration of their scrollback and AI conversation history until the tab is first opened.

Known limitation: this approach cannot be made ordering-correct

Because a background tab's shell stays live while its restoration is deferred, the tab's session can produce genuine PTY output — shell-integration chatter, direnv, a finished background job, SSH noise — without ever starting a command block, before the tab is ever activated. apply_deferred_restored_blocks's safety guard (checking is_bootstrapping_precmd_done() && active_block().started()) does not detect this as a live, non-pristine session, so on activation the deferred (older) restored history is appended after output that already arrived — the tab renders with live content above its entire restored session.

Widening the guard would only convert misordering into more frequent silent dropping of the user's scrollback, which is worse, so that is not a path forward. The two structural alternatives:

  • Materialize on first PTY output instead of first activation, so restoration is always spliced in before live content exists. Not viable as a contained change: local-session PTY bytes are read and applied to the shared TerminalModel from a dedicated background OS thread, independent of and prior to any App-side scheduling. A correct version needs real cross-thread synchronization gating PTY bytes on a hot, per-byte path.
  • True in-order insertion of restored blocks ahead of live content. BlockList's block vector and index map are structured for append-only growth; inserting older content earlier requires reindexing every subsequent block and auditing every cached block index elsewhere in the codebase (selection, scroll position, etc.) — a core-data-structure change, not a local one.

Both exceed a "smallest correct change" PR and are not implemented here. The underlying tension is structural: keeping every shell eager (to sidestep the product decision around deferring whole panes) is exactly what removes any guarantee that restored history stays behind live output, since there is always a window in which live output can reach the append-only block list first. Deferring the whole pane — including its session — has no such window, since there is no live output to interleave with; that option was set aside earlier specifically because it raises the product decision this approach was chosen to avoid. Resolving this is a decision for the feature's owner about which tradeoff to accept, not an implementation gap in this PR.

What this PR implements

  • PaneGroup::restore_pane_leaf skips passing block-list/conversation-restoration data into PaneGroup::create_session for tabs that aren't the window's initially-active tab, stashing the original snapshot plus the deferred payload in a new pending_lazy_terminal_restorations map on PaneGroup.
  • Workspace::add_restored_tab inserts a restored tab without activating it (unlike add_tab_with_pane_layout, which activates on insertion). configure_new_workspace's restore loop uses it for every tab, then activates the window's real active tab once, after the loop completes.
  • PaneGroup::snapshot_for_node returns the original, unmodified snapshot for any pane still pending, so a background tab closed (or the app quit) before ever being activated round-trips its full history intact.
  • Workspace::set_active_tab_index calls PaneGroup::materialize_lazy_tab_restorations on first activation, applying the deferred blocks (BlockList::apply_deferred_restored_blocks) and AI conversation restoration (TerminalView::restore_conversations_on_view_creation). Idempotent.
  • apply_deferred_restored_blocks declines to apply (no-ops) if its (incomplete, see above) pristine check fails, rather than force-finishing and misordering live output.
  • PaneGroup::cleanup_closed_pane and PaneGroup::clean_up_panes (the latter is the real teardown path for a closed tab, reached once UndoCloseStack's grace period expires) both release a never-activated pane's pending payload. remove_pane_for_move's call sites transfer a still-pending payload to the destination pane group instead of dropping it.
  • A background tab's title/subtitle shows a command-derived label sourced from its still-deferred blocks (SerializedBlock::plain_text_command_preview, a best-effort ANSI/OSC-stripping extractor) instead of a generic placeholder while pending. The hint is retained for the pane's lifetime rather than cleared after materialization, so a live block-list match always takes precedence but a real title is never lost if that live scan comes up empty.

Not in scope

Deferring the per-pane terminal view/session construction itself (PTY spawn, terminal model) — which the symbolicated profile above indicates is the larger share of the cost, not a residual — is a product-visible behavior change (background tabs wouldn't start their shell until opened) requiring its own decision; not attempted here.

Deferred AI conversations are temporarily invisible to cross-tab consumers of RestoredAgentConversations between restore and materialization, mirroring the existing lazy pattern for AI-document panes; not exercised on a real build (the verification runner was signed out).

There is currently no way to enable this flag on a real build even if it were safe to (override_enabled is test-only, no production path sets the preference, the Debug-menu toggle doesn't survive relaunch) — moot given the flag must stay off regardless.

Linked Issue

  • The linked issue is labeled ready-to-spec or ready-to-implement.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).

Linear: https://linear.app/warpdotdev/issue/APP-5257/memory-2195-gb-35percent-resident-65percent-compressed-mac-stable
Sentry: https://sentry.io/organizations/warpdotdev/issues/7259255054/

Testing

This is a backend/data-flow change with essentially no UI surface of its own (aside from tab titles). A maintainer with macOS + computer-use access verified the restore/materialize flow directly on a real build across two rounds:

  • Deferral genuinely works through the real restore loop and materializes correctly on activation for the non-interleaved case (full scrollback in order, live output below a "Previous session" divider, no duplication/truncation, all panes' blocks intact at the persistence layer for never-activated tabs). That verification is what surfaced the ordering limitation above.
  • Tab title while pending is fixed and verified on macOS: a background tab shows its real command-derived label, matching the flag-off reference, with no drift over time.
  • Tab title after activation was found to regress to a plain working-directory label in that same verification round. The fix for this (retaining the title hint indefinitely instead of clearing it after materialization) is implemented and covered by an automated test comparing against an eager-restoration reference, but has not been re-verified on macOS — no further macOS build is planned ahead of the shape decision above.

Automated tests (all pass): workspace::view::tests::test_restoring_a_window_defers_background_tab_scrollback_through_the_real_loop (drives the real restore loop), pane_group::tests::test_lazy_background_tab_reports_a_title_while_pending_and_after_materializing, terminal::model::block::serialized_block::tests::plain_text_command_preview_* (6 tests), terminal::model::blocks::tests::test_apply_deferred_restored_blocks_after_bootstrap / ..._is_a_noop_when_given_no_blocks / ..._does_not_corrupt_a_live_running_command, pane_group::tests::test_lazy_background_tab_scrollback_restore, pane_group::tests::test_closing_pending_pane_drops_its_lazy_restoration, pane_group::tests::test_clean_up_panes_drops_pending_lazy_restorations.

Full suites: cargo test -p warp --lib for pane_group:: (124 passed), workspace:: (210 passed), and terminal::model:: (605 passed). ./script/format --check and cargo clippy -p warp --all-targets --tests -- -D warnings clean.

  • I have manually tested my changes locally with ./script/run

Screenshots / Videos

Not applicable — no UI surface change of its own; see the macOS verification notes above.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

…PP-5257)

Root cause (per the linear-attached heap-profile analysis, Sentry issue
7259255054): on macOS startup, restoring a window's tabs eagerly fed every
tab's persisted scrollback blocks and AI conversation history into its
terminal view, even for tabs the window doesn't display at launch. The
text-layout/rendering work this triggers dominates the reported heap growth
(39.8%/3.83 GB of the profiled sample).

This change defers that specific work for any tab that isn't the window's
initially-active tab, behind a new `FeatureFlag::LazyBackgroundTabScrollbackRestore`
(default off): the terminal session/shell for every tab still starts eagerly
as before (no product-visible timing change to shell startup), but the
persisted blocks and AI conversation restoration are stashed and only applied
to the live terminal view when the tab is first activated.

- `PaneGroup::restore_pane_leaf` skips feeding block/conversation data into
  `create_session` for non-active tabs when the flag is on, and stashes the
  original snapshot plus deferred payload in a new
  `pending_lazy_terminal_restorations` map.
- `PaneGroup::snapshot_for_node` echoes back the original, unmodified snapshot
  for any pane still pending, so a tab closed or persisted before ever being
  activated round-trips its history intact.
- `Workspace::set_active_tab_index` calls the new
  `PaneGroup::materialize_lazy_tab_restorations`, which applies the deferred
  blocks (via a new `BlockList::apply_deferred_restored_blocks`, modeled on
  the existing `append_followup_shared_session_scrollback`) and conversation
  restoration (via `TerminalView::restore_conversations_on_view_creation`,
  visibility bumped to `pub(crate)`) the first time a tab is shown.

CHANGELOG-IMPROVEMENT: Reduced memory usage on startup for sessions with many restored background tabs, by deferring restoration of their scrollback and AI conversation history until the tab is first opened.

Co-Authored-By: Warp <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 15, 2026
@warp-agent-staging warp-agent-staging Bot added factory:wilson area:performance:memory Memory usage, allocation, leaks, and memory-bound performance. labels Aug 15, 2026
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

View run View conversation

…ments

Addresses an independent review of the initial implementation:

1. The deferral never actually happened: configure_new_workspace's restore
   loop called add_tab_with_pane_layout_and_active_hint, which activated
   every tab as it was inserted (activate_tab_internal), immediately
   draining each tab's pending_lazy_terminal_restorations. Added
   Workspace::add_restored_tab, a non-activating insertion path used only
   by the restore loop; the intended active tab is activated once, after
   the whole loop completes. Added an end-to-end regression test that
   drives the real configure_new_workspace loop (not just PaneGroup in
   isolation) and asserts a background tab's restoration is still pending
   after restore.

2. Materialization could truncate a live command: a background tab's shell
   stays live while deferred, so it can produce genuine content (e.g. via
   Workspace::process_sync_event_for_all_synced_pane_groups mirroring
   synced input) before activation. BlockList::apply_deferred_restored_blocks
   now checks whether the session is still pristine (bootstrapped but no
   real post-bootstrap command has started) before finishing the active
   block, and drops the deferred restoration rather than force-finish and
   misorder live output. Returns whether it actually applied. Added a
   regression test covering a live, still-running command.

3. Pending entries were never cleaned up on close or move:
   PaneGroup::cleanup_closed_pane now drops the pending entry for a
   permanently-closed pane. Added
   take_pending_lazy_terminal_restoration/insert_pending_lazy_terminal_restoration
   so remove_pane_for_move's three call sites (child-agent re-adoption,
   tab-bar drop, cross-tab pane move) transfer a still-pending payload to
   the destination pane group instead of silently dropping it. Added a
   lifecycle test for the close path.

4. Trimmed doc comments that narrated mechanics or named callers
   (restore_pane_leaf, restore_pane_tree, new_with_panes_layout,
   add_restored_tab, the pending_lazy_terminal_restorations field) down to
   why-only rationale, per AGENTS.md.

5. Moved the CHANGELOG-IMPROVEMENT line into the PR body as well.

Validation: cargo test -p warp --lib for pane_group::, workspace::, and
terminal::model::blocks:: all pass (122 + 210 + 67 tests). ./script/format
and cargo clippy -p warp --all-targets --tests -- -D warnings are clean.

Co-Authored-By: Warp <agent@warp.dev>

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Overview

Defers restored scrollback and AI-conversation restoration for background tabs at startup behind a default-off flag, targeting the 4.29 GB the APP-5257 heap profile attributes to PaneGroup::restore_pane_tree. Two rounds of findings are already fixed in df5d53d; what remains needs a human decision, so this is neither an approval nor a rejection.

Concerns

  • Needs verification on a Mac before the flag is enabled anywhere, including dogfood. This changes when a user's terminal history and agent conversations appear, and no one in the factory can run the macOS app. Please exercise four cases: launch with many restored tabs, first activation of a tab whose shell already produced output, quit before ever activating a restored tab, and closing or moving a tab that was never activated.
  • Deferred conversations are invisible to cross-tab consumers until their tab is opened. They are taken out of RestoredAgentConversations during restore and only enter BlocklistAIHistoryModel at materialization, so conversation navigation and listing cannot see a background tab's restored conversations beforehand. Confirm that temporary invisibility is acceptable, or the restoration should keep lightweight conversation metadata registered up front.
  • The follow-up this PR deliberately leaves open. Only the scrollback and conversation payload is deferred; the per-pane terminal view and session are still constructed eagerly for every restored tab, which is the remaining part of the profile's 4.29 GB. Closing that gap means dormant tabs whose shell does not start until first click — a user-visible change worth deciding on its own.

Verdict

Checks: build pass, tests pass, CI required jobs skipped while draft, visual proof missing (needs macOS)

Found: 0 critical, 0 important, 0 suggestions, 0 nits, 3 questions

Responding as wilson: Open session · View factory task

// `started()` (e.g. the script-execution stage), which is expected
// and harmless to finish here. Only bail once bootstrapping is done
// and a real post-bootstrap command has genuinely started.
if self.is_bootstrapping_precmd_done() && self.active_block().started() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When this guard fires the restored scrollback is silently dropped from the live view rather than spliced in, because BlockList.blocks is append-only and cannot interleave history ahead of live content without a reindexing change. Dropping beats corrupting a running command, and the persisted blocks on disk are untouched, but the user sees a restored tab that is missing its history with only a Sentry report to show for it. Worth confirming this is the tradeoff you want, and worth watching that report_error! after rollout to learn how often it actually fires.

Responding as wilson: Open session · View factory task

warp-agent-staging Bot and others added 2 commits August 17, 2026 23:15
macOS verification of the previous revision confirmed the deferral itself
now works correctly (background tabs' restoration stays pending until
activation, materializes cleanly on click), but found a genuine
flag-ON-only regression: a pending background tab's title/subtitle showed
a generic "New session" placeholder instead of a command-derived label,
and the real title never took over even after activation.

Root cause: `TerminalView::last_completed_command_text()` (used by the
vertical-tabs title/subtitle rendering) scans the pane's live block list
for the last completed command. For a deferred background tab that list is
empty until materialization, so it fell through to a hardcoded "New
session" placeholder. And `materialize_lazy_tab_restorations` never
emitted any event after applying the deferred blocks, so nothing told the
workspace to re-render the tab bar with the now-current state.

Fixes both, without touching TabData's optionality or the tab-bar render
path:
- Added `SerializedBlock::plain_text_command_preview`, a best-effort
  ANSI/OSC-stripping text extractor (with unit tests covering plain
  commands, CSI color codes, and an OSC title escape sequence). This is
  intentionally not the full ANSI-aware parsing `restore_block` performs;
  it only needs to produce a short, readable title string.
- `PaneGroup::restore_pane_leaf` computes this preview once from the
  deferred blocks and stores it on the new `TerminalView`, via a new
  `pending_restoration_title_hint` field and setter.
- `TerminalView::last_completed_command_text` falls back to that hint when
  the live block list has no match yet, so a pending tab shows a real
  command-derived title/subtitle instead of the generic placeholder.
- `materialize_lazy_tab_restorations` clears the hint once the real blocks
  are applied, and now emits `Event::TerminalViewStateChanged` /
  `Event::AppStateChanged` (the same events every other state-changing
  method on `PaneGroup` emits) so the workspace actually re-renders the
  tab with the fresh, real title.

Added coverage for both the pending-tab title and the post-materialization
title recovery in a single test, plus dedicated tests for the new ANSI
preview stripper.

Validation: cargo test -p warp --lib for pane_group:: (123), terminal::model::
(605), and workspace:: (210) all pass. ./script/format and
cargo clippy -p warp --all-targets --tests -- -D warnings are clean.

Co-Authored-By: Warp <agent@warp.dev>
Two scoped fixes from review, orthogonal to the structural scrollback-
ordering issue tracked separately (see PR description):

- materialize_lazy_tab_restorations unconditionally cleared
  pending_restoration_title_hint, even when apply_deferred_restored_blocks
  declined to apply (the "session no longer pristine" guard). On that path
  the tab lost both its restored scrollback and its title fallback, with
  no route back to a usable label. The hint is now only cleared once
  has_restored_command_blocks confirms the blocks actually landed.

- cleanup_closed_pane, added in the previous revision, is not on the path
  a real tab close takes: closing a tab goes through
  Workspace::remove_tab -> UndoCloseStack::handle_tab_closed, and the
  pane group is only permanently discarded later when the undo grace
  period expires, via ClosedItem::discard -> PaneGroup::clean_up_panes.
  That method never touched pending_lazy_terminal_restorations, so a
  tab closed (and eventually permanently discarded) before its background
  panes were ever activated retained their pending payloads until the
  whole PaneGroup was dropped. clean_up_panes now clears the map
  explicitly, matching cleanup_closed_pane's per-pane undo path.

Added test_clean_up_panes_drops_pending_lazy_restorations covering the
real teardown path, and removed a pane-group-level test that depended on
the pristine guard reliably firing against a real TerminalModel/PTY-backed
session; it does not (see PR description), so the earlier test was not a
trustworthy regression check.

Validation: cargo test -p warp --lib pane_group:: (124 passed) and
workspace:: (210 passed). ./script/format and
cargo clippy -p warp --all-targets --tests -- -D warnings clean.

Co-Authored-By: Warp <agent@warp.dev>
@warp-agent-staging

warp-agent-staging Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Verified on real macOS — result: failed

Built and ran this branch on a Warp-hosted macOS 26.3.1 aarch64 runner. Scenarios on df5d53d, title re-verification on a6b8ac6. 54c1eac and 082928d were never built or run — three commits sit between the verified state and the current head, so treat the fixes in them as test-covered but without runtime evidence.

Blocking: restored history renders below newer live output. With live output on a background pty before first activation, the live line lands as the first block, above the entire restored history — screenshot · video. The non-deferred tab in the same run ordered correctly. All three materializations logged applied_blocks=true; Dropping deferred scrollback restoration occurred 0 times, so the pristine guard never engaged — a pty write does not start a command block. Established on df5d53d and not retested since.

Titles: half fixed as of a6b8ac6. Pending-state labels match flag-OFF position-by-position across 7 tabs, escape sequences included (off · on). But clicking a background tab degrades the label to the cwd and it stays there; the flag-OFF control leaves every label unchanged after clicking all of them, so this is flag-caused. 082928d targets it by no longer clearing the title hint, but that is unverified at runtime.

What held up. Deferral is real: flag-ON logged defer=true for every background tab and materialize only at click time, minutes after restore; flag-OFF logged no deferrals. Quitting without opening a restored tab lost nothing (DB intact, 5 panes × 2 blocks). Closing a never-activated tab left the others whole.

Unverified, stated as gaps rather than passes: the AI-conversation half of the deferral (runner cannot sign in; every line read has_conv=false), the drop branch of apply_deferred_restored_blocks and the tab title in that state, the pane-move payload transfer (drag does not register, no detach action), and memory (5 tabs / ~4.5 KB is orders of magnitude short of the ~4.3 GB in the profile). The tested binaries carried five uncommitted log::info! lines plus an env-var flag override, so they were not byte-for-byte any pushed commit.

Responding as wilson: Open session · View factory task

macOS verification showed the tab-title fix regressed at the opposite
point: the label was correct while a background tab was pending, but
degraded to a plain cwd label right after activation. Logs showed blocks
applied and the hint cleared as designed, but the live block-list scan
that was supposed to take over produced nothing on the real (PTY-backed)
session that materialization exercises.

Rather than chase why the live scan misses immediately after
materialization on a real session, stop discarding the value that is
known to work: pending_restoration_title_hint is no longer cleared once
blocks land. It's one small string per restored pane, and it only ever
acts as a fallback behind a live-scan match in
TerminalView::last_completed_command_text, so retaining it for the pane's
lifetime is cheap and can't override a real title once the live scan
succeeds.

Strengthened test_lazy_background_tab_reports_a_title_while_pending_and_after_materializing
to assert equality against a genuine eager-restoration reference for the
same block, rather than just checking for a fixed string — the previous
version of this test passed against a value the deferred path had cached
from before materialization, without confirming it matches what real,
non-deferred restoration would show.

Validation: cargo test -p warp --lib pane_group:: (124 passed) and
workspace:: (210 passed). ./script/format and
cargo clippy -p warp --all-targets --tests -- -D warnings clean.

Co-Authored-By: Warp <agent@warp.dev>
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

Measured on macOS: the restore path costs ~7.8 MB per pane, and graphics allocation is flat

We built stock master (04a7f83) on macOS 26.3.1 with stable's allocator and profile (release + jemalloc — a default ./script/run build would have measured system libmalloc and answered a different question) and measured startup footprint against restored pane count. 40+ vmmap --summary samples.

Pane count is the only linear driver — 7.79 MB/pane, intercept 166.5 MB, linear across a 32× range with no bend (means: 177.9 / 197.6 / 221.0 / 293.6 / 358.4 / 412.7 MB at 1 / 4 / 8 / 16 / 24 / 32 panes). Launch-to-launch variance is ~13 MB, about 1.7 panes' worth.

Graphics allocation does not scale with panes. IOSurface is byte-identical at 40.7 MB with exactly 6 regions at every pane count from 1 to 32; IOAccelerator, CoreAnimation, CoreGraphics, IOKit likewise identical. Graphics total 48.4 MB at 1 pane, 48.2 MB at 32. Surfaces are per-window, not per-pane. Minimizing the window released nothing graphics-side either.

Restored content saturates. Per-pane persisted payload caps at ~2.80 MB — emitting 5× more scrollback (200k→1M lines/pane) moved restored bytes by 72 bytes and footprint not at all. Marginal cost per restored block collapses 91 KB → 13.8 KB → 0.83 KB. All per-pane growth is jemalloc heap (Memory Tag 254, an inference we could not verify against the tag constant) plus ~0.33 MB of per-pane thread stack.

This does not reach the reported footprints. At 7.8 MB/pane, the 4 GB attributed to restore needs ~510 panes and 11.6 GB needs ~1480. Nothing we could vary — pane count, scrollback volume, block count — gets within two orders of magnitude of the field reports.

A hypothesis for the gap, offered as such: in these profiles restore_pane_tree is the deepest resolved app frame sitting above a large mass of unresolved AppKit/CoreGraphics/Metal addresses, and the one fully-symbolicated profile resolved that region to AppContext::build_scene / render_views / Presenter::invalidate — general rendering entry points used app-wide, not restore-specific. Allocations from unrelated paths sharing those frames could aggregate under restore. That would explain why the measured restore cost is bounded while the profile attribution is not.

Bounds, stated plainly: Apple Paravirtual GPU (hardware-backed Metal, but supportsFamily(.apple7)/.metal3 false), so a real M-series driver allocating per-pane is not excluded; every sample had zero swapped bytes while the field footprints are largely compressed, so the pressure regime was never reached; and the scrollback generator produced one large block per pane rather than many real command blocks (the block-count sweep covers that case with synthetic DB rows).

Evidence: restored 16-pane session · second restored pane · minimized-state sample · video: scrolling, pane switch, minimize

Responding as wilson: Open session · View factory task

@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

Superseded by #15833, which bounds what AI conversation restoration materializes instead of deferring it. Deferral turned out not to be the answer here: the active tab is never deferred, so it materialized the full unbounded payload at startup regardless, and a deferred background tab materialized the identical payload later. That also makes the ordering defect documented above moot in the new approach, since nothing is deferred.

Leaving this open rather than closing it — that call is the reviewer's. The measurement and findings recorded here stand on their own and are worth keeping.

Responding as wilson: Open session · View factory task

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:performance:memory Memory usage, allocation, leaks, and memory-bound performance. cla-signed factory:wilson

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants