ReviewOS

also looking at this

stacks/dtsx

perf(isolated): benchmark declaration generation paths

#3106
Merged chris wants to merge codex/isolated-mode-benchmarks into main
4 files +215 -6
benchmark/ci-summary.tsmodified+27-0
Changes to benchmark/ci-summary.ts
@@ -62,11 +62,26 @@ interface PhaseTimingSuiteResult {
6262 fileSize: number
6363}
6464
65interface GenerationModeComparison {
66 name: string
67 inputSize: number
68 isolated: BenchmarkResult
69 semantic: BenchmarkResult
70 speedup: number
71}
72
73interface GenerationModeSuiteResult {
74 name: string
75 comparisons: GenerationModeComparison[]
76 totalTimeMs: number
77}
78
6579interface BenchmarkOutput {
6680 timestamp: string
6781 platform: string
6882 nodeVersion: string
6983 suites: SuiteResult[]
84 generationModes?: GenerationModeSuiteResult
7085 phaseTimings: PhaseTimingSuiteResult[]
7186 summary: { totalTimeMs: number, totalBenchmarks: number, avgTimeMs: number }
7287}
@@ -418,6 +433,18 @@ let md = '## Benchmark Results\n\n'
418433const runtime = typeof globalThis.Bun !== 'undefined' ? `Bun ${(globalThis as any).Bun.version}` : results.nodeVersion
419434md += `**Platform:** ${results.platform} | **Runtime:** ${runtime} | **Date:** ${results.timestamp.split('T')[0]}\n\n`
420435
436if (results.generationModes?.comparisons.length) {
437 md += '### isolatedDeclarations vs Semantic Generation\n\n'
438 md += '_Uncached end-to-end declaration generation over identical, fully annotated sources._\n\n'
439 md += '| Workload | Input | isolatedDeclarations | Semantic | Speedup |\n'
440 md += '|----------|-------|----------------------|----------|---------|\n'
441 for (const comparison of results.generationModes.comparisons) {
442 const status = comparison.speedup >= 2 ? ':green_circle:' : comparison.speedup >= 1 ? ':yellow_circle:' : ':red_circle:'
443 md += `| ${comparison.name} | ${(comparison.inputSize / 1024).toFixed(0)} KiB | ${fmt(comparison.isolated.avgTimeMs)} | ${fmt(comparison.semantic.avgTimeMs)} | ${status} **${comparison.speedup.toFixed(1)}x** |\n`
444 }
445 md += '\n'
446}
447
421448// --- Cached table ---
422449const cachedTools: { name: string, getTime: (r: CrossToolResult) => number | null }[] = [
423450 { name: '**dtsx (cached)**', getTime: r => r.dtsx.avg },
packages/dtsx/benchmark.tsmodified+110-5
Changes to packages/dtsx/benchmark.ts
@@ -6,6 +6,7 @@ import { performance } from 'node:perf_hooks'
66import { extractDeclarations } from './src/extractor'
77import { generate } from './src/generator'
88import { createStreamingProcessor, formatMemoryStats } from './src/memory'
9import { processSourceDirect } from './src/process-source'
910import { processDeclarations } from './src/processor'
1011
1112/**
@@ -40,6 +41,22 @@ interface SuiteResult {
4041 totalTimeMs: number
4142}
4243
44/** Paired end-to-end timings for the two declaration generation paths. */
45interface GenerationModeComparison {
46 name: string
47 inputSize: number
48 isolated: BenchmarkResult
49 semantic: BenchmarkResult
50 /** Semantic time divided by isolated time. Values above 1 favor isolated mode. */
51 speedup: number
52}
53
54interface GenerationModeSuiteResult {
55 name: string
56 comparisons: GenerationModeComparison[]
57 totalTimeMs: number
58}
59
4360/**
4461 * Phase timing result
4562 */
@@ -281,6 +298,76 @@ async function runGenerationBenchmarks(config: BenchmarkConfig): Promise<SuiteRe
281298 }
282299}
283300
301/**
302 * Generate valid isolated-declarations input with expensive but fully typed
303 * initializers. The public contract is identical in both modes; only semantic
304 * mode needs to inspect the initializer values.
305 */
306function generateAnnotatedInitializerFile(declarations: number, entriesPerInitializer: number): string {
307 const entries = Array.from({ length: entriesPerInitializer }, (_, index) => `'tag-${index}'`).join(', ')
308 const lines = ['export interface BenchmarkItem { id: number; tags: readonly string[] }']
309 for (let index = 0; index < declarations; index++) {
310 lines.push(`export const item${index}: BenchmarkItem = { id: ${index}, tags: [${entries}] };`)
311 }
312 return lines.join('\n')
313}
314
315/** Compare uncached end-to-end isolated and semantic declaration generation. */
316async function runGenerationModeBenchmarks(config: BenchmarkConfig): Promise<GenerationModeSuiteResult> {
317 log('\n⚡ isolatedDeclarations vs Semantic Benchmarks\n', config.verbose)
318
319 const suiteStart = performance.now()
320 const comparisons: GenerationModeComparison[] = []
321 const workloads = [
322 { name: 'Small typed initializers', declarations: 250, entries: 100 },
323 { name: 'Medium typed initializers', declarations: 500, entries: 250 },
324 { name: 'Large typed initializers', declarations: 1000, entries: 500 },
325 ]
326
327 for (const workload of workloads) {
328 const source = generateAnnotatedInitializerFile(workload.declarations, workload.entries)
329 const filename = `generation-mode-${workload.declarations}.ts`
330 const isolatedOutput = processSourceDirect(source, filename, false, ['bun'], true)
331 const semanticOutput = processSourceDirect(source, filename, false, ['bun'], false)
332 if (isolatedOutput !== semanticOutput) {
333 throw new Error(`${workload.name} produced different public declarations between generation modes`)
334 }
335
336 const modeConfig = {
337 ...config,
338 warmupIterations: Math.max(3, config.warmupIterations),
339 benchmarkIterations: Math.max(10, Math.min(config.benchmarkIterations, 30)),
340 }
341 const isolated = await runBenchmark(
342 `${workload.name} (isolated)`,
343 () => processSourceDirect(source, filename, false, ['bun'], true),
344 modeConfig,
345 source.length,
346 )
347 const semantic = await runBenchmark(
348 `${workload.name} (semantic)`,
349 () => processSourceDirect(source, filename, false, ['bun'], false),
350 modeConfig,
351 source.length,
352 )
353 const speedup = semantic.avgTimeMs / isolated.avgTimeMs
354 comparisons.push({
355 name: workload.name,
356 inputSize: source.length,
357 isolated,
358 semantic,
359 speedup,
360 })
361 log(` ${workload.name}: isolated ${isolated.avgTimeMs.toFixed(3)}ms, semantic ${semantic.avgTimeMs.toFixed(3)}ms — ${speedup.toFixed(1)}x faster`, config.verbose)
362 }
363
364 return {
365 name: 'Generation Modes',
366 comparisons,
367 totalTimeMs: performance.now() - suiteStart,
368 }
369}
370
284371/**
285372 * Memory benchmark suite
286373 */
@@ -720,6 +807,7 @@ interface BenchmarkOutput {
720807 platform: string
721808 nodeVersion: string
722809 suites: SuiteResult[]
810 generationModes: GenerationModeSuiteResult
723811 phaseTimings: PhaseTimingSuiteResult[]
724812 summary: {
725813 totalTimeMs: number
@@ -766,6 +854,7 @@ async function main() {
766854 }
767855
768856 const suites: SuiteResult[] = []
857 let generationModes: GenerationModeSuiteResult = { name: 'Generation Modes', comparisons: [], totalTimeMs: 0 }
769858 let phaseTimings: PhaseTimingSuiteResult[] = []
770859
771860 try {
@@ -773,6 +862,7 @@ async function main() {
773862 suites.push(await runExtractionBenchmarks(config))
774863 suites.push(await runSyntheticBenchmarks(config))
775864 suites.push(await runMemoryBenchmarks(config))
865 generationModes = await runGenerationModeBenchmarks(config)
776866
777867 // Run real-world benchmarks unless skipped
778868 if (!args.includes('--skip-real-world')) {
@@ -789,9 +879,12 @@ async function main() {
789879 }
790880
791881 // Calculate summary
792 const totalTimeMs = suites.reduce((sum, s) => sum + s.totalTimeMs, 0)
793 const totalBenchmarks = suites.reduce((sum, s) => sum + s.results.length, 0)
794 const avgTimeMs = suites.flatMap(s => s.results).reduce((sum, r) => sum + r.avgTimeMs, 0) / totalBenchmarks
882 const modeResults = generationModes.comparisons.flatMap(comparison => [comparison.isolated, comparison.semantic])
883 const suiteResults = suites.flatMap(suite => suite.results)
884 const allResults = [...suiteResults, ...modeResults]
885 const totalTimeMs = suites.reduce((sum, suite) => sum + suite.totalTimeMs, generationModes.totalTimeMs)
886 const totalBenchmarks = allResults.length
887 const avgTimeMs = allResults.reduce((sum, result) => sum + result.avgTimeMs, 0) / totalBenchmarks
795888
796889 // Output results
797890 if (jsonOutput) {
@@ -800,6 +893,7 @@ async function main() {
800893 platform: `${process.platform} ${process.arch}`,
801894 nodeVersion: process.version,
802895 suites,
896 generationModes,
803897 phaseTimings,
804898 summary: {
805899 totalTimeMs,
@@ -822,6 +916,13 @@ async function main() {
822916 // Print summary
823917 printSummary(suites)
824918
919 if (generationModes.comparisons.length > 0) {
920 console.log('\n⚡ isolatedDeclarations Speedup')
921 for (const comparison of generationModes.comparisons) {
922 console.log(` ${comparison.name}: ${comparison.speedup.toFixed(1)}x`)
923 }
924 }
925
825926 // Print phase timing summary
826927 if (phaseTimings.length > 0) {
827928 printPhaseTimingSummary(phaseTimings)
@@ -846,12 +947,16 @@ export {
846947 type BenchmarkConfig,
847948 type BenchmarkOutput,
848949 type BenchmarkResult,
950 generateAnnotatedInitializerFile,
849951 generateLargeTypeScriptFile,
952 type GenerationModeComparison,
953 type GenerationModeSuiteResult,
850954 type PhaseTimingResult,
851955 type PhaseTimingSuiteResult,
852956 runBenchmark,
853957 runExtractionBenchmarks,
854958 runGenerationBenchmarks,
959 runGenerationModeBenchmarks,
855960 runMemoryBenchmarks,
856961 runPhaseTimingBenchmarks,
857962 runRealWorldBenchmarks,
@@ -859,5 +964,5 @@ export {
859964 type SuiteResult,
860965}
861966
862// Run if executed directly
863main().catch(console.error)
967// Run only when executed directly; importing benchmark helpers must be side-effect free.
968if (import.meta.main) main().catch(console.error)
packages/dtsx/src/extractor/scanner.tsmodified+37-1
Changes to packages/dtsx/src/extractor/scanner.ts
@@ -723,6 +723,42 @@ function scanDeclarationsInternal(_source: string, _filename: string, _keepComme
723723 pos = len
724724 }
725725
726 /**
727 * Skip an annotated variable initializer without capturing its text.
728 *
729 * Isolated declarations call it for every explicitly annotated value. Safe
730 * single-line literals take the direct path; complex syntax falls back to
731 * the general JSX-aware statement scanner.
732 */
733 function skipAnnotatedVariableInitializer(): void {
734 pos++ // skip =
735 while (pos < len && (source.charCodeAt(pos) === CH_SPACE || source.charCodeAt(pos) === CH_TAB)) pos++
736
737 // Common isolated-declarations output is one explicitly annotated value
738 // per line. A terminated single-line literal can jump directly to its
739 // semicolon without inspecting the initializer's properties/elements.
740 const lineEnd = source.indexOf('\n', pos)
741 const physicalEnd = lineEnd === -1 ? len : lineEnd
742 const terminator = source.indexOf(';', pos)
743 if (terminator !== -1 && terminator < physicalEnd) {
744 let valueEnd = terminator - 1
745 while (valueEnd >= pos && (source.charCodeAt(valueEnd) === CH_SPACE || source.charCodeAt(valueEnd) === CH_TAB)) valueEnd--
746 const first = source.charCodeAt(pos)
747 const final = source.charCodeAt(valueEnd)
748 const isClosedLiteral = (first === CH_LBRACE && final === CH_RBRACE)
749 || (first === CH_LBRACKET && final === CH_RBRACKET)
750 || (first === CH_SQUOTE && final === CH_SQUOTE)
751 || (first === CH_DQUOTE && final === CH_DQUOTE)
752 || (first === CH_BACKTICK && final === CH_BACKTICK)
753 if (isClosedLiteral) {
754 pos = terminator + 1
755 return
756 }
757 }
758
759 skipToStatementEnd()
760 }
761
726762 /**
727763 * Skip an export re-export: { ... } [from '...'] [;]
728764 * pos should be at the opening {
@@ -1983,7 +2019,7 @@ function scanDeclarationsInternal(_source: string, _filename: string, _keepComme
19832019 // Concrete annotations need no initializer work. Broad annotations retain
19842020 // the initializer only so the processor can emit useful @defaultValue docs.
19852021 if (isolatedDeclarations && typeAnnotation && (!keepComments || !isBroadAnnotation(typeAnnotation))) {
1986 skipToStatementEnd()
2022 skipAnnotatedVariableInitializer()
19872023 }
19882024 else {
19892025 pos++ // skip =
packages/dtsx/test/generation-paths.test.tsmodified+41-0
Changes to packages/dtsx/test/generation-paths.test.ts
@@ -114,6 +114,47 @@ describe('declaration generation paths', () => {
114114 expect(declaration.value).toBeUndefined()
115115 })
116116
117 it('fast-skips terminated single-line literals without consuming following declarations', () => {
118 const source = [
119 `export const object: { nested: { value: string }, callback: () => number } = { nested: { value: ';' }, callback: () => 42 };`,
120 `export const array: readonly string[] = ['one', ';', 'three'];`,
121 `export const quoted: string = 'a value with ; and } delimiters';`,
122 'export const template: string = `a template with ; and ] delimiters`;',
123 `export const after: number = 42;`,
124 `export const sameLineObject: { value: string } = { value: 'one' }; export const sameLineAfter: { value: string } = { value: 'two' };`,
125 ].join('\n')
126
127 const isolated = processSourceIsolated(source, 'terminated.ts', false)
128 const semantic = processSourceSemantic(source, 'terminated.ts', false)
129
130 expect(isolated).toBe(semantic)
131 expect(isolated).toContain('export declare const object: { nested: { value: string }, callback: () => number };')
132 expect(isolated).toContain('export declare const array: readonly string[];')
133 expect(isolated).toContain('export declare const quoted: string;')
134 expect(isolated).toContain('export declare const template: string;')
135 expect(isolated).toContain('export declare const after: number;')
136 expect(isolated).toContain('export declare const sameLineObject: { value: string };')
137 expect(isolated).toContain('export declare const sameLineAfter: { value: string };')
138 })
139
140 it('falls back to balanced scanning for multiline and semicolonless initializers', () => {
141 const source = [
142 'export const multiline: { value: string } = {',
143 ` value: 'kept private',`,
144 '};',
145 `export const semicolonless: readonly number[] = [1, 2, 3]`,
146 `export const after: boolean = true`,
147 ].join('\n')
148
149 const isolated = processSourceIsolated(source, 'fallback.ts', false)
150 const semantic = processSourceSemantic(source, 'fallback.ts', false)
151
152 expect(isolated).toBe(semantic)
153 expect(isolated).toContain('export declare const multiline: { value: string };')
154 expect(isolated).toContain('export declare const semicolonless: readonly number[];')
155 expect(isolated).toContain('export declare const after: boolean;')
156 })
157
117158 it('preserves broad contracts and documents their initializer values', () => {
118159 const source = `
119160 export const conf: { [key: string]: string } = {