summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/utils
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/utils
parent1ebf63c70c552c95794325f40bbd278ba3e0c725 (diff)
downloadopencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.tar.gz
opencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.zip
perf(app): better memory management
Diffstat (limited to 'packages/app/src/utils')
-rw-r--r--packages/app/src/utils/persist.ts72
-rw-r--r--packages/app/src/utils/speech.ts39
-rw-r--r--packages/app/src/utils/worktree.ts49
3 files changed, 123 insertions, 37 deletions
diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts
index 129695f86..0ca3abad0 100644
--- a/packages/app/src/utils/persist.ts
+++ b/packages/app/src/utils/persist.ts
@@ -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)
diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts
index c8acf5241..201c1261b 100644
--- a/packages/app/src/utils/speech.ts
+++ b/packages/app/src/utils/speech.ts
@@ -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 = ""
diff --git a/packages/app/src/utils/worktree.ts b/packages/app/src/utils/worktree.ts
index 7c0055920..581afd553 100644
--- a/packages/app/src/utils/worktree.ts
+++ b/packages/app/src/utils/worktree.ts
@@ -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
},
}