mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-22 03:13:25 +00:00
feat(cli): capture CPU profiles with SIGPROF (#43446)
This commit is contained in:
parent
241a88a5d9
commit
7700faad81
9 changed files with 70 additions and 60 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { Argument, Command, Flag } from "effect/unstable/cli"
|
||||
import { Argument, Flag } from "effect/unstable/cli"
|
||||
import { Spec } from "../framework/spec"
|
||||
import { GlobalFlags } from "./global-flags"
|
||||
|
||||
declare const OPENCODE_CLI_NAME: string | undefined
|
||||
|
||||
|
|
@ -343,4 +342,4 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
|||
],
|
||||
})
|
||||
|
||||
export const Commands = { ...Root, spec: Root.spec.pipe(Command.withGlobalFlags(GlobalFlags.all)) }
|
||||
export const Commands = Root
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
export * as GlobalFlags from "./global-flags"
|
||||
|
||||
import { Flag, GlobalFlag } from "effect/unstable/cli"
|
||||
|
||||
export const CpuProfile = GlobalFlag.setting("cpu-profile")({
|
||||
flag: Flag.string("cpu-profile").pipe(
|
||||
Flag.withDescription("Write a CPU profile to this path when the process stops"),
|
||||
Flag.optional,
|
||||
),
|
||||
})
|
||||
|
||||
export const all = [CpuProfile] as const
|
||||
|
|
@ -1,10 +1,36 @@
|
|||
export * as CpuProfile from "./cpu-profile"
|
||||
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, FileSystem, Queue } from "effect"
|
||||
import { Session } from "node:inspector"
|
||||
import path from "node:path"
|
||||
|
||||
export function run<A, E, R>(file: string, effect: Effect.Effect<A, E, R>) {
|
||||
export const listen = Effect.gen(function* () {
|
||||
const global = yield* Global.Service
|
||||
if (process.platform === "win32") return
|
||||
const signals = yield* Queue.dropping<void>(1)
|
||||
yield* Effect.acquireRelease(
|
||||
Effect.sync(() => {
|
||||
const handler = () => Queue.offerUnsafe(signals, undefined)
|
||||
process.on("SIGPROF", handler)
|
||||
return handler
|
||||
}),
|
||||
(handler) => Effect.sync(() => process.off("SIGPROF", handler)),
|
||||
)
|
||||
yield* Effect.gen(function* () {
|
||||
yield* Queue.take(signals)
|
||||
const file = path.join(
|
||||
global.log,
|
||||
`cpu-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.cpuprofile`,
|
||||
)
|
||||
yield* run(file, Effect.sleep("10 seconds")).pipe(
|
||||
Effect.catchCause((cause) => Effect.logError("Failed to capture CPU profile", { path: file, cause })),
|
||||
)
|
||||
yield* Queue.poll(signals)
|
||||
}).pipe(Effect.forever, Effect.forkScoped({ startImmediately: true }))
|
||||
})
|
||||
|
||||
function run<A, E, R>(file: string, effect: Effect.Effect<A, E, R>) {
|
||||
const target = path.resolve(file)
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { Effect, FileSystem, Option, Scope } from "effect"
|
||||
import { Effect, FileSystem, Scope } from "effect"
|
||||
import { Command } from "effect/unstable/cli"
|
||||
import { Spec } from "./spec"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Updater } from "../services/updater"
|
||||
import { Config } from "../config"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { GlobalFlags } from "../commands/global-flags"
|
||||
import { CpuProfile } from "../cpu-profile"
|
||||
import path from "node:path"
|
||||
|
||||
export type Input<Value> =
|
||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||
|
|
@ -90,21 +87,7 @@ function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): Provided
|
|||
Command.withHandler((input) =>
|
||||
Effect.gen(function* () {
|
||||
const module = yield* Effect.promise(handler.load)
|
||||
const cpuProfile = Option.getOrUndefined(yield* GlobalFlags.CpuProfile)
|
||||
if (!cpuProfile) return yield* module.default(input)
|
||||
const target = path.resolve(cpuProfile)
|
||||
const previous = process.env.OPENCODE_CPU_PROFILE
|
||||
process.env.OPENCODE_CPU_PROFILE = target
|
||||
return yield* (
|
||||
node.name === "serve" ? CpuProfile.run(target, module.default(input)) : module.default(input)
|
||||
).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (previous === undefined) delete process.env.OPENCODE_CPU_PROFILE
|
||||
else process.env.OPENCODE_CPU_PROFILE = previous
|
||||
}),
|
||||
),
|
||||
)
|
||||
return yield* module.default(input)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { AppProcess } from "@opencode-ai/util/process"
|
|||
import { Config } from "./config"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { Heap } from "./heap"
|
||||
import { CpuProfile } from "./cpu-profile"
|
||||
|
||||
const Handlers = Runtime.handlers(Commands, {
|
||||
$: () => import("./commands/handlers/default"),
|
||||
|
|
@ -61,6 +62,7 @@ const Handlers = Runtime.handlers(Commands, {
|
|||
|
||||
Effect.gen(function* () {
|
||||
yield* Heap.listen
|
||||
yield* CpuProfile.listen
|
||||
const runFork = Effect.runForkWith(yield* Effect.context<never>())
|
||||
const uncaughtException = (cause: Error, origin: "uncaughtException" | "unhandledRejection") => {
|
||||
runFork(Effect.logError("uncaught exception", { cause, origin }))
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ export const options = Effect.fnUntraced(function* (input: { readonly checkVersi
|
|||
...selfCommand(),
|
||||
"serve",
|
||||
"--service",
|
||||
...(process.env.OPENCODE_CPU_PROFILE ? ["--cpu-profile", process.env.OPENCODE_CPU_PROFILE] : []),
|
||||
],
|
||||
}
|
||||
})
|
||||
|
|
|
|||
18
packages/cli/test/cpu-profile.test.ts
Normal file
18
packages/cli/test/cpu-profile.test.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CpuProfile } from "../src/cpu-profile"
|
||||
|
||||
test("subscribes and unsubscribes SIGPROF with the CLI scope", async () => {
|
||||
const listeners = process.listenerCount("SIGPROF")
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* CpuProfile.listen
|
||||
expect(process.listenerCount("SIGPROF")).toBe(listeners + (process.platform === "win32" ? 0 : 1))
|
||||
}),
|
||||
).pipe(Effect.provideService(Global.Service, Global.make()), Effect.provide(NodeFileSystem.layer)),
|
||||
)
|
||||
expect(process.listenerCount("SIGPROF")).toBe(listeners)
|
||||
})
|
||||
|
|
@ -19,29 +19,6 @@ test("managed service ports are stable per installation channel", () => {
|
|||
expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b"))
|
||||
})
|
||||
|
||||
test("managed service forwards the CPU profile path to the server", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-profile-"))
|
||||
const profile = path.join(root, "server.cpuprofile")
|
||||
try {
|
||||
const previous = process.env.OPENCODE_CPU_PROFILE
|
||||
process.env.OPENCODE_CPU_PROFILE = profile
|
||||
try {
|
||||
const options = await Effect.runPromise(
|
||||
ServiceConfig.options().pipe(
|
||||
Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })),
|
||||
Effect.provide(NodeFileSystem.layer),
|
||||
),
|
||||
)
|
||||
expect(options.command.slice(-2)).toEqual(["--cpu-profile", profile])
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCODE_CPU_PROFILE
|
||||
else process.env.OPENCODE_CPU_PROFILE = previous
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("local channel stores service config with the local service filename", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -236,6 +236,24 @@ problem belongs to the client, the shared server, or one project.
|
|||
- Redact API keys, authorization headers, prompts, file contents, and other
|
||||
sensitive data before sharing diagnostics.
|
||||
|
||||
### CPU profiles
|
||||
|
||||
On Linux and macOS, send `SIGPROF` to a running OpenCode process to capture its
|
||||
CPU activity. Get the background server PID from the health endpoint, then send
|
||||
the signal:
|
||||
|
||||
```sh
|
||||
opencode2 api get /api/health
|
||||
kill -SIGPROF <pid>
|
||||
```
|
||||
|
||||
One signal starts a ten-second profile and stops it automatically. OpenCode
|
||||
writes the result to its log directory as
|
||||
`cpu-<pid>-<timestamp>.cpuprofile` and logs the complete path. Additional
|
||||
`SIGPROF` signals are ignored while a profile is active. Signal-triggered CPU
|
||||
profiles are unavailable on Windows. There is no CPU profile CLI flag or
|
||||
environment variable.
|
||||
|
||||
See the [full troubleshooting guide](https://opencode.ai/v2/docs/troubleshooting)
|
||||
for service lifecycle commands, API inspection, log locations, explicit server
|
||||
connections, issue-reporting details, and local development paths.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue