diff --git a/.aztec-sync-commit b/.aztec-sync-commit
index ec0df2d1b06..485ef5a8949 100644
--- a/.aztec-sync-commit
+++ b/.aztec-sync-commit
@@ -1 +1 @@
-eb9e9f6f2b3952760822faaacb7e851e936e0800
+df3b27b8c603845598bf966100be3a21e8e442db
diff --git a/Cargo.lock b/Cargo.lock
index 37376ad7c80..589c3d179d8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2747,13 +2747,11 @@ version = "0.31.0"
 dependencies = [
  "acir",
  "clap",
- "codespan-reporting",
  "color-eyre",
  "const_format",
  "fm",
  "im",
  "inferno",
- "nargo",
  "noirc_abi",
  "noirc_artifacts",
  "noirc_driver",
diff --git a/tooling/profiler/Cargo.toml b/tooling/profiler/Cargo.toml
index d33e99f1a4c..0ccd56b791f 100644
--- a/tooling/profiler/Cargo.toml
+++ b/tooling/profiler/Cargo.toml
@@ -18,12 +18,10 @@ path = "src/main.rs"
 color-eyre.workspace = true
 clap.workspace = true
 noirc_artifacts.workspace = true
-nargo.workspace = true
 const_format.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 fm.workspace = true
-codespan-reporting.workspace = true
 inferno = "0.11.19"
 im.workspace = true
 acir.workspace = true
diff --git a/tooling/profiler/src/cli/gates_flamegraph_cmd.rs b/tooling/profiler/src/cli/gates_flamegraph_cmd.rs
index 38e7fff5f42..154ac38f4bb 100644
--- a/tooling/profiler/src/cli/gates_flamegraph_cmd.rs
+++ b/tooling/profiler/src/cli/gates_flamegraph_cmd.rs
@@ -1,19 +1,13 @@
-use std::collections::BTreeMap;
-use std::io::BufWriter;
 use std::path::{Path, PathBuf};
-use std::process::Command;
 
 use clap::Args;
-use codespan_reporting::files::Files;
 use color_eyre::eyre::{self, Context};
-use inferno::flamegraph::{from_lines, Options};
-use serde::{Deserialize, Serialize};
 
-use acir::circuit::OpcodeLocation;
-use nargo::errors::Location;
 use noirc_artifacts::debug::DebugArtifact;
-use noirc_artifacts::program::ProgramArtifact;
-use noirc_errors::reporter::line_and_column_from_span;
+
+use crate::flamegraph::{FlamegraphGenerator, InfernoFlamegraphGenerator};
+use crate::fs::read_program_from_file;
+use crate::gates_provider::{BackendGatesProvider, GatesProvider};
 
 #[derive(Debug, Clone, Args)]
 pub(crate) struct GatesFlamegraphCommand {
@@ -30,85 +24,11 @@ pub(crate) struct GatesFlamegraphCommand {
     output: String,
 }
 
-trait GatesProvider {
-    fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse>;
-}
-
-struct BackendGatesProvider {
-    backend_path: PathBuf,
-}
-
-impl GatesProvider for BackendGatesProvider {
-    fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse> {
-        let backend_gates_response =
-            Command::new(&self.backend_path).arg("gates").arg("-b").arg(artifact_path).output()?;
-
-        // Parse the backend gates command stdout as json
-        let backend_gates_response: BackendGatesResponse =
-            serde_json::from_slice(&backend_gates_response.stdout)?;
-        Ok(backend_gates_response)
-    }
-}
-
-trait FlamegraphGenerator {
-    fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>(
-        &self,
-        folded_lines: I,
-        artifact_name: &str,
-        function_name: &str,
-        output_path: &Path,
-    ) -> eyre::Result<()>;
-}
-
-struct InfernoFlamegraphGenerator {}
-
-impl FlamegraphGenerator for InfernoFlamegraphGenerator {
-    fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>(
-        &self,
-        folded_lines: I,
-        artifact_name: &str,
-        function_name: &str,
-        output_path: &Path,
-    ) -> eyre::Result<()> {
-        let flamegraph_file = std::fs::File::create(output_path)?;
-        let flamegraph_writer = BufWriter::new(flamegraph_file);
-
-        let mut options = Options::default();
-        options.hash = true;
-        options.deterministic = true;
-        options.title = format!("{}-{}", artifact_name, function_name);
-        options.subtitle = Some("Sample = Gate".to_string());
-        options.frame_height = 24;
-        options.color_diffusion = true;
-
-        from_lines(&mut options, folded_lines, flamegraph_writer)?;
-
-        Ok(())
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct BackendGatesReport {
-    acir_opcodes: usize,
-    circuit_size: usize,
-    gates_per_opcode: Vec<usize>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct BackendGatesResponse {
-    functions: Vec<BackendGatesReport>,
-}
-
-struct FoldedStackItem {
-    total_gates: usize,
-    nested_items: BTreeMap<String, FoldedStackItem>,
-}
-
 pub(crate) fn run(args: GatesFlamegraphCommand) -> eyre::Result<()> {
     run_with_provider(
         &PathBuf::from(args.artifact_path),
         &BackendGatesProvider { backend_path: PathBuf::from(args.backend_path) },
-        &InfernoFlamegraphGenerator {},
+        &InfernoFlamegraphGenerator { count_name: "gates".to_string() },
         &PathBuf::from(args.output),
     )
 }
@@ -119,7 +39,7 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>(
     flamegraph_generator: &Generator,
     output_path: &Path,
 ) -> eyre::Result<()> {
-    let program =
+    let mut program =
         read_program_from_file(artifact_path).context("Error reading program from file")?;
 
     let backend_gates_response =
@@ -127,10 +47,16 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>(
 
     let function_names = program.names.clone();
 
+    let bytecode = std::mem::take(&mut program.bytecode);
+
     let debug_artifact: DebugArtifact = program.into();
 
-    for (func_idx, (func_gates, func_name)) in
-        backend_gates_response.functions.into_iter().zip(function_names).enumerate()
+    for (func_idx, ((func_gates, func_name), bytecode)) in backend_gates_response
+        .functions
+        .into_iter()
+        .zip(function_names)
+        .zip(bytecode.functions)
+        .enumerate()
     {
         println!(
             "Opcode count: {}, Total gates by opcodes: {}, Circuit size: {}",
@@ -139,143 +65,33 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>(
             func_gates.circuit_size
         );
 
-        // Create a nested hashmap with the stack items, folding the gates for all the callsites that are equal
-        let mut folded_stack_items = BTreeMap::new();
-
-        func_gates.gates_per_opcode.into_iter().enumerate().for_each(|(opcode_index, gates)| {
-            let call_stack = &debug_artifact.debug_symbols[func_idx]
-                .locations
-                .get(&OpcodeLocation::Acir(opcode_index));
-            let location_names = if let Some(call_stack) = call_stack {
-                call_stack
-                    .iter()
-                    .map(|location| location_to_callsite_label(*location, &debug_artifact))
-                    .collect::<Vec<String>>()
-            } else {
-                vec!["unknown".to_string()]
-            };
-
-            add_locations_to_folded_stack_items(&mut folded_stack_items, location_names, gates);
-        });
-        let folded_lines = to_folded_sorted_lines(&folded_stack_items, Default::default());
-
         flamegraph_generator.generate_flamegraph(
-            folded_lines.iter().map(|as_string| as_string.as_str()),
+            func_gates.gates_per_opcode,
+            bytecode.opcodes,
+            &debug_artifact.debug_symbols[func_idx],
+            &debug_artifact,
             artifact_path.to_str().unwrap(),
             &func_name,
-            &Path::new(&output_path).join(Path::new(&format!("{}.svg", &func_name))),
+            &Path::new(&output_path).join(Path::new(&format!("{}_gates.svg", &func_name))),
         )?;
     }
 
     Ok(())
 }
 
-pub(crate) fn read_program_from_file<P: AsRef<Path>>(
-    circuit_path: P,
-) -> eyre::Result<ProgramArtifact> {
-    let file_path = circuit_path.as_ref().with_extension("json");
-
-    let input_string = std::fs::read(file_path)?;
-    let program = serde_json::from_slice(&input_string)?;
-
-    Ok(program)
-}
-
-fn location_to_callsite_label<'files>(
-    location: Location,
-    files: &'files impl Files<'files, FileId = fm::FileId>,
-) -> String {
-    let filename =
-        Path::new(&files.name(location.file).expect("should have a file path").to_string())
-            .file_name()
-            .map(|os_str| os_str.to_string_lossy().to_string())
-            .unwrap_or("invalid_path".to_string());
-    let source = files.source(location.file).expect("should have a file source");
-
-    let code_slice = source
-        .as_ref()
-        .chars()
-        .skip(location.span.start() as usize)
-        .take(location.span.end() as usize - location.span.start() as usize)
-        .collect::<String>();
-
-    // ";" is used for frame separation, and is not allowed by inferno
-    // Check code slice for ";" and replace it with 'GREEK QUESTION MARK' (U+037E)
-    let code_slice = code_slice.replace(';', "\u{037E}");
-
-    let (line, column) = line_and_column_from_span(source.as_ref(), &location.span);
-
-    format!("{}:{}:{}::{}", filename, line, column, code_slice)
-}
-
-fn add_locations_to_folded_stack_items(
-    stack_items: &mut BTreeMap<String, FoldedStackItem>,
-    locations: Vec<String>,
-    gates: usize,
-) {
-    let mut child_map = stack_items;
-    for (index, location) in locations.iter().enumerate() {
-        let current_item = child_map
-            .entry(location.clone())
-            .or_insert(FoldedStackItem { total_gates: 0, nested_items: BTreeMap::new() });
-
-        child_map = &mut current_item.nested_items;
-
-        if index == locations.len() - 1 {
-            current_item.total_gates += gates;
-        }
-    }
-}
-
-/// Creates a vector of lines in the format that inferno expects from a nested hashmap of stack items
-/// The lines have to be sorted in the following way, exploring the graph in a depth-first manner:
-/// main 100
-/// main::foo 0
-/// main::foo::bar 200
-/// main::baz 27
-/// main::baz::qux 800
-fn to_folded_sorted_lines(
-    folded_stack_items: &BTreeMap<String, FoldedStackItem>,
-    parent_stacks: im::Vector<String>,
-) -> Vec<String> {
-    folded_stack_items
-        .iter()
-        .flat_map(move |(location, folded_stack_item)| {
-            let frame_list: Vec<String> =
-                parent_stacks.iter().cloned().chain(std::iter::once(location.clone())).collect();
-            let line: String =
-                format!("{} {}", frame_list.join(";"), folded_stack_item.total_gates);
-
-            let mut new_parent_stacks = parent_stacks.clone();
-            new_parent_stacks.push_back(location.clone());
-
-            let child_lines: Vec<String> =
-                to_folded_sorted_lines(&folded_stack_item.nested_items, new_parent_stacks);
-
-            std::iter::once(line).chain(child_lines)
-        })
-        .collect()
-}
-
 #[cfg(test)]
 mod tests {
-    use acir::circuit::{OpcodeLocation, Program};
+    use acir::circuit::{Circuit, Opcode, Program};
     use color_eyre::eyre::{self};
-    use fm::{FileId, FileManager};
+    use fm::codespan_files::Files;
     use noirc_artifacts::program::ProgramArtifact;
-    use noirc_driver::DebugFile;
-    use noirc_errors::{
-        debug_info::{DebugInfo, ProgramDebugInfo},
-        Location, Span,
-    };
+    use noirc_errors::debug_info::{DebugInfo, ProgramDebugInfo};
     use std::{
-        cell::RefCell,
         collections::{BTreeMap, HashMap},
         path::{Path, PathBuf},
     };
-    use tempfile::TempDir;
 
-    use super::{BackendGatesReport, BackendGatesResponse, GatesProvider};
+    use crate::gates_provider::{BackendGatesReport, BackendGatesResponse, GatesProvider};
 
     struct TestGateProvider {
         mock_responses: HashMap<PathBuf, BackendGatesResponse>,
@@ -293,194 +109,63 @@ mod tests {
     }
 
     #[derive(Default)]
-    struct TestFlamegraphGenerator {
-        lines_received: RefCell<Vec<Vec<String>>>,
-    }
+    struct TestFlamegraphGenerator {}
 
     impl super::FlamegraphGenerator for TestFlamegraphGenerator {
-        fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>(
+        fn generate_flamegraph<'files, F>(
             &self,
-            folded_lines: I,
+            _samples_per_opcode: Vec<usize>,
+            _opcodes: Vec<Opcode<F>>,
+            _debug_symbols: &DebugInfo,
+            _files: &'files impl Files<'files, FileId = fm::FileId>,
             _artifact_name: &str,
             _function_name: &str,
-            _output_path: &std::path::Path,
+            output_path: &Path,
         ) -> eyre::Result<()> {
-            let lines = folded_lines.into_iter().map(|line| line.to_string()).collect();
-            self.lines_received.borrow_mut().push(lines);
-            Ok(())
-        }
-    }
+            let output_file = std::fs::File::create(output_path).unwrap();
+            std::io::Write::write_all(&mut std::io::BufWriter::new(output_file), b"success")
+                .unwrap();
 
-    fn find_spans_for(source: &str, needle: &str) -> Vec<Span> {
-        let mut spans = Vec::new();
-        let mut start = 0;
-        while let Some(start_idx) = source[start..].find(needle) {
-            let start_idx = start + start_idx;
-            let end_idx = start_idx + needle.len();
-            spans.push(Span::inclusive(start_idx as u32, end_idx as u32 - 1));
-            start = end_idx;
+            Ok(())
         }
-        spans
     }
 
-    struct TestCase {
-        expected_folded_sorted_lines: Vec<Vec<String>>,
-        debug_symbols: ProgramDebugInfo,
-        file_map: BTreeMap<FileId, DebugFile>,
-        gates_report: BackendGatesResponse,
-    }
+    #[test]
+    fn smoke_test() {
+        let temp_dir = tempfile::tempdir().unwrap();
 
-    fn simple_test_case(temp_dir: &TempDir) -> TestCase {
-        let source_code = r##"
-        fn main() {
-            foo();
-            bar();
-            whatever();
-        }
-        fn foo() {
-            baz();
-        }
-        fn bar () {
-            whatever()
-        }
-        fn baz () {
-            whatever()
-        }
-        "##;
+        let artifact_path = temp_dir.path().join("test.json");
 
-        let source_file_name = Path::new("main.nr");
-        let mut fm = FileManager::new(temp_dir.path());
-        let file_id = fm.add_file_with_source(source_file_name, source_code.to_string()).unwrap();
+        let artifact = ProgramArtifact {
+            noir_version: "0.0.0".to_string(),
+            hash: 27,
+            abi: noirc_abi::Abi::default(),
+            bytecode: Program { functions: vec![Circuit::default()], ..Program::default() },
+            debug_symbols: ProgramDebugInfo { debug_infos: vec![DebugInfo::default()] },
+            file_map: BTreeMap::default(),
+            names: vec!["main".to_string()],
+        };
 
-        let main_declaration_location =
-            Location::new(find_spans_for(source_code, "fn main()")[0], file_id);
-        let main_foo_call_location =
-            Location::new(find_spans_for(source_code, "foo()")[0], file_id);
-        let main_bar_call_location =
-            Location::new(find_spans_for(source_code, "bar()")[0], file_id);
-        let main_whatever_call_location =
-            Location::new(find_spans_for(source_code, "whatever()")[0], file_id);
-        let foo_baz_call_location = Location::new(find_spans_for(source_code, "baz()")[0], file_id);
-        let bar_whatever_call_location =
-            Location::new(find_spans_for(source_code, "whatever()")[1], file_id);
-        let baz_whatever_call_location =
-            Location::new(find_spans_for(source_code, "whatever()")[2], file_id);
+        // Write the artifact to a file
+        let artifact_file = std::fs::File::create(&artifact_path).unwrap();
+        serde_json::to_writer(artifact_file, &artifact).unwrap();
 
-        let mut opcode_locations = BTreeMap::<OpcodeLocation, Vec<Location>>::new();
-        // main::foo::baz::whatever
-        opcode_locations.insert(
-            OpcodeLocation::Acir(0),
-            vec![
-                main_declaration_location,
-                main_foo_call_location,
-                foo_baz_call_location,
-                baz_whatever_call_location,
+        let mock_gates_response = BackendGatesResponse {
+            functions: vec![
+                (BackendGatesReport { acir_opcodes: 0, gates_per_opcode: vec![], circuit_size: 0 }),
             ],
-        );
-
-        // main::bar::whatever
-        opcode_locations.insert(
-            OpcodeLocation::Acir(1),
-            vec![main_declaration_location, main_bar_call_location, bar_whatever_call_location],
-        );
-        // main::whatever
-        opcode_locations.insert(
-            OpcodeLocation::Acir(2),
-            vec![main_declaration_location, main_whatever_call_location],
-        );
-
-        let file_map = BTreeMap::from_iter(vec![(
-            file_id,
-            DebugFile { source: source_code.to_string(), path: source_file_name.to_path_buf() },
-        )]);
-
-        let debug_symbols = ProgramDebugInfo {
-            debug_infos: vec![DebugInfo::new(
-                opcode_locations,
-                BTreeMap::default(),
-                BTreeMap::default(),
-                BTreeMap::default(),
-            )],
         };
 
-        let backend_gates_response = BackendGatesResponse {
-            functions: vec![BackendGatesReport {
-                acir_opcodes: 3,
-                circuit_size: 100,
-                gates_per_opcode: vec![10, 20, 30],
-            }],
+        let provider = TestGateProvider {
+            mock_responses: HashMap::from([(artifact_path.clone(), mock_gates_response)]),
         };
+        let flamegraph_generator = TestFlamegraphGenerator::default();
 
-        let expected_folded_sorted_lines = vec![
-            "main.nr:2:9::fn main() 0".to_string(),
-            "main.nr:2:9::fn main();main.nr:3:13::foo() 0".to_string(),
-            "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz() 0".to_string(),
-            "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz();main.nr:14:13::whatever() 10".to_string(),
-            "main.nr:2:9::fn main();main.nr:4:13::bar() 0".to_string(),
-            "main.nr:2:9::fn main();main.nr:4:13::bar();main.nr:11:13::whatever() 20".to_string(),
-            "main.nr:2:9::fn main();main.nr:5:13::whatever() 30".to_string(),
-        ];
-
-        TestCase {
-            expected_folded_sorted_lines: vec![expected_folded_sorted_lines],
-            debug_symbols,
-            file_map,
-            gates_report: backend_gates_response,
-        }
-    }
-
-    #[test]
-    fn test_flamegraph() {
-        let temp_dir = tempfile::tempdir().unwrap();
-
-        let test_cases = vec![simple_test_case(&temp_dir)];
-        let artifact_names: Vec<_> =
-            test_cases.iter().enumerate().map(|(idx, _)| format!("test{}.json", idx)).collect();
-
-        let test_cases_with_names: Vec<_> = test_cases.into_iter().zip(artifact_names).collect();
-
-        let mut mock_responses: HashMap<PathBuf, BackendGatesResponse> = HashMap::new();
-        // Collect mock responses
-        for (test_case, artifact_name) in test_cases_with_names.iter() {
-            mock_responses.insert(
-                temp_dir.path().join(artifact_name.clone()),
-                test_case.gates_report.clone(),
-            );
-        }
-
-        let provider = TestGateProvider { mock_responses };
-
-        for (test_case, artifact_name) in test_cases_with_names.iter() {
-            let artifact_path = temp_dir.path().join(artifact_name.clone());
-
-            let artifact = ProgramArtifact {
-                noir_version: "0.0.0".to_string(),
-                hash: 27,
-                abi: noirc_abi::Abi::default(),
-                bytecode: Program::default(),
-                debug_symbols: test_case.debug_symbols.clone(),
-                file_map: test_case.file_map.clone(),
-                names: vec!["main".to_string()],
-            };
-
-            // Write the artifact to a file
-            let artifact_file = std::fs::File::create(&artifact_path).unwrap();
-            serde_json::to_writer(artifact_file, &artifact).unwrap();
-
-            let flamegraph_generator = TestFlamegraphGenerator::default();
-
-            super::run_with_provider(
-                &artifact_path,
-                &provider,
-                &flamegraph_generator,
-                temp_dir.path(),
-            )
+        super::run_with_provider(&artifact_path, &provider, &flamegraph_generator, temp_dir.path())
             .expect("should run without errors");
 
-            // Check that the flamegraph generator was called with the correct folded sorted lines
-            let calls_received = flamegraph_generator.lines_received.borrow().clone();
-
-            assert_eq!(calls_received, test_case.expected_folded_sorted_lines);
-        }
+        // Check that the output file was written to
+        let output_file = temp_dir.path().join("main_gates.svg");
+        assert!(output_file.exists());
     }
 }
diff --git a/tooling/profiler/src/cli/mod.rs b/tooling/profiler/src/cli/mod.rs
index d54a3f6167c..4c2503fbe4f 100644
--- a/tooling/profiler/src/cli/mod.rs
+++ b/tooling/profiler/src/cli/mod.rs
@@ -3,6 +3,7 @@ use color_eyre::eyre;
 use const_format::formatcp;
 
 mod gates_flamegraph_cmd;
+mod opcodes_flamegraph_cmd;
 
 const PROFILER_VERSION: &str = env!("CARGO_PKG_VERSION");
 
@@ -12,20 +13,22 @@ static VERSION_STRING: &str = formatcp!("version = {}\n", PROFILER_VERSION,);
 #[command(name="Noir profiler", author, version=VERSION_STRING, about, long_about = None)]
 struct ProfilerCli {
     #[command(subcommand)]
-    command: GatesFlamegraphCommand,
+    command: ProfilerCommand,
 }
 
 #[non_exhaustive]
 #[derive(Subcommand, Clone, Debug)]
-enum GatesFlamegraphCommand {
+enum ProfilerCommand {
     GatesFlamegraph(gates_flamegraph_cmd::GatesFlamegraphCommand),
+    OpcodesFlamegraph(opcodes_flamegraph_cmd::OpcodesFlamegraphCommand),
 }
 
 pub(crate) fn start_cli() -> eyre::Result<()> {
     let ProfilerCli { command } = ProfilerCli::parse();
 
     match command {
-        GatesFlamegraphCommand::GatesFlamegraph(args) => gates_flamegraph_cmd::run(args),
+        ProfilerCommand::GatesFlamegraph(args) => gates_flamegraph_cmd::run(args),
+        ProfilerCommand::OpcodesFlamegraph(args) => opcodes_flamegraph_cmd::run(args),
     }
     .map_err(|err| eyre::eyre!("{}", err))?;
 
diff --git a/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs b/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs
new file mode 100644
index 00000000000..e1dc1464f6f
--- /dev/null
+++ b/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs
@@ -0,0 +1,123 @@
+use std::path::{Path, PathBuf};
+
+use clap::Args;
+use color_eyre::eyre::{self, Context};
+
+use noirc_artifacts::debug::DebugArtifact;
+
+use crate::flamegraph::{FlamegraphGenerator, InfernoFlamegraphGenerator};
+use crate::fs::read_program_from_file;
+
+#[derive(Debug, Clone, Args)]
+pub(crate) struct OpcodesFlamegraphCommand {
+    /// The path to the artifact JSON file
+    #[clap(long, short)]
+    artifact_path: String,
+
+    /// The output folder for the flamegraph svg files
+    #[clap(long, short)]
+    output: String,
+}
+
+pub(crate) fn run(args: OpcodesFlamegraphCommand) -> eyre::Result<()> {
+    run_with_generator(
+        &PathBuf::from(args.artifact_path),
+        &InfernoFlamegraphGenerator { count_name: "opcodes".to_string() },
+        &PathBuf::from(args.output),
+    )
+}
+
+fn run_with_generator<Generator: FlamegraphGenerator>(
+    artifact_path: &Path,
+    flamegraph_generator: &Generator,
+    output_path: &Path,
+) -> eyre::Result<()> {
+    let mut program =
+        read_program_from_file(artifact_path).context("Error reading program from file")?;
+
+    let function_names = program.names.clone();
+
+    let bytecode = std::mem::take(&mut program.bytecode);
+
+    let debug_artifact: DebugArtifact = program.into();
+
+    for (func_idx, (func_name, bytecode)) in
+        function_names.into_iter().zip(bytecode.functions).enumerate()
+    {
+        println!("Opcode count: {}", bytecode.opcodes.len());
+
+        flamegraph_generator.generate_flamegraph(
+            bytecode.opcodes.iter().map(|_op| 1).collect(),
+            bytecode.opcodes,
+            &debug_artifact.debug_symbols[func_idx],
+            &debug_artifact,
+            artifact_path.to_str().unwrap(),
+            &func_name,
+            &Path::new(&output_path).join(Path::new(&format!("{}_opcodes.svg", &func_name))),
+        )?;
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use acir::circuit::{Circuit, Opcode, Program};
+    use color_eyre::eyre::{self};
+    use fm::codespan_files::Files;
+    use noirc_artifacts::program::ProgramArtifact;
+    use noirc_errors::debug_info::{DebugInfo, ProgramDebugInfo};
+    use std::{collections::BTreeMap, path::Path};
+
+    #[derive(Default)]
+    struct TestFlamegraphGenerator {}
+
+    impl super::FlamegraphGenerator for TestFlamegraphGenerator {
+        fn generate_flamegraph<'files, F>(
+            &self,
+            _samples_per_opcode: Vec<usize>,
+            _opcodes: Vec<Opcode<F>>,
+            _debug_symbols: &DebugInfo,
+            _files: &'files impl Files<'files, FileId = fm::FileId>,
+            _artifact_name: &str,
+            _function_name: &str,
+            output_path: &Path,
+        ) -> eyre::Result<()> {
+            let output_file = std::fs::File::create(output_path).unwrap();
+            std::io::Write::write_all(&mut std::io::BufWriter::new(output_file), b"success")
+                .unwrap();
+
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn smoke_test() {
+        let temp_dir = tempfile::tempdir().unwrap();
+
+        let artifact_path = temp_dir.path().join("test.json");
+
+        let artifact = ProgramArtifact {
+            noir_version: "0.0.0".to_string(),
+            hash: 27,
+            abi: noirc_abi::Abi::default(),
+            bytecode: Program { functions: vec![Circuit::default()], ..Program::default() },
+            debug_symbols: ProgramDebugInfo { debug_infos: vec![DebugInfo::default()] },
+            file_map: BTreeMap::default(),
+            names: vec!["main".to_string()],
+        };
+
+        // Write the artifact to a file
+        let artifact_file = std::fs::File::create(&artifact_path).unwrap();
+        serde_json::to_writer(artifact_file, &artifact).unwrap();
+
+        let flamegraph_generator = TestFlamegraphGenerator::default();
+
+        super::run_with_generator(&artifact_path, &flamegraph_generator, temp_dir.path())
+            .expect("should run without errors");
+
+        // Check that the output file was written to
+        let output_file = temp_dir.path().join("main_opcodes.svg");
+        assert!(output_file.exists());
+    }
+}
diff --git a/tooling/profiler/src/flamegraph.rs b/tooling/profiler/src/flamegraph.rs
new file mode 100644
index 00000000000..70de6e743b2
--- /dev/null
+++ b/tooling/profiler/src/flamegraph.rs
@@ -0,0 +1,300 @@
+use std::path::Path;
+use std::{collections::BTreeMap, io::BufWriter};
+
+use acir::circuit::{Opcode, OpcodeLocation};
+use color_eyre::eyre::{self};
+use fm::codespan_files::Files;
+use inferno::flamegraph::{from_lines, Options};
+use noirc_errors::debug_info::DebugInfo;
+use noirc_errors::reporter::line_and_column_from_span;
+use noirc_errors::Location;
+
+use super::opcode_formatter::format_opcode;
+
+#[derive(Debug, Default)]
+pub(crate) struct FoldedStackItem {
+    pub(crate) total_samples: usize,
+    pub(crate) nested_items: BTreeMap<String, FoldedStackItem>,
+}
+
+pub(crate) trait FlamegraphGenerator {
+    #[allow(clippy::too_many_arguments)]
+    fn generate_flamegraph<'files, F>(
+        &self,
+        samples_per_opcode: Vec<usize>,
+        opcodes: Vec<Opcode<F>>,
+        debug_symbols: &DebugInfo,
+        files: &'files impl Files<'files, FileId = fm::FileId>,
+        artifact_name: &str,
+        function_name: &str,
+        output_path: &Path,
+    ) -> eyre::Result<()>;
+}
+
+pub(crate) struct InfernoFlamegraphGenerator {
+    pub(crate) count_name: String,
+}
+
+impl FlamegraphGenerator for InfernoFlamegraphGenerator {
+    fn generate_flamegraph<'files, F>(
+        &self,
+        samples_per_opcode: Vec<usize>,
+        opcodes: Vec<Opcode<F>>,
+        debug_symbols: &DebugInfo,
+        files: &'files impl Files<'files, FileId = fm::FileId>,
+        artifact_name: &str,
+        function_name: &str,
+        output_path: &Path,
+    ) -> eyre::Result<()> {
+        let folded_lines =
+            generate_folded_sorted_lines(samples_per_opcode, opcodes, debug_symbols, files);
+
+        let flamegraph_file = std::fs::File::create(output_path)?;
+        let flamegraph_writer = BufWriter::new(flamegraph_file);
+
+        let mut options = Options::default();
+        options.hash = true;
+        options.deterministic = true;
+        options.title = format!("{}-{}", artifact_name, function_name);
+        options.frame_height = 24;
+        options.color_diffusion = true;
+        options.min_width = 0.0;
+        options.count_name = self.count_name.clone();
+
+        from_lines(
+            &mut options,
+            folded_lines.iter().map(|as_string| as_string.as_str()),
+            flamegraph_writer,
+        )?;
+
+        Ok(())
+    }
+}
+
+fn generate_folded_sorted_lines<'files, F>(
+    samples_per_opcode: Vec<usize>,
+    opcodes: Vec<Opcode<F>>,
+    debug_symbols: &DebugInfo,
+    files: &'files impl Files<'files, FileId = fm::FileId>,
+) -> Vec<String> {
+    // Create a nested hashmap with the stack items, folding the gates for all the callsites that are equal
+    let mut folded_stack_items = BTreeMap::new();
+
+    samples_per_opcode.into_iter().enumerate().for_each(|(opcode_index, gates)| {
+        let call_stack = debug_symbols.locations.get(&OpcodeLocation::Acir(opcode_index));
+        let location_names = if let Some(call_stack) = call_stack {
+            call_stack
+                .iter()
+                .map(|location| location_to_callsite_label(*location, files))
+                .chain(std::iter::once(format_opcode(&opcodes[opcode_index])))
+                .collect::<Vec<String>>()
+        } else {
+            vec!["unknown".to_string()]
+        };
+
+        add_locations_to_folded_stack_items(&mut folded_stack_items, location_names, gates);
+    });
+
+    to_folded_sorted_lines(&folded_stack_items, Default::default())
+}
+
+fn location_to_callsite_label<'files>(
+    location: Location,
+    files: &'files impl Files<'files, FileId = fm::FileId>,
+) -> String {
+    let filename =
+        Path::new(&files.name(location.file).expect("should have a file path").to_string())
+            .file_name()
+            .map(|os_str| os_str.to_string_lossy().to_string())
+            .unwrap_or("invalid_path".to_string());
+    let source = files.source(location.file).expect("should have a file source");
+
+    let code_slice = source
+        .as_ref()
+        .chars()
+        .skip(location.span.start() as usize)
+        .take(location.span.end() as usize - location.span.start() as usize)
+        .collect::<String>();
+
+    // ";" is used for frame separation, and is not allowed by inferno
+    // Check code slice for ";" and replace it with 'GREEK QUESTION MARK' (U+037E)
+    let code_slice = code_slice.replace(';', "\u{037E}");
+
+    let (line, column) = line_and_column_from_span(source.as_ref(), &location.span);
+
+    format!("{}:{}:{}::{}", filename, line, column, code_slice)
+}
+
+fn add_locations_to_folded_stack_items(
+    stack_items: &mut BTreeMap<String, FoldedStackItem>,
+    locations: Vec<String>,
+    gates: usize,
+) {
+    let mut child_map = stack_items;
+    for (index, location) in locations.iter().enumerate() {
+        let current_item = child_map.entry(location.clone()).or_default();
+
+        child_map = &mut current_item.nested_items;
+
+        if index == locations.len() - 1 {
+            current_item.total_samples += gates;
+        }
+    }
+}
+
+/// Creates a vector of lines in the format that inferno expects from a nested hashmap of stack items
+/// The lines have to be sorted in the following way, exploring the graph in a depth-first manner:
+/// main 100
+/// main::foo 0
+/// main::foo::bar 200
+/// main::baz 27
+/// main::baz::qux 800
+fn to_folded_sorted_lines(
+    folded_stack_items: &BTreeMap<String, FoldedStackItem>,
+    parent_stacks: im::Vector<String>,
+) -> Vec<String> {
+    let mut result_vector = Vec::with_capacity(folded_stack_items.len());
+
+    for (location, folded_stack_item) in folded_stack_items.iter() {
+        if folded_stack_item.total_samples > 0 {
+            let frame_list: Vec<String> =
+                parent_stacks.iter().cloned().chain(std::iter::once(location.clone())).collect();
+            let line: String =
+                format!("{} {}", frame_list.join(";"), folded_stack_item.total_samples);
+
+            result_vector.push(line);
+        };
+
+        let mut new_parent_stacks = parent_stacks.clone();
+        new_parent_stacks.push_back(location.clone());
+        let child_lines =
+            to_folded_sorted_lines(&folded_stack_item.nested_items, new_parent_stacks);
+
+        result_vector.extend(child_lines);
+    }
+
+    result_vector
+}
+
+#[cfg(test)]
+mod tests {
+    use acir::{
+        circuit::{opcodes::BlockId, Opcode, OpcodeLocation},
+        native_types::Expression,
+        FieldElement,
+    };
+    use fm::FileManager;
+    use noirc_errors::{debug_info::DebugInfo, Location, Span};
+    use std::{collections::BTreeMap, path::Path};
+
+    use super::generate_folded_sorted_lines;
+
+    fn find_spans_for(source: &str, needle: &str) -> Vec<Span> {
+        let mut spans = Vec::new();
+        let mut start = 0;
+        while let Some(start_idx) = source[start..].find(needle) {
+            let start_idx = start + start_idx;
+            let end_idx = start_idx + needle.len();
+            spans.push(Span::inclusive(start_idx as u32, end_idx as u32 - 1));
+            start = end_idx;
+        }
+        spans
+    }
+
+    #[test]
+    fn simple_test_case() {
+        let source_code = r##"
+        fn main() {
+            foo();
+            bar();
+            whatever();
+        }
+        fn foo() {
+            baz();
+        }
+        fn bar () {
+            whatever()
+        }
+        fn baz () {
+            whatever()
+        }
+        "##;
+
+        let source_file_name = Path::new("main.nr");
+        let temp_dir = tempfile::tempdir().unwrap();
+
+        let mut fm = FileManager::new(temp_dir.path());
+        let file_id = fm.add_file_with_source(source_file_name, source_code.to_string()).unwrap();
+
+        let main_declaration_location =
+            Location::new(find_spans_for(source_code, "fn main()")[0], file_id);
+        let main_foo_call_location =
+            Location::new(find_spans_for(source_code, "foo()")[0], file_id);
+        let main_bar_call_location =
+            Location::new(find_spans_for(source_code, "bar()")[0], file_id);
+        let main_whatever_call_location =
+            Location::new(find_spans_for(source_code, "whatever()")[0], file_id);
+        let foo_baz_call_location = Location::new(find_spans_for(source_code, "baz()")[0], file_id);
+        let bar_whatever_call_location =
+            Location::new(find_spans_for(source_code, "whatever()")[1], file_id);
+        let baz_whatever_call_location =
+            Location::new(find_spans_for(source_code, "whatever()")[2], file_id);
+
+        let mut opcode_locations = BTreeMap::<OpcodeLocation, Vec<Location>>::new();
+        // main::foo::baz::whatever
+        opcode_locations.insert(
+            OpcodeLocation::Acir(0),
+            vec![
+                main_declaration_location,
+                main_foo_call_location,
+                foo_baz_call_location,
+                baz_whatever_call_location,
+            ],
+        );
+
+        // main::bar::whatever
+        opcode_locations.insert(
+            OpcodeLocation::Acir(1),
+            vec![main_declaration_location, main_bar_call_location, bar_whatever_call_location],
+        );
+        // main::whatever
+        opcode_locations.insert(
+            OpcodeLocation::Acir(2),
+            vec![main_declaration_location, main_whatever_call_location],
+        );
+
+        let debug_info = DebugInfo::new(
+            opcode_locations,
+            BTreeMap::default(),
+            BTreeMap::default(),
+            BTreeMap::default(),
+        );
+
+        let samples_per_opcode = vec![10, 20, 30];
+
+        let expected_folded_sorted_lines = vec![
+            "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz();main.nr:14:13::whatever();opcode::arithmetic 10".to_string(),
+            "main.nr:2:9::fn main();main.nr:4:13::bar();main.nr:11:13::whatever();opcode::arithmetic 20".to_string(),
+            "main.nr:2:9::fn main();main.nr:5:13::whatever();opcode::memory::init 30".to_string(),
+        ];
+
+        let opcodes: Vec<Opcode<FieldElement>> = vec![
+            Opcode::AssertZero(Expression::default()),
+            Opcode::AssertZero(Expression::default()),
+            Opcode::MemoryInit {
+                block_id: BlockId(0),
+                init: vec![],
+                block_type: acir::circuit::opcodes::BlockType::Memory,
+            },
+        ];
+
+        let actual_folded_sorted_lines = generate_folded_sorted_lines(
+            samples_per_opcode,
+            opcodes,
+            &debug_info,
+            fm.as_file_map(),
+        );
+
+        assert_eq!(expected_folded_sorted_lines, actual_folded_sorted_lines);
+    }
+}
diff --git a/tooling/profiler/src/fs.rs b/tooling/profiler/src/fs.rs
new file mode 100644
index 00000000000..e8eec2cbb14
--- /dev/null
+++ b/tooling/profiler/src/fs.rs
@@ -0,0 +1,15 @@
+use std::path::Path;
+
+use color_eyre::eyre;
+use noirc_artifacts::program::ProgramArtifact;
+
+pub(crate) fn read_program_from_file<P: AsRef<Path>>(
+    circuit_path: P,
+) -> eyre::Result<ProgramArtifact> {
+    let file_path = circuit_path.as_ref().with_extension("json");
+
+    let input_string = std::fs::read(file_path)?;
+    let program = serde_json::from_slice(&input_string)?;
+
+    Ok(program)
+}
diff --git a/tooling/profiler/src/gates_provider.rs b/tooling/profiler/src/gates_provider.rs
new file mode 100644
index 00000000000..caed2666426
--- /dev/null
+++ b/tooling/profiler/src/gates_provider.rs
@@ -0,0 +1,37 @@
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use color_eyre::eyre::{self};
+use serde::{Deserialize, Serialize};
+
+pub(crate) trait GatesProvider {
+    fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse>;
+}
+
+pub(crate) struct BackendGatesProvider {
+    pub(crate) backend_path: PathBuf,
+}
+
+impl GatesProvider for BackendGatesProvider {
+    fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse> {
+        let backend_gates_response =
+            Command::new(&self.backend_path).arg("gates").arg("-b").arg(artifact_path).output()?;
+
+        // Parse the backend gates command stdout as json
+        let backend_gates_response: BackendGatesResponse =
+            serde_json::from_slice(&backend_gates_response.stdout)?;
+        Ok(backend_gates_response)
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct BackendGatesReport {
+    pub(crate) acir_opcodes: usize,
+    pub(crate) circuit_size: usize,
+    pub(crate) gates_per_opcode: Vec<usize>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct BackendGatesResponse {
+    pub(crate) functions: Vec<BackendGatesReport>,
+}
diff --git a/tooling/profiler/src/main.rs b/tooling/profiler/src/main.rs
index 8e08644de23..215feb0a4e7 100644
--- a/tooling/profiler/src/main.rs
+++ b/tooling/profiler/src/main.rs
@@ -4,6 +4,10 @@
 #![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
 
 mod cli;
+mod flamegraph;
+mod fs;
+mod gates_provider;
+mod opcode_formatter;
 
 use std::env;
 
diff --git a/tooling/profiler/src/opcode_formatter.rs b/tooling/profiler/src/opcode_formatter.rs
new file mode 100644
index 00000000000..aba92c95d85
--- /dev/null
+++ b/tooling/profiler/src/opcode_formatter.rs
@@ -0,0 +1,53 @@
+use acir::circuit::{directives::Directive, opcodes::BlackBoxFuncCall, Opcode};
+
+fn format_blackbox_function(call: &BlackBoxFuncCall) -> String {
+    match call {
+        BlackBoxFuncCall::AES128Encrypt { .. } => "aes128_encrypt".to_string(),
+        BlackBoxFuncCall::AND { .. } => "and".to_string(),
+        BlackBoxFuncCall::XOR { .. } => "xor".to_string(),
+        BlackBoxFuncCall::RANGE { .. } => "range".to_string(),
+        BlackBoxFuncCall::SHA256 { .. } => "sha256".to_string(),
+        BlackBoxFuncCall::Blake2s { .. } => "blake2s".to_string(),
+        BlackBoxFuncCall::Blake3 { .. } => "blake3".to_string(),
+        BlackBoxFuncCall::SchnorrVerify { .. } => "schnorr_verify".to_string(),
+        BlackBoxFuncCall::PedersenCommitment { .. } => "pedersen_commitment".to_string(),
+        BlackBoxFuncCall::PedersenHash { .. } => "pedersen_hash".to_string(),
+        BlackBoxFuncCall::EcdsaSecp256k1 { .. } => "ecdsa_secp256k1".to_string(),
+        BlackBoxFuncCall::EcdsaSecp256r1 { .. } => "ecdsa_secp256r1".to_string(),
+        BlackBoxFuncCall::MultiScalarMul { .. } => "multi_scalar_mul".to_string(),
+        BlackBoxFuncCall::EmbeddedCurveAdd { .. } => "embedded_curve_add".to_string(),
+        BlackBoxFuncCall::Keccak256 { .. } => "keccak256".to_string(),
+        BlackBoxFuncCall::Keccakf1600 { .. } => "keccakf1600".to_string(),
+        BlackBoxFuncCall::RecursiveAggregation { .. } => "recursive_aggregation".to_string(),
+        BlackBoxFuncCall::BigIntAdd { .. } => "big_int_add".to_string(),
+        BlackBoxFuncCall::BigIntSub { .. } => "big_int_sub".to_string(),
+        BlackBoxFuncCall::BigIntMul { .. } => "big_int_mul".to_string(),
+        BlackBoxFuncCall::BigIntDiv { .. } => "big_int_div".to_string(),
+        BlackBoxFuncCall::BigIntFromLeBytes { .. } => "big_int_from_le_bytes".to_string(),
+        BlackBoxFuncCall::BigIntToLeBytes { .. } => "big_int_to_le_bytes".to_string(),
+        BlackBoxFuncCall::Poseidon2Permutation { .. } => "poseidon2_permutation".to_string(),
+        BlackBoxFuncCall::Sha256Compression { .. } => "sha256_compression".to_string(),
+    }
+}
+
+fn format_directive_kind<F>(directive: &Directive<F>) -> String {
+    match directive {
+        Directive::ToLeRadix { .. } => "to_le_radix".to_string(),
+    }
+}
+
+fn format_opcode_kind<F>(opcode: &Opcode<F>) -> String {
+    match opcode {
+        Opcode::AssertZero(_) => "arithmetic".to_string(),
+        Opcode::BlackBoxFuncCall(call) => format!("blackbox::{}", format_blackbox_function(call)),
+        Opcode::MemoryOp { .. } => "memory::op".to_string(),
+        Opcode::MemoryInit { .. } => "memory::init".to_string(),
+        Opcode::Directive(directive) => format!("directive::{}", format_directive_kind(directive)),
+        Opcode::BrilligCall { .. } => "brillig_call".to_string(),
+        Opcode::Call { .. } => "acir_call".to_string(),
+    }
+}
+
+pub(crate) fn format_opcode<F>(opcode: &Opcode<F>) -> String {
+    format!("opcode::{}", format_opcode_kind(opcode))
+}