perf(mobile): release closed Markdown document cache entries (#26159)

This commit is contained in:
Neil
2026-10-07 22:41:19 -07:00
committed by GitHub
parent 586e6bc2e7
commit 7c0e76cb1a
3 changed files with 249 additions and 27 deletions
@@ -0,0 +1,203 @@
import { randomBytes } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import { hookMount, performHookAction } from '../test-support/rpc-recording/hook-mount'
import { mountFixture } from '../test-support/rpc-recording/recorder-fixture-shape'
import type { RpcResponse } from '../transport/types'
import type { MarkdownDocState, MobileSessionTab } from './mobile-session-route-types'
import { useMobileSessionDocumentReaders } from './use-mobile-session-document-readers'
import { useMobileSessionCloseActions } from './use-mobile-session-close-actions'
type ReaderScope = Parameters<typeof useMobileSessionDocumentReaders>[0]
type CloseScope = Parameters<typeof useMobileSessionCloseActions>[0]
function success(content: string): RpcResponse {
return {
id: 'read',
ok: true,
result: { content, version: 'version', isDirty: false, editable: false },
_meta: { runtimeId: 'host' }
}
}
function mountDocuments(readReply: () => Promise<RpcResponse>) {
let docs = new Map<string, MarkdownDocState>()
const tabs: { current: MobileSessionTab[] } = { current: [] }
const setDocs: ReaderScope['setMarkdownDocs'] = (update) => {
docs = typeof update === 'function' ? update(docs) : update
}
const closeReply = vi.fn(async (): Promise<RpcResponse> => success(''))
const client = {
sendRequest: vi.fn(async (method: string): Promise<RpcResponse> =>
method === 'markdown.readTab' || method === 'files.read' ? readReply() : closeReply()
)
}
let readers: ReturnType<typeof useMobileSessionDocumentReaders> | undefined
let close: ReturnType<typeof useMobileSessionCloseActions> | undefined
const hook = hookMount(() => {
readers = useMobileSessionDocumentReaders(
mountFixture<ReaderScope>({
worktreeId: 'folder-workspace',
client,
setMarkdownDocs: setDocs,
setFileDocs: () => {}
})
)
close = useMobileSessionCloseActions(
mountFixture<CloseScope>({
worktreeId: 'folder-workspace',
client,
sessionTabsRef: tabs,
setSessionTabs: () => {},
setFileDocs: () => {},
setMarkdownDocs: setDocs,
reconcileBufferedDraftsRef: { current: () => {} },
closedTabTombstonesRef: { current: new Map() },
activeSessionTabIdRef: { current: null },
selectedSessionTabIdRef: { current: null },
activeSessionTabTypeRef: { current: null },
activeHandleRef: { current: null },
setActiveSessionTabId: () => {},
setActiveHandle: () => {}
})
)
})
hook.mount()
const tab = (id: string) =>
mountFixture<Extract<MobileSessionTab, { type: 'markdown' }>>({
type: 'markdown',
id,
relativePath: id + '.md',
isDirty: false
})
return {
get docs() {
return docs
},
closeReply,
unmount: hook.unmount,
read(id: string) {
const target = tab(id)
tabs.current = [target]
return performHookAction(() => readers?.readMarkdownTab(target))
},
close(id: string) {
return performHookAction(() => close?.handleCloseSessionTab(tab(id)))
},
dirty(id: string) {
const doc = docs.get(id)
if (doc?.status !== 'ready') {
throw new Error('Missing ready document')
}
docs.set(id, { ...doc, localContent: 'phone draft', isDirty: true })
}
}
}
function delayedReply() {
let resolve: (reply: RpcResponse) => void = () => {}
const promise = new Promise<RpcResponse>((done) => {
resolve = done
})
return { promise, resolve }
}
describe('mobile Markdown document lifetime', () => {
it('releases 40 MiB of clean content after twenty reads and successful closes', async () => {
const h = mountDocuments(async () => success(randomBytes(1536 * 1024).toString('base64')))
try {
for (let index = 0; index < 20; index += 1) {
await h.read('tab-' + index)
const doc = h.docs.get('tab-' + index)
expect(doc?.status === 'ready' && Buffer.byteLength(doc.content)).toBe(2 * 1024 * 1024)
await h.close('tab-' + index)
}
expect(h.docs.size).toBe(0)
} finally {
h.unmount()
}
})
it.each(['success', 'failure', 'disk fallback'])(
'keeps a closed read released after a late %s',
async (kind) => {
const delayed = delayedReply()
const readReply = vi
.fn()
.mockReturnValueOnce(delayed.promise)
.mockResolvedValue(success('disk'))
const h = mountDocuments(readReply)
try {
const reading = h.read('closed')
await h.close('closed')
delayed.resolve(
kind === 'success'
? success('late')
: {
id: 'read',
ok: false,
error: {
code: kind === 'disk fallback' ? 'renderer_unavailable' : 'tab_not_found',
message: 'Unavailable'
},
_meta: { runtimeId: 'host' }
}
)
await reading
expect(h.docs.size).toBe(0)
if (kind === 'disk fallback') {
expect(readReply).toHaveBeenCalledTimes(2)
}
} finally {
h.unmount()
}
}
)
it('retains the document when the host refuses close', async () => {
const h = mountDocuments(async () => success('kept'))
try {
await h.read('kept')
const doc = h.docs.get('kept')
h.closeReply.mockResolvedValue({
id: 'close',
ok: false,
error: { code: 'refused', message: 'Refused' },
_meta: { runtimeId: 'host' }
})
await h.close('kept')
expect(h.docs.get('kept')).toBe(doc)
} finally {
h.unmount()
}
})
it('preserves unsaved phone drafts after close', async () => {
const h = mountDocuments(async () => success('original'))
try {
await h.read('draft')
h.dirty('draft')
const draft = h.docs.get('draft')
await h.close('draft')
expect(h.docs.get('draft')).toBe(draft)
} finally {
h.unmount()
}
})
it('keeps a newer read when an older request finishes later', async () => {
const old = delayedReply()
const h = mountDocuments(
vi.fn().mockReturnValueOnce(old.promise).mockResolvedValue(success('new'))
)
try {
const first = h.read('same')
await h.read('same')
const current = h.docs.get('same')
old.resolve(success('old'))
await first
expect(h.docs.get('same')).toBe(current)
} finally {
h.unmount()
}
})
})
@@ -16,6 +16,7 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
terminalsRef,
setSessionTabs,
setFileDocs,
setMarkdownDocs,
sessionTabsRef,
reconcileBufferedDraftsRef,
closedTabTombstonesRef,
@@ -119,6 +120,17 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
})
)
if (response.accepted) {
if (tab.type === 'markdown') {
setMarkdownDocs((prev) => {
const doc = prev.get(tab.id)
if (!doc || (doc.status === 'ready' && doc.isDirty)) {
return prev
}
const next = new Map(prev)
next.delete(tab.id)
return next
})
}
if (tab.type === 'file') {
setFileDocs((prev) => {
if (!prev.has(tab.id)) {
@@ -17,7 +17,8 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
if (!client) {
return
}
setMarkdownDocs((prev) => new Map(prev).set(tab.id, { status: 'loading' }))
const loading = { status: 'loading' } as const
setMarkdownDocs((prev) => new Map(prev).set(tab.id, loading))
try {
const response = await markdownTabRead.request(client, {
worktree: `id:${worktreeId}`,
@@ -26,19 +27,21 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
if (response.ok) {
const result = markdownTabRead.interpret(response)
setMarkdownDocs((prev) =>
new Map(prev).set(tab.id, {
status: 'ready',
content: result.content,
localContent: result.content,
baseVersion: result.version,
isDirty: false,
editable: result.editable === true,
stale: result.isDirty,
readOnlyReason: result.readOnlyReason,
...(result.truncated === true
? { truncated: true, byteLength: result.byteLength }
: {})
})
prev.get(tab.id) === loading
? new Map(prev).set(tab.id, {
status: 'ready',
content: result.content,
localContent: result.content,
baseVersion: result.version,
isDirty: false,
editable: result.editable === true,
stale: result.isDirty,
readOnlyReason: result.readOnlyReason,
...(result.truncated === true
? { truncated: true, byteLength: result.byteLength }
: {})
})
: prev
)
return
}
@@ -57,22 +60,26 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
}
const fileResult = fallback.value
setMarkdownDocs((prev) =>
new Map(prev).set(
tab.id,
buildMarkdownDiskFallbackDoc({
content: fileResult.content,
truncated: fileResult.truncated,
byteLength: fileResult.byteLength,
tabIsDirty: tab.isDirty
})
)
prev.get(tab.id) === loading
? new Map(prev).set(
tab.id,
buildMarkdownDiskFallbackDoc({
content: fileResult.content,
truncated: fileResult.truncated,
byteLength: fileResult.byteLength,
tabIsDirty: tab.isDirty
})
)
: prev
)
} catch (err) {
setMarkdownDocs((prev) =>
new Map(prev).set(tab.id, {
status: 'error',
message: documentReadErrorMessage(err, "Couldn't load markdown")
})
prev.get(tab.id) === loading
? new Map(prev).set(tab.id, {
status: 'error',
message: documentReadErrorMessage(err, "Couldn't load markdown")
})
: prev
)
}
},