Skip to content

Commit 320cd11

Browse files
Apply PR #40427: some experimental perf improvements
2 parents 838dc45 + 1b0e4e4 commit 320cd11

47 files changed

Lines changed: 1916 additions & 310 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/app/e2e/performance/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,32 @@ Benchmarks do not assert machine-dependent performance budgets. Streaming proces
5656

5757
Committed smoke and regression tests continue to own correctness coverage for pagination, tab paint, context resize, collapse state, and composer spacing.
5858

59+
## Desktop profiler
60+
61+
The desktop profiler launches the existing production build directly, creates isolated desktop state, chooses an available CDP port, and writes reports under the OS temporary directory by default.
62+
63+
```sh
64+
bun run profile:desktop --help
65+
```
66+
67+
Create a private partial snapshot from the default local database and run Home once:
68+
69+
```sh
70+
bun run profile:desktop --partial-snapshot-out /tmp/opencode-perf.db \
71+
--window-end 2026-08-04T06:14:26.878Z \
72+
--scenarios home,calibration --skip-build
73+
```
74+
75+
Repeat against the immutable partial snapshot:
76+
77+
```sh
78+
bun run profile:desktop --mode partial-snapshot --db /tmp/opencode-perf.db \
79+
--window-end 2026-08-04T06:14:26.878Z \
80+
--scenarios home,calibration --runs 3 --skip-build
81+
```
82+
83+
Partial snapshots contain private application data and must not be committed or shared. The profiler copies each partial snapshot to a per-run working database and remaps selected project paths to temporary workspaces, leaving the source snapshot unchanged. `PROFILE_SUMMARY` is the compact comparison output; `PROFILE_REPORT` points to the complete JSON report with the database hash, invocation parameters, raw runs, and attribution data.
84+
5985
## Chrome traces
6086

