perf(app): cache storage namespaces and batch writes in the renderer - #47704
Conversation
Every persisted() write was one IPC round trip and every mount one read per key. The desktop platform now hands persisted() a NamespaceStorage: one bulk load per namespace, Map reads after that, and writes coalesced into one StorageUpdate per 100 ms window, flushed before the IPC runtime disposes and when the window is hidden. Other windows learn about writes through a StorageChanged event so their copies stay fresh. Mirrors VS Code's Storage class.
flush() cleared the pending maps before the driver had accepted the batch, and those maps were the only guard used by the initial load, accept(), and the retry path. A pending load or an older external change could overwrite a value in flight, and a failed batch could requeue a value a later batch had already replaced. Each write now records a sequence number that stays until the host accepts that exact write. Batches are also handed to the driver synchronously instead of behind the previous reply, so a flush on pagehide is on the wire before the renderer goes away.
3f820aa to
37c2ce0
Compare
|
Addressed in #2 / #3 — per-key sequence numbers. Every local write records #1 — posting, not a handshake. The loss you describe comes from I'd rather not add a close/reload veto handshake on top: it's a new main↔renderer protocol for a case the synchronous post already covers, and the remaining window (writes made in the last ≤100 ms and never reaching |
Acks and StorageChanged events reach a window on different paths, so an event for an older write could arrive after the ack for a newer one and, with the local sequence guard already cleared, overwrite the cache. The main state store now stamps every update with a monotonic revision, returned in the ack and carried by change events and namespace loads. The renderer keeps the revision behind each cached key and drops events that are not newer; an event held back while a local write was in flight is applied after the ack when the host ordered it later.
|
Addressed in Host revision. The main state store stamps every Renderer.
Tests added in |
…olds The load merged snapshot entries into the cache but never removed keys the snapshot lacked, so an insert event that arrived before the load resolved survived a snapshot taken after the key was deleted, and the floor then rejected the deletion event that would have corrected it. The snapshot is now applied as the whole truth at its revision: keys it lacks are removed unless a local write or a newer event already owns them.
|
Addressed in The load now treats the snapshot as the whole truth at its revision: before merging its entries, any cached key the snapshot lacks is removed, unless a local write owns it or an event newer than the snapshot already placed it. Your sequence (insert @41 event lands before the load, snapshot @42 omits the key, delete @42 event is below the floor) now ends with the cache reading Test added: |
First of three layers replacing the renderer's persistence write path with the model VS Code uses (
vs/base/parts/storage→Memento→ per-resource backups). This layer is the transport: one bulk load per namespace, Map reads after that, one bulk write per flush window.Stack: #47704 (this) ← #47705 (
persisted()as a Memento) ← #47706 (large draft text as chunked blobs)Before
Every
persisted()mount did oneStorageGetIPC per key, and every setter call did oneStorageSetIPC. A tab close was 5–9 round trips; a window mount was one per persisted key.After
packages/app/src/runtime/persistence/namespace.ts—createNamespaceStorage(driver, name): anAsyncStoragethat loads the namespace once, serves reads from memory, coalesces writes into oneupdatepernamespaceFlushDelay(100 ms), and exposesflush(). A failed update keeps unsuperseded entries queued. Writes queued while the load is in flight win over the loaded snapshot.renderer/platform/storage.ts) builds one namespace perplatform.storage(name), flushes them all before the IPC runtime disposes (onBeforeDisposeinipc-client.ts) and when the window is hidden.StorageGet/Set/Delete/Keys/Length→StorageItems+StorageUpdate(+ unchangedStorageClear). Main's state store gainsitems(name)/update(name, insert, remove)over the existing write-behind.StorageUpdatebroadcastsStorageChangedto every other window, which applies it viaaccept()— never overriding a key that window has queued. Same shape as VS Code'sonDidChangeItemsExternal.persisted()and every call site are untouched; the seam was alreadyplatform.storage(name).Host round trips per tab close
In-process benchmark on the real
persisted()stores a tab close touches (tabs,tabs.recent,tabs.info,tabs.panes,tabs.closed).v2