PageSourceSearch

https://noir-lang.org/docs/assets/js/2e5004de.d33624ef.js

js noir-lang.org collected 2026-10-03 22:50:10 UTC 18,811 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkdocs=globalThis.webpackChunkdocs||[]).push([[482],{95329(e,i,n){n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>s,default:()=>p,frontMatter:()=>a,metadata:()=>r,toc:()=>c});const r=JSON.parse('{"id":"tooling/profiler","title":"Profiler","description":"Learn about the Noir Profiler, how to generate execution flamegraphs, identify bottlenecks, and visualize optimizations.","source":"@site/processed-docs/tooling/profiler.md","sourceDirName":"tooling","slug":"/tooling/profiler","permalink":"/docs/dev/tooling/profiler","draft":false,"unlisted":false,"editUrl":"https://github.com/noir-lang/noir/edit/master/docs/docs/tooling/profiler.md","tags":[],"version":"current","frontMatter":{"title":"Profiler","description":"Learn about the Noir Profiler, how to generate execution flamegraphs, identify bottlenecks, and visualize optimizations.","keywords":["profiling","profiler","flamegraph"]},"sidebar":"sidebar","previous":{"title":"Setting up shell completions","permalink":"/docs/dev/tooling/shell_completions"},"next":{"title":"Dev Containers","permalink":"/docs/dev/tooling/devcontainer"}}');var t=n(74848),o=n(28453);const a={title:"Profiler",description:"Learn about the Noir Profiler, how to generate execution flamegraphs, identify bottlenecks, and visualize optimizations.",keywords:["profiling","profiler","flamegraph"]},s=void 0,l={},c=[{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"Profiling ACIR opcodes",id:"profiling-acir-opcodes",level:3},{value:"Create a demonstrative project",id:"create-a-demonstrative-project",level:4},{value:"Flamegraphing",id:"flamegraphing",level:4},{value:"Visualizing optimizations",id:"visualizing-optimizations",level:4},{value:"Searching",id:"searching",level:4},{value:"Profiling proving backend gates",id:"profiling-proving-backend-gates",level:3},{value:"Flamegraphing",id:"flamegraphing-1",level:4},{value:"Understanding bottlenecks",id:"understanding-bottlenecks",level:4},{value:"Profiling execution traces (unconstrained)",id:"profiling-execution-traces-unconstrained",level:3},{value:"Updating the demonstrative project",id:"updating-the-demonstrative-project",level:4},{value:"Flamegraphing",id:"flamegraphing-2",level:4},{value:"Balancing proving and execution optimizations",id:"balancing-proving-and-execution-optimizations",level:4}];function d(e){const i={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.p,{children:"The profiler is a sampling profiler designed to analyze and visualize Noir programs. It assists developers to identify bottlenecks by mapping execution data back to the original source code."}),"\n",(0,t.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(i.p,{children:"The profiler is automatically installed with Nargo starting noirup v0.1.4."}),"\n",(0,t.jsxs)(i.p,{children:["Check if the profiler is already installed by running ",(0,t.jsx)(i.code,{children:"noir-profiler --version"}),". If the profiler is not found, update noirup and install the profiler by ",(0,t.jsx)(i.a,{href:"/docs/dev/getting_started_manually#installing-nargo",children:"reinstalling both noirup and Nargo"}),"."]}),"\n",(0,t.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(i.h3,{id:"profiling-acir-opcodes",children:"Profiling ACIR opcodes"}),"\n",(0,t.jsxs)(i.p,{children:["The profiler provides the ability to flamegraph a Noir program's ACIR opcodes footprint. This is useful for ",(0,t.jsx)(i.em,{children:"approximately"})," identifying bottlenecks in constrained execution and proving of Noir programs."]}),"\n",(0,t.jsxs)(i.admonition,{type:"note",children:[(0,t.jsxs)(i.p,{children:['"',(0,t.jsx)(i.em,{children:"Approximately"}),'" as:']}),(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsx)(i.li,{children:"Execution speeds depend on the constrained execution trace compiled from ACIR opcodes"}),"\n",(0,t.jsx)(i.li,{children:"Proving speeds depend on the how the proving backend of choice interprets the ACIR opcodes"}),"\n"]})]}),"\n",(0,t.jsx)(i.h4,{id:"create-a-demonstrative-project",children:"Create a demonstrative project"}),"\n",(0,t.jsx)(i.p,{children:"Let's start by creating a simple Noir program that aims to zero out an array past some dynamic index."}
1),"\n",(0,t.jsxs)(i.p,{children:["Run ",(0,t.jsx)(i.code,{children:"nargo new program"})," to create a new project named ",(0,t.jsx)(i.em,{children:"program"}),", then copy in the following as source code:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-rust",children:"fn main(ptr: pub u32, mut array: [u32; 32]) -> pub [u32; 32] {\n    for i in 0..32 {\n        if i > ptr {\n            array[i] = 0;\n        }\n    }\n    array\n}\n"})}),"\n",(0,t.jsxs)(i.p,{children:["Change into the project directory and compile the program using ",(0,t.jsx)(i.code,{children:"nargo compile"}),". We are now ready to try out the profiler."]}),"\n",(0,t.jsx)(i.h4,{id:"flamegraphing",children:"Flamegraphing"}),"\n",(0,t.jsx)(i.p,{children:"Let's take a granular look at our program's ACIR opcode footprint using the profiler, running:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-sh",children:"noir-profiler opcodes --artifact-path ./target/program.json --output ./target/\n"})}),"\n",(0,t.jsxs)(i.p,{children:["The command generates a flamegraph in your ",(0,t.jsx)(i.em,{children:"target"})," folder that maps the number of ACIR opcodes to their corresponding locations in your program's source code."]}),"\n",(0,t.jsx)(i.p,{children:"Opening the flamegraph in a web browser will provide a more interactive experience, allowing you to click into different regions of the graph and examine them."}),"\n",(0,t.jsx)(i.p,{children:"Flamegraph of the demonstrative project generated with Nargo v1.0.0-beta.2:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"ACIR Flamegraph Unoptimized",src:n(17820).A+"",width:"2196",height:"198"})}),"\n",(0,t.jsxs)(i.p,{children:["The demonstrative project consists of 387 ACIR opcodes in total. From the flamegraph, we can see that the majority come from the write to ",(0,t.jsx)(i.code,{children:"array[i]"}),"."]}),"\n",(0,t.jsx)(i.p,{children:"With insight into our program's bottleneck, let's optimize it."}),"\n",(0,t.jsx)(i.h4,{id:"visualizing-optimizations",children:"Visualizing optimizations"}),"\n",(0,t.jsxs)(i.p,{children:["We can improve our program's performance using ",(0,t.jsx)(i.a,{href:"/docs/dev/language/unconstrained",children:"unconstrained functions"}),"."]}),"\n",(0,t.jsx)(i.p,{children:"Let's replace expensive array writes with array gets with the new code below:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-rust",children:"fn main(ptr: pub u32, array: [u32; 32]) -> pub [u32; 32] {\n    // Safety: Sets all elements after `ptr` in `array` to zero.\n    let zeroed_array = unsafe { zero_out_array(ptr, array) };\n    for i in 0..32 {\n        if i > ptr {\n            assert_eq(zeroed_array[i], 0);\n        } else {\n            assert_eq(zeroed_array[i], array[i]);\n        }\n    }\n    zeroed_array\n}\n\nunconstrained fn zero_out_array(ptr: u32, mut array: [u32; 32]) -> [u32; 32] {\n    for i in 0..32 {\n        if i > ptr {\n            array[i] = 0;\n        }\n    }\n    array\n}\n"})}),"\n",(0,t.jsx)(i.p,{children:"Instead of writing our array in a fully constrained context, we first write our array inside an unconstrained function. Then, we assert every value in the array returned from the unconstrained function in a constrained context."}),"\n",(0,t.jsx)(i.p,{children:"This brings the ACIR opcodes count of our program down to a total of 284 opcodes:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"ACIR Flamegraph Optimized",src:n(7515).A+"",width:"2192",height:"212"})}),"\n",(0,t.jsx)(i.h4,{id:"searching",children:"Searching"}),"\n",(0,t.jsxs)(i.p,{children:["The ",(0,t.jsx)(i.code,{children:"i > ptr"})," region in the above image is highlighted purple as we were searching for it."]}),"\n",(0,t.jsx)(i.p,{children:'Click "Search" in the top right corner of the flamegraph to start a search (e.g. i > ptr).'}),"\n",(0,t.jsx)(i.p,{children:'Check "Matched" in the bottom right corner to learn the percentage out of total opcodes associated with the search (e.g. 43.3%).'}),"\n",(0,t.jsxs)(i.admonition,{type:"tip",children:[(0,t.jsxs)(i.p,{children:["If you try searching for ",(0,t.jsx)(i.co
1de,{children:"memory::op"})," before and after the optimization, you will find that the search will no longer have matches after the optimization."]}),(0,t.jsx)(i.p,{children:"This comes from the optimization removing the use of a dynamic array (i.e. an array with a dynamic index, that is its values rely on witness inputs). After the optimization, the program reads from two arrays with known constant indices, replacing the original memory operations with simple arithmetic operations."})]}),"\n",(0,t.jsx)(i.h3,{id:"profiling-proving-backend-gates",children:"Profiling proving backend gates"}),"\n",(0,t.jsx)(i.p,{children:"The profiler further provides the ability to flamegraph a Noir program's proving backend gates footprint. This is useful for fully identifying proving bottlenecks of Noir programs."}),"\n",(0,t.jsxs)(i.p,{children:["This feature depends on the proving backend you are using and whether it supports the profiler with a gate profiling API. We will use ",(0,t.jsx)(i.a,{href:"https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg",children:"Barretenberg"})," as an example here."]}),"\n",(0,t.jsx)(i.h4,{id:"flamegraphing-1",children:"Flamegraphing"}),"\n",(0,t.jsx)(i.p,{children:"Let's take a granular look at our program's proving backend gates footprint using the profiler, running:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-sh",children:"noir-profiler gates --artifact-path ./target/program.json --backend-path bb --output ./target -- --include_gates_per_opcode\n"})}),"\n",(0,t.jsxs)(i.p,{children:["The ",(0,t.jsx)(i.code,{children:"--backend-path"})," flag takes in the path to your proving backend binary."]}),"\n",(0,t.jsx)(i.p,{children:"The above command assumes you have Barretenberg (bb) installed and that its path is saved in your PATH. If that is not the case, you can pass in the absolute path to your proving backend binary instead."}),"\n",(0,t.jsx)(i.p,{children:"Flamegraph of the optimized demonstrative project generated with bb v0.76.4:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Gates Flamegraph Optimized",src:n(49714).A+"",width:"2162",height:"286"})}),"\n",(0,t.jsx)(i.p,{children:"The demonstrative project consists of 3,062 proving backend gates in total."}),"\n",(0,t.jsxs)(i.admonition,{type:"note",children:[(0,t.jsxs)(i.p,{children:["If you try searching for ",(0,t.jsx)(i.code,{children:"i > ptr"})," in the source code, you will notice that this call stack is only contributing 3.8% of the total proving backend gates, versus the 43.3% ACIR opcodes it contributes."]}),(0,t.jsx)(i.p,{children:"This illustrates that number of ACIR opcodes are at best approximations of proving performances, where actual proving performances depend on how the proving backend interprets and translates ACIR opcodes into proving gates."})]}),"\n",(0,t.jsx)(i.h4,{id:"understanding-bottlenecks",children:"Understanding bottlenecks"}),"\n",(0,t.jsx)(i.p,{children:"Profiling your program with different parameters is good way to understand your program's bottlenecks as it scales."}),"\n",(0,t.jsxs)(i.p,{children:["From the flamegraph above, you will notice that ",(0,t.jsx)(i.code,{children:"blackbox::range"})," contributes the majority of the backend gates. This comes from how Barretenberg UltraHonk uses lookup tables for its range gates under the hood, which comes with a considerable but fixed setup cost in terms of proving gates."]}),"\n",(0,t.jsx)(i.p,{children:"If our array is larger, range gates would become a much smaller percentage of our total circuit. See this flamegraph for the same optimized program but with an array of size 2,048 (versus originally 32) in comparison:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Gates Flamegraph Optimized 2048",src:n(37623).A+"",width:"2128",height:"288"})}),"\n",(0,t.jsxs)(i.p,{children:["Where ",(0,t.jsx)(i.code,{children:"blackbox::range"})," contributes a considerably smaller portion of the total proving gates."]}),"\n",(0,t.jsx)(i.p,{children:"Every proving backend interprets ACIR opcodes differently, so it is important to profile proving backend gates to get the full picture of proving performance."}),"\n",(0,t.jsx)(i.p,{children:"As additional reference, this is the flamegraph of the pre-optimization demonstrative project at array size 32:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Gates Flamegraph Unoptimized",src:n(42321).A+"",width:"2196",height:"220"})}),"\n",(0,t.jsx)(i.p,{children:"And at array size 2,048:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Gates Flamegraph Unoptimized 2048",src:n(42462).A+"",width:"2198",height:"214"})}),"\n",(0,t.jsx)(i.h3,{id:"profiling-execution-traces-unconstrained",children:"Profiling execution traces (unconstrained)"}),"\n",(0,t.jsx)(i.p,{children:"The profiler also provides the ability to flamegraph a N
1oir program's execution trace. This is useful for identifying execution bottlenecks of Noir programs."}),"\n",(0,t.jsx)(i.p,{children:"The profiler supports profiling fully unconstrained Noir programs at this moment."}),"\n",(0,t.jsx)(i.h4,{id:"updating-the-demonstrative-project",children:"Updating the demonstrative project"}),"\n",(0,t.jsxs)(i.p,{children:["Let's turn our demonstrative program into an unconstrained program by adding an ",(0,t.jsx)(i.code,{children:"unconstrained"})," modifier to the main function:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-rust",children:"unconstrained fn main(...){...}\n"})}),"\n",(0,t.jsx)(i.p,{children:"Since we are profiling the execution trace, we will also need to provide a set of inputs to execute the program with."}),"\n",(0,t.jsxs)(i.p,{children:["Run ",(0,t.jsx)(i.code,{children:"nargo check"})," to generate a ",(0,t.jsx)(i.em,{children:"Prover.toml"})," file, which you can fill it in with:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-toml",children:"ptr = 1\narray = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n"})}),"\n",(0,t.jsx)(i.h4,{id:"flamegraphing-2",children:"Flamegraphing"}),"\n",(0,t.jsx)(i.p,{children:"Let's take a granular look at our program's unconstrained execution trace footprint using the profiler, running:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-sh",children:"noir-profiler execution-opcodes --artifact-path ./target/program.json --prover-toml-path Prover.toml --output ./target\n"})}),"\n",(0,t.jsxs)(i.p,{children:["This is similar to the ",(0,t.jsx)(i.code,{children:"opcodes"})," command, except it additionally takes in the ",(0,t.jsx)(i.em,{children:"Prover.toml"})," file to profile execution with a specific set of inputs."]}),"\n",(0,t.jsxs)(i.p,{children:["Flamegraph of the demonstrative project generated with Nargo v1.0.0-beta.2:\n",(0,t.jsx)(i.img,{alt:"Brillig Trace "Optimized"",src:n(19445).A+"",width:"2424",height:"302"})]}),"\n",(0,t.jsx)(i.p,{children:"Note that unconstrained Noir functions compile down to Brillig opcodes, which is what the counts in this flamegraph stand for, rather than constrained ACIR opcodes like in the previous section."}),"\n",(0,t.jsx)(i.h4,{id:"balancing-proving-and-execution-optimizations",children:"Balancing proving and execution optimizations"}),"\n",(0,t.jsxs)(i.p,{children:["Rewriting constrained operations with unconstrained operations like what we did in ",(0,t.jsx)(i.a,{href:"#visualizing-optimizations",children:"the optimization section"})," helps remove ACIR opcodes (hence shorter proving times), but would introduce more Brillig opcodes (hence longer execution times)."]}),"\n",(0,t.jsxs)(i.p,{children:["For example, we can find a 13.9% match ",(0,t.jsx)(i.code,{children:"new_array"})," in the flamegraph above."]}),"\n",(0,t.jsxs)(i.p,{children:["In contrast, if we profile the pre-optimization demonstrative project:\n",(0,t.jsx)(i.img,{alt:"Brillig Trace Initial Program",src:n(76596).A+"",width:"2428",height:"252"})]}),"\n",(0,t.jsxs)(i.p,{children:["You will notice that it does not contain ",(0,t.jsx)(i.code,{children:"new_array"})," and executes a smaller total of 1,582 Brillig opcodes (versus 2,125 Brillig opcodes post-optimization)."]}),"\n",(0,t.jsx)(i.p,{children:"As new unconstrained functions were added, it is reasonable that the program would consist of more Brillig opcodes. That said, the tradeoff is often easily justifiable by the fact that proving speeds are more commonly the major bottleneck of Noir programs versus execution speeds."}),"\n",(0,t.jsx)(i.p,{children:"This is however good to keep in mind in case you start noticing execution speeds being the bottleneck of your program, or if you are simply looking to optimize your program's execution speeds."})]})}function p(e={}){const{wrapper:i}={...(0,o.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},7515(e,i,n){const r=n.p+"assets/images/acir-flamegraph-optimized-ba11bb17cfc7666ada23ca2d4f3aee0b.png";n.d(i,["A",0,r])},17820(e,i,n){const r=n.p+"assets/images/acir-flamegraph-unoptimized-546afbec080a85b8a07de754c3e2e263.png";n.d(i,["A",0,r])},76596(e,i,n){const r=n.p+"assets/images/brillig-trace-initial-32-8f9a440c570b9454b996d375a9385b63.png";n.d(i,["A",0,r])},19445(e,i,n){const r=n.p+"assets/images/brillig-trace-opt-32-089b0dbe8f328deac4f3bc71bc952d7d.png";n.d(i,["A",0,r])},37623(e,i,n){const r=n.p+"assets/images/gates-flamegraph-optimized-2048-dd97d0996d6cd357e24fd386c2c47c6e.png";n.d(i,["A",0,r])}
1,49714(e,i,n){const r=n.p+"assets/images/gates-flamegraph-optimized-17ab2888d7480ddf5e82b9944be2c829.png";n.d(i,["A",0,r])},42462(e,i,n){const r=n.p+"assets/images/gates-flamegraph-unoptimized-2048-ac68b8f6840d1fe864f4cdb6ee00bc18.png";n.d(i,["A",0,r])},42321(e,i,n){const r=n.p+"assets/images/gates-flamegraph-unoptimized-acf36aa9b749d4f0766d4e2a09689d60.png";n.d(i,["A",0,r])},28453(e,i,n){n.d(i,{R:()=>a,x:()=>s});var r=n(96540);const t={},o=r.createContext(t);function a(e){const i=r.useContext(o);return r.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function s(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),r.createElement(o.Provider,{value:i},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.