6187
Set `OPENCODE_PERFORMANCE_TRACE_DIR` to emit a standard Chrome DevTools trace for every benchmark page automatically:
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { Database } from "bun:sqlite"
2+
import { mkdir, rm } from "node:fs/promises"
3+
import path from "node:path"
4+
import { progress } from "./progress"
5+
import type { Options, Target } from "./types"
6+
7+
export async function createPartialSnapshot(source: string, destination: string, options: Options, targets: Target[]) {
8+
await mkdir(path.dirname(destination), { recursive: true })
9+
await rm(destination, { force: true })
10+
const input = new Database(source, { readonly: true })
11+
const schema = input
12+
.query(
13+
`SELECT type, name, sql FROM sqlite_schema
14+
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'
15+
ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 ELSE 2 END, name`,
16+
)
17+
.all() as { type: string; name: string; sql: string }[]
18+
input.close()
19+
20+
const output = new Database(destination, { create: true })
21+
output.run("PRAGMA foreign_keys = OFF")
22+
schema.filter((item) => item.type === "table").forEach((item) => output.run(item.sql))
23+
output.run("ATTACH DATABASE ? AS source", source)
24+
const selected = [...new Set(targets.map((target) => target.id))]
25+
const placeholders = selected.map(() => "?").join(",")
26+
27+
for (const table of schema.filter((item) => item.type === "table").map((item) => item.name)) {
28+
progress("copying partial snapshot table", { table })
29+
if (table === "event") continue
30+
if (table === "message") {
31+
output.run(
32+
`INSERT INTO main.message SELECT * FROM source.message
33+
WHERE (time_created >= ? AND time_created < ? AND session_id IN (
34+
SELECT id FROM source.session WHERE parent_id IS NULL
35+
)) OR session_id IN (${placeholders})`,
36+
options.windowStart,
37+
options.windowEnd,
38+
...selected,
39+
)
40+
continue
41+
}
42+
if (table === "part") {
43+
output.run("INSERT INTO main.part SELECT * FROM source.part WHERE message_id IN (SELECT id FROM main.message)")
44+
continue
45+
}
46+
if (["session_context_epoch", "session_input", "session_message", "session_share", "todo"].includes(table)) {
47+
output.run(
48+
`INSERT INTO main."${table}" SELECT * FROM source."${table}" WHERE session_id IN (${placeholders})`,
49+
...selected,
50+
)
51+
continue
52+
}
53+
output.run(`INSERT INTO main."${table}" SELECT * FROM source."${table}"`)
54+
}
55+
output.run("DETACH DATABASE source")
56+
schema.filter((item) => item.type !== "table").forEach((item) => output.run(item.sql))
57+
output.close()
58+
}
59+
60+
export async function fingerprint(file: string) {
61+
const input = Bun.file(file)
62+
const hasher = new Bun.CryptoHasher("sha256")
63+
for await (const chunk of input.stream()) hasher.update(chunk)
64+
return { bytes: input.size, sha256: hasher.digest("hex") }
65+
}
66+
67+
export function loadCorpus(options: Options) {
68+
const database = new Database(options.database, { readonly: true })
69+
database.run("PRAGMA query_only = ON")
70+
const sessions = database
71+
.query(
72+
`SELECT id, project_id AS projectID, directory, title
73+
FROM session AS candidate
74+
WHERE parent_id IS NULL
75+
AND EXISTS (
76+
SELECT 1 FROM message
77+
WHERE session_id = candidate.id AND time_created >= ? AND time_created < ?
78+
)`,
79+
)
80+
.all(options.windowStart, options.windowEnd) as { id: string; projectID: string; directory: string; title: string }[]
81+
const messageRows = database.query(
82+
`SELECT id, data FROM message
83+
WHERE session_id = ? AND time_created >= ? AND time_created < ?
84+
ORDER BY time_created, id`,
85+
)
86+
const partRows = database.query(`SELECT data FROM part WHERE message_id = ? ORDER BY id`)
87+
const ranked = sessions
88+
.map((session) => {
89+
const messages = messageRows.all(session.id, options.windowStart, options.windowEnd) as {
90+
id: string
91+
data: string
92+
}[]
93+
const parts = messages.flatMap((message) => partRows.all(message.id) as { data: string }[])
94+
return {
95+
...session,
96+
bytes:
97+
messages.reduce((sum, message) => sum + Buffer.byteLength(message.data), 0) +
98+
parts.reduce((sum, part) => sum + Buffer.byteLength(part.data), 0),
99+
messages: messages.length,
100+
parts: parts.length,
101+
userTurns: messages.filter((message) => JSON.parse(message.data).role === "user").length,
102+
}
103+
})
104+
.filter((session) => session.messages > 0)
105+
.sort((a, b) => a.bytes - b.bytes || a.id.localeCompare(b.id))
106+
if (ranked.length === 0) throw new Error("No sessions found in the profile window")
107+
const select = (label: Target["label"], percentile: number) => ({
108+
label,
109+
...ranked[Math.max(0, Math.ceil(ranked.length * percentile) - 1)]!,
110+
})
111+
const targets = [select("p50", 0.5), select("p95", 0.95), select("max", 1)] satisfies Target[]
112+
const typingText = loadTypingText(database, partRows, messageRows, targets[2]!, options)
113+
const projectIDs = [...new Set(ranked.map((session) => session.projectID))]
114+
database.close()
115+
return { targets, typingText, projectIDs }
116+
}
117+
118+
function loadTypingText(
119+
database: Database,
120+
partRows: ReturnType<Database["query"]>,
121+
messageRows: ReturnType<Database["query"]>,
122+
target: Target,
123+
options: Options,
124+
) {
125+
const messages = messageRows.all(target.id, options.windowStart, options.windowEnd) as { id: string; data: string }[]
126+
const text = messages
127+
.filter((message) => JSON.parse(message.data).role === "user")
128+
.flatMap((message) =>
129+
(partRows.all(message.id) as { data: string }[]).flatMap((part) => {
130+
const data = JSON.parse(part.data)
131+
return data.type === "text" && typeof data.text === "string" ? [data.text] : []
132+
}),
133+
)
134+
.sort((a, b) => b.length - a.length)[0]
135+
if (!text) throw new Error("No real user prompt found for composer profiling")
136+
return text
137+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Database } from "bun:sqlite"
2+
import { mkdir } from "node:fs/promises"
3+
import path from "node:path"
4+
import type { Options } from "./types"
5+
6+
export async function prepareDesktopState(
7+
options: Options,
8+
databasePath: string,
9+
userData: string,
10+
run: number,
11+
projectIDs: string[],
12+
) {
13+
const database = new Database(databasePath)
14+
const projects = database.query("SELECT id, worktree, sandboxes FROM project ORDER BY id").all() as {
15+
id: string
16+
worktree: string
17+
sandboxes: string
18+
}[]
19+
const selected = new Set(projectIDs)
20+
const profileProjects = projects.filter((project) => selected.has(project.id))
21+
const worktrees =
22+
options.mode === "partial-snapshot"
23+
? await remapDirectories(database, profileProjects, path.join(options.output, "workspaces", String(run)))
24+
: profileProjects.map((project) => project.worktree)
25+
database.close()
26+
27+
await Bun.write(
28+
path.join(userData, "opencode.settings"),
29+
JSON.stringify({ firstLaunchOnboardingComplete: true, oldLayoutEligible: true, tauriMigrated: true }),
30+
)
31+
await Bun.write(
32+
path.join(userData, "opencode.global.dat"),
33+
JSON.stringify({
34+
server: JSON.stringify({
35+
list: [],
36+
projects: { local: worktrees.map((worktree) => ({ worktree, expanded: true })) },
37+
lastProject: worktrees[0] ? { local: worktrees[0] } : {},
38+
recentlyClosed: {},
39+
}),
40+
}),
41+
)
42+
}
43+
44+
async function remapDirectories(
45+
database: Database,
46+
projects: { id: string; worktree: string; sandboxes: string }[],
47+
root: string,
48+
) {
49+
await mkdir(root, { recursive: true })
50+
const mappings = new Map<string, string>()
51+
const worktrees = await Promise.all(
52+
projects.map(async (project, index) => {
53+
const worktree = path.join(root, `project-${String(index + 1).padStart(3, "0")}`)
54+
await mkdir(worktree, { recursive: true })
55+
mappings.set(project.worktree, worktree)
56+
const sandboxes = JSON.parse(project.sandboxes) as string[]
57+
const nextSandboxes = await Promise.all(
58+
sandboxes.map(async (sandbox, sandboxIndex) => {
59+
const next = path.join(worktree, `sandbox-${sandboxIndex + 1}`)
60+
await mkdir(next, { recursive: true })
61+
mappings.set(sandbox, next)
62+
return next
63+
}),
64+
)
65+
database.run("UPDATE project SET worktree = ?, sandboxes = ? WHERE id = ?", worktree, JSON.stringify(nextSandboxes), project.id)
66+
return worktree
67+
}),
68+
)
69+
const byProject = new Map(projects.map((project, index) => [project.id, worktrees[index]!]))
70+
const sessions = database.query("SELECT id, project_id, directory FROM session").all() as {
71+
id: string
72+
project_id: string
73+
directory: string
74+
}[]
75+
const directories = database.query("SELECT * FROM project_directory").all() as {
76+
project_id: string
77+
directory: string
78+
type: string | null
79+
strategy: string | null
80+
time_created: number
81+
}[]
82+
const selected = new Set(projects.map((project) => project.id))
83+
const nextDirectories = await Promise.all(
84+
directories.filter((item) => selected.has(item.project_id)).map(async (item, index) => {
85+
const directory =
86+
mappings.get(item.directory) ?? path.join(byProject.get(item.project_id) ?? root, `directory-${index + 1}`)
87+
await mkdir(directory, { recursive: true })
88+
return { ...item, directory }
89+
}),
90+
)
91+
database.transaction(() => {
92+
sessions.filter((session) => selected.has(session.project_id)).forEach((session) =>
93+
database.run(
94+
"UPDATE session SET directory = ? WHERE id = ?",
95+
mappings.get(session.directory) ?? byProject.get(session.project_id) ?? worktrees[0]!,
96+
session.id,
97+
),
98+
)
99+
database.run(
100+
`DELETE FROM project_directory WHERE project_id IN (${projects.map(() => "?").join(",")})`,
101+
...projects.map((project) => project.id),
102+
)
103+
nextDirectories.forEach((item) =>
104+
database.run(
105+
`INSERT INTO project_directory (project_id, directory, type, strategy, time_created)
106+
VALUES (?, ?, ?, ?, ?)`,
107+
item.project_id,
108+
item.directory,
109+
item.type,
110+
item.strategy,
111+
item.time_created,
112+
),
113+
)
114+
})()
115+
return worktrees
116+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Database } from "bun:sqlite"
2+
import { afterAll, expect, test } from "bun:test"
3+
import { mkdir, rm } from "node:fs/promises"
4+
import path from "node:path"
5+
import { createPartialSnapshot, fingerprint } from "./corpus"
6+
import { parseOptions } from "./options"
7+
8+
const directory = path.join(import.meta.dir, `.tmp-${process.pid}`)
9+
const source = path.join(directory, "source.db")
10+
const partialSnapshot = path.join(directory, "partial-snapshot.db")
11+
await mkdir(directory, { recursive: true })
12+
const database = new Database(source, { create: true })
13+
database.run("CREATE TABLE sample (value TEXT NOT NULL)")
14+
database.run("INSERT INTO sample VALUES ('repeatable')")
15+
database.close()
16+
17+
afterAll(() => rm(directory, { recursive: true, force: true }))
18+
19+
test("parses a portable fixed-window partial snapshot invocation", () => {
20+
const options = parseOptions([
21+
"--mode",
22+
"partial-snapshot",
23+
"--db",
24+
source,
25+
"--window-end",
26+
"2026-08-04T06:14:26.878Z",
27+
"--window-hours",
28+
"24",
29+
"--scenarios",
30+
"home,calibration",
31+
"--runs",
32+
"3",
33+
"--skip-build",
34+
])!
35+
36+
expect(options.database).toBe(source)
37+
expect(options.windowEnd).toBe(1_785_824_066_878)
38+
expect(options.windowStart).toBe(1_785_737_666_878)
39+
expect(options.scenarios).toEqual(["home", "calibration"])
40+
expect(options.runs).toBe(3)
41+
expect(options.build).toBe(false)
42+
})
43+
44+
test("creates a consistent private partial database snapshot", async () => {
45+
const options = parseOptions(["--db", source, "--window-end", "2026-08-04T06:14:26.878Z"])!
46+
await createPartialSnapshot(source, partialSnapshot, options, [])
47+
const copy = new Database(partialSnapshot, { readonly: true })
48+
expect(copy.query("SELECT value FROM sample").get()).toEqual({ value: "repeatable" })
49+
copy.close()
50+
expect(await fingerprint(partialSnapshot)).toEqual({
51+
bytes: expect.any(Number),
52+
sha256: expect.stringMatching(/^[a-f0-9]{64}$/),
53+
})
54+
})

0 commit comments

Comments
 (0)