v0.2.0 — tabs, and then groups
A tab is a whole engine
There is no lightweight representation of a tab in rumb. A tab owns an offscreen rendering context derived from the shared headless parent, a Servo WebView built into it, and the delegate that watches it. Ten tabs are ten WebViews, ten script threads, ten JS heaps, ten framebuffers.
tick() calls spin_event_loop once and that drives every tab — background tabs keep loading — but only the active tab paints and exports a dma-buf into the shared slot. The compositor only ever sees one framebuffer. set_active un-throttles and shows the tab gaining focus, and throttles and hides every other live one. That is what stops a background tab painting forever: a blinking caret on a page you switched away from will otherwise repaint at full rate, and leak while it does.
The per-tab rendering context also settled an older problem. Resizing now swaps the offscreen FBO instead of resizing the surfman surface — which is what used to panic (servo/servo#38369) and why the window-resize item had sat in the backlog since May. The render loop can now match Servo's render size to the content area every frame, so CSS media queries fire against the real viewport.
Giving the memory back
A tab idle long enough gets discarded: dropping its WebView sends CloseWebView to the constellation and takes the script thread, JS heap, DOM and layout with it. The rendering context is kept, so waking is a rebuild against the same FBO rather than fresh GL-texture churn. URL, title, favicon and group membership survive; waking reloads from the stored URL.
Dropping it is not enough on Linux. The teardown frees the memory inside the process, but glibc keeps it pooled and RSS never falls. So every five seconds the render loop calls malloc_trim(0) and, in the same pass, logs RSS from /proc/self/statm next to the tab count and the active URL. The comment above that watchdog calls it “the 24 GB incident”.
The guards are where the care went. Never the active tab, never a pinned one, never one already asleep; then idle past the threshold, no title or favicon change inside a signal window — at least five seconds even in instant mode, so a tab still loading with a blinking title is not killed under itself — and not playing media. Past those, a read-only JavaScript probe runs inside the page: true if a video or audio element is playing, if an editable field holds a value differing from its default, or if window.onbeforeunload is a function. No monkey-patching of fetch or WebSocket, which would risk breaking the site it is inspecting; any error or non-boolean answer counts as busy, so nothing is discarded that could not be inspected.
The probe exists because the embedder API gives it nothing: Servo exposes neither live fetch or XHR, nor an open WebSocket or SSE stream, nor dirty forms, nor a registered beforeunload. The probe recovers the last two from inside the page and cannot see the first three at all — a tab holding an open WebSocket, quiet and idle, is still eligible for suspension. The fifteen-minute default makes that unlikely, not impossible, and the doc comment says so.
Groups are a layout problem
Group membership is one field on the tab: an optional group id. The name, colour and collapsed state live in the shell, keyed by that id. Everything else about groups is rendering.
The strip walks the tabs in order. An ungrouped tab renders as a lone chip; the first member of a group triggers the whole group to render at that position — pill, then every member chip — and later members are skipped. A group looks contiguous because moving one moves all of it: move_group drains members out highest-index-first so lower indices stay valid, restores their order, and re-inserts them as one run among the non-members. Working out where the active tab ended up afterwards earns its own paragraph in the source.
The visual went through several forms in one morning: a colour dot per chip, then a coloured bottom border on each grouped chip so adjacent members formed a continuous line, then one line drawn for the whole block. A horizontal underline makes no sense in a vertical strip, so there it became a rail down the left of the column.
One pass was wrong in both directions. A collapsed group got a badge showing how many tabs it was hiding; forty minutes later the badge was deleted outright. A collapsed group is now just its name, and hides every member.
Six passes at a drop cursor
Drag and drop took more commits than the group model it serves. The first form filled the hovered drop target with a highlight, which tells you which tab you are over — not the same question as where the dragged tab will land. It became a two-pixel accent line on the leading edge of the hovered chip, left in a horizontal strip and top in a vertical one, meaning: the drop inserts here.
Making the cursor precise exposed a bug the filled highlight had hidden. move_tab inserted at the target index directly, but removing the dragged tab first shifts every later index down by one, so a rightward move landed one slot late and reordering only ever worked leftward. The fix is three lines and the comment explaining it is longer than the code.
The end of the strip took three attempts. A trailing drop zone after the + button worked but pushed the + away from the last tab and let the insertion cursor appear past it; shrinking it and moving it before the + fixed the cursor and left the gap. The answer was to delete the zone and make the + itself the “insert at end” target.
The last gap was the front of the strip: if a group sits at position zero there is no chip to drop before it, because dropping on the group's first tab makes the dragged tab join the group. So the pill became a drop target too — a tab dropped on it lands ungrouped, immediately before the group.
What v0.2.0 does not fix
Everything still runs in one process. The backlog entry on site isolation names its own trigger — tabs landing in the shell — and that trigger fired with this release. It was not done. A page that crashes Servo takes every tab with it, and every origin shares one address space.
The DevTools panel is one tab of a real inspector: Elements only, no console, no network, no debugger. It paints at most 150 tree rows per frame and summarises the rest, because the whole shell re-renders every frame while Servo ticks and an unbounded tree would lock the UI at 60fps. The breadth-first auto-expand written for it is marked dead code until the tree is virtualised.
The chrome is hardcoded in Spanish. Not configured in Spanish — hardcoded, as string literals inside the render functions. There is no localisation layer and no string table to translate against, and the seams already show: a tab with no title reads “New Tab” when the label comes from the engine and “nueva pestaña” when it comes from the strip renderer, in the same window.
The vertical strip's maximum width was set to 460 px and tightened to 320 the same morning, which is the kind of number that gets picked twice because nobody has decided what it is for. None of that is a list of things we ran out of time for. It is what shipped.