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 { scopedReadToolNames } from '../../shared/scoped-data-tools'
|
||||||
import { readBoundedFile } from '../workspace-file-access'
|
import { readBoundedFile } from '../workspace-file-access'
|
||||||
import { terminateProcessTreeAndWait } from './child-process-termination'
|
import { terminateProcessTreeAndWait } from './child-process-termination'
|
||||||
|
import { CONTINUE_HOST_LAYOUT_VERSION } from './continue-host-layout'
|
||||||
|
|
||||||
const supportedVersion = '1.5.47'
|
const supportedVersion = '1.5.47'
|
||||||
const supportedBundleHashes = new Set([
|
const supportedBundleHashes = new Set([
|
||||||
@@ -1052,7 +1053,7 @@ export class ContinueHostAdapter {
|
|||||||
const digest = sourceHash.slice(0, 16)
|
const digest = sourceHash.slice(0, 16)
|
||||||
const targetRoot = join(
|
const targetRoot = join(
|
||||||
this.options.cacheRoot,
|
this.options.cacheRoot,
|
||||||
`host-v7-${supportedVersion}-${digest}`
|
`host-v${CONTINUE_HOST_LAYOUT_VERSION}-${supportedVersion}-${digest}`
|
||||||
)
|
)
|
||||||
const targetDist = join(targetRoot, 'dist')
|
const targetDist = join(targetRoot, 'dist')
|
||||||
const targetBundle = join(targetDist, 'index.js')
|
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 { createTrayIcon } from './tray-icon'
|
||||||
import { resolveBundledRuntimePaths } from './agent/bundled-runtimes'
|
import { resolveBundledRuntimePaths } from './agent/bundled-runtimes'
|
||||||
import type { ContinueHostLauncher } from './agent/continue-host-adapter'
|
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 { resolvePortableUserDataPath } from './portable-user-data'
|
||||||
import { BrowserService } from './browser/browser-service'
|
import { BrowserService } from './browser/browser-service'
|
||||||
import { SubagentService } from './assistant/subagent-service'
|
import { SubagentService } from './assistant/subagent-service'
|
||||||
@@ -1626,6 +1628,14 @@ if (hasSingleInstanceLock) {
|
|||||||
startupMark('main:load-main-window')
|
startupMark('main:load-main-window')
|
||||||
removeDeviceSharingIpc = registerDeviceSharingIpc(mainWindow,
|
removeDeviceSharingIpc = registerDeviceSharingIpc(mainWindow,
|
||||||
new DeviceSharingService(join(app.getPath('userData'), 'device-sharing-settings.json'), app.getVersion()))
|
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(() => {
|
setImmediate(() => {
|
||||||
void repairStaleWindowsNotificationShortcuts({
|
void repairStaleWindowsNotificationShortcuts({
|
||||||
platform: process.platform,
|
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