rumb
← logbook

What the browser keeps

Memory is a feature, and so is forgetting

Four of this batch's changes are about what the engine holds on to: a cache that kept everything and lost it all at exit, cookies that existed only if you quit politely, a devtools server recording a session nobody was watching, and a page that reports what its memory is for. One more is the opposite problem — something held on to for exactly one frame too long.

The cache had one tier and the wrong limit

The HTTP cache held every response in memory, counted by entries rather than by size, and lost all of it at exit. Counting entries means a thousand favicons and a thousand video segments cost the same, which is the one thing a size limit is supposed to prevent.

It now has two tiers. The memory tier is bounded by the bytes of the bodies it holds, configured by network_http_cache_memory_size. The disk tier keeps complete responses across restarts under Opts::cache_dir, up to network_http_cache_disk_size, and only the public session has one — a temporary session with a disk cache would not be temporary.

Each file is written under a temporary name and renamed into place. A crash mid-write therefore leaves the previous file intact rather than a truncated one, which matters more for a cache than it sounds: a half-written cache entry is not a missing response, it is a wrong one.

Cookies, written while running

The public cookie jar, the HSTS list and the auth cache were written to config_dir only when the resource thread received Exit. An embedder that crashed, or was killed, lost every cookie of the session — and an embedder under development is killed constantly.

A background thread now writes them every 30 seconds when they have changed, through the same temporary-file-and-rename route. Because the file on disk can now hold session cookies, what gets written had to be decided rather than assumed.

A devtools server with no audience

The devtools server kept every network event, including the body of each response, and a copy of the text of every source it saw, inline scripts included, for as long as it ran. With no client connected to show them to, that was every page ever loaded, held in memory, for the lifetime of the process.

Without a client, network events are now dropped. Source actors are still created, so a source is listed the moment a client does connect, but they hold no text until there is someone to send it to.

about:memory, in words

The page listed the raw paths of the memory reports, which is the right data and the wrong presentation: js-main-runtime/compartments/system tells you where a number came from, not whether it is a problem.

It now names each part in plain words, says whether the memory goes away and when, and groups the reports by page. It can also measure again, ask every page to collect garbage, and open a set of heavy sites to put the browser under load — the three things you actually do when you are chasing a leak, rather than reading a number once.

One build note that is easy to miss: jemalloc has to be built with its statistics enabled, or stats.allocated and the rest do not exist and the report has no figures for the heap at all. The text of the page is in Spanish, like the rest of the chrome.

SAME BUFFER ID, THREE FRAMES fence import no fence reuse the import fence import again the producer hands out a fence once per write, which is the only signal that the contents moved the old rule: keep the import while the buffer id is unchanged a write under a kept import shows the previous contents — a black page after a resize on NVIDIA The buffer id says which buffer it is. It never said whether anything was written to it.

The import that outlived a write

The other direction of the same mistake. GPUI kept its import of a dmabuf for as long as the buffer id was unchanged, which sounds right: the id says which buffer it is, and the buffer has not been swapped.

The id says which buffer it is. It never said whether anything had been written to it. An import kept across a write by the producer goes on presenting what the buffer held before — which on NVIDIA showed up as a black page after a resize, the most confusing possible symptom for a caching bug, because nothing about it points at a cache.

The producer hands out a fence once per write, and that is the only signal that the contents moved. A frame that gets a fence imports the buffer again; frames with nothing new reuse the import.

Two more, while we are counting things held wrongly

document.visibilityState was hidden for every document until it was reactivated from session history, and never changed after that. A page that waits to be visible before it loads or plays something therefore never did. A document that loads in a webview that is not throttled now starts out visible; throttling a webview, which is how an embedder says it has stopped showing it, hides the document and fires visibilitychange, and un-throttling reverses it.

And a closed crossbeam receiver is always ready, so once one sender of a receiver set was gone, every later select returned immediately with the same ChannelClosed and the receivers that were still open were never read again. The closed receiver is replaced with one that is never ready, which keeps the ids of the others unchanged — the detail that makes the fix a fix rather than a renumbering.

What is still unbounded

The disk cache has a size limit and no eviction policy worth the name beyond it. about:memory reports the heap only when jemalloc was built to say so, which means the figures are absent in exactly the builds a user would be running.

And the devtools server's restraint is conditional on no client being connected. Attach one to a long session and the hoarding resumes, because the data a client might ask for is still everything it has ever seen.

BEFORE: EVERY RESPONSE IN MEMORY, COUNTED BY ENTRIES, LOST AT EXIT memory tier bounded by the bytes of its bodies network_http_cache_memory_size disk tier complete responses under cache_dir network_http_cache_disk_size survives a restart · public session only temporary name, then rename a crash mid-write keeps the previous file The cookie jar, the HSTS list and the auth cache take the same route, written every 30 seconds when changed, not only at exit.
Bounded by bytes rather than by a count of entries, and written through a rename so a crash mid-write keeps the previous file.