Skip to main content
The Noir profiler (noir-profiler) generates flamegraphs that map circuit costs back to the original source code. It supports three profiling modes:

Installation

The profiler is installed automatically with Nargo starting from noirup v0.1.4. Verify the installation:
If not found, reinstall using noirup:

Quick circuit info with nargo info

Before reaching for the profiler, nargo info gives a fast overview of a compiled circuit’s opcode count without generating a flamegraph:
Example output:
Use nargo info to track the overall size of a circuit as you make changes. Use the profiler when you need to understand which source locations are responsible for that size.

Profiling ACIR opcodes

ACIR opcodes are the intermediate representation consumed by proving backends. Profiling ACIR opcodes gives an approximate view of which parts of your program contribute most to circuit size and proving time.

Example program

Create a new project and add the following source:
Compile it:

Generating a flamegraph

This generates an HTML flamegraph file in ./target/. Open it in a browser for an interactive view. Flamegraph output: Each frame in the graph represents a source location. The width is proportional to the number of ACIR opcodes attributed to that location. Click on a frame to zoom in. In the example above, 387 ACIR opcodes are generated. The flamegraph reveals that the majority come from the array[i] = 0 write inside the loop — a dynamic array write that requires memory opcodes for each element.

Searching the flamegraph

Click Search in the top-right corner and type a source expression (e.g. i > ptr). The matching frames are highlighted, and the Matched percentage in the bottom-right shows the fraction of total opcodes they account for.

Optimizing based on profiler results

After identifying a bottleneck, you can apply unconstrained functions to move expensive operations out of the constrained circuit. Before optimization: 387 ACIR opcodes — dynamic array writes dominate. After optimization: 284 ACIR opcodes — array writes moved to an unconstrained function, constrained circuit only checks results.
The optimization removes the dynamic memory writes from the constrained circuit, replacing them with cheaper arithmetic assertions. Re-running the profiler confirms the reduction.
Search for memory::op in the flamegraph before and after this optimization. After the optimization, the search will have no matches because the dynamic array access has been eliminated.

Profiling proving backend gates

ACIR opcodes are only an approximation of proving cost. Different backends translate ACIR into different numbers of proving gates. Profile gates directly for an accurate picture.
This feature requires a proving backend that supports the profiler’s gate profiling API. The example below uses Barretenberg (bb).

Generating a gates flamegraph

  • --backend-path — path to the proving backend binary. If bb is on your PATH, use bb directly; otherwise provide the absolute path.
  • Arguments after -- are passed to the backend.

Interpreting gate counts

The gate flamegraph shows how the backend’s gate count is distributed across source locations. This distribution can differ significantly from the ACIR opcode flamegraph. For example, Barretenberg’s UltraHonk uses lookup tables for range checks, which carry a fixed setup cost regardless of circuit size. This means blackbox::range may appear as a dominant contributor in small circuits but shrink to a small fraction in larger ones. Example with array size 32: blackbox::range contributes most gates. Example with array size 2048: blackbox::range contributes a much smaller percentage because the fixed setup cost is amortized over many more total gates. This illustrates why it is useful to profile with different input sizes to understand how your circuit scales.

Profiling unconstrained execution

The profiler can also measure the Brillig (unconstrained VM) opcode count for programs that run entirely in unconstrained mode.

Preparing the program

Add the unconstrained modifier to the main function from the earlier example:
Generate a Prover.toml with inputs:
Edit Prover.toml:

Generating an execution flamegraph

The output is a flamegraph of Brillig opcode counts mapped to source locations. Unlike the ACIR flamegraph, this reflects actual execution cost for a specific set of inputs.

Balancing proving and execution

Moving constrained operations to unconstrained functions reduces ACIR opcodes (and proving time) but increases Brillig opcodes (and execution time). The profiler lets you measure both sides of this tradeoff. For most Noir programs, proving time dominates, so reducing ACIR opcodes is typically the right priority. However, if execution time becomes a bottleneck, use execution-opcodes profiling to identify where Brillig time is spent.

Workflow summary

1

Check overall circuit size

2

Generate an ACIR opcode flamegraph

Open the generated HTML file in a browser.
3

Identify the dominant source locations

Use Search in the flamegraph to find specific expressions. Look for frames with the widest spans.
4

Apply optimizations

Common techniques:
  • Move expensive array writes into unconstrained functions and assert the results.
  • Avoid dynamic array indexing where possible.
  • Use compile-time constants instead of runtime inputs.
5

Re-profile to measure the improvement

Recompile and regenerate the flamegraph. Compare opcode counts before and after.
6

Profile backend gates if needed

Use this when ACIR opcode counts do not accurately reflect proving performance for your backend.