diff options
| author | adamelmore <[email protected]> | 2026-01-27 14:51:34 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-27 15:25:07 -0600 |
| commit | 842f17d6d97c52d1efac66a8dca298f6ca692a56 (patch) | |
| tree | fd45e06f014dc5a7e72e509bb46f4c3ec1fb444c /packages/app/src/context/file.tsx | |
| parent | 1ebf63c70c552c95794325f40bbd278ba3e0c725 (diff) | |
| download | opencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.tar.gz opencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.zip | |
perf(app): better memory management
Diffstat (limited to 'packages/app/src/context/file.tsx')
| -rw-r--r-- | packages/app/src/context/file.tsx | 72 |
1 files changed, 70 insertions, 2 deletions
diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 16deacfe8..7509334ed 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -151,6 +151,28 @@ const WORKSPACE_KEY = "__workspace__" const MAX_FILE_VIEW_SESSIONS = 20 const MAX_VIEW_FILES = 500 +const MAX_FILE_CONTENT_ENTRIES = 40 +const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024 + +const contentLru = new Map<string, number>() + +function approxBytes(content: FileContent) { + const patchBytes = + content.patch?.hunks.reduce((total, hunk) => { + return total + hunk.lines.reduce((sum, line) => sum + line.length, 0) + }, 0) ?? 0 + + return (content.content.length + (content.diff?.length ?? 0) + patchBytes) * 2 +} + +function touchContent(path: string, bytes?: number) { + const prev = contentLru.get(path) + if (prev === undefined && bytes === undefined) return + const value = bytes ?? prev ?? 0 + contentLru.delete(path) + contentLru.set(path, value) +} + type ViewSession = ReturnType<typeof createViewSession> type ViewCacheEntry = { @@ -315,10 +337,40 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ dir: { "": { expanded: true } }, }) + const evictContent = (keep?: Set<string>) => { + const protectedSet = keep ?? new Set<string>() + const total = () => { + return Array.from(contentLru.values()).reduce((sum, bytes) => sum + bytes, 0) + } + + while (contentLru.size > MAX_FILE_CONTENT_ENTRIES || total() > MAX_FILE_CONTENT_BYTES) { + const path = contentLru.keys().next().value + if (!path) return + + if (protectedSet.has(path)) { + touchContent(path) + if (contentLru.size <= protectedSet.size) return + continue + } + + contentLru.delete(path) + if (!store.file[path]) continue + setStore( + "file", + path, + produce((draft) => { + draft.content = undefined + draft.loaded = false + }), + ) + } + } + createEffect(() => { scope() inflight.clear() treeInflight.clear() + contentLru.clear() setStore("file", {}) setTree("node", {}) setTree("dir", { "": { expanded: true } }) @@ -399,15 +451,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ .read({ path }) .then((x) => { if (scope() !== directory) return + const content = x.data setStore( "file", path, produce((draft) => { draft.loaded = true draft.loading = false - draft.content = x.data + draft.content = content }), ) + + if (!content) return + touchContent(path, approxBytes(content)) + evictContent(new Set([path])) }) .catch((e) => { if (scope() !== directory) return @@ -597,7 +654,18 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ listDir(parent, { force: true }) }) - const get = (input: string) => store.file[normalize(input)] + const get = (input: string) => { + const path = normalize(input) + const file = store.file[path] + const content = file?.content + if (!content) return file + if (contentLru.has(path)) { + touchContent(path) + return file + } + touchContent(path, approxBytes(content)) + return file + } const scrollTop = (input: string) => view().scrollTop(normalize(input)) const scrollLeft = (input: string) => view().scrollLeft(normalize(input)) |
