perf(disk): remove runtime caches left by earlier versions after startup
This commit is contained in:
@@ -55,6 +55,7 @@ import { readBoundedResponseText } from './bounded-response'
|
||||
import { scopedReadToolNames } from '../../shared/scoped-data-tools'
|
||||
import { readBoundedFile } from '../workspace-file-access'
|
||||
import { terminateProcessTreeAndWait } from './child-process-termination'
|
||||
import { CONTINUE_HOST_LAYOUT_VERSION } from './continue-host-layout'
|
||||
|
||||
const supportedVersion = '1.5.47'
|
||||
const supportedBundleHashes = new Set([
|
||||
@@ -1052,7 +1053,7 @@ export class ContinueHostAdapter {
|
||||
const digest = sourceHash.slice(0, 16)
|
||||
const targetRoot = join(
|
||||
this.options.cacheRoot,
|
||||
`host-v7-${supportedVersion}-${digest}`
|
||||
`host-v${CONTINUE_HOST_LAYOUT_VERSION}-${supportedVersion}-${digest}`
|
||||
)
|
||||
const targetDist = join(targetRoot, 'dist')
|
||||
const targetBundle = join(targetDist, 'index.js')
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Prepared Continue host directories are `host-v<layout>-…`; older layouts are never read again. */
|
||||
export const CONTINUE_HOST_LAYOUT_VERSION = 7
|
||||
@@ -65,6 +65,8 @@ import {
|
||||
import { createTrayIcon } from './tray-icon'
|
||||
import { resolveBundledRuntimePaths } from './agent/bundled-runtimes'
|
||||
import type { ContinueHostLauncher } from './agent/continue-host-adapter'
|
||||
import { CONTINUE_HOST_LAYOUT_VERSION } from './agent/continue-host-layout'
|
||||
import { removeStaleRuntimeCaches } from './stale-runtime-cache-cleanup'
|
||||
import { resolvePortableUserDataPath } from './portable-user-data'
|
||||
import { BrowserService } from './browser/browser-service'
|
||||
import { SubagentService } from './assistant/subagent-service'
|
||||
@@ -1626,6 +1628,14 @@ if (hasSingleInstanceLock) {
|
||||
startupMark('main:load-main-window')
|
||||
removeDeviceSharingIpc = registerDeviceSharingIpc(mainWindow,
|
||||
new DeviceSharingService(join(app.getPath('userData'), 'device-sharing-settings.json'), app.getVersion()))
|
||||
// Disk only: old versions' runtime caches, removed well after the first frame.
|
||||
setTimeout(() => {
|
||||
void removeStaleRuntimeCaches({
|
||||
userDataPath: app.getPath('userData'),
|
||||
appVersion: app.getVersion(),
|
||||
continueHostLayoutVersion: CONTINUE_HOST_LAYOUT_VERSION
|
||||
})
|
||||
}, 30_000).unref()
|
||||
setImmediate(() => {
|
||||
void repairStaleWindowsNotificationShortcuts({
|
||||
platform: process.platform,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// @vitest-environment node
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { removeStaleRuntimeCaches, staleRuntimeCacheDirectories } from './stale-runtime-cache-cleanup'
|
||||
|
||||
const directories: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('stale runtime cache cleanup', () => {
|
||||
it('selects only older app-version and Continue layout directories', () => {
|
||||
expect(staleRuntimeCacheDirectories({
|
||||
opencode: ['0.12.8', '0.15.8', '0.15.9', '0.16.0-beta.1', 'cache', 'custom', '1.0'],
|
||||
continueHost: ['host-v1-1.5.47-a', 'host-v6-1.5.47-a', 'host-v7-1.5.47-a', 'host-v7-1.6.0-b',
|
||||
'host-v8-1.5.47-a', 'isolated-global', 'isolated-global-123', 'host-v7-1.5.47-a.staging-x', 'sessions']
|
||||
}, { appVersion: '0.15.9', continueHostLayoutVersion: 7 })).toEqual({
|
||||
opencode: ['0.12.8', '0.15.8', '0.16.0-beta.1'],
|
||||
continueHost: ['host-v1-1.5.47-a', 'host-v6-1.5.47-a']
|
||||
})
|
||||
})
|
||||
|
||||
it('removes stale directories and leaves everything else in the user data directory', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-cache-cleanup-'))
|
||||
directories.push(root)
|
||||
const make = async (path: string): Promise<void> => {
|
||||
await mkdir(join(root, path), { recursive: true })
|
||||
await writeFile(join(root, path, 'file.txt'), 'x')
|
||||
}
|
||||
await make('opencode-runtime/0.15.8/config')
|
||||
await make('opencode-runtime/0.15.9/config')
|
||||
await make('continue-host/host-v6-1.5.47-a/dist')
|
||||
await make('continue-host/host-v7-1.5.47-a/dist')
|
||||
await make('continue-host/isolated-global')
|
||||
await make('continue-host/sessions')
|
||||
await writeFile(join(root, 'continue-host', 'config.yaml'), 'x')
|
||||
await make('models/embedding')
|
||||
|
||||
const { removed } = await removeStaleRuntimeCaches({
|
||||
userDataPath: root, appVersion: '0.15.9', continueHostLayoutVersion: 7
|
||||
})
|
||||
|
||||
expect(removed.map((path) => path.slice(root.length + 1).replaceAll('\\', '/')).sort())
|
||||
.toEqual(['continue-host/host-v6-1.5.47-a', 'opencode-runtime/0.15.8'])
|
||||
for (const kept of ['opencode-runtime/0.15.9/config/file.txt', 'continue-host/host-v7-1.5.47-a/dist/file.txt',
|
||||
'continue-host/isolated-global/file.txt', 'continue-host/sessions/file.txt', 'continue-host/config.yaml',
|
||||
'models/embedding/file.txt']) {
|
||||
expect(existsSync(join(root, kept))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('does nothing when the cache roots do not exist', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-cache-cleanup-'))
|
||||
directories.push(root)
|
||||
await expect(removeStaleRuntimeCaches({ userDataPath: root, appVersion: '0.15.9', continueHostLayoutVersion: 7 }))
|
||||
.resolves.toEqual({ removed: [] })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { readdir, rm } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Removes runtime caches left by earlier GoodBuddy versions (PERF, disk).
|
||||
*
|
||||
* - `opencode-runtime/<app version>/`: OpenCode's shared config and skills
|
||||
* are prepared per app version and only the current version's directory is
|
||||
* ever used, so every other version-named directory is stale (~50 MB each).
|
||||
* - `continue-host/host-v<layout>-…/`: patched Continue host bundles; only
|
||||
* the current layout is read. Older layouts are stale. Directories of the
|
||||
* current layout (other Continue versions or digests) are left alone, as
|
||||
* are run directories, logs, sessions and config files.
|
||||
*
|
||||
* Only names that match these exact patterns are touched, never anything
|
||||
* else in the user data directory. Failures (a file in use, permissions) are
|
||||
* ignored; the next start tries again.
|
||||
*/
|
||||
export type StaleRuntimeCacheCleanupOptions = {
|
||||
userDataPath: string
|
||||
appVersion: string
|
||||
continueHostLayoutVersion: number
|
||||
}
|
||||
|
||||
const appVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u
|
||||
const continueHostPattern = /^host-v(\d+)-/u
|
||||
|
||||
async function directoryNames(path: string): Promise<string[]> {
|
||||
try {
|
||||
return (await readdir(path, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
||||
.map((entry) => entry.name)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function staleRuntimeCacheDirectories(
|
||||
names: { opencode: readonly string[]; continueHost: readonly string[] },
|
||||
options: Pick<StaleRuntimeCacheCleanupOptions, 'appVersion' | 'continueHostLayoutVersion'>
|
||||
): { opencode: string[]; continueHost: string[] } {
|
||||
return {
|
||||
opencode: names.opencode.filter((name) =>
|
||||
appVersionPattern.test(name) && name !== options.appVersion),
|
||||
continueHost: names.continueHost.filter((name) => {
|
||||
const match = continueHostPattern.exec(name)
|
||||
return match !== null && Number(match[1]) < options.continueHostLayoutVersion
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeStaleRuntimeCaches(
|
||||
options: StaleRuntimeCacheCleanupOptions
|
||||
): Promise<{ removed: string[] }> {
|
||||
const opencodeRoot = join(options.userDataPath, 'opencode-runtime')
|
||||
const continueRoot = join(options.userDataPath, 'continue-host')
|
||||
const stale = staleRuntimeCacheDirectories({
|
||||
opencode: await directoryNames(opencodeRoot),
|
||||
continueHost: await directoryNames(continueRoot)
|
||||
}, options)
|
||||
const targets = [
|
||||
...stale.opencode.map((name) => join(opencodeRoot, name)),
|
||||
...stale.continueHost.map((name) => join(continueRoot, name))
|
||||
]
|
||||
const removed: string[] = []
|
||||
// One at a time: this runs in the background and should not compete for disk.
|
||||
for (const target of targets) {
|
||||
try {
|
||||
await rm(target, { recursive: true, force: true, maxRetries: 2, retryDelay: 200 })
|
||||
removed.push(target)
|
||||
} catch {
|
||||
// Left for the next start.
|
||||
}
|
||||
}
|
||||
return { removed }
|
||||
}
|
||||
Reference in New Issue
Block a user