Repository navigation
Stop a sign-out from wrapping Chrome's file-system usage to about 4 GB - #492
Merged
Merged
Conversation
Chrome never saves the bytes written through a sync access handle to the origin's usage counter, so after a restart it has forgotten them. The library and search index are written entirely that way, and the wipe on sign-out, account deletion or a hand-over then removed them with a plain `removeEntry`, which subtracts their real size from a counter that never held it. The counter went negative and Chrome reported it as about 4 GB, as seen on two Macs. This is Chromium issue 436552046 (https://issues.chromium.org/issues/436552046), which gives the same workaround. It is more than a wrong number in Settings: where a browser caps a site's quota below the wrapped figure (Brave, at 2 GiB), the origin reads as over quota, and Keep offline and files added offline, which ask `estimate()` for room before writing, refuse. `wipeStoreFiles` now hands the database files to a small dedicated worker (`shrink-worker.ts`), which empties each one through a sync access handle before removing it, so the removal subtracts nothing. Anything the worker cannot do (no workers, no sync handles, a file another tab holds, a worker that errors or takes over two seconds) is removed on the page as before, and the close, retry and per-account rules are unchanged. Checked in a throwaway Chrome profile, writing the library in a session that started from a valid saved counter, then restarting and signing out: the saved counter went to -1,760,736 bytes (reported as 4,293 MB) before this change and stays at 4,640 after it. When Chrome has instead measured the disk at launch, the emptied bytes stay counted and the counter ends about the library's size high (1.8 MB for the demo library) where a plain removal was exact; `shrink.ts` records that trade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wipe tries the store files up to four times while other tabs let go of them, and each try started a new worker and could wait the full two seconds for it. A worker that never answered could therefore hold a sign-out for about eight seconds beyond the bound the comment in `wipeLocalStores` promised. Each wipe now carries a `ShrinkState`: once its worker fails to start, errors or runs past `SHRINK_WAIT_MS`, the later tries go straight to the page's plain removal. The state is per wipe, so the next sign-out tries the worker again. The comment now counts that one wait in the bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's exclusivity. The comment on `shrinkAndRemove` read as if any file another tab had open would refuse the sync access handle and be skipped. wa-sqlite's cooperative VFS lets a file go between transactions, so an idle library elsewhere is usually emptied and removed, just as the plain `removeEntry` removed it before. The comment now says so, points at the close the wipe broadcasts first, and names the brief window after `close` in which another engine could take the emptied file back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `flush()` after `truncate(0)` reads as if durability mattered, which it does not: the file is removed straight after. It stays because truncate, flush, close is the sequence the saved counter was measured with in Chrome, and a sequence without the flush has not been measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract test asks about any new declaration in `wipe.ts`, since the wipe's message on the library channel is shared between builds. `ShrinkState` is not part of that message: it lives inside one tab's wipe and only decides whether that wipe starts its worker again. A tab on the previous build and one on this build still exchange the same close message, so no page needs to reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview: https://unbent-pr-492.of2668.workers.devSign in with Continue on the dev instance: a preview cannot register Runs against the shared Worker |
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.
Chrome never saves bytes written through a
FileSystemSyncAccessHandleto the origin's usage counter, so after a restart it has forgotten them. The library and its search index are written entirely that way (wa-sqlite'sOPFSCoopSyncVFS), and the wipe on sign-out, account deletion or a hand-over removed them with a plainremoveEntry, which subtracts their real size from a counter that never held it. The counter went negative and Chrome reported it as about 4 GB, as seen on two Macs. This is Chromium issue 436552046, which gives the same workaround.It matters beyond Settings: where a browser caps a site's quota below the wrapped figure (Brave, at 2 GiB), the origin reads as over quota, and Keep offline and files added offline, which ask
estimate()for room before writing, refuse.What changed
wipeStoreFileshands the database files to a new small worker (store/shrink-worker.ts, logic instore/shrink.ts), which empties each file through a sync access handle (truncate(0),flush,close) and then removes it, so the removal subtracts nothing.createWritable, which Chrome counts properly.CLOSE_WAIT_MS, the retries and the per-account rules are unchanged. The worker chunk is in the service worker's precache, so an offline sign-out still uses it.contract.jsonrecords the wipe's new localShrinkStateas fitting: the channel message between builds is unchanged.Verified
Headless Chrome stable, a throwaway profile, the real app on a worktree dev server with the demo seed, sign-out from the account menu, Chrome quit gracefully between sessions and the saved counter (
File System/<n>/t/.usage) read from disk.The second row is the trade: when Chrome has measured the disk, the emptied bytes stay counted. Nothing on the page can tell the two cases apart, so the counter ends high by at most the library's size at the last measurement, instead of below zero.
shrink.tsrecords this.Unit tests cover the worker path and each fallback (no worker, worker errors, worker silent, no sync handles, a held file retried).
pnpm -r typecheckand the web suite (2,771 tests) pass.Not covered: the fallbacks in a real browser, Safari and Firefox, the two-second bound on a phone, and account deletion or a hand-over in Chrome (they share the wipe with sign-out, which was run). This does not fix Chrome's undercount after a restart; it only stops the wraparound.
🤖 Generated with Claude Code