Files
oxc-project__oxc/.github/scripts/generate-benchmark-matrix.js
Boshen d174661221 ci(benchmark): add CodSpeed memory measurement for pipeline benchmarks (#24559)
Adds CodSpeed's [Memory Instrument](https://codspeed.io/docs/instruments/memory) for the `pipeline` benchmarks: peak memory, total allocated bytes, allocation count, and a memory timeline per benchmark case, tracked with baseline comparison on PRs alongside the existing CPU simulation runs.

## Changes

- `generate-benchmark-matrix.js`: entry construction is factored into a `buildMatrixEntries()` helper used by all five construction sites. All entries now carry an explicit `mode`: `simulation` for every component, `simulation,memory` for `pipeline` — so the pipeline job runs both instruments and inherits the existing changed-file affection logic.
- `benchmark.yml`: the action's `mode:` input comes from the matrix. Job names and count are unchanged.

No build-step changes needed: `cargo-codspeed` maps memory mode to the same `analysis` build the workflow already stages (`MeasurementMode::Memory` → `BuildMode::Analysis`), and the pinned action v4.18.5 supports memory mode (added in v4.17.0) including comma-separated multi-mode runs.

Notes:

- The first memory numbers on this PR will have no baseline; comparisons start after the post-merge main run.
- Memory metrics reflect the bench crate's deliberate `NeverGrowInPlaceAllocator` worst-case realloc behavior — consistent run-to-run, which is what matters for regression tracking.

AI Disclosure: This was generated with Claude Code, Fable 5. It has been tested and reviewed by me for correctness.
2026-07-15 16:12:52 +00:00

227 lines
6.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
// oxlint-disable no-console
/**
* Generate a dynamic matrix for benchmark jobs based on affected components.
* This script determines which benchmark components need to run based on changed files.
*/
const process = require("process");
const { getChangedFiles } = require("./get-changed-files.js");
const { getCrateDependencies } = require("./utils.js");
// All available benchmark components
const ALL_COMPONENTS = [
"lexer",
"parser",
"transformer",
"semantic",
"minifier",
"codegen",
"formatter",
"pipeline",
"react_compiler",
"linter",
];
// Files that when changed affect all benchmarks
const GLOBAL_FILES = [
"Cargo.lock",
"rust-toolchain.toml",
".github/workflows/benchmark.yml",
".github/scripts/generate-benchmark-matrix.js",
];
/**
* Check if any global files have changed that affect all benchmarks
* @param {string[]} changedFiles - Array of changed file paths
* @returns {boolean} True if global changes detected
*/
function checkGlobalChanges(changedFiles) {
if (!changedFiles || changedFiles.length === 0) {
return false;
}
for (const file of changedFiles) {
if (GLOBAL_FILES.some((globalFile) => file === globalFile || file.endsWith(`/${globalFile}`))) {
console.error(`Global file changed: ${file}`);
return true;
}
}
return false;
}
/**
* Map component to its feature
* @param {string} component - Component name
* @returns {string} Feature name
*/
function getFeatureForComponent(component) {
if (component === "linter") {
return "linter";
}
if (component === "react_compiler") {
return "react_compiler";
}
return "compiler";
}
/**
* Build matrix entries for components.
* `pipeline` additionally runs the memory instrument in the same job.
* @param {string[]} components - Component names
* @returns {Array<{component: string, feature: string, mode: string}>} Matrix entries
*/
function buildMatrixEntries(components) {
return components.map((component) => ({
component,
feature: getFeatureForComponent(component),
mode: component === "pipeline" ? "simulation,memory" : "simulation",
}));
}
/** @type {Map<string, string[]>} Cache of cargo tree results keyed by feature */
const depsCache = new Map();
/**
* Get dependencies for a specific benchmark component
* @param {string} component - Component name
* @returns {string[]} Array of dependency names
*/
function getComponentDependencies(component) {
const feature = getFeatureForComponent(component);
const cached = depsCache.get(feature);
if (cached) {
return cached;
}
const deps = getCrateDependencies("oxc_benchmark", {
features: feature,
noDefaultFeatures: true,
});
if (deps.length === 0) {
console.error(`Warning: Could not get dependencies for ${component} (feature: ${feature})`);
}
depsCache.set(feature, deps);
return deps;
}
/**
* Check if a component is affected by the changed files
* @param {string} component - Component name
* @param {string[]} changedFiles - Array of changed file paths
* @returns {boolean} True if component is affected
*/
function isComponentAffected(component, changedFiles) {
if (!changedFiles || changedFiles.length === 0) {
return false;
}
// Get component dependencies
const dependencies = getComponentDependencies(component);
console.error(`Component ${component} dependencies: ${dependencies.join(", ")}`);
// Check if any dependency files changed
for (const dep of dependencies) {
const depPath = `crates/${dep}/`;
if (changedFiles.some((file) => file.startsWith(depPath))) {
console.error(` Component ${component} affected by changes in ${depPath}`);
return true;
}
}
return false;
}
/**
* Determine which components are affected by changes
* @returns {Promise<Array<{component: string, feature: string, mode: string}>>} Matrix entries for affected components
*/
async function determineAffectedComponents() {
const changedFiles = await getChangedFiles();
// Manual trigger - run all benchmarks
if (changedFiles === null) {
return buildMatrixEntries(ALL_COMPONENTS);
}
// Check for global changes
if (checkGlobalChanges(changedFiles)) {
console.error("Global changes detected - will run all benchmarks");
return buildMatrixEntries(ALL_COMPONENTS);
}
// Check benchmark and common task files - affects all components
if (
changedFiles.some(
(file) => file.startsWith("tasks/benchmark/") || file.startsWith("tasks/common/"),
)
) {
console.error("Benchmark/common task files changed - will run all benchmarks");
return buildMatrixEntries(ALL_COMPONENTS);
}
// If no crate files changed, no benchmarks need to run
if (!changedFiles.some((file) => file.startsWith("crates/"))) {
console.error("No crate files changed - skipping cargo tree and all benchmarks");
return [];
}
// Check each component individually
const affectedComponents = [];
for (const component of ALL_COMPONENTS) {
console.error(`\nChecking component: ${component}`);
if (isComponentAffected(component, changedFiles)) {
affectedComponents.push(component);
}
}
if (affectedComponents.length === 0) {
console.error("\nNo components were affected by the changes");
} else {
console.error(`\nAffected components: ${affectedComponents.join(", ")}`);
}
return buildMatrixEntries(affectedComponents);
}
/**
* Main entry point
*/
async function main() {
try {
const affectedComponents = await determineAffectedComponents();
// Output the matrix as JSON array
// This will be captured by GitHub Actions
console.log(JSON.stringify(affectedComponents));
// Set GitHub Actions notice
if (affectedComponents.length === 0) {
console.error(
"::notice title=No benchmarks to run::No components were affected by the changes",
);
} else {
const componentNames = affectedComponents.map((obj) => obj.component).join(", ");
console.error(`::notice title=Running benchmarks::Affected components: ${componentNames}`);
}
process.exit(0);
} catch (error) {
console.error("Error generating benchmark matrix:", error);
// On error, run all benchmarks as a fallback
const fallbackMatrix = buildMatrixEntries(ALL_COMPONENTS);
console.log(JSON.stringify(fallbackMatrix));
process.exit(0);
}
}
// Run the script
void main();