Skip to content

Commit f0000b3

Browse files
edyedy
authored andcommitted
perf: implement lazy command loading to improve CLI startup time and add a startup benchmark script.
1 parent 76eee23 commit f0000b3

5 files changed

Lines changed: 83 additions & 27 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bun
2+
3+
import { spawnSync } from "child_process"
4+
import path from "path"
5+
6+
const dir = path.resolve(import.meta.dirname, "..")
7+
8+
const commands = [
9+
["--version"],
10+
["--help"],
11+
["completion"],
12+
["debug", "paths"],
13+
]
14+
15+
console.log("=== Startup Benchmark ===")
16+
for (const cmd of commands) {
17+
const times: number[] = []
18+
for (let i = 0; i < 5; i++) {
19+
const start = performance.now()
20+
spawnSync("bun", ["run", "--conditions=browser", "src/index.ts", ...cmd], {
21+
cwd: dir,
22+
stdio: "ignore",
23+
})
24+
const end = performance.now()
25+
times.push(end - start)
26+
}
27+
times.sort((a, b) => a - b)
28+
const median = times[Math.floor(times.length / 2)]
29+
console.log(`${cmd.join(" ").padEnd(20)} median: ${median.toFixed(2)}ms (${times.map((t) => t.toFixed(0)).join(", ")} ms)`)
30+
}

packages/opencode/src/cli/cmd/tui.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,8 @@ export function resolveThreadDirectory(project?: string, envPWD = process.env.PW
6969
return Filesystem.resolve(cwd)
7070
}
7171

72-
export const TuiThreadCommand = cmd({
73-
command: "$0 [project]",
74-
describe: "start opencode tui",
75-
builder: (yargs) =>
76-
withNetworkOptions(yargs)
72+
export const tuiBuilder = (yargs: any) =>
73+
withNetworkOptions(yargs)
7774
.positional("project", {
7875
type: "string",
7976
describe: "path to start opencode in",
@@ -140,7 +137,12 @@ export const TuiThreadCommand = cmd({
140137
.option("demo", {
141138
type: "boolean",
142139
hidden: true,
143-
}),
140+
})
141+
142+
export const TuiThreadCommand = cmd({
143+
command: "$0 [project]",
144+
describe: "start opencode tui",
145+
builder: tuiBuilder,
144146
handler: async (args) => {
145147
if (args.replay === true) {
146148
UI.error("--replay is not supported; replay is enabled by default")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { CommandModule } from "yargs"
2+
3+
export function lazyCommand<T = {}>(
4+
moduleObj: CommandModule<{}, T>,
5+
loader: () => Promise<any>,
6+
exportName?: string,
7+
): CommandModule<{}, T> {
8+
return {
9+
command: moduleObj.command,
10+
aliases: moduleObj.aliases,
11+
describe: moduleObj.describe,
12+
builder: moduleObj.builder as any,
13+
async handler(args) {
14+
const mod = await loader()
15+
const target = exportName ? mod[exportName] : (mod.default || Object.values(mod)[0])
16+
return target.handler(args)
17+
},
18+
}
19+
}

packages/opencode/src/cli/ui.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { EOL } from "os"
2-
import { Schema } from "effect"
32
import { logo as glyphs } from "./logo"
43

54
const wordmark = [
@@ -9,7 +8,10 @@ const wordmark = [
98
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
109
]
1110

12-
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("UICancelledError", {}) {}
11+
export class CancelledError extends Error {
12+
readonly _tag = "UICancelledError"
13+
override readonly name = "UICancelledError"
14+
}
1315

1416
export const Style = {
1517
TEXT_HIGHLIGHT: "\x1b[96m",

packages/opencode/src/index.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import { PluginCommand } from "./cli/cmd/plug"
1818
import { DbCommand } from "./cli/cmd/db"
1919
import { UI } from "./cli/ui"
2020
import { InstallationVersion } from "@opencode-ai/core/installation/version"
21-
import { FormatError } from "./cli/error"
2221
import { EOL } from "os"
2322
import { errorMessage } from "./util/error"
24-
import { Heap } from "./cli/heap"
23+
import { lazyCommand } from "./cli/lazy-command"
2524

2625
const args = hideBin(process.argv)
2726

@@ -63,7 +62,10 @@ const cli = yargs(args)
6362
process.env.OPENCODE_PURE = "1"
6463
}
6564

66-
Heap.start()
65+
if (process.env.OPENCODE_AUTO_HEAP_SNAPSHOT) {
66+
const { Heap } = await import("./cli/heap")
67+
Heap.start()
68+
}
6769

6870
process.env.AGENT = "1"
6971
process.env.OPENCODE = "1"
@@ -75,22 +77,22 @@ const cli = yargs(args)
7577
})
7678
.usage("")
7779
.completion("completion", "generate shell completion script")
78-
.command(McpCommand)
79-
.command(TuiThreadCommand)
80-
.command(AttachCommand)
81-
.command(RunCommand)
82-
.command(GenerateCommand)
83-
.command(DebugCommand)
84-
.command(ProvidersCommand)
85-
.command(AgentCommand)
86-
.command(UpgradeCommand)
87-
.command(UninstallCommand)
88-
.command(ServeCommand)
89-
.command(ModelsCommand)
90-
.command(ExportCommand)
91-
.command(SessionCommand)
92-
.command(PluginCommand)
93-
.command(DbCommand)
80+
.command(lazyCommand(McpCommand, () => import("./cli/cmd/mcp"), "McpCommand"))
81+
.command(lazyCommand(TuiThreadCommand, () => import("./cli/cmd/tui"), "TuiThreadCommand"))
82+
.command(lazyCommand(AttachCommand, () => import("./cli/cmd/attach"), "AttachCommand"))
83+
.command(lazyCommand(RunCommand, () => import("./cli/cmd/run"), "RunCommand"))
84+
.command(lazyCommand(GenerateCommand, () => import("./cli/cmd/generate"), "GenerateCommand"))
85+
.command(lazyCommand(DebugCommand, () => import("./cli/cmd/debug"), "DebugCommand"))
86+
.command(lazyCommand(ProvidersCommand, () => import("./cli/cmd/providers"), "ProvidersCommand"))
87+
.command(lazyCommand(AgentCommand, () => import("./cli/cmd/agent"), "AgentCommand"))
88+
.command(lazyCommand(UpgradeCommand, () => import("./cli/cmd/upgrade"), "UpgradeCommand"))
89+
.command(lazyCommand(UninstallCommand, () => import("./cli/cmd/uninstall"), "UninstallCommand"))
90+
.command(lazyCommand(ServeCommand, () => import("./cli/cmd/serve"), "ServeCommand"))
91+
.command(lazyCommand(ModelsCommand, () => import("./cli/cmd/models"), "ModelsCommand"))
92+
.command(lazyCommand(ExportCommand, () => import("./cli/cmd/export"), "ExportCommand"))
93+
.command(lazyCommand(SessionCommand, () => import("./cli/cmd/session"), "SessionCommand"))
94+
.command(lazyCommand(PluginCommand, () => import("./cli/cmd/plug"), "PluginCommand"))
95+
.command(lazyCommand(DbCommand, () => import("./cli/cmd/db"), "DbCommand"))
9496
.fail((msg, err) => {
9597
if (
9698
msg?.startsWith("Unknown argument") ||
@@ -116,6 +118,7 @@ try {
116118
await cli.parse()
117119
}
118120
} catch (e) {
121+
const { FormatError } = await import("./cli/error")
119122
const formatted = FormatError(e)
120123
if (formatted) UI.error(formatted)
121124
if (formatted === undefined) {

0 commit comments

Comments
 (0)