Files
orca/src/main/usage/usage-scan-worker-event-loop.test.ts
T
Jinjing 0232c03c43 Refactor editor header file rename to breadcrumb morph UI (#21265)
* Refactor editor header file rename to a breadcrumb morph UI

- Display full breadcrumb (repo name + parent dirs) during rename for context
- Separate basename field from extension suffix to clarify what users edit
- Replace blur-to-commit with explicit confirm/cancel buttons
- Auto-attach extension to basename; respect explicitly typed extensions
- Add comprehensive tests for rename scenarios and edge cases

* Fix markdown rename: drop blur-commit, handle IME, track active file

- Blur no longer commits, preventing accidental renames when focus moves
- IME composition keys properly handled for CJK input support
- File switches during rename now cancel the operation
- Extension display improved to show final filename
- Comprehensive test coverage added for edge cases

* Simplify markdown rename to inline field without breadcrumb or buttons

Replaces breadcrumb-morph rename with confirm/cancel buttons with a
simple inline field accepting full filenames. Commits on Enter, blur, or
Escape to cancel, matching tab bar and file explorer behavior. Adds
renameCancelledRef to prevent blur-commit after Escape. Removes unused
i18n strings for buttons and simplifies state by dropping extension
pinning and breadcrumb display.

* Handle blur race when file changes during rename

When switching files mid-rename, React may deliver the old input's
blur event after the new file renders, causing a stale rename commit.

Mark the rename as cancelled when the active file changes, and add
test coverage verifying stale blur events are ignored.

* Test blur-race condition in hook unit test

Move blur-commit-after-file-change test from EditorPanelHeaderPath
integration tests to useEditorHeaderFileRename unit test. Tests the
blur-handling logic at the hook level where it belongs.
2026-09-21 22:55:43 -07:00

228 lines
8.3 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { build } from 'esbuild'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { Worker } from 'node:worker_threads'
import { scanCodexUsageFiles } from '../codex-usage/scanner'
import { UsageScanWorkerClient, scanCodexUsageOnWorker } from './usage-scan-worker-client'
import type { UsageScanWorktreeRef } from './usage-provider-contract'
// Why this test exists: "the scan no longer blocks the main process" is not a
// stopwatch claim. It measures the *calling* thread's event-loop active time —
// the milliseconds that thread spent running JS rather than parked in the
// loop's poll phase.
//
// One case runs both arms over one corpus so they are compared against each
// other rather than each against a fixed threshold: the identical scan goes
// first through the pre-worker calling-thread path, then through the worker,
// and the worker arm must cost the caller a fraction of the JS time.
//
// Active milliseconds, not the active/wall fraction (issue #18788): CPU
// contention drags the calling-thread arm's *fraction* down toward the
// worker's — a loaded ubuntu runner measured 0.76 against a 0.8 floor — while
// stretching wall time, which widens the millisecond gap instead.
//
// The presence preconditions on both arms are load-bearing. An arm that
// silently scanned nothing would otherwise satisfy the comparison trivially.
const FILE_COUNT = 600
const EVENTS_PER_FILE = 60
const EXPECTED_EVENTS = FILE_COUNT * EVENTS_PER_FILE
const TOKENS_PER_EVENT = 200
const WORKTREES: UsageScanWorktreeRef[] = [
{
repoId: 'repo-1',
worktreeId: 'wt-1',
path: '/tmp/orca-usage-oracle-project',
displayName: 'demo'
}
]
let corpusRoot = ''
let workerEntryPath = ''
function buildRolloutLines(sessionId: string, seed: number): string {
const lines: string[] = [
JSON.stringify({
timestamp: '2026-01-01T00:00:00.000Z',
type: 'session_meta',
payload: { id: sessionId, cwd: '/tmp/orca-usage-oracle-project' }
}),
JSON.stringify({
timestamp: '2026-01-01T00:00:01.000Z',
type: 'turn_context',
payload: { cwd: '/tmp/orca-usage-oracle-project', model: 'gpt-5.6-sol' }
})
]
// Seeded per file so no two rollouts mint the same event key; identical keys
// would be deduped as fork copies and shrink the corpus the scan actually parses.
let total = seed * 10_000_000
for (let index = 0; index < EVENTS_PER_FILE; index++) {
total += TOKENS_PER_EVENT
lines.push(
JSON.stringify({
timestamp: new Date(Date.UTC(2026, 0, 1, 1, 0, index % 60)).toISOString(),
type: 'event_msg',
payload: {
type: 'token_count',
info: {
total_token_usage: {
input_tokens: total,
cached_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: total
},
last_token_usage: {
input_tokens: TOKENS_PER_EVENT,
cached_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: TOKENS_PER_EVENT
}
},
// Padding so each line is rollout-shaped rather than trivially short.
text: 'x'.repeat(200)
}
})
)
}
return `${lines.join('\n')}\n`
}
function writeCorpus(root: string): void {
const sessionsDir = join(root, 'codex-runtime-home', 'home', 'sessions', '2026', '01', '01')
mkdirSync(sessionsDir, { recursive: true })
for (let index = 0; index < FILE_COUNT; index++) {
writeFileSync(
join(sessionsDir, `rollout-${String(index).padStart(6, '0')}.jsonl`),
buildRolloutLines(`session-${index}`, index + 1)
)
}
}
type Occupancy = {
/** Milliseconds the calling thread spent running JS during the span. */
activeMs: number
/** Same span as a fraction of wall time; reported, not asserted on. */
activeRatio: number
wallMs: number
}
async function measureCallerOccupancy<T>(
run: () => Promise<T>
): Promise<{ value: T; occupancy: Occupancy }> {
const before = performance.eventLoopUtilization()
const startedAt = performance.now()
const value = await run()
const wallMs = performance.now() - startedAt
const delta = performance.eventLoopUtilization(before)
return {
value,
occupancy: { activeMs: delta.active, activeRatio: delta.active / wallMs, wallMs }
}
}
function formatOccupancy(label: string, occupancy: Occupancy): string {
return `${label}: active ${occupancy.activeMs.toFixed(1)}ms of ${occupancy.wallMs.toFixed(1)}ms wall (ratio ${occupancy.activeRatio.toFixed(3)})`
}
function createWorkerClient(): UsageScanWorkerClient {
return new UsageScanWorkerClient({
workerFactory: () => new Worker(workerEntryPath),
log: () => {}
})
}
/**
* Run `fn` with both Codex session lanes pointed at the fixture.
* Why the real process environment and not the Worker `env` option: that option
* only replaces the worker's JS-visible `process.env`, while `os.homedir()`
* reads the OS environment the threads share — so a worker given `HOME` there
* still scanned the developer's real ~/.codex.
*/
async function withCorpusEnv<T>(fn: () => Promise<T>): Promise<T> {
const previous = {
ORCA_USER_DATA_PATH: process.env.ORCA_USER_DATA_PATH,
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE
}
process.env.ORCA_USER_DATA_PATH = corpusRoot
process.env.HOME = corpusRoot
process.env.USERPROFILE = corpusRoot
try {
return await fn()
} finally {
for (const [name, value] of Object.entries(previous)) {
restoreEnv(name, value)
}
}
}
beforeAll(async () => {
corpusRoot = mkdtempSync(join(tmpdir(), 'orca-usage-scan-oracle-'))
writeCorpus(corpusRoot)
workerEntryPath = join(corpusRoot, 'usage-scan-worker-entry.cjs')
// Why bundle here: `new Worker` needs JavaScript, and the production entry is
// emitted by the app build. Bundling the same source keeps the oracle running
// the real scanner instead of a stand-in.
await build({
entryPoints: [resolve(__dirname, 'usage-scan-worker-entry.ts')],
outfile: workerEntryPath,
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
external: ['electron']
})
}, 120_000)
afterAll(() => {
rmSync(corpusRoot, { recursive: true, force: true })
})
describe('usage scan worker event-loop occupancy', () => {
it('costs the calling thread a fraction of the JS time the same scan does inline', async () => {
// Calling thread first: the baseline is measured with no worker alive, and
// the worker arm then reads an OS cache the inline arm already warmed,
// which can only understate the gap being asserted.
const caller = await measureCallerOccupancy(() => scanCodexUsageOnCallingThread())
expect(caller.value.processedFiles).toHaveLength(FILE_COUNT)
expect(caller.value.sessions).toHaveLength(FILE_COUNT)
expect(caller.value.dailyAggregates).toHaveLength(1)
expect(caller.value.dailyAggregates[0]?.eventCount).toBe(EXPECTED_EVENTS)
const client = createWorkerClient()
const worker = await measureCallerOccupancy(() =>
withCorpusEnv(() => scanCodexUsageOnWorker((body) => client.scan(body), WORKTREES, []))
)
expect(worker.value.source).toHaveLength(FILE_COUNT)
expect(worker.value.sessions).toHaveLength(FILE_COUNT)
expect(worker.value.dailyAggregates).toHaveLength(1)
expect(worker.value.dailyAggregates[0]?.eventCount).toBe(EXPECTED_EVENTS)
// The caller still pays to post the request and structured-clone a
// 600-file result back, so this is a fifth, not a rout. Measured margin is
// ~50x idle and ~90x under CPU contention.
expect(
worker.occupancy.activeMs,
`${formatOccupancy('worker', worker.occupancy)}; ${formatOccupancy('calling thread', caller.occupancy)}`
).toBeLessThan(caller.occupancy.activeMs / 5)
}, 120_000)
})
function scanCodexUsageOnCallingThread(): ReturnType<typeof scanCodexUsageFiles> {
return withCorpusEnv(() => scanCodexUsageFiles(WORKTREES, []))
}
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name]
return
}
process.env[name] = value
}