perf(tooling): reach a fully populated sidebar in both profilers
Idle and streaming cost both depend on how much of the sidebar is mounted, so the scenario setup is now shared. --expand-projects seeds the persisted collapse state; --expand-sessions clicks every "Show more sessions" control, which cannot be seeded because pagination is component state. Both run before the measured window, so it stays input-free. Session expansion must run after the sidebar has populated, not straight after the load event, or the controls do not exist yet. Also replaces a spread push over collected trace events, which overflowed the call stack once a populated sidebar produced chunks of hundreds of thousands of events, and the equivalent spread in the heap-maximum calculation.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Scenario setup shared by the idle and streaming profilers.
|
||||
*
|
||||
* Idle and streaming cost both depend on how much of the sidebar is mounted,
|
||||
* so both commands need the same way to reach a heavily populated sidebar.
|
||||
* Setup always runs before the measured window.
|
||||
*/
|
||||
|
||||
import { evaluateValue, wait } from "./cdp.mjs"
|
||||
|
||||
/**
|
||||
* Expands every project. The sidebar persists the ids of collapsed projects,
|
||||
* so an empty list expands everything. Requires a reload to take effect.
|
||||
*/
|
||||
export const expandProjects = async (client) => {
|
||||
await evaluateValue(client, `localStorage.setItem("oc.sessions.projectCollapse", "[]")`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks every "Show more sessions" control until none remain.
|
||||
*
|
||||
* Session list pagination is component state, so unlike project collapse it
|
||||
* cannot be seeded through storage. The controls only exist once the sidebar
|
||||
* has populated, so call this after the page has settled, never straight after
|
||||
* the load event.
|
||||
*
|
||||
* Matching is a case-insensitive substring test, which assumes the English UI
|
||||
* locale; a non-English locale expands nothing and reports zero.
|
||||
*/
|
||||
export const expandSessionLists = async (client, { passes = 40, settleMs = 400 } = {}) => {
|
||||
let totalClicked = 0
|
||||
for (let pass = 0; pass < passes; pass += 1) {
|
||||
const clicked = await evaluateValue(client, `(() => {
|
||||
const controls = [...document.querySelectorAll("button")]
|
||||
.filter((button) => (button.textContent ?? "").toLowerCase().includes("show more"))
|
||||
for (const control of controls) control.click()
|
||||
return controls.length
|
||||
})()`)
|
||||
if (!clicked) break
|
||||
totalClicked += clicked
|
||||
await wait(settleMs)
|
||||
}
|
||||
return totalClicked
|
||||
}
|
||||
+12
-17
@@ -29,6 +29,7 @@ import process from "node:process"
|
||||
import { CdpClient, createPageTarget, evaluateValue, launchChrome, reservePort, resolveChrome, wait } from "./perf/cdp.mjs"
|
||||
import { buildIdleProbeSource, IDLE_PROBE_GLOBAL } from "./perf/idle-probe.mjs"
|
||||
import { summarizeCpuProfile } from "./perf/cpu-profile.mjs"
|
||||
import { expandProjects, expandSessionLists } from "./perf/scenario.mjs"
|
||||
import { growthPerSecond, metricMap, round } from "./perf/metrics.mjs"
|
||||
|
||||
const HELP = `Usage: bun run profile:idle -- [options]
|
||||
@@ -42,6 +43,10 @@ Options:
|
||||
--then-tab <name> After settling, navigate to this tab without a
|
||||
reload, then record. Use it to measure what a
|
||||
surface keeps doing after the user leaves it.
|
||||
--expand-sessions Click every "Show more sessions" control until the
|
||||
sidebar has no collapsed session lists left, so all
|
||||
session rows are mounted. Clicks happen before the
|
||||
recording window, which stays input-free.
|
||||
--expand-projects Expand every project in the sidebar before
|
||||
recording, which mounts a row per worktree and
|
||||
session directory
|
||||
@@ -74,6 +79,7 @@ const parseArgs = (argv) => {
|
||||
thenTab: null,
|
||||
panels: [],
|
||||
expandProjects: false,
|
||||
expandSessions: false,
|
||||
duration: 30,
|
||||
settle: 15,
|
||||
output: null,
|
||||
@@ -99,6 +105,7 @@ const parseArgs = (argv) => {
|
||||
else if (value === "--then-tab") options.thenTab = argv[++index]
|
||||
else if (value === "--panel") options.panels.push(argv[++index])
|
||||
else if (value === "--expand-projects") options.expandProjects = true
|
||||
else if (value === "--expand-sessions") options.expandSessions = true
|
||||
else if (value === "--label") options.label = argv[++index]
|
||||
else if (value === "--duration") options.duration = Number(argv[++index])
|
||||
else if (value === "--settle") options.settle = Number(argv[++index])
|
||||
@@ -243,7 +250,7 @@ const buildSummary = ({ options, before, after, samples, cpu, probe, elapsedSeco
|
||||
frames: Number(after.Frames ?? 0),
|
||||
heapStartMb: round(heapSamples.at(0) ?? 0),
|
||||
heapEndMb: round(heapSamples.at(-1) ?? 0),
|
||||
heapMaxMb: round(Math.max(0, ...heapSamples)),
|
||||
heapMaxMb: round(heapSamples.reduce((max, value) => Math.max(max, value), 0)),
|
||||
heapGrowthMbPerSecond: growthPerSecond(samples, "jsHeapUsedMb"),
|
||||
},
|
||||
cpuProfile: cpu,
|
||||
@@ -357,10 +364,7 @@ const main = async () => {
|
||||
await loaded
|
||||
|
||||
if (options.expandProjects) {
|
||||
// The sidebar persists the ids of collapsed projects, so an empty list
|
||||
// expands everything. This is the state in issue #1472: one mounted
|
||||
// directory-bound row per worktree and session.
|
||||
await evaluateValue(client, `localStorage.setItem("oc.sessions.projectCollapse", "[]")`)
|
||||
await expandProjects(client)
|
||||
console.log("Expanded every project in the sidebar.")
|
||||
}
|
||||
|
||||
@@ -374,18 +378,9 @@ const main = async () => {
|
||||
console.log(`Loaded ${options.url}; settling for ${options.settle}s before recording.`)
|
||||
await wait(options.settle * 1000)
|
||||
|
||||
if (options.thenTab) {
|
||||
// Route changes normally come from clicks; driving history directly
|
||||
// reaches the same router path without generating input work inside the
|
||||
// recorded window, and leaves already-mounted surfaces mounted.
|
||||
await evaluateValue(client, `(() => {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set("tab", ${JSON.stringify(options.thenTab)})
|
||||
window.history.pushState({}, "", url.toString())
|
||||
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
|
||||
return true
|
||||
})()`)
|
||||
console.log(`Navigated to tab "${options.thenTab}" without reloading; settling ${options.settle}s again.`)
|
||||
if (options.expandSessions) {
|
||||
const expanded = await expandSessionLists(client)
|
||||
console.log(`Expanded ${expanded} collapsed session lists; settling ${options.settle}s again.`)
|
||||
await wait(options.settle * 1000)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { CdpClient, createPageTarget, evaluateValue, launchChrome, reservePort,
|
||||
import { buildIdleProbeSource, IDLE_PROBE_GLOBAL } from "./perf/idle-probe.mjs"
|
||||
import { summarizeCpuProfile } from "./perf/cpu-profile.mjs"
|
||||
import { growthPerSecond, metricMap, round, summarizeLongTasks, summarizeTraceEvents } from "./perf/metrics.mjs"
|
||||
import { expandProjects, expandSessionLists } from "./perf/scenario.mjs"
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
||||
const cliPath = join(repoRoot, "packages/web/bin/cli.js")
|
||||
@@ -47,6 +48,9 @@ Options:
|
||||
--port <port> OpenChamber CLI port (default: from --url)
|
||||
--dir <path> Session directory (default: repository root)
|
||||
--session <id> Reuse this session instead of creating one
|
||||
--expand-projects Expand every project in the sidebar before recording
|
||||
--expand-sessions Click every "Show more sessions" control before
|
||||
recording, so all session rows are mounted
|
||||
--view-session <id> Display this session while the prompt streams into
|
||||
another one. Measures what an idle session costs
|
||||
while a different session is active in the
|
||||
@@ -79,6 +83,8 @@ const parseArgs = (argv) => {
|
||||
dir: repoRoot,
|
||||
session: null,
|
||||
viewSession: null,
|
||||
expandProjects: false,
|
||||
expandSessions: false,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
model: null,
|
||||
agent: null,
|
||||
@@ -108,6 +114,8 @@ const parseArgs = (argv) => {
|
||||
else if (value === "--dir") options.dir = argv[++index]
|
||||
else if (value === "--session") options.session = argv[++index]
|
||||
else if (value === "--view-session") options.viewSession = argv[++index]
|
||||
else if (value === "--expand-projects") options.expandProjects = true
|
||||
else if (value === "--expand-sessions") options.expandSessions = true
|
||||
else if (value === "--prompt") options.prompt = argv[++index]
|
||||
else if (value === "--model") options.model = argv[++index]
|
||||
else if (value === "--agent") options.agent = argv[++index]
|
||||
@@ -364,7 +372,11 @@ const main = async () => {
|
||||
})
|
||||
|
||||
const traceEvents = []
|
||||
const unsubscribeTrace = client.on("Tracing.dataCollected", ({ value }) => traceEvents.push(...(value ?? [])))
|
||||
// A spread push overflows the call stack once a chunk carries hundreds of
|
||||
// thousands of events, which a heavily populated sidebar easily produces.
|
||||
const unsubscribeTrace = client.on("Tracing.dataCollected", ({ value }) => {
|
||||
for (const event of value ?? []) traceEvents.push(event)
|
||||
})
|
||||
|
||||
let loaded = client.once("Page.loadEventFired", 60_000)
|
||||
await client.send("Page.navigate", { url: target.toString() })
|
||||
@@ -375,6 +387,10 @@ const main = async () => {
|
||||
localStorage.setItem("openchamber_sync_perf", "1")
|
||||
localStorage.setItem("openchamber_stream_perf", "1")
|
||||
`)
|
||||
if (options.expandProjects) {
|
||||
await expandProjects(client)
|
||||
console.log("Expanded every project in the sidebar.")
|
||||
}
|
||||
loaded = client.once("Page.loadEventFired", 60_000)
|
||||
await client.send("Page.reload", { ignoreCache: false })
|
||||
await loaded
|
||||
@@ -382,6 +398,12 @@ const main = async () => {
|
||||
console.log(`Opened the session; settling for ${options.settle}s.`)
|
||||
await wait(options.settle * 1000)
|
||||
|
||||
if (options.expandSessions) {
|
||||
const expanded = await expandSessionLists(client)
|
||||
console.log(`Expanded ${expanded} collapsed session lists; settling ${options.settle}s again.`)
|
||||
await wait(options.settle * 1000)
|
||||
}
|
||||
|
||||
await evaluateValue(client, `globalThis[${JSON.stringify(IDLE_PROBE_GLOBAL)}]?.start()`)
|
||||
await evaluateValue(client, `window.__openchamberSyncPerformance?.reset()`)
|
||||
await evaluateValue(client, `window.__openchamberStreamPerformance?.setEnabled(true)`)
|
||||
@@ -553,7 +575,7 @@ const main = async () => {
|
||||
listenerGrowth: delta("JSEventListeners"),
|
||||
heapStartMb: round(heapSamples.at(0) ?? 0),
|
||||
heapEndMb: round(heapSamples.at(-1) ?? 0),
|
||||
heapMaxMb: round(Math.max(0, ...heapSamples)),
|
||||
heapMaxMb: round(heapSamples.reduce((max, value) => Math.max(max, value), 0)),
|
||||
heapGrowthMbPerSecond: growthPerSecond(samples, "jsHeapUsedMb"),
|
||||
},
|
||||
frameLiveness,
|
||||
|
||||
Reference in New Issue
Block a user