mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
perf(docs): prefetch CLI help concurrently and memoize doc-graph probes
gen-cli-help launched its twelve source-mode dsh --help processes one after another, each spending most of its time waiting on profile boot I/O; they now start concurrently within the default worker budget, each with its own temporary DSH_HOME, and the existing comparison consumes them in the original order. gen-doc-graphs memoizes ts.sys file, directory, realpath, and directory-listing probes for one run. Generated pages are byte-identical; CLI help drops from about 52 s to 7 s and doc graphs from about 47 s to 33 s. Refs #5852
This commit is contained in:
1 parent
67b875a5bf
commit
703c8f659c
4 files changed
+338
-28
No files matched your search
@@ -1,7 +1,30 @@
|
||||
/** Command discovery and complete output preservation for the CLI reference. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectCliHelp, parseCliHelp } from './gen-cli-help.ts'
|
||||
import { collectCliHelp, parseCliHelp, prefetchHelp, topLevelHelpArgs } from './gen-cli-help.ts'
|
||||
|
||||
interface PendingRead {
|
||||
args: string
|
||||
signal: AbortSignal
|
||||
resolve(stdout: string): void
|
||||
reject(error: Error): void
|
||||
}
|
||||
|
||||
/** A read function whose calls stay pending until the test settles them. */
|
||||
function controlledReads(): { reads: PendingRead[]; read: (args: readonly string[], signal: AbortSignal) => Promise<string> } {
|
||||
const reads: PendingRead[] = []
|
||||
return {
|
||||
reads,
|
||||
read: (args, signal) => new Promise<string>((resolve, reject) => {
|
||||
reads.push({ args: args.join(' '), signal, resolve, reject })
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/** Let every queued promise continuation run. */
|
||||
async function flush(): Promise<void> {
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
}
|
||||
|
||||
describe('CLI help discovery', () => {
|
||||
it('reads command rows while retaining wrapped descriptions and examples', () => {
|
||||
@@ -48,4 +71,64 @@ describe('CLI help discovery', () => {
|
||||
it.each(['', ' \r\n\n'])('rejects blank help %j', (help) => {
|
||||
expect(() => parseCliHelp(help)).toThrow('CLI help returned empty output')
|
||||
})
|
||||
|
||||
it('lists top-level invocations in visit order', async () => {
|
||||
const visited: string[] = []
|
||||
await collectCliHelp(async (args) => {
|
||||
visited.push(args.join(' '))
|
||||
return `Usage: ${args.join(' ')}\n`
|
||||
}, ['web', 'acp'])
|
||||
expect(topLevelHelpArgs(['web', 'acp']).map(args => args.join(' '))).toEqual(visited)
|
||||
expect(visited).toEqual(['', 'plugin', 'acp', '--profile acp', 'web', '--profile web'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLI help prefetch', () => {
|
||||
it('starts prefetched reads within the limit and serves them in request order', async () => {
|
||||
const { reads, read } = controlledReads()
|
||||
const reader = prefetchHelp(read, [['a'], ['b'], ['c']], 2)
|
||||
await flush()
|
||||
expect(reads.map(entry => entry.args)).toEqual(['a', 'b'])
|
||||
reads[1]?.resolve('B')
|
||||
await flush()
|
||||
expect(reads.map(entry => entry.args)).toEqual(['a', 'b', 'c'])
|
||||
reads[0]?.resolve('A')
|
||||
reads[2]?.resolve('C')
|
||||
await expect(reader.readHelp(['a'])).resolves.toBe('A')
|
||||
await expect(reader.readHelp(['b'])).resolves.toBe('B')
|
||||
await expect(reader.readHelp(['c'])).resolves.toBe('C')
|
||||
const nested = reader.readHelp(['c', 'nested'])
|
||||
const repeated = reader.readHelp(['a'])
|
||||
await flush()
|
||||
expect(reads.map(entry => entry.args)).toEqual(['a', 'b', 'c', 'c nested', 'a'])
|
||||
reads[3]?.resolve('N')
|
||||
reads[4]?.resolve('A again')
|
||||
await expect(nested).resolves.toBe('N')
|
||||
await expect(repeated).resolves.toBe('A again')
|
||||
await reader.close()
|
||||
})
|
||||
|
||||
it('reports a failure when its invocation is requested and settles started reads on close', async () => {
|
||||
const { reads, read } = controlledReads()
|
||||
const reader = prefetchHelp(read, [['a'], ['b'], ['c']], 1)
|
||||
await flush()
|
||||
const failure = new Error('a failed')
|
||||
reads[0]?.reject(failure)
|
||||
await flush()
|
||||
expect(reads.map(entry => entry.args)).toEqual(['a', 'b'])
|
||||
await expect(reader.readHelp(['a'])).rejects.toBe(failure)
|
||||
let closed = false
|
||||
const closing = reader.close().then(() => { closed = true })
|
||||
await flush()
|
||||
expect(reads[1]?.signal.aborted).toBe(true)
|
||||
expect(closed).toBe(false)
|
||||
reads[1]?.reject(new Error('b aborted'))
|
||||
await closing
|
||||
expect(reads.map(entry => entry.args)).toEqual(['a', 'b'])
|
||||
await expect(reader.readHelp(['c'])).rejects.toMatchObject({ name: 'AbortError' })
|
||||
})
|
||||
|
||||
it.each([0, 1.5])('rejects concurrency %j', (concurrency) => {
|
||||
expect(() => prefetchHelp(async () => '', [], concurrency)).toThrow('concurrency must be a positive integer')
|
||||
})
|
||||
})
|
||||
+127
-24
@@ -12,6 +12,7 @@ import {
|
||||
translationPairPaths,
|
||||
} from './translation-pairing-record.ts'
|
||||
import { parseTranslationPairingManifest, translationPairSourcePredicate } from './translation-pairing.ts'
|
||||
import { defaultParallelism } from './parallelism.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const paths = translationPairPaths('docs/cli-help.md')
|
||||
@@ -56,6 +57,96 @@ function addAlias(page: HelpPage, invocation: string, path: ReadonlySet<HelpPage
|
||||
for (const [name, child] of page.children) addAlias(child, `${invocation} ${name}`, childPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* List the top-level help invocations in the order {@link collectCliHelp} visits them.
|
||||
* @param profiles - shipped profile names whose explicit and shorthand forms must be documented.
|
||||
* @returns launcher help, plugin help, then each profile's shorthand and `--profile` forms in sorted profile order.
|
||||
*/
|
||||
export function topLevelHelpArgs(profiles: readonly string[]): string[][] {
|
||||
return [[], ['plugin'], ...[...profiles].sort().flatMap(profile => [[profile], ['--profile', profile]])]
|
||||
}
|
||||
|
||||
/** Help reads started ahead of an in-order consumer. */
|
||||
export interface PrefetchedHelpReader {
|
||||
/**
|
||||
* Return the result of one prefetched invocation, or start a read for an invocation that was not prefetched.
|
||||
* Each prefetched result is served once; a repeated request starts a new read.
|
||||
* @param args - help invocation arguments.
|
||||
* @returns the read's stdout, or its rejection.
|
||||
*/
|
||||
readHelp: (args: readonly string[]) => Promise<string>
|
||||
/**
|
||||
* Abort the shared signal so queued reads never start and running reads stop.
|
||||
* @returns a promise that resolves after every started read settles, whether or not its result was consumed.
|
||||
*/
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Start help reads for known invocations before a sequential consumer requests them.
|
||||
* Prefetched and on-demand reads share one first-in, first-out limit. A rejected read
|
||||
* reaches the consumer when it requests that invocation; an unconsumed rejection is
|
||||
* observed only by {@link PrefetchedHelpReader.close}.
|
||||
* @param read - run one help invocation; once `signal` aborts it must stop its work and settle only after that work has stopped.
|
||||
* @param prefetch - invocations to start immediately, in the order the consumer requests them.
|
||||
* @param concurrency - maximum number of reads in flight.
|
||||
* @returns a reader whose `close` the caller awaits after consumption, including after a failure.
|
||||
*/
|
||||
export function prefetchHelp(
|
||||
read: (args: readonly string[], signal: AbortSignal) => Promise<string>,
|
||||
prefetch: readonly (readonly string[])[],
|
||||
concurrency: number,
|
||||
): PrefetchedHelpReader {
|
||||
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
||||
throw new Error(`gen-cli-help: concurrency must be a positive integer, got ${String(concurrency)}`)
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const settled: Promise<void>[] = []
|
||||
const waiting: (() => void)[] = []
|
||||
let active = 0
|
||||
const acquire = async (): Promise<void> => {
|
||||
if (active < concurrency) {
|
||||
active++
|
||||
return
|
||||
}
|
||||
// release() hands its slot to the oldest waiter without decrementing `active`.
|
||||
await new Promise<void>((resolveSlot) => { waiting.push(resolveSlot) })
|
||||
}
|
||||
const release = (): void => {
|
||||
const next = waiting.shift()
|
||||
if (next === undefined) active--
|
||||
else next()
|
||||
}
|
||||
const start = (args: readonly string[]): Promise<string> => {
|
||||
const result = (async () => {
|
||||
await acquire()
|
||||
try {
|
||||
controller.signal.throwIfAborted()
|
||||
return await read(args, controller.signal)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
})()
|
||||
// Attaching this handler also keeps an unconsumed rejection from surfacing as unhandled.
|
||||
settled.push(result.then(() => undefined, () => undefined))
|
||||
return result
|
||||
}
|
||||
const prefetched = new Map(prefetch.map(args => [JSON.stringify(args), start(args)]))
|
||||
return {
|
||||
readHelp: (args) => {
|
||||
const key = JSON.stringify(args)
|
||||
const result = prefetched.get(key)
|
||||
if (result === undefined) return start(args)
|
||||
prefetched.delete(key)
|
||||
return result
|
||||
},
|
||||
async close() {
|
||||
controller.abort()
|
||||
await Promise.all(settled)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect launcher, plugin, and profile help even when root help lists no commands.
|
||||
* @param readHelp - run one help invocation and return its complete stdout.
|
||||
@@ -81,38 +172,50 @@ export async function collectCliHelp(
|
||||
return page
|
||||
}
|
||||
const top = new Set<HelpPage>()
|
||||
await visit([], top)
|
||||
await visit(['plugin'], top)
|
||||
for (const profile of [...profiles].sort()) {
|
||||
await visit([profile], top)
|
||||
await visit(['--profile', profile], top)
|
||||
}
|
||||
for (const args of topLevelHelpArgs(profiles)) await visit(args, top)
|
||||
return [...pages.values()]
|
||||
}
|
||||
|
||||
async function collectHelp(): Promise<HelpPage[]> {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-cli-help-'))
|
||||
const environment = {
|
||||
...process.env,
|
||||
DSH_HOME: join(directory, 'harness'),
|
||||
DSH_AGENTS_HOME: join(directory, 'agents'),
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
NO_COLOR: '1',
|
||||
}
|
||||
for (const name of ['NODE_OPTIONS', 'FORCE_COLOR']) Reflect.deleteProperty(environment, name)
|
||||
try {
|
||||
return await collectCliHelp(async (args) => {
|
||||
const { stdout, stderr } = await execFileAsync(process.execPath, [
|
||||
'--import', 'tsx/esm', 'apps/cli/src/bin.ts', ...args, '--help',
|
||||
], {
|
||||
cwd: root, env: environment, encoding: 'utf8', timeout: 30_000,
|
||||
killSignal: 'SIGKILL', maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
const profiles = Object.keys(PROFILE_TEMPLATES)
|
||||
// Concurrent profile boots share the repository worker budget.
|
||||
const reader = prefetchHelp(async (args, signal) => {
|
||||
// Each invocation boots against its own empty Harness and agents homes.
|
||||
const home = mkdtempSync(join(directory, 'invocation-'))
|
||||
const environment = {
|
||||
...process.env,
|
||||
DSH_HOME: join(home, 'harness'),
|
||||
DSH_AGENTS_HOME: join(home, 'agents'),
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
NO_COLOR: '1',
|
||||
}
|
||||
for (const name of ['NODE_OPTIONS', 'FORCE_COLOR']) Reflect.deleteProperty(environment, name)
|
||||
const help = execFileAsync(process.execPath, [
|
||||
'--import', 'tsx/esm', 'apps/cli/src/bin.ts', ...args, '--help',
|
||||
], {
|
||||
cwd: root, env: environment, encoding: 'utf8', timeout: 30_000,
|
||||
killSignal: 'SIGKILL', maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
// Killing directly, not through execFile's signal option, settles the read only after the child closes.
|
||||
const abort = (): void => { help.child.kill('SIGKILL') }
|
||||
signal.addEventListener('abort', abort, { once: true })
|
||||
try {
|
||||
const { stdout, stderr } = await help
|
||||
if (stderr !== '') throw new Error(`${['dsh', ...args].join(' ')} --help wrote diagnostics: ${stderr}`)
|
||||
return stdout
|
||||
}, Object.keys(PROFILE_TEMPLATES))
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abort)
|
||||
}
|
||||
}, topLevelHelpArgs(profiles), defaultParallelism())
|
||||
try {
|
||||
return await collectCliHelp(reader.readHelp, profiles)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
try {
|
||||
await reader.close()
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
* Tests for the event-relation collector's demand-driven call-site indexing:
|
||||
* the single-file fast path and the global fallback must recover the same
|
||||
* helper-parameter event names, including shapes that defeat the locality
|
||||
* proof (alias escapes and global script files).
|
||||
* proof (alias escapes and global script files). The filesystem-probe memo
|
||||
* must answer repeated probes from one result and restore `ts.sys`.
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { collectPackageSources, EventRelationCollector } from './gen-doc-graphs.ts'
|
||||
import { collectPackageSources, EventRelationCollector, withMemoizedFileSystemProbes } from './gen-doc-graphs.ts'
|
||||
import { TypeScriptProject } from './ts-project.ts'
|
||||
|
||||
const FIXTURE: Record<string, string> = {
|
||||
@@ -96,3 +98,68 @@ describe('event relation call-site indexing', () => {
|
||||
expect(dispatchersOf(['pkgc'], 'pkgc/script-event')).toEqual(['pkgc'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('memoized filesystem probes', () => {
|
||||
function countingSystem(): { system: ts.System; calls: string[] } {
|
||||
const calls: string[] = []
|
||||
const system: ts.System = {
|
||||
...ts.sys,
|
||||
fileExists: (path) => { calls.push(`file ${path}`); return path === 'present' },
|
||||
directoryExists: (path) => { calls.push(`directory ${path}`); return true },
|
||||
realpath: (path) => { calls.push(`realpath ${path}`); return `/real/${path}` },
|
||||
readDirectory: (path, extensions) => { calls.push(`read ${path} ${String(extensions)}`); return [`${path}/a.ts`] },
|
||||
getDirectories: (path) => { calls.push(`directories ${path}`); return [`${path}/child`] },
|
||||
}
|
||||
return { system, calls }
|
||||
}
|
||||
|
||||
it('answers repeated probes from the first result and restores the system', () => {
|
||||
const { system, calls } = countingSystem()
|
||||
const original = { ...system }
|
||||
const results = withMemoizedFileSystemProbes(() => {
|
||||
const listed = system.readDirectory('dir', ['.ts'])
|
||||
listed.push('mutated')
|
||||
return [
|
||||
system.fileExists('present'), system.fileExists('present'), system.fileExists('absent'),
|
||||
system.directoryExists('dir'), system.directoryExists('dir'),
|
||||
system.realpath?.('link'), system.realpath?.('link'),
|
||||
system.readDirectory('dir', ['.ts']), system.readDirectory('dir', ['.tsx']),
|
||||
system.getDirectories('dir'), system.getDirectories('dir'),
|
||||
]
|
||||
}, system)
|
||||
expect(results).toEqual([
|
||||
true, true, false, true, true, '/real/link', '/real/link',
|
||||
['dir/a.ts'], ['dir/a.ts'], ['dir/child'], ['dir/child'],
|
||||
])
|
||||
expect(calls).toEqual([
|
||||
'read dir .ts', 'file present', 'file absent', 'directory dir', 'realpath link', 'read dir .tsx', 'directories dir',
|
||||
])
|
||||
expect({ ...system }).toEqual(original)
|
||||
})
|
||||
|
||||
it('records no result for a throwing probe and restores the system when the run throws', () => {
|
||||
const { system } = countingSystem()
|
||||
const original = { ...system }
|
||||
let attempts = 0
|
||||
system.fileExists = () => {
|
||||
attempts++
|
||||
if (attempts === 1) throw new Error('transient')
|
||||
return true
|
||||
}
|
||||
const replaced = { ...system }
|
||||
expect(() => withMemoizedFileSystemProbes(() => {
|
||||
expect(() => system.fileExists('path')).toThrow('transient')
|
||||
expect(system.fileExists('path')).toBe(true)
|
||||
throw new Error('run failed')
|
||||
}, system)).toThrow('run failed')
|
||||
expect(attempts).toBe(2)
|
||||
expect({ ...system }).toEqual(replaced)
|
||||
expect(replaced).not.toEqual(original)
|
||||
})
|
||||
|
||||
it('builds the same Program through the memoized system', () => {
|
||||
const memoized = withMemoizedFileSystemProbes(() => new TypeScriptProject(root))
|
||||
expect(memoized.sourceFiles().map(sourceFile => sourceFile.fileName))
|
||||
.toEqual(project.sourceFiles().map(sourceFile => sourceFile.fileName))
|
||||
})
|
||||
})
|
||||
@@ -1758,8 +1758,65 @@ function renderIndex(docs: GraphDoc[]): string {
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function memoize<A extends readonly unknown[], R>(
|
||||
compute: (...args: A) => R,
|
||||
key: (...args: A) => string,
|
||||
copy: (value: R) => R,
|
||||
): (...args: A) => R {
|
||||
const results = new Map<string, R>()
|
||||
return (...args) => {
|
||||
const id = key(...args)
|
||||
const recorded = results.get(id)
|
||||
if (recorded !== undefined) return copy(recorded)
|
||||
const value = compute(...args)
|
||||
results.set(id, value)
|
||||
return copy(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run read-only analysis with memoized TypeScript filesystem probes.
|
||||
*
|
||||
* While `run` executes, `fileExists`, `directoryExists`, `realpath`, `readDirectory`,
|
||||
* and `getDirectories` on `system` return the first result recorded for each distinct
|
||||
* argument list, so every compiler host and config parse built on it, including the
|
||||
* Typert programs and the event-relation Program, shares one view of the filesystem.
|
||||
* The workspace must not change while `run` executes. A probe that throws records
|
||||
* nothing, and array results are copied for each caller. The original methods are
|
||||
* restored when `run` returns or throws.
|
||||
* @param run - read-only analysis that runs under the memo.
|
||||
* @param system - TypeScript system whose probes are memoized.
|
||||
* @returns the value returned by `run`.
|
||||
*/
|
||||
export function withMemoizedFileSystemProbes<T>(run: () => T, system: ts.System = ts.sys): T {
|
||||
const probes = ['fileExists', 'directoryExists', 'realpath', 'readDirectory', 'getDirectories'] as const
|
||||
const descriptors = probes.map(name => [name, Object.getOwnPropertyDescriptor(system, name)] as const)
|
||||
const original: ts.System = { ...system }
|
||||
const same = <V>(value: V): V => value
|
||||
const copied = (value: string[]): string[] => [...value]
|
||||
system.fileExists = memoize((path: string) => original.fileExists(path), path => path, same)
|
||||
system.directoryExists = memoize((path: string) => original.directoryExists(path), path => path, same)
|
||||
if (original.realpath !== undefined) system.realpath = memoize(original.realpath.bind(original), path => path, same)
|
||||
system.readDirectory = memoize(
|
||||
(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) =>
|
||||
original.readDirectory(path, extensions, exclude, include, depth),
|
||||
(path, extensions, exclude, include, depth) => JSON.stringify([path, extensions, exclude, include, depth]),
|
||||
copied,
|
||||
)
|
||||
system.getDirectories = memoize((path: string) => original.getDirectories(path), path => path, copied)
|
||||
try {
|
||||
return run()
|
||||
} finally {
|
||||
for (const [name, descriptor] of descriptors) {
|
||||
if (descriptor === undefined) Reflect.deleteProperty(system, name)
|
||||
else Object.defineProperty(system, name, descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const docs = renderDocs()
|
||||
// renderDocs reads the workspace only; generated pages are written after the memo is removed.
|
||||
const docs = withMemoizedFileSystemProbes(renderDocs)
|
||||
if (process.argv.includes('--check')) {
|
||||
const stale: string[] = []
|
||||
for (const doc of docs) {
|
||||
|
||||
Reference in new issue
Block a user