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