flamegraph/
lib.rs

1use std::{
2    env,
3    fs::File,
4    io::{BufReader, BufWriter, Cursor, Read, Write},
5    path::PathBuf,
6    process::{exit, Command, ExitStatus, Stdio},
7    str::FromStr,
8};
9
10#[cfg(unix)]
11use std::os::unix::process::ExitStatusExt;
12
13#[cfg(target_os = "linux")]
14use inferno::collapse::perf::{Folder, Options as CollapseOptions};
15
16#[cfg(target_os = "macos")]
17use inferno::collapse::xctrace::Folder;
18
19#[cfg(not(any(target_os = "linux", target_os = "macos")))]
20use inferno::collapse::dtrace::{Folder, Options as CollapseOptions};
21
22#[cfg(unix)]
23use signal_hook::consts::{SIGINT, SIGTERM};
24
25use anyhow::{anyhow, bail, Context};
26use clap::{
27    builder::{PossibleValuesParser, TypedValueParser},
28    Args,
29};
30use inferno::{collapse::Collapse, flamegraph::color::Palette, flamegraph::from_reader};
31use rustc_demangle::demangle_stream;
32
33pub enum Workload {
34    Command(Vec<String>),
35    Pid(Vec<u32>),
36    ReadPerf(PathBuf),
37}
38
39#[cfg(target_os = "linux")]
40mod arch {
41    use std::time::Duration;
42
43    use indicatif::{ProgressBar, ProgressStyle};
44
45    use super::*;
46
47    pub const SPAWN_ERROR: &str = "could not spawn perf";
48    pub const WAIT_ERROR: &str = "unable to wait for perf child command to exit";
49
50    pub(crate) fn initial_command(
51        workload: Workload,
52        sudo: Option<Option<&str>>,
53        freq: u32,
54        custom_cmd: Option<String>,
55        verbose: bool,
56        ignore_status: bool,
57    ) -> anyhow::Result<Option<PathBuf>> {
58        let perf = if let Ok(path) = env::var("PERF") {
59            path
60        } else {
61            if Command::new("perf")
62                .arg("--help")
63                .stderr(Stdio::null())
64                .stdout(Stdio::null())
65                .status()
66                .is_err()
67            {
68                bail!("perf is not installed or not present in $PATH");
69            }
70
71            String::from("perf")
72        };
73        let mut command = sudo_command(&perf, sudo);
74
75        let args = custom_cmd.unwrap_or(format!("record -F {freq} --call-graph dwarf,64000 -g"));
76
77        let mut perf_output = None;
78        let mut args = args.split_whitespace();
79        while let Some(arg) = args.next() {
80            command.arg(arg);
81
82            // Detect if user is setting `perf record`
83            // output file with `-o`. If so, save it in
84            // order to correctly compute perf's output in
85            // `Self::output`.
86            if arg == "-o" {
87                let next_arg = args.next().context("missing '-o' argument")?;
88                command.arg(next_arg);
89                perf_output = Some(PathBuf::from(next_arg));
90            }
91        }
92
93        let perf_output = match perf_output {
94            Some(path) => path,
95            None => {
96                command.arg("-o");
97                command.arg("perf.data");
98                PathBuf::from("perf.data")
99            }
100        };
101
102        match workload {
103            Workload::Command(c) => {
104                command.args(&c);
105            }
106            Workload::Pid(p) => {
107                if let Some((first, pids)) = p.split_first() {
108                    let mut arg = first.to_string();
109
110                    for pid in pids {
111                        arg.push(',');
112                        arg.push_str(&pid.to_string());
113                    }
114
115                    command.arg("-p");
116                    command.arg(arg);
117                }
118            }
119            Workload::ReadPerf(_) => (),
120        }
121
122        run(command, verbose, ignore_status);
123        Ok(Some(perf_output))
124    }
125
126    pub fn output(
127        perf_output: Option<PathBuf>,
128        script_no_inline: bool,
129        sudo: Option<Option<&str>>,
130    ) -> anyhow::Result<Vec<u8>> {
131        // We executed `perf record` with sudo, and will be executing `perf script` with sudo,
132        // so that we can resolve privileged kernel symbols from /proc/kallsyms.
133        let perf = env::var("PERF").unwrap_or_else(|_| "perf".to_string());
134        let mut command = sudo_command(&perf, sudo);
135
136        command.arg("script");
137
138        // Force reading perf.data owned by another uid if it happened to be created earlier.
139        command.arg("--force");
140
141        if script_no_inline {
142            command.arg("--no-inline");
143        }
144
145        if let Some(perf_output) = perf_output {
146            command.arg("-i");
147            command.arg(perf_output);
148        }
149
150        // perf script can take a long time to run. Notify the user that it is running
151        // by using a spinner. Note that if this function exits before calling
152        // spinner.finish(), then the spinner will be completely removed from the terminal.
153        let spinner = ProgressBar::new_spinner().with_prefix("Running perf script");
154        spinner.set_style(
155            ProgressStyle::with_template("{prefix} [{elapsed}]: {spinner:.green}").unwrap(),
156        );
157        spinner.enable_steady_tick(Duration::from_millis(500));
158
159        let result = command.output().context("unable to call perf script");
160        spinner.finish();
161        let output = result?;
162        if !output.status.success() {
163            bail!(
164                "unable to run 'perf script': ({}) {}",
165                output.status,
166                std::str::from_utf8(&output.stderr)?
167            );
168        }
169        Ok(output.stdout)
170    }
171}
172
173#[cfg(target_os = "macos")]
174mod arch {
175    use super::*;
176
177    pub const SPAWN_ERROR: &str = "could not spawn xctrace";
178    pub const WAIT_ERROR: &str = "unable to wait for xctrace record child command to exit";
179
180    pub(crate) fn initial_command(
181        workload: Workload,
182        sudo: Option<Option<&str>>,
183        freq: u32,
184        custom_cmd: Option<String>,
185        verbose: bool,
186        ignore_status: bool,
187    ) -> anyhow::Result<Option<PathBuf>> {
188        if freq != 997 {
189            bail!("xctrace doesn't support custom frequency");
190        }
191        if custom_cmd.is_some() {
192            bail!("xctrace doesn't support custom command");
193        }
194        let xctrace = env::var("XCTRACE").unwrap_or_else(|_| "xctrace".to_string());
195        let trace_file = PathBuf::from("cargo-flamegraph.trace");
196        let mut command = sudo_command(&xctrace, sudo);
197        command
198            .arg("record")
199            .arg("--template")
200            .arg("Time Profiler")
201            .arg("--output")
202            .arg(&trace_file);
203        match workload {
204            Workload::Command(args) => {
205                command
206                    .arg("--target-stdout")
207                    .arg("-")
208                    .arg("--launch")
209                    .arg("--")
210                    .args(args);
211            }
212            Workload::Pid(pid) => {
213                match &*pid {
214                    [pid] => {
215                        // xctrace could accept multiple --attach <pid> arguments,
216                        // but it will only profile the last pid provided.
217                        command.arg("--attach").arg(pid.to_string());
218                    }
219                    _ => {
220                        bail!("xctrace only supports profiling a single process at a time");
221                    }
222                }
223            }
224            Workload::ReadPerf(_) => {}
225        }
226        run(command, verbose, ignore_status);
227        Ok(Some(trace_file))
228    }
229
230    pub fn output(
231        trace_file: Option<PathBuf>,
232        script_no_inline: bool,
233        _sudo: Option<Option<&str>>,
234    ) -> anyhow::Result<Vec<u8>> {
235        if script_no_inline {
236            bail!("--no-inline is only supported on Linux");
237        }
238
239        let xctrace = env::var("XCTRACE").unwrap_or_else(|_| "xctrace".to_string());
240        let trace_file = trace_file.context("no trace file found.")?;
241        let output = Command::new(xctrace)
242            .arg("export")
243            .arg("--input")
244            .arg(&trace_file)
245            .arg("--xpath")
246            .arg(r#"/trace-toc/*/data/table[@schema="time-profile"]"#)
247            .output()
248            .context("run xctrace export failed.")?;
249        std::fs::remove_dir_all(&trace_file)
250            .with_context(|| anyhow!("remove trace({}) failed.", trace_file.to_string_lossy()))?;
251        if !output.status.success() {
252            bail!(
253                "unable to run 'xctrace export': ({}) {}",
254                output.status,
255                String::from_utf8_lossy(&output.stderr)
256            );
257        }
258        Ok(output.stdout)
259    }
260}
261
262#[cfg(not(any(target_os = "linux", target_os = "macos")))]
263mod arch {
264    use super::*;
265
266    pub const SPAWN_ERROR: &str = "could not spawn dtrace";
267    pub const WAIT_ERROR: &str = "unable to wait for dtrace child command to exit";
268
269    #[cfg(target_os = "macos")]
270    fn base_dtrace_command(sudo: Option<Option<&str>>) -> Command {
271        // If DTrace is spawned from a parent process (or grandparent process etc.) running in Rosetta-emulated x86 mode
272        // on an ARM mac, it will fail to trace the child process with a confusing syntax error in its stdlib .d file.
273        // If the flamegraph binary, or the cargo binary, have been compiled as x86, this can cause all tracing to fail.
274        // To work around that, we unconditionally wrap dtrace on MacOS in the "arch -64/-32" wrapper so it's always
275        // running in the native architecture matching the bit width (32 oe 64) with which "flamegraph" was compiled.
276        // NOTE that dtrace-as-x86 won't trace a deliberately-cross-compiled x86 binary running under Rosetta regardless
277        // of "arch" wrapping; attempts to do that will fail with "DTrace cannot instrument translated processes".
278        // NOTE that using the ARCHPREFERENCE environment variable documented here
279        // (https://www.unix.com/man-page/osx/1/arch/) would be a much simpler solution to this issue, but it does not
280        // seem to have any effect on dtrace when set (via Command::env, shell export, or std::env in the spawning
281        // process).
282        let mut command = sudo_command("arch", sudo);
283
284        #[cfg(target_pointer_width = "64")]
285        command.arg("-64".to_string());
286        #[cfg(target_pointer_width = "32")]
287        command.arg("-32".to_string());
288
289        command.arg(env::var("DTRACE").unwrap_or_else(|_| "dtrace".to_string()));
290        command
291    }
292
293    #[cfg(not(target_os = "macos"))]
294    fn base_dtrace_command(sudo: Option<Option<&str>>) -> Command {
295        let dtrace = env::var("DTRACE").unwrap_or_else(|_| "dtrace".to_string());
296        sudo_command(&dtrace, sudo)
297    }
298
299    pub(crate) fn initial_command(
300        workload: Workload,
301        sudo: Option<Option<&str>>,
302        freq: u32,
303        custom_cmd: Option<String>,
304        verbose: bool,
305        ignore_status: bool,
306    ) -> anyhow::Result<Option<PathBuf>> {
307        let mut command = base_dtrace_command(sudo);
308
309        let dtrace_script = custom_cmd.unwrap_or(format!(
310            "profile-{freq} /pid == $target/ \
311             {{ @[ustack(100)] = count(); }}",
312        ));
313
314        command.arg("-x");
315        command.arg("ustackframes=100");
316
317        command.arg("-n");
318        command.arg(&dtrace_script);
319
320        command.arg("-o");
321        command.arg("cargo-flamegraph.stacks");
322
323        match workload {
324            Workload::Command(c) => {
325                let mut escaped = String::new();
326                for (i, arg) in c.iter().enumerate() {
327                    if i > 0 {
328                        escaped.push(' ');
329                    }
330                    escaped.push_str(&arg.replace(' ', "\\ "));
331                }
332
333                command.arg("-c");
334                command.arg(&escaped);
335
336                #[cfg(target_os = "windows")]
337                {
338                    let mut help_test = crate::arch::base_dtrace_command(None);
339
340                    let dtrace_found = help_test
341                        .arg("--help")
342                        .stderr(Stdio::null())
343                        .stdout(Stdio::null())
344                        .status()
345                        .is_ok();
346                    if !dtrace_found {
347                        let mut command_builder = Command::new(&c[0]);
348                        command_builder.args(&c[1..]);
349                        print_command(&command_builder, verbose);
350
351                        let trace = blondie::trace_command(command_builder, false)
352                            .map_err(|err| anyhow!("could not find dtrace and could not profile using blondie: {err:?}"))?;
353
354                        let f = std::fs::File::create("./cargo-flamegraph.stacks")
355                            .context("unable to create temporary file 'cargo-flamegraph.stacks'")?;
356                        let mut f = std::io::BufWriter::new(f);
357                        trace.write_dtrace(&mut f).map_err(|err| {
358                            anyhow!("unable to write dtrace output to 'cargo-flamegraph.stacks': {err:?}")
359                        })?;
360
361                        return Ok(None);
362                    }
363                }
364            }
365            Workload::Pid(p) => {
366                for p in p {
367                    command.arg("-p");
368                    command.arg(p.to_string());
369                }
370            }
371            Workload::ReadPerf(_) => (),
372        }
373
374        run(command, verbose, ignore_status);
375        Ok(None)
376    }
377
378    pub fn output(
379        _: Option<PathBuf>,
380        script_no_inline: bool,
381        sudo: Option<Option<&str>>,
382    ) -> anyhow::Result<Vec<u8>> {
383        if script_no_inline {
384            bail!("--no-inline is only supported on Linux");
385        }
386
387        // Ensure the file is readable by the current user if dtrace was run
388        // with sudo.
389        if sudo.is_some() {
390            #[cfg(unix)]
391            if let Ok(user) = env::var("USER") {
392                Command::new("sudo")
393                    .args(["chown", user.as_str(), "cargo-flamegraph.stacks"])
394                    .spawn()
395                    .expect(arch::SPAWN_ERROR)
396                    .wait()
397                    .expect(arch::WAIT_ERROR);
398            }
399        }
400
401        let mut buf = vec![];
402        let mut f = File::open("cargo-flamegraph.stacks")
403            .context("failed to open dtrace output file 'cargo-flamegraph.stacks'")?;
404
405        f.read_to_end(&mut buf)
406            .context("failed to read dtrace expected output file 'cargo-flamegraph.stacks'")?;
407
408        std::fs::remove_file("cargo-flamegraph.stacks")
409            .context("unable to remove temporary file 'cargo-flamegraph.stacks'")?;
410
411        // Workaround #32 - fails parsing invalid utf8 dtrace output
412        //
413        // Intermittently, invalid utf-8 is found in cargo-flamegraph.stacks, which
414        // causes parsing to blow up with the error:
415        //
416        // > unable to collapse generated profile data: Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }
417        //
418        // So here we just lossily re-encode to hopefully work around the underlying problem
419        let string = String::from_utf8_lossy(&buf);
420        let reencoded_buf = string.as_bytes().to_owned();
421
422        if reencoded_buf != buf {
423            println!("Lossily converted invalid utf-8 found in cargo-flamegraph.stacks");
424        }
425
426        Ok(reencoded_buf)
427    }
428}
429
430fn sudo_command(command: &str, sudo: Option<Option<&str>>) -> Command {
431    let sudo = match sudo {
432        Some(sudo) => sudo,
433        None => return Command::new(command),
434    };
435
436    let mut c = Command::new("sudo");
437    if let Some(sudo_args) = sudo {
438        c.arg(sudo_args);
439    }
440    c.arg(command);
441    c
442}
443
444fn run(mut command: Command, verbose: bool, ignore_status: bool) {
445    print_command(&command, verbose);
446    let mut recorder = command.spawn().expect(arch::SPAWN_ERROR);
447    let exit_status = recorder.wait().expect(arch::WAIT_ERROR);
448
449    // only stop if perf exited unsuccessfully, but
450    // was not killed by a signal (assuming that the
451    // latter case usually means the user interrupted
452    // it in some way)
453    if !ignore_status && terminated_by_error(exit_status) {
454        eprintln!(
455            "failed to sample program, exited with code: {:?}",
456            exit_status.code()
457        );
458        exit(1);
459    }
460}
461
462#[cfg(unix)]
463fn terminated_by_error(status: ExitStatus) -> bool {
464    status
465        .signal() // the default needs to be true because that's the neutral element for `&&`
466        .map_or(true, |code| code != SIGINT && code != SIGTERM)
467        && !status.success()
468        // on macOS, xctrace captures Ctrl+C and exits with code 54
469        && !(cfg!(target_os = "macos") && status.code() == Some(54))
470}
471
472#[cfg(not(unix))]
473fn terminated_by_error(status: ExitStatus) -> bool {
474    !status.success()
475}
476
477fn print_command(cmd: &Command, verbose: bool) {
478    if verbose {
479        println!("command {:?}", cmd);
480    }
481}
482
483pub fn generate_flamegraph_for_workload(workload: Workload, opts: Options) -> anyhow::Result<()> {
484    // Handle SIGINT with an empty handler. This has the
485    // implicit effect of allowing the signal to reach the
486    // process under observation while we continue to
487    // generate our flamegraph.  (ctrl+c will send the
488    // SIGINT signal to all processes in the foreground
489    // process group).
490    #[cfg(unix)]
491    let handler = unsafe {
492        signal_hook::low_level::register(SIGINT, || {}).expect("cannot register signal handler")
493    };
494
495    let sudo = opts.root.as_ref().map(|inner| inner.as_deref());
496
497    let perf_output = if let Workload::ReadPerf(perf_file) = workload {
498        Some(perf_file)
499    } else {
500        arch::initial_command(
501            workload,
502            sudo,
503            opts.frequency(),
504            opts.custom_cmd,
505            opts.verbose,
506            opts.ignore_status,
507        )?
508    };
509
510    #[cfg(unix)]
511    signal_hook::low_level::unregister(handler);
512
513    let output = arch::output(perf_output, opts.script_no_inline, sudo)?;
514
515    let mut demangled_output = vec![];
516
517    demangle_stream(&mut Cursor::new(output), &mut demangled_output, false)
518        .context("unable to demangle")?;
519
520    let perf_reader = BufReader::new(&*demangled_output);
521
522    let mut collapsed = vec![];
523
524    let collapsed_writer = BufWriter::new(&mut collapsed);
525
526    #[cfg(target_os = "linux")]
527    let mut folder = {
528        let mut collapse_options = CollapseOptions::default();
529        collapse_options.skip_after = opts.flamegraph_options.skip_after.clone();
530        Folder::from(collapse_options)
531    };
532
533    #[cfg(target_os = "macos")]
534    let mut folder = Folder::default();
535
536    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
537    let mut folder = {
538        let collapse_options = CollapseOptions::default();
539        Folder::from(collapse_options)
540    };
541
542    folder
543        .collapse(perf_reader, collapsed_writer)
544        .context("unable to collapse generated profile data")?;
545
546    if let Some(command) = opts.post_process {
547        let command_vec = shlex::split(&command)
548            .ok_or_else(|| anyhow!("unable to parse post-process command"))?;
549
550        let mut child = Command::new(
551            command_vec
552                .first()
553                .ok_or_else(|| anyhow!("unable to parse post-process command"))?,
554        )
555        .args(command_vec.get(1..).unwrap_or(&[]))
556        .stdin(Stdio::piped())
557        .stdout(Stdio::piped())
558        .spawn()
559        .with_context(|| format!("unable to execute {:?}", command_vec))?;
560
561        let mut stdin = child
562            .stdin
563            .take()
564            .ok_or_else(|| anyhow::anyhow!("unable to capture post-process stdin"))?;
565
566        let mut stdout = child
567            .stdout
568            .take()
569            .ok_or_else(|| anyhow::anyhow!("unable to capture post-process stdout"))?;
570
571        let thread_handle = std::thread::spawn(move || -> anyhow::Result<_> {
572            let mut collapsed_processed = Vec::new();
573            stdout.read_to_end(&mut collapsed_processed).context(
574                "unable to read the processed stacks from the stdout of the post-process process",
575            )?;
576            Ok(collapsed_processed)
577        });
578
579        stdin
580            .write_all(&collapsed)
581            .context("unable to write the raw stacks to the stdin of the post-process process")?;
582        drop(stdin);
583
584        anyhow::ensure!(
585            child.wait()?.success(),
586            "post-process exited with a non zero exit code"
587        );
588
589        collapsed = thread_handle.join().unwrap()?;
590    }
591
592    let collapsed_reader = BufReader::new(&*collapsed);
593
594    let flamegraph_filename = opts.output;
595    println!("writing flamegraph to {:?}", flamegraph_filename);
596    let flamegraph_file = File::create(&flamegraph_filename)
597        .context("unable to create flamegraph.svg output file")?;
598
599    let flamegraph_writer = BufWriter::new(flamegraph_file);
600
601    let mut inferno_opts = opts.flamegraph_options.into_inferno();
602    from_reader(&mut inferno_opts, collapsed_reader, flamegraph_writer)
603        .context("unable to generate a flamegraph from the collapsed stack data")?;
604
605    if opts.open {
606        opener::open(&flamegraph_filename).context(format!(
607            "failed to open '{}'",
608            flamegraph_filename.display()
609        ))?;
610    }
611
612    Ok(())
613}
614
615#[derive(Debug, Args)]
616pub struct Options {
617    /// Print extra output to help debug problems
618    #[clap(short, long)]
619    pub verbose: bool,
620
621    /// Output file
622    #[clap(short, long, default_value = "flamegraph.svg")]
623    output: PathBuf,
624
625    /// Open the output .svg file with default program
626    #[clap(long)]
627    open: bool,
628
629    /// Run with root privileges (using `sudo`). Accepts an optional argument containing command line options which will be passed to sudo
630    #[clap(long, value_name = "SUDO FLAGS")]
631    pub root: Option<Option<String>>,
632
633    /// Sampling frequency in Hz [default: 997]
634    #[clap(short = 'F', long = "freq")]
635    frequency: Option<u32>,
636
637    /// Custom command for invoking perf/dtrace
638    #[clap(short, long = "cmd")]
639    custom_cmd: Option<String>,
640
641    #[clap(flatten)]
642    flamegraph_options: FlamegraphOptions,
643
644    /// Ignores perf's exit code
645    #[clap(long)]
646    ignore_status: bool,
647
648    /// Disable inlining for perf script because of performance issues
649    #[clap(long = "no-inline")]
650    script_no_inline: bool,
651
652    /// Run a command to process the folded stacks, taking the input from stdin and outputting to
653    /// stdout.
654    #[clap(long)]
655    post_process: Option<String>,
656}
657
658impl Options {
659    pub fn check(&self) -> anyhow::Result<()> {
660        // Manually checking conflict because structopts `conflicts_with` leads
661        // to a panic in completion generation for zsh at the moment (see #158)
662        match self.frequency.is_some() && self.custom_cmd.is_some() {
663            true => Err(anyhow!(
664                "Cannot pass both a custom command and a frequency."
665            )),
666            false => Ok(()),
667        }
668    }
669
670    pub fn frequency(&self) -> u32 {
671        self.frequency.unwrap_or(997)
672    }
673}
674
675#[derive(Debug, Args)]
676pub struct FlamegraphOptions {
677    /// Set title text in SVG
678    #[clap(long, value_name = "STRING")]
679    pub title: Option<String>,
680
681    /// Set second level title text in SVG
682    #[clap(long, value_name = "STRING")]
683    pub subtitle: Option<String>,
684
685    /// Colors are selected such that the color of a function does not change between runs
686    #[clap(long)]
687    pub deterministic: bool,
688
689    /// Plot the flame graph up-side-down
690    #[clap(short, long)]
691    pub inverted: bool,
692
693    /// Generate stack-reversed flame graph
694    #[clap(long)]
695    pub reverse: bool,
696
697    /// Set embedded notes in SVG
698    #[clap(long, value_name = "STRING")]
699    pub notes: Option<String>,
700
701    /// Omit functions smaller than <FLOAT> pixels
702    #[clap(long, default_value = "0.01", value_name = "FLOAT")]
703    pub min_width: f64,
704
705    /// Image width in pixels
706    #[clap(long)]
707    pub image_width: Option<usize>,
708
709    /// Color palette
710    #[clap(
711        long,
712        value_parser = PossibleValuesParser::new(Palette::VARIANTS).map(|s| Palette::from_str(&s).unwrap())
713    )]
714    pub palette: Option<Palette>,
715
716    /// Cut off stack frames below <FUNCTION>; may be repeated
717    #[cfg(target_os = "linux")]
718    #[clap(long, value_name = "FUNCTION")]
719    pub skip_after: Vec<String>,
720
721    /// Produce a flame chart (sort by time, do not merge stacks)
722    #[clap(long = "flamechart", conflicts_with = "reverse")]
723    pub flame_chart: bool,
724}
725
726impl FlamegraphOptions {
727    pub fn into_inferno(self) -> inferno::flamegraph::Options<'static> {
728        let mut options = inferno::flamegraph::Options::default();
729        if let Some(title) = self.title {
730            options.title = title;
731        }
732        options.subtitle = self.subtitle;
733        options.deterministic = self.deterministic;
734        if self.inverted {
735            options.direction = inferno::flamegraph::Direction::Inverted;
736        }
737        options.reverse_stack_order = self.reverse;
738        options.notes = self.notes.unwrap_or_default();
739        options.min_width = self.min_width;
740        options.image_width = self.image_width;
741        if let Some(palette) = self.palette {
742            options.colors = palette;
743        }
744        options.flame_chart = self.flame_chart;
745
746        options
747    }
748}