Release swapchain buffers on bg tabs - #147
Merged
Merged
Conversation
series.md cites `scripts/research/41/ten-tab-footprint.py` as the instrument behind row S1, so the script has to live in the repo with the doc that reads its output. It stages one optimized dev slot the way termwars' memory harness does -- seeded font, ten inert tabs, calibrated geometry, every pane read back at the benchmark grid -- then settles and samples `footprint` over the slot's pid set, and prints one JSON document. Staging comes from termwars' adapter rather than a second implementation, so a grid read-back mismatch fails here the way it fails there. This is the number to read while changing DanTerm, not a claim: one slot, one arm, no interleaved control. A claim stays a paired harness run recorded in the series. The script stages gate and done files under `/.run` for a slot that is already gone when it exits, so `.gitignore` covers that directory. Claude-Session: https://claude.ai/code/session_01Qa8frwM3hrN87APj1EttDz
The tier-1 script grows a `--hold` flag: it prints its document with the measured pids as soon as the samples are in, then keeps the slot up until stdin gives a line or SIGINT arrives. That is what a per-class capture needs -- the same staged process the samples came from, still alive. `vmmap` and `footprint` on the held slot at this revision read 31 `IOSurface` regions holding 607,649,792 bytes, all dirty. Thirty of them are `2720x1860` BGRA, one per buffer of ten depth-3 pane swapchains, and ten of those are shared with the WindowServer. F2 predicted 607,104,000 by arithmetic; the 0.09% difference is 16 KB page rounding. The largest class left is `MALLOC_SMALL` at 25 MB of a 36,603,176-byte remainder. H1 is confirmed by allocation class. F4 records the capture, S2 records the reading, and the ledger closes T2. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Research 41 has to attribute the app's IOSurface footprint to the panes that own it, and the objects that own the surfaces were the only ones that could answer without a counter that can drift. A store now reports the kernel's own allocated size for its surface -- the page-rounded figure a vmmap IOSurface line sums (41/F4), not the tight bytesPerRow product -- and a swapchain builds its census by asking each buffer it holds, so a chain that failed to build every buffer it asked for cannot report the size it wanted. `holds` goes with it: the owner of a replaced rotation still retains the frame on screen, and it needs to tell a store the live rotation holds from one it has outlived. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Research 41 needs the app to say what its panes' buffers cost, beside the process footprint it samples, and with zero distinguishable from unmeasured (41/D1). The answer is derived when the query is answered: the runtime walks the installed panes, each pane's view asks its own rotation, and nothing increments or decrements anywhere, so the number cannot drift from what the process holds. A pane whose session has no presentation to measure is named in `panes.unmeasured` and reads as `"swapchain":"unmeasured"` -- a string, distinct from the `null` that means a pane holding no buffers -- and is left out of every sum and of the visible/hidden split. Every aggregate carries its count. The census also counts a displayed frame the live rotation no longer holds: a replaced rotation leaves its predecessor's store retained until the successor presents, and a walk of rotations alone would miss exactly that surface. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…tion The ten-tab document now carries a `surfaces` block, read with `danterm surfaces` from the sampled process before the slot stops, so every tier-1 reading arrives with the app's own attribution beside its total. A read that failed is recorded as unmeasured rather than as a zero. `S3` is the first row whose attribution comes from the app: 607,518,720 bytes over 30 stores in 10 chains, 9 of 10 panes hidden. That is exactly `F4`'s 30-region pane line; the 131,072 bytes between it and `F4`'s IOSurface class total are that capture's one CoreUI image surface, which no pane owns. `D4` records why the number is derived from the live panes and not counted, and what the rejected registry would and would not have caught. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Research 41's T3 needs what a tab switch costs before Phase 3 makes a hidden pane's swapchain something reveal has to rebuild, and no instrument here could answer it. The frame-rate log sums a second of publishes, renders and layer displays; a reveal happens once, inside one of those windows, against a frame arrival the window has already averaged away. `DANTERM_PRESENTATION_EVENT_LOG` now names a file that receives one JSON line per pane presentation moment -- the view's creation, each visibility transition, and every frame that reaches the layer -- each with the monotonic timestamp it happened at. A reader pairs a `reveal` with the next `attach` on the same pane and subtracts, which is the whole latency question in the app's own clock, with no screen capture and no cross-process clock in the path. Only a real transition is traced: a redundant push of the visibility a pane already has writes nothing, so a switch with presentation work to do stays distinguishable from one without. The line is written where it happens rather than buffered, because a flush would otherwise land inside the interval being measured. `scripts/research/41/tab-switch-latency.py` drives the three cases on the ten-tab staging and prints one JSON document. The launcher's passthrough allowlist gains the variable; the script forwards it through a shim, because termwars' adapter takes no launch variables of its own. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Research 41's T3 found that revealing a hidden tab at this revision presents nothing at all: the pane's last frame is still attached to its layer, the pane is idle, and no frame follows the reveal. So the reveal case has no latency to report, and the cost Phase 3 would add -- a fresh depth-3 swapchain and a full render on every reveal -- had no number. The trace gains a `rebuild` event, recorded when a pane gives up a rotation it actually held, so the `attach` that follows can be paired with it. That prices allocate-plus-full-render where the app already forces it today, on a theme change, and `tab-switch-latency.py` reads it as a fourth case beside the three switches a user can make. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
The control Phase 3 has to be judged against. Twelve reveals of nine hidden tabs at `2c544f84` recorded twelve `reveal` events and zero frames: a hidden pane's last frame never leaves its layer, the pane is idle, so revealing it costs the reconcile and the visibility push and no pixels at all. There is no reveal-to-frame latency here because there is no frame. So the number that matters is what a reveal would cost once it has to build buffers first, and F5 prices it where the app already forces that work: 16.59 ms median (n=12) to allocate a fresh depth-3 swapchain and render all 60 rows at 2720x1860, with a 43 ms tail. A cold first presentation is 18.90 ms (n=12). Both round trips of the request are dominated by spawning the CLI, and the warm reselect -- which can do no reveal work by construction -- is the slower of the two, so nothing the reveal does shows above that noise. That makes the direction gate concrete. Visible-lifetime release has nothing to show until it has rebuilt, so its reveal is the 16.59 ms case at best; the frozen-surface and purgeable-volatile shapes can present immediately and could beat it. D2 stays open, with one of its four findings now in hand. S4 records the footprint at the same commit in the same session, so the row carries both halves of the trade. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
A throwaway build marks a hidden pane's surfaces purgeable-volatile after detaching the layer contents, and marks them non-volatile and renders again on reveal. Measured, then reverted; the diff is saved under readings/ so it can be reapplied. Both of T6's questions are answered. Volatile IOSurface pages leave phys_footprint at once on this macOS, with no memory pressure: 645,039,424 bytes on the clean tree against 98,501,952 on the throwaway, taken in one session, and vmmap reads 27 of the 30 pane surfaces PURGE=V with 0K dirty. The render server had not released the surface by the time it went volatile: 44 of 44 hides marked one still-in-use buffer, after a committed and flushed detaching transaction. The reveal is the other half. It presents a real frame in 1.37 ms (n=12), against F5's no frame at all, and against a 6.32 ms from-scratch rebuild in the same run. So this shape beats visible-lifetime release on memory and on latency at once, and beats one frozen surface per hidden pane on memory. The seam it leaves is the in-use surface. A discard under real pressure would hand undefined pixels to whatever composites the hidden window. The ideal shape marks only the surfaces reported free; what that costs of the saving is unmeasured, and it is what D2 still owes. F8 records the API cited from the SDK headers, the readings table with its control, the PURGE census, the isInUse counts, and the reveal latency. Series rows S5 and S6. T6 -> VETTING. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
A new `TerminalFrameBackingStore` cleared its whole IOSurface, which made every page of every buffer resident at creation. With ten idle tabs that is 405 MB of memory holding pixels nothing ever displayed: each pane owns a depth-3 swapchain and an idle pane renders into one of the three. The clear guaranteed nothing. A full render fills the whole frame rect in `.copy` blend mode, and the surface is the grid exactly, so it decides every pixel. The incremental path runs only on a buffer that already holds a complete frame, and a store reaches a layer only after a render. The zeroing is inherited from the malloc-backed store, where it was what made raw allocated memory initialized before CoreGraphics wrote it. The gate that replaces it is behavioral: poison the surface memory, render full, and compare every pixel against a direct render. It fails if a full render ever stops covering the surface, which is the one thing dropping the clear depends on. The ten-tab empty arm falls from 645,301,568 to 240,764,008 bytes (research/41 S5, S6). The saving is idle-only: a pane that renders three frames takes 39 MB of it back (research/41 F7). Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
F7 records the pair: 645,301,568 bytes with the eager surface clear and 240,764,008 without it, one session, control first, same commit and same staging (S5, S6). The 404,537,560-byte difference is the twenty buffers ten idle panes never render into, to within 0.1%. The finding also writes down what the clear guaranteed -- nothing any code path asks for -- and proves the saving is idle-only, the way the investigation rules require: three staged panes given twelve echoed lines each took 117 MB of it back, 39 MB per pane, which is two surfaces each. The surface census reads the same mapped bytes in both arms, which is what D4 said would happen once residency and mapping diverge. The ledger moves T5 to VETTING. It decides nothing about H1: the term returns as soon as the tabs are used, so T7 still has to choose a lifetime. F2's scratch note was never committed, so its citation carries the documented allow-missing marker and docs-lint passes again. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
T5 and T6 ran in parallel worktrees and both claimed series rows S5/S6 and the control reading's filename. T6 keeps S5/S6; T5's control and throwaway become S7/S8, its control reading moves to a t5-control filename, and the ledger keeps one VETTING entry per task. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Hidden panes become detached and untrusted. Commit 1 of T8 is the ideal (visible-lifetime release, no presentation while hidden); commit 2 adds the purgeable-volatile fast path for buffers the render server reports free, so a reveal with intact pages costs 1.37 ms instead of a rebuild. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
Hiding a pane detaches its frame in a committed and flushed transaction, forgets the store that held it, and gives up the swapchain. While hidden the pane presents nothing: the single render path returns at once, so a theme swap, a font change, a backing-scale move, a window color-space change, a published frame, and an armed presentation retry all leave the pane with no buffers. The reveal reconciles the geometry, then renders exactly once -- and only when nothing else put a frame back, so a pane with hidden-time output pays one render, not two. This is research/41 D2's ideal, visible-lifetime release, landed on its own. At the ten-tab staging D2 measures, twenty-seven of thirty pane surfaces belong to hidden panes, and this is the structure in which a hidden pane cannot hold any of them. What it costs is the reveal, which becomes a from-scratch rebuild; D2's second commit adds the volatile fast path and T9 reports both numbers. The reveal defers its render out of synchronizePresentation for one reason: a grid submitted on reveal republishes through the controller, and rendering the old plan first would build a rotation that republish immediately replaces -- two rebuilds for one tab switch. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
… commit 2) The swapchain gains the two entry points that own purgeability. `releasePixels` marks every buffer the render server reports free purgeable-volatile and says how many it still holds; `reclaimPixels` takes the pages back and reports what the kernel did with them. Between the two the rotation refuses to write any buffer at all, so a released buffer cannot be rendered into whoever asks. Both the in-use report and the purgeability call are injected the way the in-use report already was, so the headless suite reaches every outcome -- including the discarded one, which needs real memory pressure on a machine. The hide now keeps the rotation and releases its pixels instead of throwing it away, which is what makes a reveal 1.37 ms rather than a from-scratch rebuild (research/41 F8). The buffer that was attached is still in use after a committed and flushed detach -- 44 hides of 44 in F8 -- and neither IOSurface nor QuartzCore declares a notification for the use count changing, so the pane arms a bounded per-refresh retry that asks again. The retry is inert on teardown, on a reveal, and on a rotation replaced while hidden. A reveal that finds any buffer discarded or unrestorable replaces the rotation, which is T25's one answer to a trust break. A surface goes volatile only while it is detached and reported free, which is the same premise the swapchain already writes under. Pin four in IOSurfaceLayerContentsTests observes that premise on a real compositor: a hidden layer's detached surface frees while a sibling presents, goes volatile while free, is never re-acquired, and comes back with its pages and its bytes intact. `danterm surfaces` now reports the buffers by purgeability with a non-volatile-bytes aggregate. Mapped bytes do not move when a pane hides, so without that field the census could not be reconciled to a footprint reading at all. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…h is a wash The ten-tab idle baseline is 56,902,928 bytes with both commits and 56,935,312 with commit 1 alone, taken in one session on one machine: 91% below S4's 644,777,256, and the two arms 0.06% apart. A reveal presents a real frame in 4.76 ms and 4.61 ms respectively, where F5 measured no frame at all. Two of D2's open uncertainties are answered. The render server does free a hidden layer's ex-attached surface -- 1 of 3 buffers is still in use at every hide, exactly as F8 measured, and the bounded retry finds it free after 1 to 3 ticks, so the residual is 0 surfaces rather than the 0 to 182 MB D2 allowed for. And the volatile fast path bought no measurable latency over the ideal, because T5 made the rebuild the ideal pays cost 4.6 ms rather than F5's 16.59 ms. D2 wrote down what that answer means -- dropping commit 2 leaves the ideal in the tree -- and T9 is where it is decided, beside the tier-2 pair and the memory_pressure discard reading neither commit could take. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…h/41 T8, commit 2)" This reverts commit 471e8c0. The fast path did not earn its structure. Measured in one session against commit 1 alone (research/41 F9), it took 32,384 bytes of a 56.9 MB idle footprint -- 0.06%, inside the spread of either arm -- and a reveal cost 4.76 ms with the pages kept against 4.61 ms with the rotation rebuilt from scratch. Its premise was F8's 4.6x advantage, and that premise had already expired: F8 compared a volatile reveal against a 16.59 ms rebuild, and T5 had since removed the eager clear, so the rebuild the ideal actually pays in this tree is 4.6 ms. There is no advantage left to buy. So the price is all that remains: a purgeability state per buffer, a bounded per-refresh retry, a three-way reclaim outcome, and five census fields on a CLI contract. D2 pre-committed to this outcome in as many words -- if commit 2's state does not earn its latency, dropping it leaves the ideal in the tree. Commit 1 is that ideal and it stays: a hidden pane owns no pixels, and every trust break while hidden is answered by replacement on reveal. Pin four in IOSurfaceLayerContentsTests keeps the half that is still load bearing. That a surface detached from a layer which then presents nothing still frees while a sibling presents is commit 1's residual term, not the fast path's: it is why a hidden pane's last buffer leaves this process at all. The purgeable half of the pin goes with the code it gated. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
F9 measured T8's two commits against each other in one session, and the second one did not earn itself: 32,384 bytes of a 56.9 MB idle footprint, which is 0.06% and inside both spreads, and a reveal of 4.76 ms against the ideal's 4.61 ms, which is the same number. D5 records why the premise expired rather than the measurement being wrong. F8 priced the fast path against a 16.59 ms rebuild; T5 then removed the eager surface clear, so the rebuild the ideal actually pays in this tree costs 4.6 ms and there is no advantage left to buy. D2 pre-committed to exactly this: if commit 2's state does not earn its latency, dropping it leaves the ideal in the tree. D5 also writes down what was given up -- the discard path stays unmeasured, and the ideal's price is a rebuild that a slower machine or a much larger grid could make expensive again -- and what would reopen it: a reveal-to-frame median above 8 ms on the tier-1 staging. That is about one refresh at 120 Hz, so it is where a person can see the delay, and it is twice what is recoverable today, since F8's 1.37 ms floor leaves at most 3.3 ms in F9's 4.6 ms median. D2 keeps its text as the reasoning at the gate, with a line at the top pointing here. F9 keeps its numbers with a line saying they were acted on. S9 and S10 keep their rows; the commit-2 row names the revert. T9 now has one arm to read instead of two. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…dule name termwars' DanTerm adapter now resolves its slot launcher per instance, so one run can build two checkouts. Assigning the old module constant stopped doing anything, and this script's trace never reached the slot: every case came back `unmeasured` with zero events. The instrument said so rather than reporting a number, which is why it was caught, but the reading was lost. The adapter exposes `launcher` publicly for this substitution, so the shim is set on the adapter the script staged. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…floor One harness run built four checkouts and interleaved their reps, so the pair, its A/A control and an intermediate revision share a session, a display, a scale, a font and a grid by construction. That is what the doc's own rule on contemporaneous comparison asks for, and it is the first time this doc has had it. `T2b` gives the series its noise floor: the same commit from two checkouts differs by 81,896 bytes (0.145%) on the empty arm and 999,424 (0.366%) on the scrollback arm. `T9` reads the claim against that floor. The empty arm goes 644,089,152 to 56,525,712 (-91.22%) and the scrollback arm 821,396,896 to 272,974,880 (-66.77%) -- 7,175 and 549 times the floor. `T5` is 62% of the empty-arm delta and `T8` commit 1 is 38%. A reveal presents a real frame in 5.39 ms where it presented none at all, which is under `D5`'s 8 ms reopening bar, so the fast path stays out. Two method results ride along. Tier 1 and tier 2 agree to within 0.99% at the same commit, as `D3` asserts. And the tier-1 script reading one build twice in one session differs by 3.2x the tier-2 A/A floor, which is why `D3` puts a claim at tier 2. All 24 trials report one pid and an empty `missingPids` on every sample, and the 56 samples inside a trial are byte-identical, so a row's spread is the span of its three rep medians. The doc stays open: `T4`'s scrollback census and `T10`'s remainder are untouched, and the discard path went with the code `D5` reverted. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
… 57.7 MB `H1`'s 607 MB term is gone, so the remainder is most of the footprint. `T10` reads it on a held slot at `0dc62749` with `vmmap`, `footprint`, and `heap`, and it is not the undifferentiated mix `H4` predicted. `MALLOC_SMALL` is 25 MB (45.4%) and `IOSurface` is 20,381,696 bytes (35.3%). Nothing else in the process reaches 4.6%. The surface class is the visible pane's three stores: 60,751,872 mapped and one resident, the 20,250,624-byte buffer the render server is displaying. The other two read `0K`, which is `T5`'s removed clear at the region level. `heap` splits the malloc term: 18,057,352 live bytes in 105,478 nodes over six zones, plus 33% zone fragmentation. No class is over 6.4%. The largest per-pane class is the grid's cell storage, 368,640 bytes per pane, and all ten panes' terminal state together is 18% of what the one displayed buffer costs. So `H4` is rejected as written and its reading survives for a different reason: both dominant classes are irreducible. `H5` says the resident surface is the floor of showing a pane, `H6` says the heap is a long tail with no owner worth a plan, and `H7` says a third of `MALLOC_SMALL` is allocator slack. `T12`, `T13`, and `T14` are their gates. Docs and readings only. Finding `F12`, row `S21`. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…6 MB per pane Doc 15's census probe is headless only: no `danterm` query reads `Terminal.memoryCensus` from a live pane. So the census ran on the termwars scrollback arm's own feed reproduced at 170x60 -- 10,000 lines, each exactly 170 columns -- and the process reading was taken beside it on a held slot with its own empty-arm control at the same commit and session. A pane holding that history charges 13,855,944 bytes: 13,715,424 of arena, a 140,496-byte index at 1.01%, 24 bytes of side table, and no style table at all. That is 82.59% of the 16 MiB budget, 8.116 arena bytes per retained cell against a 16-byte live stride, and no heap allocation per retained row. Both terms H3's competing explanation named are absent, so H3 is confirmed and the doc leaves the line alone. The census also corrects the headline. Ten of those panes plus the allocator's rounding on them are 147,556,320 bytes, 63% of the 233 MB arm delta this session measured and 68% of F10's tier-2 216,449,168. The rest is 2.16 MB per pane of app-side heap, 1.78 MB per pane of allocator slack, the visible pane's two IOSurfaces faulting in (40.6 MB, which is F10's 39 MB read by class), and 5.5 MB of CoreServices only the writing arm has. The class sum closes to 0.43%. F11 is the finding; the ledger pointed T4 at F6, which was never written and cannot be now, so the pointer moves. S21 and S22 are the rows, and T11 opens on the app-side term with a falsification gate: give `danterm` a per-pane census so the residual becomes a measurement. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
T4 and T10 ran in parallel worktrees and both claimed series row S21. T10 keeps S21; T4's scrollback reading and its control become S22 and S23, the ledger reads T10, T11, T12-T14 in order, and the Outcome carries both the remainder census and the scrollback verdict with one open-items list. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
`danterm surfaces` is an instrument, not a control: it reports the app's internal state for an investigation, and nothing that drives DanTerm needs it. A bare top-level verb put it next to `ls`, `focus`, and `roster`, which callers do drive the app with. The new `debug` verb holds diagnostic reads. `danterm surfaces` becomes `danterm debug surfaces`, and the wire method, request case, and runtime command follow it: `debug.surfaces` / `.debugSurfaces` / `.readDebugSurfaces`. Nothing outside this branch called the old spelling, so it is gone rather than aliased. T11's per-pane terminal census will join the namespace as `debug memory`. The catalog entry sits beside `doctor` in help order, with the other diagnostics. The research/41 scripts and prose, and the CLI skill, name the shipped spelling; the raw readings keep the spelling they were taken with. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
… labels fixed H5 is closed rather than left open. T4's own capture answers it: the scrollback arm reads 58.1M dirty over 5 IOSurface regions against the empty arm's 19.4M at the same commit in the same session, and the region listing names all three of the visible pane's stores fully dirty. So the resident term is one buffer at idle and the swapchain depth under output, and H5 is rejected as written. D6 and D7 record two decisions the doc had made but never wrote down: the eager surface clear comes out (on F7), and the scrollback line is at its budget floor and this doc leaves it (on F11 and H3). Both carry the alternatives their finding weighed and a reopening condition. T5, T4, F7 and F11 point at them. The rest is correction. F11 cited S23 where the scrollback row is S22. T5 and T6 were ticked but statused VETTING; both are done. T0 still owed two consecutive HEAD runs that S19 and S20 took. F5's table said "four cases" over five rows, so the cases are numbered. The index row named T4 as the open work when T11 to T14 are. Shares derived from footprint and vmmap figures are now binary throughout, and the README states that convention once. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…1 review) `T8` made a hidden pane detach its layer contents and discard its swapchain. The hide was fed by `effectivePaneVisibility(in:windowVisible:)`, so "hidden" meant either the model's own visibility or an occluded window. Mission Control and App Expose composite an occluded window's live layer tree, so a covered DanTerm would have shown bare background in every pane. Split the two inputs at the seam where they already exist. Pane visibility now reads the model alone and is the one fact that decides pixel ownership. Window occlusion joins system sleep on the rendering gate: `AppRuntime` combines them into `renderingAvailable` and pushes that through `setRenderingAvailable`, which suspends planning and presentation and touches no buffer. The memory win is unchanged, because the nine unselected tabs of a ten-tab session are hidden by the model either way. Also drop an inverted pin in `IOSurfaceLayerContentsTests.swift`. It asserted the render server still held a detached surface right after the hide's own flush, which froze one machine's timing as a requirement and would fail on hardware that releases sooner. The frame-bounded loop below it is the real gate. Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
…h scripts fail loudly (research/41 review)
The `debug surfaces` output contract promised `visible` and
`displayedOutsideSwapchainBytes` on every `perPane` entry, but an unmeasured
pane carries only `paneId` and `swapchain`. The contract and the SKILL.md prose
now name that narrower entry. A census test pins the third shape: a pane that
answered and holds no buffers, whose `swapchain` is `null`.
The two research scripts now create `.run` before `mkdtemp` uses it. The
footprint script catches only the transport and decoding failures around the
census read, so a bug in the script fails the document instead of hiding as
`unmeasured`, and it nests the app's reply as `surfaces: {status, census}`
rather than writing a `status` key into it. The latency script raises when the
presentation trace recorded no staging events, so an instrument that never armed
cannot report every sample as a missing frame; it also refuses `--tabs 1`, which
had no hidden tab to reveal and spun forever, and its `first_pair` walks past an
opening event with no frame instead of giving up on the whole drain.
Drops `bareSurfacesVerbIsNoLongerACommand`: the bare `danterm surfaces` verb
never shipped, so there is no old spelling to guard.
Claude-Session: https://claude.ai/code/session_01WdNVVvMvyGgeyCGwzfsubi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.