perf(desktop): collect orphaned draft blobs after the window is up (#49797)

This commit is contained in:
Luke Parker
2026-09-19 10:34:25 +10:00
committed by GitHub
parent dfde4628e3
commit dd95e3f2ed
2 changed files with 27 additions and 9 deletions
@@ -17,17 +17,29 @@ describe("draft store", () => {
expect(database.db.all(sql`SELECT key FROM document`)).toEqual([])
})
test("stores blobs by content hash and collects unreferenced ones on open", () => {
test("stores blobs by content hash and collects unreferenced ones shortly after open", async () => {
const database = openDatabase(":memory:")
const first = createDraftStore(database.db, { delay: 1_000 })
let clock = 0
const first = createDraftStore(database.db, { delay: 1_000, now: () => clock })
const used = first.putBlob(new Uint8Array([1, 2, 3]))
const unused = first.putBlob(new Uint8Array([4, 5, 6]))
expect(first.putBlob(new Uint8Array([1, 2, 3]))).toBe(used)
first.set("doc", JSON.stringify({ parts: [{ blob: { id: used } }] }))
first.flush()
const second = createDraftStore(database.db, { delay: 1_000 })
expect(second.getBlob(used)).toEqual(new Uint8Array([1, 2, 3]))
expect(second.getBlob(unused)).toBeNull()
first.close()
// The startup collection runs off the window's critical path and keeps the usual grace, so a
// blob from a session that ended within the grace period is still there...
const second = createDraftStore(database.db, { delay: 1_000, now: () => clock, collectDelay: 0 })
await new Promise((resolve) => setTimeout(resolve, 5))
expect(second.getBlob(unused)).toEqual(new Uint8Array([4, 5, 6]))
second.close()
// ...and gone once the grace has passed.
clock = blobGrace + 1
const third = createDraftStore(database.db, { delay: 1_000, now: () => clock, collectDelay: 0 })
await new Promise((resolve) => setTimeout(resolve, 5))
expect(third.getBlob(used)).toEqual(new Uint8Array([1, 2, 3]))
expect(third.getBlob(unused)).toBeNull()
third.close()
})
test("collects a retired chunk only once it is unreferenced and past the grace period", () => {
+10 -4
View File
@@ -27,11 +27,14 @@ const referenced = (value: unknown) => sql`
export function createDraftStore(
db: Database,
input: { delay?: number; onError?: (error: unknown) => void; now?: () => number } = {},
input: { delay?: number; collectDelay?: number; onError?: (error: unknown) => void; now?: () => number } = {},
) {
const now = input.now ?? Date.now
// Nothing outside this process can hold a blob id at startup, so no grace applies.
collectBlobs(db, Infinity)
// Orphans left by an earlier session are collected once the window is up rather than before it:
// the scan walks every stored document, and the usual grace keeps anything a renderer has
// uploaded in the meantime.
const startup = setTimeout(() => collectBlobs(db, now() - blobGrace), input.collectDelay ?? 10_000)
startup.unref()
let collected = now()
let orphans = false
const byKey = eq(document.key, sql.placeholder("key"))
@@ -104,7 +107,10 @@ export function createDraftStore(
return data ? (data as Uint8Array<ArrayBuffer>) : null
},
flush: writer.flush,
close: writer.close,
close() {
clearTimeout(startup)
writer.close()
},
}
}