compiler/scripts/profile-rust-port.ts TYPESCRIPT 641 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78/**9 * Performance profiling script for Rust vs JS React Compiler.10 *11 * Runs both compilers on all fixtures without debug logging,12 * collects fine-grained timing data at every stage, and reports13 * aggregate performance breakdowns.14 *15 * Usage: npx tsx compiler/scripts/profile-rust-port.ts [flags]16 *17 * Flags:18 *   --release    Build and use release-mode Rust binary19 *   --json       Output JSON instead of formatted tables20 *   --limit N    Max fixtures to profile (default: all)21 */2223import * as babel from '@babel/core';24import {execSync} from 'child_process';25import fs from 'fs';26import path from 'path';2728import {parseConfigPragmaForTests} from '../packages/babel-plugin-react-compiler/src/Utils/TestUtils';2930const REPO_ROOT = path.resolve(__dirname, '../..');3132// --- Parse flags ---33const rawArgs = process.argv.slice(2);34const releaseMode = rawArgs.includes('--release');35const jsonMode = rawArgs.includes('--json');36const limitIdx = rawArgs.indexOf('--limit');37const limitArg = limitIdx >= 0 ? parseInt(rawArgs[limitIdx + 1], 10) : 0;3839// --- ANSI colors ---40const useColor = !jsonMode;41const BOLD = useColor ? '\x1b[1m' : '';42const DIM = useColor ? '\x1b[2m' : '';43const RED = useColor ? '\x1b[0;31m' : '';44const GREEN = useColor ? '\x1b[0;32m' : '';45const YELLOW = useColor ? '\x1b[0;33m' : '';46const CYAN = useColor ? '\x1b[0;36m' : '';47const RESET = useColor ? '\x1b[0m' : '';4849// --- Build native module ---50const NATIVE_DIR = path.join(51  REPO_ROOT,52  'compiler/packages/babel-plugin-react-compiler-rust/native',53);54const NATIVE_NODE_PATH = path.join(NATIVE_DIR, 'index.node');5556if (!jsonMode) {57  console.log(58    `Building Rust native module (${releaseMode ? 'release' : 'debug'})...`,59  );60}6162// Profiling needs symbol names; override the release profile's strip=true.63const cargoBuildArgs = releaseMode64  ? "--release --config 'profile.release.strip=false' -p react_compiler_napi"65  : '-p react_compiler_napi';6667try {68  execSync(`~/.cargo/bin/cargo build ${cargoBuildArgs}`, {69    cwd: path.join(REPO_ROOT, 'compiler/crates'),70    stdio: jsonMode ? ['inherit', 'pipe', 'inherit'] : 'inherit',71    shell: true,72  });73} catch {74  console.error('ERROR: Failed to build Rust native module.');75  process.exit(1);76}7778// Copy the built dylib as index.node79const TARGET_DIR = path.join(80  REPO_ROOT,81  releaseMode ? 'compiler/target/release' : 'compiler/target/debug',82);83const dylib = fs.existsSync(84  path.join(TARGET_DIR, 'libreact_compiler_napi.dylib'),85)86  ? path.join(TARGET_DIR, 'libreact_compiler_napi.dylib')87  : path.join(TARGET_DIR, 'libreact_compiler_napi.so');8889if (!fs.existsSync(dylib)) {90  console.error(`ERROR: Could not find built native module in ${TARGET_DIR}`);91  process.exit(1);92}93fs.copyFileSync(dylib, NATIVE_NODE_PATH);9495// --- Load plugins ---96const tsPlugin = require('../packages/babel-plugin-react-compiler/src').default;97const {extractScopeInfo} =98  require('../packages/babel-plugin-react-compiler-rust/src/scope') as typeof import('../packages/babel-plugin-react-compiler-rust/src/scope');99const {resolveOptions} =100  require('../packages/babel-plugin-react-compiler-rust/src/options') as typeof import('../packages/babel-plugin-react-compiler-rust/src/options');101const {compileWithRustProfiled} =102  require('../packages/babel-plugin-react-compiler-rust/src/bridge') as typeof import('../packages/babel-plugin-react-compiler-rust/src/bridge');103104// --- Types ---105interface TimingEntry {106  name: string;107  duration_us: number;108}109110interface BridgeTiming {111  jsStringifyAst_us: number;112  jsStringifyScope_us: number;113  jsStringifyOptions_us: number;114  napiCall_us: number;115  jsParseResult_us: number;116}117118interface FixtureProfile {119  fixture: string;120  sizeBytes: number;121  tsTotal_us: number;122  rustTotal_us: number;123  rustScopeExtraction_us: number;124  rustBridge: BridgeTiming;125  rustPasses: TimingEntry[];126}127128// --- Discover fixtures ---129function discoverFixtures(rootPath: string): string[] {130  const results: string[] = [];131  function walk(dir: string): void {132    for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {133      const fullPath = path.join(dir, entry.name);134      if (entry.isDirectory()) {135        walk(fullPath);136      } else if (137        /\.(js|jsx|ts|tsx)$/.test(entry.name) &&138        !entry.name.endsWith('.expect.md')139      ) {140        results.push(fullPath);141      }142    }143  }144  walk(rootPath);145  results.sort();146  return results;147}148149// --- Compile fixture with TS compiler (no debug logging) ---150function compileWithTS(fixturePath: string): number {151  const source = fs.readFileSync(fixturePath, 'utf8');152  const firstLine = source.substring(0, source.indexOf('\n'));153  const pragmaOpts = parseConfigPragmaForTests(firstLine, {154    compilationMode: 'all',155  });156157  const isFlow = firstLine.includes('@flow');158  const isScript = firstLine.includes('@script');159  const parserPlugins: string[] = isFlow160    ? ['flow', 'jsx']161    : ['typescript', 'jsx'];162163  const start = performance.now();164  try {165    babel.transformSync(source, {166      filename: fixturePath,167      sourceType: isScript ? 'script' : 'module',168      parserOpts: {plugins: parserPlugins},169      plugins: [170        [171          tsPlugin,172          {173            ...pragmaOpts,174            compilationMode: 'all' as const,175            panicThreshold: 'all_errors' as const,176          },177        ],178      ],179      configFile: false,180      babelrc: false,181    });182  } catch {183    // Ignore errors - we still measure timing184  }185  const end = performance.now();186  return Math.round((end - start) * 1000); // microseconds187}188189// --- Compile fixture with Rust compiler (profiled) ---190function compileWithRustProfile(fixturePath: string): {191  total_us: number;192  scopeExtraction_us: number;193  bridge: BridgeTiming;194  passes: TimingEntry[];195} {196  const source = fs.readFileSync(fixturePath, 'utf8');197  const firstLine = source.substring(0, source.indexOf('\n'));198  const pragmaOpts = parseConfigPragmaForTests(firstLine, {199    compilationMode: 'all',200  });201202  const isFlow = firstLine.includes('@flow');203  const isScript = firstLine.includes('@script');204  const parserPlugins: string[] = isFlow205    ? ['flow', 'jsx']206    : ['typescript', 'jsx'];207208  // Parse the AST via Babel (same as the real plugin)209  const parseResult = babel.transformSync(source, {210    filename: fixturePath,211    sourceType: isScript ? 'script' : 'module',212    parserOpts: {plugins: parserPlugins},213    plugins: [214      // Use a minimal plugin that captures the AST and scope info215      function capturePlugin(_api: typeof babel): babel.PluginObj {216        return {217          name: 'capture',218          visitor: {219            Program: {220              enter(prog, pass): void {221                // Resolve options222                const opts = resolveOptions(223                  {224                    ...pragmaOpts,225                    compilationMode: 'all',226                    panicThreshold: 'all_errors',227                  },228                  pass.file,229                  fixturePath,230                  pass.file.ast,231                );232233                // Extract scope info (timed)234                const scopeStart = performance.now();235                let scopeInfo;236                try {237                  scopeInfo = extractScopeInfo(prog);238                } catch {239                  // Store failed result240                  (pass as any).__profileResult = {241                    total_us: Math.round(242                      (performance.now() - scopeStart) * 1000,243                    ),244                    scopeExtraction_us: Math.round(245                      (performance.now() - scopeStart) * 1000,246                    ),247                    bridge: {248                      jsStringifyAst_us: 0,249                      jsStringifyScope_us: 0,250                      jsStringifyOptions_us: 0,251                      napiCall_us: 0,252                      jsParseResult_us: 0,253                    },254                    passes: [],255                  };256                  return;257                }258                const scopeEnd = performance.now();259260                const totalStart = performance.now();261                try {262                  const profiled = compileWithRustProfiled(263                    pass.file.ast,264                    scopeInfo,265                    opts,266                    pass.file.code ?? null,267                  );268                  const totalEnd = performance.now();269270                  (pass as any).__profileResult = {271                    total_us: Math.round((totalEnd - totalStart) * 1000),272                    scopeExtraction_us: Math.round(273                      (scopeEnd - scopeStart) * 1000,274                    ),275                    bridge: profiled.bridgeTiming,276                    passes: profiled.rustTiming,277                  };278                } catch {279                  const totalEnd = performance.now();280                  (pass as any).__profileResult = {281                    total_us: Math.round((totalEnd - totalStart) * 1000),282                    scopeExtraction_us: Math.round(283                      (scopeEnd - scopeStart) * 1000,284                    ),285                    bridge: {286                      jsStringifyAst_us: 0,287                      jsStringifyScope_us: 0,288                      jsStringifyOptions_us: 0,289                      napiCall_us: 0,290                      jsParseResult_us: 0,291                    },292                    passes: [],293                  };294                }295                prog.skip();296              },297            },298          },299        };300      },301    ],302    configFile: false,303    babelrc: false,304  });305306  // Extract the profile result stored by the plugin307  const result = (parseResult as any)?.metadata?.__profileResult ??308    (parseResult as any)?.__profileResult ?? {309      total_us: 0,310      scopeExtraction_us: 0,311      bridge: {312        jsStringifyAst_us: 0,313        jsStringifyScope_us: 0,314        jsStringifyOptions_us: 0,315        napiCall_us: 0,316        jsParseResult_us: 0,317      },318      passes: [],319    };320321  return result;322}323324// --- Compile fixture with Rust compiler (simpler approach using direct API) ---325function compileWithRustDirect(fixturePath: string): {326  total_us: number;327  scopeExtraction_us: number;328  bridge: BridgeTiming;329  passes: TimingEntry[];330} {331  const source = fs.readFileSync(fixturePath, 'utf8');332  const firstLine = source.substring(0, source.indexOf('\n'));333  const pragmaOpts = parseConfigPragmaForTests(firstLine, {334    compilationMode: 'all',335  });336337  const isFlow = firstLine.includes('@flow');338  const isScript = firstLine.includes('@script');339  const parserPlugins: string[] = isFlow340    ? ['flow', 'jsx']341    : ['typescript', 'jsx'];342343  // Parse the AST via Babel344  let ast: babel.types.File | null = null;345  let scopeInfo: any = null;346  let opts: any = null;347  let scopeExtraction_us = 0;348349  try {350    babel.transformSync(source, {351      filename: fixturePath,352      sourceType: isScript ? 'script' : 'module',353      parserOpts: {plugins: parserPlugins},354      plugins: [355        function capturePlugin(_api: typeof babel): babel.PluginObj {356          return {357            name: 'capture-for-profile',358            visitor: {359              Program: {360                enter(prog, pass): void {361                  ast = pass.file.ast;362                  opts = resolveOptions(363                    {364                      ...pragmaOpts,365                      compilationMode: 'all',366                      panicThreshold: 'all_errors',367                    },368                    pass.file,369                    fixturePath,370                    pass.file.ast,371                  );372373                  const scopeStart = performance.now();374                  try {375                    scopeInfo = extractScopeInfo(prog);376                  } catch {377                    scopeInfo = null;378                  }379                  scopeExtraction_us = Math.round(380                    (performance.now() - scopeStart) * 1000,381                  );382                  prog.skip();383                },384              },385            },386          };387        },388      ],389      configFile: false,390      babelrc: false,391    });392  } catch {393    // Parse error or other babel failure - skip this fixture394  }395396  if (ast == null || scopeInfo == null || opts == null) {397    return {398      total_us: 0,399      scopeExtraction_us,400      bridge: {401        jsStringifyAst_us: 0,402        jsStringifyScope_us: 0,403        jsStringifyOptions_us: 0,404        napiCall_us: 0,405        jsParseResult_us: 0,406      },407      passes: [],408    };409  }410411  const totalStart = performance.now();412  try {413    const profiled = compileWithRustProfiled(ast, scopeInfo, opts, source);414    const totalEnd = performance.now();415416    return {417      total_us: Math.round((totalEnd - totalStart) * 1000),418      scopeExtraction_us,419      bridge: profiled.bridgeTiming,420      passes: profiled.rustTiming,421    };422  } catch {423    const totalEnd = performance.now();424    return {425      total_us: Math.round((totalEnd - totalStart) * 1000),426      scopeExtraction_us,427      bridge: {428        jsStringifyAst_us: 0,429        jsStringifyScope_us: 0,430        jsStringifyOptions_us: 0,431        napiCall_us: 0,432        jsParseResult_us: 0,433      },434      passes: [],435    };436  }437}438439// --- Main ---440const DEFAULT_FIXTURES_DIR = path.join(441  REPO_ROOT,442  'compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler',443);444445let fixtures = discoverFixtures(DEFAULT_FIXTURES_DIR);446if (limitArg > 0) {447  fixtures = fixtures.slice(0, limitArg);448}449450if (fixtures.length === 0) {451  console.error('No fixtures found.');452  process.exit(1);453}454455if (!jsonMode) {456  console.log(`\nProfiling ${BOLD}${fixtures.length}${RESET} fixtures...`);457}458459// --- Warmup pass ---460if (!jsonMode) {461  console.log(`${DIM}Warmup pass (results discarded)...${RESET}`);462}463for (const fixturePath of fixtures) {464  compileWithTS(fixturePath);465  compileWithRustDirect(fixturePath);466}467468// --- Profile pass ---469if (!jsonMode) {470  console.log(`Profiling...`);471}472473const profiles: FixtureProfile[] = [];474475for (const fixturePath of fixtures) {476  const relPath = path.relative(REPO_ROOT, fixturePath);477  const sizeBytes = fs.statSync(fixturePath).size;478479  const tsTotal_us = compileWithTS(fixturePath);480  const rustResult = compileWithRustDirect(fixturePath);481482  profiles.push({483    fixture: relPath,484    sizeBytes,485    tsTotal_us,486    rustTotal_us: rustResult.scopeExtraction_us + rustResult.total_us,487    rustScopeExtraction_us: rustResult.scopeExtraction_us,488    rustBridge: rustResult.bridge,489    rustPasses: rustResult.passes,490  });491}492493// --- Aggregation ---494const totalTS = profiles.reduce((sum, p) => sum + p.tsTotal_us, 0);495const totalRust = profiles.reduce((sum, p) => sum + p.rustTotal_us, 0);496const ratio = totalRust / totalTS;497498// Aggregate pass timing499const passAggregates = new Map<string, {total_us: number; values: number[]}>();500501function addPassTiming(name: string, duration_us: number): void {502  let agg = passAggregates.get(name);503  if (!agg) {504    agg = {total_us: 0, values: []};505    passAggregates.set(name, agg);506  }507  agg.total_us += duration_us;508  agg.values.push(duration_us);509}510511for (const profile of profiles) {512  // Bridge phases513  addPassTiming('JS: extractScopeInfo', profile.rustScopeExtraction_us);514  addPassTiming('JS: JSON.stringify AST', profile.rustBridge.jsStringifyAst_us);515  addPassTiming(516    'JS: JSON.stringify scope',517    profile.rustBridge.jsStringifyScope_us,518  );519  addPassTiming(520    'JS: JSON.stringify options',521    profile.rustBridge.jsStringifyOptions_us,522  );523  addPassTiming('JS: JSON.parse result', profile.rustBridge.jsParseResult_us);524525  // Rust passes526  for (const pass of profile.rustPasses) {527    addPassTiming(`Rust: ${pass.name}`, pass.duration_us);528  }529}530531function percentile(values: number[], p: number): number {532  const sorted = [...values].sort((a, b) => a - b);533  const idx = Math.ceil((p / 100) * sorted.length) - 1;534  return sorted[Math.max(0, idx)];535}536537// --- Output ---538if (jsonMode) {539  const output = {540    build: releaseMode ? 'release' : 'debug',541    fixtureCount: fixtures.length,542    totalTS_us: totalTS,543    totalRust_us: totalRust,544    ratio: Math.round(ratio * 100) / 100,545    passAggregates: Object.fromEntries(546      [...passAggregates.entries()].map(([name, agg]) => [547        name,548        {549          total_us: agg.total_us,550          avg_us: Math.round(agg.total_us / agg.values.length),551          p95_us: percentile(agg.values, 95),552          count: agg.values.length,553        },554      ]),555    ),556    fixtures: profiles,557  };558  console.log(JSON.stringify(output, null, 2));559} else {560  console.log('');561  console.log(`${BOLD}=== Summary ===${RESET}`);562  console.log(563    `Build: ${CYAN}${releaseMode ? 'release' : 'debug'}${RESET} | Fixtures: ${BOLD}${fixtures.length}${RESET} | Warmup: done`,564  );565  console.log('');566567  const tsMs = (totalTS / 1000).toFixed(1);568  const rustMs = (totalRust / 1000).toFixed(1);569  const ratioStr = ratio.toFixed(2);570  const ratioColor = ratio <= 1.0 ? GREEN : ratio <= 1.5 ? YELLOW : RED;571  console.log(572    `Total: TS ${BOLD}${tsMs}ms${RESET} | Rust ${BOLD}${rustMs}ms${RESET} | Ratio ${ratioColor}${ratioStr}x${RESET}`,573  );574  console.log('');575576  // --- Pass breakdown table ---577  console.log(`${BOLD}=== Rust Time Breakdown (aggregate) ===${RESET}`);578579  // Sort by total time descending580  const sortedPasses = [...passAggregates.entries()].sort(581    (a, b) => b[1].total_us - a[1].total_us,582  );583584  const header = `${'Phase'.padEnd(50)} ${'Total(ms)'.padStart(10)} ${'%'.padStart(6)} ${'Avg(us)'.padStart(9)} ${'P95(us)'.padStart(9)}`;585  console.log(`${DIM}${header}${RESET}`);586587  for (const [name, agg] of sortedPasses) {588    const totalMs = (agg.total_us / 1000).toFixed(1);589    const pct = ((agg.total_us / totalRust) * 100).toFixed(1);590    const avg = Math.round(agg.total_us / agg.values.length);591    const p95 = percentile(agg.values, 95);592593    console.log(594      `${name.padEnd(50)} ${totalMs.padStart(10)} ${(pct + '%').padStart(6)} ${String(avg).padStart(9)} ${String(p95).padStart(9)}`,595    );596  }597  console.log('');598599  // --- Top 20 slowest fixtures ---600  console.log(`${BOLD}=== Top 20 Slowest Fixtures (Rust) ===${RESET}`);601  const sortedFixtures = [...profiles].sort(602    (a, b) => b.rustTotal_us - a.rustTotal_us,603  );604  const topN = sortedFixtures.slice(0, 20);605606  const fHeader = `${'Fixture'.padEnd(55)} ${'Size'.padStart(6)} ${'TS(ms)'.padStart(8)} ${'Rust(ms)'.padStart(9)} ${'Ratio'.padStart(7)} ${'Bottleneck'.padStart(20)}`;607  console.log(`${DIM}${fHeader}${RESET}`);608609  for (const p of topN) {610    const shortName =611      p.fixture.length > 54 ? '...' + p.fixture.slice(-51) : p.fixture;612    const sizeStr =613      p.sizeBytes > 1024614        ? (p.sizeBytes / 1024).toFixed(0) + 'K'615        : String(p.sizeBytes);616    const tsMsStr = (p.tsTotal_us / 1000).toFixed(2);617    const rustMsStr = (p.rustTotal_us / 1000).toFixed(2);618    const fixtureRatio = p.tsTotal_us > 0 ? p.rustTotal_us / p.tsTotal_us : 0;619    const ratioStr = fixtureRatio.toFixed(1) + 'x';620    const ratioColor =621      fixtureRatio <= 1.0 ? GREEN : fixtureRatio <= 1.5 ? YELLOW : RED;622623    // Find bottleneck pass624    let bottleneck = '';625    if (p.rustPasses.length > 0) {626      const sorted = [...p.rustPasses].sort(627        (a, b) => b.duration_us - a.duration_us,628      );629      const top = sorted[0];630      const pct = ((top.duration_us / p.rustTotal_us) * 100).toFixed(0);631      bottleneck = `${top.name} (${pct}%)`;632    }633    const bottleneckStr =634      bottleneck.length > 19 ? bottleneck.slice(0, 19) + '…' : bottleneck;635636    console.log(637      `${shortName.padEnd(55)} ${sizeStr.padStart(6)} ${tsMsStr.padStart(8)} ${rustMsStr.padStart(9)} ${ratioColor}${ratioStr.padStart(7)}${RESET} ${bottleneckStr.padStart(20)}`,638    );639  }640}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.