diff --git a/yarn-project/aztec/bootstrap.sh b/yarn-project/aztec/bootstrap.sh index bd4f131a2f5e..c27fba277781 100755 --- a/yarn-project/aztec/bootstrap.sh +++ b/yarn-project/aztec/bootstrap.sh @@ -4,13 +4,14 @@ source $(git rev-parse --show-toplevel)/ci3/source_bootstrap repo_root=$(git rev-parse --show-toplevel) export NARGO=${NARGO:-$repo_root/noir/noir-repo/target/release/nargo} export BB=${BB:-$repo_root/barretenberg/cpp/build/bin/bb} +export PROFILER_PATH=${PROFILER_PATH:-$repo_root/noir/noir-repo/target/release/noir-profiler} hash=$(../bootstrap.sh hash) function test_cmds { - for test in src/cli/**/*.test.ts; do - echo "$hash:ISOLATE=1:NAME=aztec/$test NARGO=$NARGO BB=$BB yarn-project/scripts/run_test.sh aztec/$test" - done + # All CLI tests share test/mixed-workspace/target so they must run sequentially + # in a single jest invocation (--runInBand is set by run_test.sh). + echo "$hash:ISOLATE=1:NAME=aztec/cli NARGO=$NARGO BB=$BB PROFILER_PATH=$PROFILER_PATH yarn-project/scripts/run_test.sh aztec/src/cli" } case "$cmd" in diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index bf73218d3949..e5573d79c880 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -53,9 +53,13 @@ case $cmd in aztec start "$@" ;; - new|init|flamegraph) + new|init) $script_dir/${cmd}.sh "$@" ;; + flamegraph) + echo "Warning: 'aztec flamegraph' is deprecated. Use 'aztec profile flamegraph' instead." >&2 + aztec profile flamegraph "$@" + ;; *) aztec $cmd "$@" ;; diff --git a/yarn-project/aztec/scripts/extract_function.js b/yarn-project/aztec/scripts/extract_function.js deleted file mode 100644 index c73c8ba9aa58..000000000000 --- a/yarn-project/aztec/scripts/extract_function.js +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node -import fs from 'fs/promises'; -import path from 'path'; - -// Simple script to extract a contract function as a separate Noir artifact. -// We need to use this since the transpiling that we do on public functions make the contract artifacts -// unreadable by noir tooling, since they are no longer following the noir artifact format. -async function main() { - let [contractArtifactPath, functionName] = process.argv.slice(2); - if (!contractArtifactPath || !functionName) { - console.log('Usage: node extractFunctionAsNoirArtifact.js '); - return; - } - - const contractArtifact = JSON.parse(await fs.readFile(contractArtifactPath, 'utf8')); - const func = contractArtifact.functions.find(f => f.name === functionName); - if (!func) { - console.error(`Function ${functionName} not found in ${contractArtifactPath}`); - return; - } - - const artifact = { - noir_version: contractArtifact.noir_version, - hash: 0, - abi: func.abi, - bytecode: func.bytecode, - debug_symbols: func.debug_symbols, - file_map: contractArtifact.file_map, - expression_width: { - Bounded: { - width: 4, - }, - }, - }; - - const outputDir = path.dirname(contractArtifactPath); - const outputName = path.basename(contractArtifactPath, '.json') + `-${functionName}.json`; - - const outPath = path.join(outputDir, outputName); - - await fs.writeFile(outPath, JSON.stringify(artifact, null, 2)); -} - -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/yarn-project/aztec/scripts/flamegraph.sh b/yarn-project/aztec/scripts/flamegraph.sh deleted file mode 100755 index 48763ef0d793..000000000000 --- a/yarn-project/aztec/scripts/flamegraph.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -eu - -# If first arg is -h or --help, print usage. -if [ $# -lt 2 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then - cat << 'EOF' -Aztec Flamegraph - Generate a gate count flamegraph for an aztec contract function. - -Usage: aztec flamegraph - -Options: - -h, --help Print help - -Will output an svg at /--flamegraph.svg. -You can open it in your browser to view it. - -EOF - exit 0 -fi - -cleanup() { - set +e - if [ -f "$function_artifact" ]; then - rm -f "$function_artifact" - fi -} - -trap cleanup EXIT - -# Get the directory of the script -script_dir=$(realpath $(dirname $0)) - -PROFILER=${PROFILER_PATH:-noir-profiler} -BB=${BB:-bb} - -# first console arg is contract name in camel case or path to contract artifact -contract=$1 - -# second console arg is the contract function -function=$2 - -if [ ! -f "$contract" ]; then - echo "Error: Contract artifact not found at: $contract" - exit 1 -fi -artifact_path=$contract -function_artifact="${artifact_path%%.json}-${function}.json" -output_dir=$(dirname "$artifact_path") - -# Extract artifact for the specific function. -node $script_dir/extract_function.js "$artifact_path" $function - -# Generate the flamegraph -$PROFILER gates --artifact-path "$function_artifact" --backend-path "$BB" --backend-gates-command "gates" --output "$output_dir" --scheme chonk --include_gates_per_opcode - -# Save as $artifact_name-$function-flamegraph.svg -output_file="${function_artifact%%.json}-flamegraph.svg" -mv "$output_dir/__aztec_nr_internals__${function}_gates.svg" "$output_file" -echo "Flamegraph generated at: $output_file" diff --git a/yarn-project/aztec/src/cli/cmds/compile.ts b/yarn-project/aztec/src/cli/cmds/compile.ts index c3b5cdd2e51c..9737eeb9b312 100644 --- a/yarn-project/aztec/src/cli/cmds/compile.ts +++ b/yarn-project/aztec/src/cli/cmds/compile.ts @@ -1,25 +1,11 @@ import type { LogFn } from '@aztec/foundation/log'; -import { execFileSync, spawn } from 'child_process'; +import { execFileSync } from 'child_process'; import type { Command } from 'commander'; import { readFile, writeFile } from 'fs/promises'; import { readArtifactFiles } from './utils/artifacts.js'; - -/** Spawns a command with inherited stdio and rejects on non-zero exit. */ -function run(cmd: string, args: string[]): Promise { - return new Promise((resolve, reject) => { - const child = spawn(cmd, args, { stdio: 'inherit' }); - child.on('error', reject); - child.on('close', code => { - if (code !== 0) { - reject(new Error(`${cmd} exited with code ${code}`)); - } else { - resolve(); - } - }); - }); -} +import { run } from './utils/spawn.js'; /** Returns paths to contract artifacts in the target directory. */ async function collectContractArtifacts(): Promise { diff --git a/yarn-project/aztec/src/cli/cmds/profile.ts b/yarn-project/aztec/src/cli/cmds/profile.ts index 7ae5005e9268..d7248025074a 100644 --- a/yarn-project/aztec/src/cli/cmds/profile.ts +++ b/yarn-project/aztec/src/cli/cmds/profile.ts @@ -2,6 +2,7 @@ import type { LogFn } from '@aztec/foundation/log'; import type { Command } from 'commander'; +import { profileFlamegraph } from './profile_flamegraph.js'; import { profileGates } from './profile_gates.js'; export function injectProfileCommand(program: Command, log: LogFn): Command { @@ -13,5 +14,12 @@ export function injectProfileCommand(program: Command, log: LogFn): Command { .description('Display gate counts for all compiled Aztec artifacts in a target directory.') .action((targetDir: string) => profileGates(targetDir, log)); + profile + .command('flamegraph') + .argument('', 'Path to the compiled contract artifact JSON') + .argument('', 'Name of the contract function to profile') + .description('Generate a gate count flamegraph SVG for a contract function.') + .action((artifactPath: string, functionName: string) => profileFlamegraph(artifactPath, functionName, log)); + return program; } diff --git a/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts new file mode 100644 index 000000000000..4a91bf417717 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts @@ -0,0 +1,51 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import { existsSync, readFileSync, rmSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..'); +const CLI = join(PACKAGE_ROOT, 'dest/bin/index.js'); +const WORKSPACE = join(PACKAGE_ROOT, 'test/mixed-workspace'); +const TARGET = join(WORKSPACE, 'target'); +const CONTRACT_ARTIFACT = join(TARGET, 'simple_contract-SimpleContract.json'); + +describe('aztec profile flamegraph', () => { + const svgPath = join(TARGET, 'simple_contract-SimpleContract-private_function-flamegraph.svg'); + + beforeAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + runCompile(); + runFlamegraph(CONTRACT_ARTIFACT, 'private_function'); + }, 300_000); + + afterAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + }); + + it('generates a valid flamegraph SVG', () => { + expect(existsSync(svgPath)).toBe(true); + const content = readFileSync(svgPath, 'utf-8'); + expect(content).toContain(''); + }); +}); + +function runCompile() { + try { + execFileSync('node', [CLI, 'compile'], { cwd: WORKSPACE, stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`compile failed:\n${e.stderr?.toString() ?? e.message}`); + } +} + +function runFlamegraph(artifactPath: string, functionName: string) { + try { + execFileSync('node', [CLI, 'profile', 'flamegraph', artifactPath, functionName], { + encoding: 'utf-8', + stdio: 'pipe', + }); + } catch (e: any) { + throw new Error(`profile flamegraph failed:\n${e.stderr?.toString() ?? e.message}`); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts new file mode 100644 index 000000000000..78b4743d715e --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts @@ -0,0 +1,63 @@ +import type { LogFn } from '@aztec/foundation/log'; + +import { readFile, rename, rm, writeFile } from 'fs/promises'; +import { basename, dirname, join } from 'path'; + +import { makeFunctionArtifact } from './profile_utils.js'; +import type { CompiledArtifact } from './utils/artifacts.js'; +import { run } from './utils/spawn.js'; + +/** Generates a gate count flamegraph SVG for a single contract function. */ +export async function profileFlamegraph(artifactPath: string, functionName: string, log: LogFn): Promise { + const raw = await readFile(artifactPath, 'utf-8'); + const artifact: CompiledArtifact = JSON.parse(raw); + + if (!Array.isArray(artifact.functions)) { + throw new Error(`${artifactPath} does not appear to be a contract artifact (no functions array)`); + } + + const func = artifact.functions.find(f => f.name === functionName); + if (!func) { + const available = artifact.functions.map(f => f.name).join(', '); + throw new Error(`Function "${functionName}" not found in artifact. Available: ${available}`); + } + if (func.is_unconstrained) { + throw new Error(`Function "${functionName}" is unconstrained and cannot be profiled`); + } + + const outputDir = dirname(artifactPath); + const contractName = basename(artifactPath, '.json'); + const functionArtifact = join(outputDir, `${contractName}-${functionName}.json`); + + try { + await writeFile(functionArtifact, makeFunctionArtifact(artifact, func)); + + const profiler = process.env.PROFILER_PATH ?? 'noir-profiler'; + const bb = process.env.BB ?? 'bb'; + + await run(profiler, [ + 'gates', + '--artifact-path', + functionArtifact, + '--backend-path', + bb, + '--backend-gates-command', + 'gates', + '--output', + outputDir, + '--scheme', + 'chonk', + '--include_gates_per_opcode', + ]); + + // noir-profiler names the SVG using the internal function name which + // retains the __aztec_nr_internals__ prefix in the bytecode metadata. + const srcSvg = join(outputDir, `__aztec_nr_internals__${functionName}_gates.svg`); + const destSvg = join(outputDir, `${contractName}-${functionName}-flamegraph.svg`); + await rename(srcSvg, destSvg); + + log(`Flamegraph written to ${destSvg}`); + } finally { + await rm(functionArtifact, { force: true }); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_utils.ts b/yarn-project/aztec/src/cli/cmds/profile_utils.ts index 2a7b2c805e2f..e604419bc631 100644 --- a/yarn-project/aztec/src/cli/cmds/profile_utils.ts +++ b/yarn-project/aztec/src/cli/cmds/profile_utils.ts @@ -44,7 +44,7 @@ export async function discoverArtifacts( } /** Extracts a contract function as a standalone program artifact JSON string. */ -function makeFunctionArtifact(artifact: CompiledArtifact, func: ContractFunction) { +export function makeFunctionArtifact(artifact: CompiledArtifact, func: ContractFunction) { /* eslint-disable camelcase */ return JSON.stringify({ noir_version: artifact.noir_version, diff --git a/yarn-project/aztec/src/cli/cmds/utils/spawn.ts b/yarn-project/aztec/src/cli/cmds/utils/spawn.ts new file mode 100644 index 000000000000..53514e06d931 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/utils/spawn.ts @@ -0,0 +1,16 @@ +import { spawn } from 'child_process'; + +/** Spawns a command with inherited stdio and rejects on non-zero exit. */ +export function run(cmd: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('error', reject); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`${cmd} exited with code ${code}`)); + } else { + resolve(); + } + }); + }); +}