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

@@ -18,7 +18,52 @@ const LEGACY_STORAGE = "default.dat"
const GLOBAL_STORAGE = "opencode.global.dat"
const LOCAL_PREFIX = "opencode."
const fallback = { disabled: false }
const cache = new Map<string, string>()
const CACHE_MAX_ENTRIES = 500
const CACHE_MAX_BYTES = 8 * 1024 * 1024
type CacheEntry = { value: string; bytes: number }
const cache = new Map<string, CacheEntry>()
const cacheTotal = { bytes: 0 }
function cacheDelete(key: string) {
const entry = cache.get(key)
if (!entry) return
cacheTotal.bytes -= entry.bytes
cache.delete(key)
}
function cachePrune() {
for (;;) {
if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return
const oldest = cache.keys().next().value as string | undefined
if (!oldest) return
cacheDelete(oldest)
}
}
function cacheSet(key: string, value: string) {
const bytes = value.length * 2
if (bytes > CACHE_MAX_BYTES) {
cacheDelete(key)
return
}
const entry = cache.get(key)
if (entry) cacheTotal.bytes -= entry.bytes
cache.delete(key)
cache.set(key, { value, bytes })
cacheTotal.bytes += bytes
cachePrune()
}
function cacheGet(key: string) {
const entry = cache.get(key)
if (!entry) return
cache.delete(key)
cache.set(key, entry)
return entry.value
}
function quota(error: unknown) {
if (error instanceof DOMException) {
@@ -63,9 +108,11 @@ function evict(storage: Storage, keep: string, value: string) {
for (const item of items) {
storage.removeItem(item.key)
cacheDelete(item.key)
try {
storage.setItem(keep, value)
cacheSet(keep, value)
return true
} catch (error) {
if (!quota(error)) throw error
@@ -78,6 +125,7 @@ function evict(storage: Storage, keep: string, value: string) {
function write(storage: Storage, key: string, value: string) {
try {
storage.setItem(key, value)
cacheSet(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
@@ -85,13 +133,17 @@ function write(storage: Storage, key: string, value: string) {
try {
storage.removeItem(key)
cacheDelete(key)
storage.setItem(key, value)
cacheSet(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
}
return evict(storage, key, value)
const ok = evict(storage, key, value)
if (!ok) cacheSet(key, value)
return ok
}
function snapshot(value: unknown) {
@@ -148,7 +200,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
return {
getItem: (key) => {
const name = item(key)
const cached = cache.get(name)
const cached = cacheGet(name)
if (fallback.disabled && cached !== undefined) return cached
const stored = (() => {
@@ -160,12 +212,12 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
}
})()
if (stored === null) return cached ?? null
cache.set(name, stored)
cacheSet(name, stored)
return stored
},
setItem: (key, value) => {
const name = item(key)
cache.set(name, value)
cacheSet(name, value)
if (fallback.disabled) return
try {
if (write(localStorage, name, value)) return
@@ -177,7 +229,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
},
removeItem: (key) => {
const name = item(key)
cache.delete(name)
cacheDelete(name)
if (fallback.disabled) return
try {
localStorage.removeItem(name)
@@ -191,7 +243,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
function localStorageDirect(): SyncStorage {
return {
getItem: (key) => {
const cached = cache.get(key)
const cached = cacheGet(key)
if (fallback.disabled && cached !== undefined) return cached
const stored = (() => {
@@ -203,11 +255,11 @@ function localStorageDirect(): SyncStorage {
}
})()
if (stored === null) return cached ?? null
cache.set(key, stored)
cacheSet(key, stored)
return stored
},
setItem: (key, value) => {
cache.set(key, value)
cacheSet(key, value)
if (fallback.disabled) return
try {
if (write(localStorage, key, value)) return
@@ -218,7 +270,7 @@ function localStorageDirect(): SyncStorage {
fallback.disabled = true
},
removeItem: (key) => {
cache.delete(key)
cacheDelete(key)
if (fallback.disabled) return
try {
localStorage.removeItem(key)

View File

@@ -78,6 +78,7 @@ export function createSpeechRecognition(opts?: {
let lastInterimSuffix = ""
let shrinkCandidate: string | undefined
let commitTimer: number | undefined
let restartTimer: number | undefined
const cancelPendingCommit = () => {
if (commitTimer === undefined) return
@@ -85,6 +86,26 @@ export function createSpeechRecognition(opts?: {
commitTimer = undefined
}
const clearRestart = () => {
if (restartTimer === undefined) return
window.clearTimeout(restartTimer)
restartTimer = undefined
}
const scheduleRestart = () => {
clearRestart()
if (!shouldContinue) return
if (!recognition) return
restartTimer = window.setTimeout(() => {
restartTimer = undefined
if (!shouldContinue) return
if (!recognition) return
try {
recognition.start()
} catch {}
}, 150)
}
const commitSegment = (segment: string) => {
const nextCommitted = appendSegment(committedText, segment)
if (nextCommitted === committedText) return
@@ -214,17 +235,14 @@ export function createSpeechRecognition(opts?: {
}
recognition.onerror = (e: { error: string }) => {
clearRestart()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
if (e.error === "no-speech" && shouldContinue) {
setStore("interim", "")
if (opts?.onInterim) opts.onInterim("")
setTimeout(() => {
try {
recognition?.start()
} catch {}
}, 150)
scheduleRestart()
return
}
shouldContinue = false
@@ -232,6 +250,7 @@ export function createSpeechRecognition(opts?: {
}
recognition.onstart = () => {
clearRestart()
sessionCommitted = ""
pendingHypothesis = ""
cancelPendingCommit()
@@ -243,22 +262,20 @@ export function createSpeechRecognition(opts?: {
}
recognition.onend = () => {
clearRestart()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setStore("isRecording", false)
if (shouldContinue) {
setTimeout(() => {
try {
recognition?.start()
} catch {}
}, 150)
scheduleRestart()
}
}
}
const start = () => {
if (!recognition) return
clearRestart()
shouldContinue = true
sessionCommitted = ""
pendingHypothesis = ""
@@ -274,6 +291,7 @@ export function createSpeechRecognition(opts?: {
const stop = () => {
if (!recognition) return
shouldContinue = false
clearRestart()
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""
@@ -287,6 +305,7 @@ export function createSpeechRecognition(opts?: {
onCleanup(() => {
shouldContinue = false
clearRestart()
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""

View File

@@ -13,7 +13,21 @@ type State =
}
const state = new Map<string, State>()
const waiters = new Map<string, Array<(state: State) => void>>()
const waiters = new Map<
string,
{
promise: Promise<State>
resolve: (state: State) => void
}
>()
function deferred() {
const box = { resolve: (_: State) => {} }
const promise = new Promise<State>((resolve) => {
box.resolve = resolve
})
return { promise, resolve: box.resolve }
}
export const Worktree = {
get(directory: string) {
@@ -27,32 +41,33 @@ export const Worktree = {
},
ready(directory: string) {
const key = normalize(directory)
state.set(key, { status: "ready" })
const list = waiters.get(key)
if (!list) return
const next = { status: "ready" } as const
state.set(key, next)
const waiter = waiters.get(key)
if (!waiter) return
waiters.delete(key)
for (const fn of list) fn({ status: "ready" })
waiter.resolve(next)
},
failed(directory: string, message: string) {
const key = normalize(directory)
state.set(key, { status: "failed", message })
const list = waiters.get(key)
if (!list) return
const next = { status: "failed", message } as const
state.set(key, next)
const waiter = waiters.get(key)
if (!waiter) return
waiters.delete(key)
for (const fn of list) fn({ status: "failed", message })
waiter.resolve(next)
},
wait(directory: string) {
const key = normalize(directory)
const current = state.get(key)
if (current && current.status !== "pending") return Promise.resolve(current)
return new Promise<State>((resolve) => {
const list = waiters.get(key)
if (!list) {
waiters.set(key, [resolve])
return
}
list.push(resolve)
})
const existing = waiters.get(key)
if (existing) return existing.promise
const waiter = deferred()
waiters.set(key, waiter)
return waiter.promise
},
}