perf(app): better memory management
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user