mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f04d1e19c | |||
| b52d5a62a8 |
@@ -185,6 +185,33 @@ pmap -x <pid> | sort -k3 -nr | head -25
|
||||
|
||||
Heap serialization itself can temporarily increase RSS and allocator high-water marks, so record `ps`/`smaps_rollup` both before and after capture. Large anonymous mappings with a comparatively small live heap require native-allocation or allocator investigation; they cannot be explained from JavaScript retainer paths alone.
|
||||
|
||||
## CPU profiles
|
||||
|
||||
The CLI installs a `SIGPROF` listener on non-Windows processes in `packages/cli/src/cpu-profile.ts`. One signal starts a ten-second CPU profile and stops it automatically; additional signals are ignored while a profile is active. There is no CPU profile CLI flag or environment variable.
|
||||
|
||||
1. Get the PID from the health endpoint. For shared-service performance, target the server PID returned here rather than the short wrapper or TUI process:
|
||||
|
||||
```bash
|
||||
opencode2 api get /api/health
|
||||
```
|
||||
|
||||
Use `bun dev api get /api/health` instead when targeting the local/dev channel.
|
||||
|
||||
2. Start the capture:
|
||||
|
||||
```bash
|
||||
kill -PROF <server-pid>
|
||||
```
|
||||
|
||||
3. Wait for `CPU profile written` in the channel's log before opening the file. Profiles are written to the same log directory as `cpu-<pid>-<timestamp>.cpuprofile`; the log's `path=` field is authoritative:
|
||||
|
||||
```bash
|
||||
grep 'CPU profile' ~/.local/share/opencode/log/opencode.log | tail
|
||||
find ~/.local/share/opencode/log -maxdepth 1 -name 'cpu-<server-pid>-*.cpuprofile' -printf '%T@ %s %p\n' | sort -nr | head
|
||||
```
|
||||
|
||||
Use `opencode-local.log` for a local/dev process. Load the completed `.cpuprofile` in Chrome DevTools or another V8 CPU profile viewer and inspect the hottest functions, call stacks, and self time during the controlled workload.
|
||||
|
||||
## Debugger
|
||||
|
||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useFile } from "@/context/file"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { PROMPT_FILE_DRAG_TYPE } from "@opencode-ai/session-ui/v2/prompt-input/drag"
|
||||
import "@opencode-ai/ui/v2/file-tree-v2.css"
|
||||
import {
|
||||
createEffect,
|
||||
@@ -93,7 +92,6 @@ const FileTreeNodeV2 = (
|
||||
onDragStart={(event: DragEvent) => {
|
||||
if (!local.draggable) return
|
||||
event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
event.dataTransfer?.setData(PROMPT_FILE_DRAG_TYPE, local.node.path)
|
||||
event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
|
||||
withFileDragImage(event)
|
||||
|
||||
@@ -3,7 +3,6 @@ import { encodeFilePath } from "@/context/file/path"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { PROMPT_FILE_DRAG_TYPE } from "@opencode-ai/session-ui/v2/prompt-input/drag"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
@@ -158,7 +157,6 @@ const FileTreeNode = (
|
||||
onDragStart={(event: DragEvent) => {
|
||||
if (!local.draggable) return
|
||||
event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
event.dataTransfer?.setData(PROMPT_FILE_DRAG_TYPE, local.node.path)
|
||||
event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
|
||||
withFileDragImage(event)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { onMount } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { isPromptAttachmentDrag } from "@opencode-ai/session-ui/v2/prompt-input/drag"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -165,13 +164,13 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
|
||||
const handleGlobalDragOver = (event: DragEvent) => {
|
||||
if (input.isDialogActive()) return
|
||||
if (!isPromptAttachmentDrag(event)) return
|
||||
|
||||
event.preventDefault()
|
||||
const hasFiles = event.dataTransfer?.types.includes("Files")
|
||||
const hasText = event.dataTransfer?.types.includes("text/plain")
|
||||
if (hasFiles) {
|
||||
input.setDraggingType("image")
|
||||
} else {
|
||||
} else if (hasText) {
|
||||
input.setDraggingType("@mention")
|
||||
}
|
||||
}
|
||||
@@ -185,7 +184,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
|
||||
const handleGlobalDrop = async (event: DragEvent) => {
|
||||
if (input.isDialogActive()) return
|
||||
if (!isPromptAttachmentDrag(event)) return
|
||||
|
||||
event.preventDefault()
|
||||
input.setDraggingType(null)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -321,4 +320,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"),
|
||||
@@ -59,6 +60,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] : []),
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"./v2/*.css": "./src/v2/components/*.css",
|
||||
"./v2/*": "./src/v2/components/*.tsx",
|
||||
"./v2/prompt-input": "./src/v2/components/prompt-input/index.tsx",
|
||||
"./v2/prompt-input/drag": "./src/v2/components/prompt-input/drag.ts",
|
||||
"./v2/prompt-input/interaction": "./src/v2/components/prompt-input/interaction.ts",
|
||||
"./v2/prompt-input/store": "./src/v2/components/prompt-input/store.ts",
|
||||
"./v2/prompt-input/types": "./src/v2/components/prompt-input/types.ts"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { onMount } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import type { PromptInputV2Attachment, PromptInputV2Prompt } from "./types"
|
||||
import { isPromptAttachmentDrag } from "./drag"
|
||||
|
||||
const accepted = [
|
||||
"image/png",
|
||||
@@ -178,7 +177,6 @@ export function createPromptInputV2Attachments(
|
||||
}
|
||||
const handleDrop = async (event: DragEvent) => {
|
||||
if (input.isDialogActive()) return
|
||||
if (!isPromptAttachmentDrag(event)) return
|
||||
event.preventDefault()
|
||||
input.setDraggingType(null)
|
||||
const plainText = event.dataTransfer?.getData("text/plain")
|
||||
@@ -195,10 +193,9 @@ export function createPromptInputV2Attachments(
|
||||
onMount(() => {
|
||||
makeEventListener(document, "dragover", (event) => {
|
||||
if (input.isDialogActive()) return
|
||||
if (!isPromptAttachmentDrag(event)) return
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer?.types.includes("Files")) input.setDraggingType("image")
|
||||
else input.setDraggingType("@mention")
|
||||
else if (event.dataTransfer?.types.includes("text/plain")) input.setDraggingType("@mention")
|
||||
})
|
||||
makeEventListener(document, "dragleave", (event) => {
|
||||
if (!input.isDialogActive() && !event.relatedTarget) input.setDraggingType(null)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { isPromptAttachmentDrag, PROMPT_FILE_DRAG_TYPE } from "./drag"
|
||||
|
||||
describe("prompt attachment drag", () => {
|
||||
test("ignores ordinary text and link drags", () => {
|
||||
expect(drag(["text/plain"])).toBe(false)
|
||||
expect(drag(["text/plain", "text/uri-list"])).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts files and OpenCode file tree entries", () => {
|
||||
expect(drag(["Files"])).toBe(true)
|
||||
expect(drag(["text/plain", PROMPT_FILE_DRAG_TYPE])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
function drag(types: string[]) {
|
||||
return isPromptAttachmentDrag({ dataTransfer: { types } as unknown as DataTransfer })
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export const PROMPT_FILE_DRAG_TYPE = "application/x-opencode-file"
|
||||
|
||||
export function isPromptAttachmentDrag(event: Pick<DragEvent, "dataTransfer">) {
|
||||
const types = event.dataTransfer?.types
|
||||
if (!types) return false
|
||||
return types.includes("Files") || types.includes(PROMPT_FILE_DRAG_TYPE)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
class="group/prompt-input relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
|
||||
classList={{
|
||||
"shadow-[var(--v2-elevation-raised)]": !props.borderUnderlay,
|
||||
"outline outline-1 outline-v2-icon-icon-info outline-dashed": state.drag === "active",
|
||||
"border border-v2-icon-icon-info border-dashed": state.drag === "active",
|
||||
}}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createEffect, on, type Accessor } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { createPromptInputV2Attachments, type PromptInputV2AttachmentConfig } from "./attachments"
|
||||
import { isPromptAttachmentDrag } from "./drag"
|
||||
import { createPromptInputV2Store, type PromptInputV2StoreInput } from "./store"
|
||||
import type {
|
||||
PromptInputV2Attachment,
|
||||
@@ -401,19 +400,16 @@ export function createPromptInputV2Controller(input: {
|
||||
target.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertFromPaste", data: text }))
|
||||
},
|
||||
onDragEnter(event: DragEvent) {
|
||||
if (attachments && !isPromptAttachmentDrag(event)) return
|
||||
event.preventDefault()
|
||||
dispatch({ type: "drag.enter" })
|
||||
},
|
||||
onDragOver(event: DragEvent) {
|
||||
if (attachments && !isPromptAttachmentDrag(event)) return
|
||||
event.preventDefault()
|
||||
},
|
||||
onDragLeave() {
|
||||
dispatch({ type: "drag.leave" })
|
||||
},
|
||||
onDrop(event: DragEvent) {
|
||||
if (attachments && !isPromptAttachmentDrag(event)) return
|
||||
event.preventDefault()
|
||||
dispatch({ type: "drag.leave" })
|
||||
if (attachments) {
|
||||
|
||||
Reference in New Issue
Block a user