perf(app): better memory management

This commit is contained in:
adamelmore
2026-01-27 14:51:34 -06:00
parent 1ebf63c70c
commit 842f17d6d9
18 changed files with 1185 additions and 82 deletions

View File

@@ -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))