diff --git a/app/src/components/variation/index.tsx b/app/src/components/variation/index.tsx
index 36412eef6..2b4c9abf1 100644
--- a/app/src/components/variation/index.tsx
+++ b/app/src/components/variation/index.tsx
@@ -5,7 +5,9 @@ import { ERROR_MESSAGES } from "@/constants";
import { VariationChart } from "@/components/variation/chart";
import { VariationTable } from "@/components/variation/table";
import { PackageCountTable } from "@/components/variation/package-count-table";
+import { ProcessCountTable } from "@/components/variation/process-count-table";
import { usePackageCountData } from "@/hooks/use-package-count-data";
+import { useProcessCountData } from "@/hooks/use-process-count-data";
import { useFixtureFilter } from "@/contexts/fixture-filter-context";
import {
sortFixtures,
@@ -14,6 +16,7 @@ import {
getFixtureId,
getAvailablePackageManagers,
getAvailablePackageManagersFromPackageCount,
+ getAvailablePackageManagersFromProcessCount,
isTaskExecutionVariation,
isRegistryVariation,
} from "@/lib/utils";
@@ -46,6 +49,11 @@ export const VariationPage = () => {
loading: packageCountLoading,
error: packageCountError,
} = usePackageCountData(variation as Variation);
+ const {
+ processCountData,
+ loading: processCountLoading,
+ error: processCountError,
+ } = useProcessCountData(variation as Variation);
const { enabledFixtures } = useFixtureFilter();
// Handle deep linking to sections and fixtures
@@ -111,6 +119,9 @@ export const VariationPage = () => {
const filteredPackageCountData = packageCountData.filter((item) =>
enabledFixtures.has(item.fixture),
);
+ const filteredProcessCountData = processCountData.filter((item) =>
+ enabledFixtures.has(item.fixture),
+ );
// Filter package managers to only show those with data for this variation
const packageManagers = getAvailablePackageManagers(
@@ -125,6 +136,13 @@ export const VariationPage = () => {
allPackageManagers,
);
+ // Filter package managers for process count data to only show those with actual data
+ const processCountPackageManagers =
+ getAvailablePackageManagersFromProcessCount(
+ filteredProcessCountData,
+ allPackageManagers,
+ );
+
// Check if this is a task execution variation or registry variation
const isTaskExecution = isTaskExecutionVariation(variation as string);
const isRegistry = isRegistryVariation(variation as string);
@@ -137,6 +155,7 @@ export const VariationPage = () => {
perPackageChart: "Task Execution Time by Fixture",
perPackageTable: "Task Execution Time Data",
packageCountTable: "Package Count Data",
+ processCountTable: "Spawned Processes Data",
}
: isRegistry
? {
@@ -145,6 +164,7 @@ export const VariationPage = () => {
perPackageChart: "Registry Install Time by Fixture",
perPackageTable: "Registry Install Time Data",
packageCountTable: "Package Count Data",
+ processCountTable: "Spawned Processes Data",
}
: {
totalChart: "Total Install Time by Fixture",
@@ -152,6 +172,7 @@ export const VariationPage = () => {
perPackageChart: "Per Package Install Time by Fixture",
perPackageTable: "Per Package Install Time Data",
packageCountTable: "Package Count Data",
+ processCountTable: "Spawned Processes Data",
};
// Section IDs for deep linking
@@ -161,6 +182,7 @@ export const VariationPage = () => {
totalTable: createSectionId(titles.totalTable),
perPackageTable: createSectionId(titles.perPackageTable),
packageCountTable: createSectionId(titles.packageCountTable),
+ processCountTable: createSectionId(titles.processCountTable),
};
return (
@@ -240,6 +262,28 @@ export const VariationPage = () => {
/>
) : null}
+
+ {/* 6. Spawned processes data table */}
+ {processCountLoading ? (
+
+ Loading process count data...
+
+ ) : processCountError ? (
+
+ Error loading process count data: {processCountError}
+
+ ) : filteredProcessCountData.length > 0 ? (
+
+ ) : null}
diff --git a/app/src/components/variation/process-count-table.tsx b/app/src/components/variation/process-count-table.tsx
new file mode 100644
index 000000000..21dd9624d
--- /dev/null
+++ b/app/src/components/variation/process-count-table.tsx
@@ -0,0 +1,310 @@
+import { useMemo, useState } from "react";
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ flexRender,
+ createColumnHelper,
+} from "@tanstack/react-table";
+import { ArrowUp, ArrowDown, ArrowUpDown, Terminal } from "lucide-react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import {
+ getPackageManagerDisplayName,
+ getPackageManagerVersion,
+ getFixtureDisplayName,
+ sortFixtures,
+ createSectionId,
+ isRegistryVariation,
+} from "@/lib/utils";
+import { ShareButton } from "@/components/share-button";
+import { usePackageManagerFilter } from "@/contexts/package-manager-filter-context";
+
+import type {
+ Fixture,
+ PackageCountEntry,
+ ProcessCountTableRow,
+ PackageManager,
+ PackageManagerVersions,
+} from "@/types/chart-data";
+import type { SortingState } from "@tanstack/react-table";
+
+interface ProcessCountTableProps {
+ title: string;
+ description?: string;
+ processCountData: ProcessCountTableRow[];
+ packageManagers: PackageManager[];
+ versions?: PackageManagerVersions;
+ currentVariation: string;
+}
+
+interface TransposedProcessCountRow {
+ packageManager: PackageManager;
+ processCountsByFixture: Partial
>;
+}
+
+const columnHelper = createColumnHelper();
+
+export const ProcessCountTable = ({
+ title,
+ description,
+ processCountData,
+ packageManagers,
+ versions,
+ currentVariation,
+}: ProcessCountTableProps) => {
+ const { enabledPackageManagers } = usePackageManagerFilter();
+ const [sorting, setSorting] = useState([]);
+ const isRegistry = isRegistryVariation(currentVariation);
+ const showVersions = !isRegistry;
+
+ // Filter package managers based on global filter
+ const filteredPackageManagers = useMemo(
+ () => packageManagers.filter((pm) => enabledPackageManagers.has(pm)),
+ [packageManagers, enabledPackageManagers],
+ );
+
+ const fixtures = useMemo(
+ () =>
+ sortFixtures(
+ Array.from(new Set(processCountData.map((item) => item.fixture))) as Fixture[],
+ ),
+ [processCountData],
+ );
+
+ const transposedData = useMemo(
+ () =>
+ filteredPackageManagers.map((packageManager) => {
+ const processCountsByFixture: Partial> = {};
+
+ processCountData.forEach((fixtureResult) => {
+ const entry = fixtureResult.processCounts[packageManager];
+ if (entry && typeof entry.count === "number") {
+ processCountsByFixture[fixtureResult.fixture] = entry;
+ }
+ });
+
+ return {
+ packageManager,
+ processCountsByFixture,
+ };
+ }),
+ [filteredPackageManagers, processCountData],
+ );
+
+ const columns = useMemo(
+ () => [
+ columnHelper.accessor("packageManager", {
+ header: isRegistry ? "Registry" : "Package Manager",
+ cell: (info) => {
+ const packageManager = info.getValue();
+ const version = showVersions
+ ? getPackageManagerVersion(packageManager, versions)
+ : undefined;
+ const displayName = getPackageManagerDisplayName(packageManager, {
+ isRegistryVariation: isRegistry,
+ });
+ return version ? (
+
+
{displayName}
+
{version}
+
+ ) : (
+ {displayName}
+ );
+ },
+ enableSorting: true,
+ sortingFn: (rowA, rowB) =>
+ getPackageManagerDisplayName(rowA.original.packageManager, {
+ isRegistryVariation: isRegistry,
+ }).localeCompare(
+ getPackageManagerDisplayName(rowB.original.packageManager, {
+ isRegistryVariation: isRegistry,
+ }),
+ ),
+ }),
+ ...fixtures.map((fixture) =>
+ columnHelper.accessor((row) => row.processCountsByFixture[fixture], {
+ id: fixture,
+ header: () => {getFixtureDisplayName(fixture)},
+ cell: (info) => {
+ const entry = info.getValue();
+ if (entry && typeof entry.count === "number") {
+ const { count, minCount, maxCount } = entry;
+
+ if (
+ minCount !== undefined &&
+ maxCount !== undefined &&
+ minCount !== maxCount
+ ) {
+ return (
+
+
{count.toLocaleString()}
+
+ ({minCount.toLocaleString()}-{maxCount.toLocaleString()})
+
+
+ );
+ }
+
+ return (
+
+ {count.toLocaleString()}
+
+ );
+ }
+ return (
+
+ -
+
+ );
+ },
+ enableSorting: true,
+ sortingFn: (rowA, rowB, columnId) => {
+ const entryA = rowA.getValue(columnId) as
+ | { count: number }
+ | undefined;
+ const entryB = rowB.getValue(columnId) as
+ | { count: number }
+ | undefined;
+
+ if (!entryA && !entryB) return 0;
+ if (!entryA) return 1;
+ if (!entryB) return -1;
+
+ return entryA.count - entryB.count;
+ },
+ }),
+ ),
+ ],
+ [fixtures, versions, showVersions, isRegistry],
+ );
+
+ const table = useReactTable({
+ data: transposedData,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ enableSortingRemoval: false,
+ state: {
+ sorting,
+ },
+ onSortingChange: setSorting,
+ });
+
+ return (
+
+
+
+
+ {title}
+
+
+ {description && (
+
{description}
+ )}
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.column.getCanSort() ? (
+
+ ) : (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No process count data available.
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/app/src/hooks/use-process-count-data.ts b/app/src/hooks/use-process-count-data.ts
new file mode 100644
index 000000000..31f626b24
--- /dev/null
+++ b/app/src/hooks/use-process-count-data.ts
@@ -0,0 +1,89 @@
+import { useState, useEffect } from "react";
+import type {
+ Variation,
+ Fixture,
+ ProcessCountData,
+ ProcessCountTableRow,
+} from "@/types/chart-data";
+import { sortFixtures } from "@/lib/utils";
+
+interface UseProcessCountDataReturn {
+ processCountData: ProcessCountTableRow[];
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+}
+
+export const useProcessCountData = (
+ variation: Variation,
+): UseProcessCountDataReturn => {
+ const [processCountData, setProcessCountData] = useState<
+ ProcessCountTableRow[]
+ >([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fixtures: Fixture[] = sortFixtures([
+ "next",
+ "astro",
+ "svelte",
+ "vue",
+ "large",
+ "babylon",
+ "run",
+ ]);
+
+ const fetchProcessCountData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const results: ProcessCountTableRow[] = [];
+
+ for (const fixture of fixtures) {
+ try {
+ const url = `/latest/${fixture}-${variation}-process-count.json`;
+ const response = await fetch(url);
+
+ if (response.ok) {
+ const processCounts: ProcessCountData = await response.json();
+ results.push({
+ fixture,
+ variation,
+ processCounts,
+ });
+ } else {
+ console.warn(
+ `Process count data not found for ${fixture}-${variation}`,
+ );
+ }
+ } catch (e) {
+ console.warn(
+ `Failed to load process count data for ${fixture}-${variation}:`,
+ e,
+ );
+ }
+ }
+
+ setProcessCountData(results);
+ } catch (e) {
+ const errorMessage =
+ e instanceof Error ? e.message : "Unknown error occurred";
+ console.error("Error fetching process count data:", errorMessage);
+ setError(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchProcessCountData();
+ }, [variation]);
+
+ return {
+ processCountData,
+ loading,
+ error,
+ refetch: fetchProcessCountData,
+ };
+};
diff --git a/app/src/lib/utils.ts b/app/src/lib/utils.ts
index c977eb0fe..5f41d22ad 100644
--- a/app/src/lib/utils.ts
+++ b/app/src/lib/utils.ts
@@ -9,6 +9,7 @@ import type {
BenchmarkChartData,
FixtureResult,
PackageCountData,
+ ProcessCountData,
} from "@/types/chart-data";
interface VariationCategory {
@@ -616,3 +617,28 @@ export const getAvailablePackageManagersFromPackageCount = (
return allPackageManagers.filter((pm) => availablePackageManagers.has(pm));
};
+
+export const getAvailablePackageManagersFromProcessCount = (
+ processCountData: Array<{ processCounts?: ProcessCountData }>,
+ allPackageManagers: PackageManager[],
+): PackageManager[] => {
+ const availablePackageManagers = new Set();
+
+ processCountData.forEach((item) => {
+ if (item.processCounts) {
+ allPackageManagers.forEach((pm) => {
+ const entry = item.processCounts?.[pm as keyof ProcessCountData];
+ if (
+ entry &&
+ typeof entry === "object" &&
+ entry.count &&
+ entry.count > 0
+ ) {
+ availablePackageManagers.add(pm);
+ }
+ });
+ }
+ });
+
+ return allPackageManagers.filter((pm) => availablePackageManagers.has(pm));
+};
diff --git a/app/src/types/chart-data.ts b/app/src/types/chart-data.ts
index 99718c5c8..366870324 100644
--- a/app/src/types/chart-data.ts
+++ b/app/src/types/chart-data.ts
@@ -175,6 +175,28 @@ export interface PackageCountTableRow {
packageCounts: PackageCountData;
}
+export interface ProcessCountData {
+ npm?: PackageCountEntry;
+ yarn?: PackageCountEntry;
+ pnpm?: PackageCountEntry;
+ berry?: PackageCountEntry;
+ zpm?: PackageCountEntry;
+ deno?: PackageCountEntry;
+ bun?: PackageCountEntry;
+ vlt?: PackageCountEntry;
+ nx?: PackageCountEntry;
+ turbo?: PackageCountEntry;
+ node?: PackageCountEntry;
+ aws?: PackageCountEntry;
+ cloudsmith?: PackageCountEntry;
+}
+
+export interface ProcessCountTableRow {
+ fixture: Fixture;
+ variation: Variation;
+ processCounts: ProcessCountData;
+}
+
export function isBenchmarkChartData(
data: unknown,
): data is BenchmarkChartData {
diff --git a/scripts/collect-process-count.js b/scripts/collect-process-count.js
new file mode 100644
index 000000000..306b1e1d6
--- /dev/null
+++ b/scripts/collect-process-count.js
@@ -0,0 +1,94 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read the output folder from first positional argument
+const outputFolder = process.argv[2];
+if (!outputFolder) {
+ throw new Error('Output folder not found');
+}
+
+// Try to read existing process-count.json or start with empty object
+const processCountPath = path.resolve(outputFolder, 'process-count.json');
+let result;
+try {
+ const existingData = fs.readFileSync(processCountPath, 'utf8');
+ result = JSON.parse(existingData);
+} catch (error) {
+ // File doesn't exist or can't be parsed, start with empty object
+ result = {};
+}
+
+// Define the possible count files and their corresponding package manager names
+const countFiles = [
+ { filename: 'npm-process-count.txt', pmName: 'npm' },
+ { filename: 'yarn-process-count.txt', pmName: 'yarn' },
+ { filename: 'berry-process-count.txt', pmName: 'berry' },
+ { filename: 'zpm-process-count.txt', pmName: 'zpm' },
+ { filename: 'pnpm-process-count.txt', pmName: 'pnpm' },
+ { filename: 'vlt-process-count.txt', pmName: 'vlt' },
+ { filename: 'bun-process-count.txt', pmName: 'bun' },
+ { filename: 'deno-process-count.txt', pmName: 'deno' },
+ { filename: 'nx-process-count.txt', pmName: 'nx' },
+ { filename: 'turbo-process-count.txt', pmName: 'turbo' },
+ { filename: 'node-process-count.txt', pmName: 'node' },
+];
+
+let found = false;
+for (const { filename, pmName } of countFiles) {
+ const filePath = path.resolve(outputFolder, filename);
+ let fileContent;
+ try {
+ fileContent = fs.readFileSync(filePath, 'utf8');
+ } catch {
+ // File doesn't exist or can't be read, try next one
+ continue;
+ }
+ found = true;
+ console.log('Read file path:', filePath);
+
+ // Parse each line as a number
+ const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
+ const countValues = lines.map(line => parseInt(line.trim(), 10)).filter(num => !isNaN(num));
+
+ // If there are no valid count values, log a warning and skip
+ if (countValues.length === 0) {
+ console.warn(`No valid count values found for ${pmName}`);
+ continue;
+ }
+
+ // Keep references to found count values
+ let count, minCount, maxCount;
+
+ if (countValues.every(value => value === countValues[0])) {
+ // All values are the same
+ count = countValues[0];
+ } else {
+ // Different values found
+ minCount = Math.min(...countValues);
+ maxCount = Math.max(...countValues);
+ // Calculate average
+ const sum = countValues.reduce((acc, val) => acc + val, 0);
+ count = Math.round(sum / countValues.length);
+ }
+
+ // Update the result object
+ if (minCount !== undefined && maxCount !== undefined) {
+ result[pmName] = { count, minCount, maxCount };
+ } else {
+ result[pmName] = { count };
+ }
+
+ // Remove the temp file after reading it
+ fs.unlinkSync(filePath);
+}
+
+// If no count file was found, log a warning and exit
+if (!found) {
+ console.warn('Could not read any temporary process count file');
+ process.exit(0);
+}
+
+// Save the updated result back to the JSON file
+fs.writeFileSync(processCountPath, JSON.stringify(result, null, 2));
+
+console.log('Successfully collected process count data');
diff --git a/scripts/process-count.sh b/scripts/process-count.sh
new file mode 100644
index 000000000..43b4aaaaf
--- /dev/null
+++ b/scripts/process-count.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+# Counts spawned processes during a package manager install using strace.
+#
+# Usage:
+# bash process-count.sh [prepare-command]
+#
+# Runs a single install wrapped in `strace -f -e trace=execve` and counts
+# the number of execve() calls. The raw count is appended to
+# /-process-count.txt (one number per line, matching
+# the pattern used by package-count.sh).
+#
+# This script is meant to run OUTSIDE the timed benchmark (strace adds
+# overhead). It is invoked by `collect_process_count` in common.sh after
+# the hyperfine run completes.
+
+set -Eeuo pipefail
+
+if [ -z "${1:-}" ]; then
+ echo "Error: output folder is required"
+ exit 1
+fi
+OUTPUT_FOLDER="$1"
+
+if [ -z "${2:-}" ]; then
+ echo "Error: package manager name is required"
+ exit 1
+fi
+PM_NAME="$2"
+
+if [ -z "${3:-}" ]; then
+ echo "Error: install command is required"
+ exit 1
+fi
+INSTALL_CMD="$3"
+
+PREPARE_CMD="${4:-}"
+
+STRACE_LOG="$OUTPUT_FOLDER/${PM_NAME}-strace.log"
+PROCESS_COUNT_FILE="$OUTPUT_FOLDER/${PM_NAME}-process-count.txt"
+
+# Ensure strace is available
+if ! command -v strace &>/dev/null; then
+ echo "Warning: strace not found, skipping process count for $PM_NAME"
+ exit 0
+fi
+
+# Run optional prepare step (e.g. clean + setup)
+if [ -n "$PREPARE_CMD" ]; then
+ echo "[process-count] Running prepare for $PM_NAME..."
+ eval "$PREPARE_CMD" || true
+fi
+
+# Run the install command under strace
+echo "[process-count] Running strace'd install for $PM_NAME..."
+strace -f -e trace=execve -o "$STRACE_LOG" \
+ bash -c "$INSTALL_CMD" > /dev/null 2>&1 || true
+
+# Count execve calls (each line with 'execve(' is one spawned process)
+if [ -f "$STRACE_LOG" ]; then
+ COUNT=$(grep -c 'execve(' "$STRACE_LOG" 2>/dev/null || echo "0")
+ echo "$COUNT" >> "$PROCESS_COUNT_FILE"
+ echo "[process-count] $PM_NAME spawned $COUNT processes"
+
+ # Remove strace log to save space (can be large)
+ rm -f "$STRACE_LOG"
+else
+ echo "Warning: strace log not found for $PM_NAME"
+fi
diff --git a/scripts/process-results.sh b/scripts/process-results.sh
index 701240f96..e7d15d3ae 100644
--- a/scripts/process-results.sh
+++ b/scripts/process-results.sh
@@ -78,6 +78,24 @@ print_package_count() {
echo
}
+print_process_count() {
+ local file=$1
+ local fixture=$2
+ local variation=$3
+
+ if [ ! -f "$file" ]; then
+ echo "Warning: Process count file $file not found"
+ return 1
+ fi
+
+ echo "=== PROCESS COUNT: $fixture ($variation) ==="
+ if ! node -p "console.table(JSON.parse(fs.readFileSync('$file', 'utf8')))"; then
+ echo "Warning: Could not parse process count from $file"
+ return 1
+ fi
+ echo
+}
+
# Process and copy results
echo "Processing results..."
@@ -99,6 +117,14 @@ for fixture in next astro svelte vue large babylon; do
else
echo "Warning: No package count found for $fixture & $variation"
fi
+
+ if process_count_file=$(resolve_result_path "$fixture" "$variation" "process-count.json"); then
+ print_process_count "$process_count_file" "$fixture" "$variation"
+ cp "$process_count_file" "results/$DATE/$fixture-$variation-process-count.json"
+ cp "$process_count_file" "results/latest/$fixture-$variation-process-count.json"
+ else
+ echo "Warning: No process count found for $fixture & $variation"
+ fi
done
done
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 2e99df8dd..21f9e17d6 100644
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -18,7 +18,7 @@ fi
# Install system dependencies
echo "Installing system dependencies..."
-sudo apt-get update && sudo apt-get install -y jq
+sudo apt-get update && sudo apt-get install -y jq strace
# Install Hyperfine v1.19.0+ (required for --conclude flag)
# Ubuntu 24.04 apt ships v1.18.0 which lacks --conclude, so we install from GitHub releases
diff --git a/scripts/variations/cache+lockfile+node_modules.sh b/scripts/variations/cache+lockfile+node_modules.sh
index df6da6c0d..5c7960027 100644
--- a/scripts/variations/cache+lockfile+node_modules.sh
+++ b/scripts/variations/cache+lockfile+node_modules.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/cache+lockfile.sh b/scripts/variations/cache+lockfile.sh
index bee1924dc..7a7dff729 100644
--- a/scripts/variations/cache+lockfile.sh
+++ b/scripts/variations/cache+lockfile.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/cache+node_modules.sh b/scripts/variations/cache+node_modules.sh
index 27b4d3ec1..a2f5b4409 100644
--- a/scripts/variations/cache+node_modules.sh
+++ b/scripts/variations/cache+node_modules.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/cache.sh b/scripts/variations/cache.sh
index 2cee69b40..314892f71 100644
--- a/scripts/variations/cache.sh
+++ b/scripts/variations/cache.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/clean.sh b/scripts/variations/clean.sh
index 478a75ed4..d42589ca6 100644
--- a/scripts/variations/clean.sh
+++ b/scripts/variations/clean.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/common.sh b/scripts/variations/common.sh
index b3469fd89..79a1f4002 100644
--- a/scripts/variations/common.sh
+++ b/scripts/variations/common.sh
@@ -74,14 +74,24 @@ BENCH_SETUP_NX=""
BENCH_SETUP_TURBO=""
BENCH_SETUP_NODE=""
-BENCH_COMMAND_NPM="npm install --no-audit --no-fund --silent >> $BENCH_OUTPUT_FOLDER/npm-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_YARN="corepack yarn@1 install --silent > $BENCH_OUTPUT_FOLDER/yarn-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_BERRY="corepack yarn@latest install > $BENCH_OUTPUT_FOLDER/berry-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_ZPM="yarn install --silent > $BENCH_OUTPUT_FOLDER/zpm-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_PNPM="corepack pnpm@latest install --silent > $BENCH_OUTPUT_FOLDER/pnpm-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_VLT="vlt install --view=silent > $BENCH_OUTPUT_FOLDER/vlt-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_BUN="bun install --silent > $BENCH_OUTPUT_FOLDER/bun-output-\${HYPERFINE_ITERATION}.log 2>&1"
-BENCH_COMMAND_DENO="deno install --allow-scripts --quiet > $BENCH_OUTPUT_FOLDER/deno-output-\${HYPERFINE_ITERATION}.log 2>&1"
+# Bare install commands (no log redirection) — used by strace process counting
+BENCH_INSTALL_NPM="npm install --no-audit --no-fund --silent"
+BENCH_INSTALL_YARN="corepack yarn@1 install --silent"
+BENCH_INSTALL_BERRY="corepack yarn@latest install"
+BENCH_INSTALL_ZPM="yarn install --silent"
+BENCH_INSTALL_PNPM="corepack pnpm@latest install --silent"
+BENCH_INSTALL_VLT="vlt install --view=silent"
+BENCH_INSTALL_BUN="bun install --silent"
+BENCH_INSTALL_DENO="deno install --allow-scripts --quiet"
+
+BENCH_COMMAND_NPM="$BENCH_INSTALL_NPM >> $BENCH_OUTPUT_FOLDER/npm-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_YARN="$BENCH_INSTALL_YARN > $BENCH_OUTPUT_FOLDER/yarn-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_BERRY="$BENCH_INSTALL_BERRY > $BENCH_OUTPUT_FOLDER/berry-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_ZPM="$BENCH_INSTALL_ZPM > $BENCH_OUTPUT_FOLDER/zpm-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_PNPM="$BENCH_INSTALL_PNPM > $BENCH_OUTPUT_FOLDER/pnpm-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_VLT="$BENCH_INSTALL_VLT > $BENCH_OUTPUT_FOLDER/vlt-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_BUN="$BENCH_INSTALL_BUN > $BENCH_OUTPUT_FOLDER/bun-output-\${HYPERFINE_ITERATION}.log 2>&1"
+BENCH_COMMAND_DENO="$BENCH_INSTALL_DENO > $BENCH_OUTPUT_FOLDER/deno-output-\${HYPERFINE_ITERATION}.log 2>&1"
# Clean up & create the results directory
rm -rf "$BENCH_OUTPUT_FOLDER"
@@ -131,3 +141,66 @@ collect_package_count() {
node "$BENCH_SCRIPTS/collect-package-count.js" "$BENCH_OUTPUT_FOLDER"
}
+
+# Function to collect spawned process counts via strace.
+# Runs a single strace'd install for each included package manager under the
+# same state as the benchmark variation. BENCH_PREPARE_BASE must be set by the
+# calling variation script before invoking this function.
+collect_process_count() {
+ if ! command -v strace &>/dev/null; then
+ echo "Warning: strace not available, skipping process count collection"
+ return 0
+ fi
+
+ echo "=== Collecting spawned process counts ==="
+
+ # Map of pm name -> setup command and bare install command
+ local -A PM_SETUP=(
+ [npm]="$BENCH_SETUP_NPM"
+ [yarn]="$BENCH_SETUP_YARN"
+ [berry]="$BENCH_SETUP_BERRY"
+ [zpm]="$BENCH_SETUP_ZPM"
+ [pnpm]="$BENCH_SETUP_PNPM"
+ [vlt]="$BENCH_SETUP_VLT"
+ [bun]="$BENCH_SETUP_BUN"
+ [deno]="$BENCH_SETUP_DENO"
+ )
+ local -A PM_INSTALL=(
+ [npm]="$BENCH_INSTALL_NPM"
+ [yarn]="$BENCH_INSTALL_YARN"
+ [berry]="$BENCH_INSTALL_BERRY"
+ [zpm]="$BENCH_INSTALL_ZPM"
+ [pnpm]="$BENCH_INSTALL_PNPM"
+ [vlt]="$BENCH_INSTALL_VLT"
+ [bun]="$BENCH_INSTALL_BUN"
+ [deno]="$BENCH_INSTALL_DENO"
+ )
+ local -A PM_INCLUDE=(
+ [npm]="$BENCH_INCLUDE_NPM"
+ [yarn]="$BENCH_INCLUDE_YARN"
+ [berry]="$BENCH_INCLUDE_BERRY"
+ [zpm]="$BENCH_INCLUDE_ZPM"
+ [pnpm]="$BENCH_INCLUDE_PNPM"
+ [vlt]="$BENCH_INCLUDE_VLT"
+ [bun]="$BENCH_INCLUDE_BUN"
+ [deno]="$BENCH_INCLUDE_DENO"
+ )
+
+ for pm in npm yarn berry zpm pnpm vlt bun deno; do
+ if [ -n "${PM_INCLUDE[$pm]:-}" ]; then
+ local prepare_cmd="$BENCH_PREPARE_BASE"
+ local setup="${PM_SETUP[$pm]:-}"
+ if [ -n "$setup" ]; then
+ prepare_cmd="$prepare_cmd; $setup"
+ fi
+
+ bash "$BENCH_SCRIPTS/process-count.sh" \
+ "$BENCH_OUTPUT_FOLDER" \
+ "$pm" \
+ "${PM_INSTALL[$pm]}" \
+ "$prepare_cmd"
+ fi
+ done
+
+ node "$BENCH_SCRIPTS/collect-process-count.js" "$BENCH_OUTPUT_FOLDER"
+}
diff --git a/scripts/variations/lockfile+node_modules.sh b/scripts/variations/lockfile+node_modules.sh
index df1b68e70..9286b3bd4 100644
--- a/scripts/variations/lockfile+node_modules.sh
+++ b/scripts/variations/lockfile+node_modules.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/lockfile.sh b/scripts/variations/lockfile.sh
index b8096678f..6febad757 100644
--- a/scripts/variations/lockfile.sh
+++ b/scripts/variations/lockfile.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count
diff --git a/scripts/variations/node_modules.sh b/scripts/variations/node_modules.sh
index 57b6a36bc..c3a49c58f 100644
--- a/scripts/variations/node_modules.sh
+++ b/scripts/variations/node_modules.sh
@@ -36,3 +36,4 @@ hyperfine --ignore-failure \
${BENCH_INCLUDE_DENO:+--command-name="deno" "$BENCH_COMMAND_DENO"}
collect_package_count
+collect_process_count