summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/file.tsx
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-27 14:51:34 -0600
committeradamelmore <[email protected]>2026-01-27 15:25:07 -0600
commit842f17d6d97c52d1efac66a8dca298f6ca692a56 (patch)
treefd45e06f014dc5a7e72e509bb46f4c3ec1fb444c /packages/app/src/context/file.tsx
parent1ebf63c70c552c95794325f40bbd278ba3e0c725 (diff)
downloadopencode-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.tsx72
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))