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

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

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: 100, entries: 50 },
323 { name: 'Medium typed initializers', declarations: 500, entries: 100 },
324 { name: 'Large typed initializers', declarations: 1000, entries: 250 },
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)