mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-29 05:53:24 +08:00
245 lines
9.4 KiB
TypeScript
245 lines
9.4 KiB
TypeScript
// Measures the heap retained by Location service graphs and by the models.dev
|
|
// source plugin. Everything runs against a temporary global directory with a
|
|
// temporary home, an in-memory database, no filesystem watchers, and no network,
|
|
// so it never touches a live server, database, or user configuration.
|
|
//
|
|
// bun run script/benchmark-location-memory.ts [--locations 6] [--plugins 8] [--json out.json]
|
|
//
|
|
// "retained" numbers are heapUsed after two forced GCs. RSS is reported separately.
|
|
// Source definitions and materialized candidates are counted independently: the
|
|
// source plugin alone may have no available providers in an unauthenticated run.
|
|
import fs from "fs/promises"
|
|
import os from "os"
|
|
import path from "path"
|
|
import { heapStats } from "bun:jsc"
|
|
import { Effect, Layer, Logger, Scope } from "effect"
|
|
import { FetchHttpClient } from "effect/unstable/http"
|
|
import { Global } from "@opencode/util/global"
|
|
import { LayerNode } from "@opencode/util/effect/layer-node"
|
|
import { AppNodeBuilder } from "../src/effect/app-node-builder"
|
|
import { Bus } from "../src/bus"
|
|
import { Database } from "../src/database/database"
|
|
import { Integration } from "../src/integration"
|
|
import { Location } from "../src/location"
|
|
import { LocationServiceMap } from "../src/location-service-map"
|
|
import { ModelsDev } from "../src/models-dev"
|
|
import { Model } from "../src/model"
|
|
import { Plugin } from "../src/plugin"
|
|
import { ModelsDevPlugin } from "../src/plugin/models-dev"
|
|
import { Provider } from "../src/provider"
|
|
import { AbsolutePath } from "../src/schema"
|
|
import { Watcher } from "../src/filesystem/watcher"
|
|
import { location } from "../test/fixture/location"
|
|
import { providerHost, host, integrationHost } from "../test/plugin/host"
|
|
|
|
const args = process.argv.slice(2)
|
|
const flag = (name: string, fallback: number) => {
|
|
const index = args.indexOf(`--${name}`)
|
|
if (index === -1) return fallback
|
|
const value = Number(args[index + 1])
|
|
if (!Number.isInteger(value) || value < 1) {
|
|
console.error(`--${name} must be a positive integer`)
|
|
process.exit(1)
|
|
}
|
|
return value
|
|
}
|
|
const locationCount = flag("locations", 6)
|
|
const pluginCount = flag("plugins", 8)
|
|
const jsonIndex = args.indexOf("--json")
|
|
const jsonPath = jsonIndex === -1 ? undefined : args[jsonIndex + 1]
|
|
|
|
type Sample = { heapUsed: number; rss: number; objects: number }
|
|
type Population = {
|
|
providers: number
|
|
definitions: number
|
|
availableProviders: number
|
|
models: number
|
|
availableModels: number
|
|
}
|
|
|
|
const sample = (): Sample => {
|
|
Bun.gc(true)
|
|
Bun.gc(true)
|
|
const usage = process.memoryUsage()
|
|
return { heapUsed: usage.heapUsed, rss: usage.rss, objects: heapStats().objectCount }
|
|
}
|
|
|
|
const mib = (bytes: number) => (bytes / 1024 / 1024).toFixed(2).padStart(8)
|
|
|
|
const median = (values: ReadonlyArray<number>) => {
|
|
const sorted = values.toSorted((a, b) => a - b)
|
|
const middle = Math.floor(sorted.length / 2)
|
|
const upper = sorted[middle] ?? 0
|
|
return sorted.length % 2 === 0 ? ((sorted[middle - 1] ?? 0) + upper) / 2 : upper
|
|
}
|
|
|
|
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-location-memory-")))
|
|
const globalLayer = Global.layerWith({
|
|
home: path.join(root, "home"),
|
|
data: path.join(root, "data"),
|
|
cache: path.join(root, "cache"),
|
|
config: path.join(root, "config"),
|
|
state: path.join(root, "state"),
|
|
tmp: path.join(root, "tmp"),
|
|
bin: path.join(root, "cache", "bin"),
|
|
log: path.join(root, "data", "log"),
|
|
repos: path.join(root, "data", "repos"),
|
|
})
|
|
const replacements = [
|
|
Global.node.replace(globalLayer),
|
|
ModelsDev.node.replace(ModelsDev.configured({ fetch: false })),
|
|
Watcher.node.replace(Watcher.configured({ enabled: false })),
|
|
]
|
|
|
|
const population = Effect.gen(function* () {
|
|
const providers = yield* Provider.Service
|
|
const models = yield* Model.Service
|
|
const snapshot = yield* providers.snapshot()
|
|
return {
|
|
providers: snapshot.records.size,
|
|
definitions: Array.from(snapshot.records.values()).reduce((total, record) => total + record.models.size, 0),
|
|
availableProviders: snapshot.available.length,
|
|
models: (yield* models.all()).length,
|
|
availableModels: (yield* models.available()).length,
|
|
} satisfies Population
|
|
})
|
|
|
|
// One full Location graph per directory, retained for the rest of the run, the
|
|
// way a long-running server retains every directory a client has touched.
|
|
const locationsProgram = Effect.gen(function* () {
|
|
const locations = yield* LocationServiceMap.Service
|
|
const scope = yield* Scope.Scope
|
|
const before = sample()
|
|
const deltas: number[] = []
|
|
const rss: number[] = []
|
|
const populations: Population[] = []
|
|
let previous = before
|
|
for (let index = 0; index < locationCount; index++) {
|
|
const directory = path.join(root, "projects", `location-${index}`)
|
|
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
|
const context = yield* locations
|
|
.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(directory) }))
|
|
.pipe(Scope.provide(scope))
|
|
const plugins = yield* Plugin.Service.pipe(Effect.provideContext(context))
|
|
yield* plugins.awaitActivation
|
|
// Force the actual read models before sampling, including lazy materialization.
|
|
populations.push(yield* population.pipe(Effect.provideContext(context)))
|
|
const current = sample()
|
|
deltas.push(current.heapUsed - previous.heapUsed)
|
|
rss.push(current.rss)
|
|
previous = current
|
|
}
|
|
return {
|
|
before,
|
|
after: previous,
|
|
deltas,
|
|
rss,
|
|
populations,
|
|
}
|
|
}).pipe(Effect.scoped)
|
|
|
|
// The models.dev plugin alone, against real Provider, Model and Integration states
|
|
// per instance, isolates shared definitions and materialization from the full graph.
|
|
const pluginProgram = Effect.gen(function* () {
|
|
const modelsDev = yield* ModelsDev.Service
|
|
const snapshot = yield* modelsDev.get()
|
|
const scope = yield* Scope.Scope
|
|
const before = sample()
|
|
const deltas: number[] = []
|
|
const populations: Population[] = []
|
|
let previous = before
|
|
for (let index = 0; index < pluginCount; index++) {
|
|
const directory = AbsolutePath.make(path.join(root, "plugins", `instance-${index}`))
|
|
const locationLayer = Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(location(Location.Ref.make({ directory }))),
|
|
)
|
|
const context = yield* Layer.build(
|
|
AppNodeBuilder.build(LayerNode.group([Provider.node, Model.node, Integration.node, Bus.node]), [
|
|
Location.node.replace(locationLayer),
|
|
...replacements,
|
|
]),
|
|
).pipe(Scope.provide(scope))
|
|
const providers = yield* Provider.Service.pipe(Effect.provideContext(context))
|
|
const integration = yield* Integration.Service.pipe(Effect.provideContext(context))
|
|
yield* ModelsDevPlugin.effect(
|
|
host({ provider: providerHost(providers), integration: integrationHost(integration) }),
|
|
).pipe(Effect.provideService(ModelsDev.Service, modelsDev), Effect.provideContext(context), Scope.provide(scope))
|
|
yield* integration.list()
|
|
populations.push(yield* population.pipe(Effect.provideContext(context)))
|
|
const current = sample()
|
|
deltas.push(current.heapUsed - previous.heapUsed)
|
|
previous = current
|
|
}
|
|
return {
|
|
before,
|
|
after: previous,
|
|
deltas,
|
|
populations,
|
|
snapshot: {
|
|
providers: snapshot.length,
|
|
models: snapshot.reduce((total, provider) => total + provider.models.length, 0),
|
|
},
|
|
}
|
|
}).pipe(Effect.scoped)
|
|
|
|
const program = Effect.gen(function* () {
|
|
const plugin = yield* pluginProgram.pipe(
|
|
Effect.provide(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, ModelsDev.node]), replacements)),
|
|
)
|
|
const locations = yield* locationsProgram.pipe(
|
|
Effect.provide(
|
|
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, LocationServiceMap.node]), replacements),
|
|
),
|
|
)
|
|
return { plugin, locations }
|
|
}).pipe(
|
|
Effect.provide(Logger.layer([])),
|
|
Effect.provideService(
|
|
FetchHttpClient.Fetch,
|
|
Object.assign(() => Promise.reject(new Error("Network disabled in location memory benchmark")), {
|
|
preconnect: () => {},
|
|
}),
|
|
),
|
|
)
|
|
|
|
const result = await Effect.runPromise(program)
|
|
await fs.rm(root, { recursive: true, force: true }).catch(() => undefined)
|
|
|
|
console.log(
|
|
`models.dev snapshot: ${result.plugin.snapshot.providers} providers, ${result.plugin.snapshot.models} models`,
|
|
)
|
|
console.log(`ModelsDevPlugin instances: ${pluginCount}`)
|
|
console.log(` populations: ${JSON.stringify(result.plugin.populations)}`)
|
|
console.log(` retained heap per instance (MiB): ${result.plugin.deltas.map((delta) => mib(delta).trim()).join(", ")}`)
|
|
console.log(` median per instance: ${mib(median(result.plugin.deltas))} MiB`)
|
|
console.log(`Location graphs: ${locationCount}`)
|
|
console.log(` populations: ${JSON.stringify(result.locations.populations)}`)
|
|
console.log(
|
|
` retained heap per location (MiB): ${result.locations.deltas.map((delta) => mib(delta).trim()).join(", ")}`,
|
|
)
|
|
console.log(` median per location: ${mib(median(result.locations.deltas))} MiB`)
|
|
console.log(
|
|
` heapUsed before ${mib(result.locations.before.heapUsed)} MiB -> after ${mib(result.locations.after.heapUsed)} MiB`,
|
|
)
|
|
console.log(` rss before ${mib(result.locations.before.rss)} MiB -> after ${mib(result.locations.after.rss)} MiB`)
|
|
|
|
if (jsonPath) {
|
|
await fs.mkdir(path.dirname(jsonPath), { recursive: true })
|
|
await fs.writeFile(
|
|
jsonPath,
|
|
JSON.stringify(
|
|
{
|
|
revision: process.env.OPENCODE_BENCH_REVISION,
|
|
bun: Bun.version,
|
|
bunRevision: Bun.revision,
|
|
locationCount,
|
|
pluginCount,
|
|
...result,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
}
|