Skip to content

Commit

Permalink
Add pprof profile macro (nexus-xyz#261)
Browse files Browse the repository at this point in the history
# Helper macros for Host development

This crate provides a few helper macros for Host development.

## Usage

Add this to your `Cargo.toml`:

```toml
[dependencies]
nexus-macro = { path = "../macro" }
nexus-profiler = { path = "../macro/profiler" }
```

## Profile macros

### Requirements

Install `go` programming language.

#### MacOS

```bash
brew install go
```

#### Linux

```bash
sudo apt install golang-go
```

### How to use `#[profile]` macro

At any host function, you can use `#[profile]` to profile the function.

```rust
use nexus_macro::profile;

#[profile]
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
    /// .....
    /// .....
}

#[profile("is_satisfied.pb")]
pub fn is_satisfied<C: CommitmentScheme<G>>(
        &self,
        U: &R1CSInstance<G, C>,
        W: &R1CSWitness<G>,
        pp: &C::PP,
    ) -> Result<(), Error> {
        /// ...
        /// ....
    }

```

When the profile output name is undefined, the function name is used as default with suffix `.pb`.
The first macro will write the profile data to `eval_inst.pb`. The second macro will write the profile data to `is_satisfied.pb`.

You can open the `.pb` file with `go tool pprof -http=127.0.0.1:8000 [function_name].pb`

### Warning

*Note:* This macro is supposed to be used for one-time-call function. It won't accumulate data if you call the function multiple times.

The wrong way to use this macro.

```rust
#[profile]
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
    /// ...
}

pub fn eval_inst_top(vm: &mut NexusVM<impl Memory>) -> Result<()> {
    for _ in 0..100 {
        eval_inst(vm)?;
    }
}
```
The first example will only profile the last call of `eval_inst` and omit 99 calls.


The right way to use this macro.

```rust
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
    /// ...
}

#[profile]
pub fn eval_inst_top(vm: &mut NexusVM<impl Memory>) -> Result<()> {
    for _ in 0..100 {
        eval_inst(vm)?;
    }
}
```

The second example will profile the whole `eval_inst_top` function.

Co-authored-by: duc-nx <>
  • Loading branch information
duc-nx authored Aug 6, 2024
1 parent fdededf commit 15bfc3f
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ members = [
"spartan",
"core",
"jolt",
"sdk",
"sdk",
"macro",
"macro/profiler",
]
default-members = [
"vm",
Expand Down
20 changes: 20 additions & 0 deletions macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "nexus-macro"
edition.workspace = true
version.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
publish.workspace = true

[lib]
proc-macro = true

[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
proc-macro-crate = "3.1.0"
nexus-profiler = { path = "./profiler" }
132 changes: 132 additions & 0 deletions macro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Helper macros for Host development

This crate provides a few helper macros for Host development.

## Requirements

Install `go` programming language.

#### MacOS

```bash
brew install go
```

#### Linux

```bash
sudo apt install golang-go
```


## Usage

### How to use `#[profile]` macro in SDK

For SDK user, this macro is already provided in `Cargo.toml` in `nexus-sdk` crate.


The `nexus_macro` is re-exported in `nexus-sdk`.

To use `#[profile]` macro in SDK, one must import the profiler dependency:

```toml
[dependencies]
nexus-profiler = { git = "https://github.com/nexus-xyz/nexus-zkvm/macro/profiler" }
```

```rust
use nexus_sdk::nexus_macro::profile;

#[profile]
fn compile_program(opts: CompileOpts) {
Nova::compile(&opts).expect("failed to compile guest program")
}
```

In SDK, you must include the `use sdk::nexus_macro::profile;` to use the `#[profile]` macro.
The code above will generate 1 file `compile_program.pb` in the current directory.

You can open the `.pb` file with `go tool pprof -http=127.0.0.1:8000 compile_program.pb`


### How to use `#[profile]` macro in development

For Nexus developer, if you want to use this macro in your crate.
Add this to your crate `Cargo.toml`:

```toml
[dependencies]
nexus-macro = { path = "../macro/" }
nexus-profiler = { path = "../macro/profiler" }
```

At any host function, you can use `#[profile]` to profile the function.

```rust
use nexus_macro::profile;

#[profile]
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
/// .....
/// .....
}

#[profile("is_satisfied.pb")]
pub fn is_satisfied<C: CommitmentScheme<G>>(
&self,
U: &R1CSInstance<G, C>,
W: &R1CSWitness<G>,
pp: &C::PP,
) -> Result<(), Error> {
/// ...
/// ....
}

```

When the profile output name is undefined, the function name is used as default with suffix `.pb`.
The first macro will write the profile data to `eval_inst.pb`. The second macro will write the profile data to `is_satisfied.pb`.

Build with `cargo build --release` to build in release mode.

You can open the `.pb` file with `go tool pprof -http=127.0.0.1:8000 [function_name].pb`


### Warning

*Note:* This macro is supposed to be used for one-time-call function. It won't accumulate data if you call the function multiple times.

The wrong way to use this macro.

```rust
#[profile]
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
/// ...
}

pub fn eval_inst_top(vm: &mut NexusVM<impl Memory>) -> Result<()> {
for _ in 0..100 {
eval_inst(vm)?;
}
}
```
The first example will only profile the last call of `eval_inst` and omit 99 calls.


The right way to use this macro.

```rust
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
/// ...
}

#[profile]
pub fn eval_inst_top(vm: &mut NexusVM<impl Memory>) -> Result<()> {
for _ in 0..100 {
eval_inst(vm)?;
}
}
```

The second example will profile the whole `eval_inst_top` function.
2 changes: 2 additions & 0 deletions macro/profiler/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
Cargo.lock
13 changes: 13 additions & 0 deletions macro/profiler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "nexus-profiler"
edition.workspace = true
version.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
publish.workspace = true

[dependencies]
pprof = { version = "0.13", features = ["prost-codec", "frame-pointer"] }
10 changes: 10 additions & 0 deletions macro/profiler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Nexus Profiler

This is a profiling module for the Nexus project.

The goal is to provide a simple and easy-to-use profiling solution for Nexus, allowing developers to quickly identify performance bottlenecks and optimize their code.

## Features

- `pprof` profiler via `nexus_macro::profile` macro

2 changes: 2 additions & 0 deletions macro/profiler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub mod profiler;
29 changes: 29 additions & 0 deletions macro/profiler/src/profiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::fs::File;
use std::io::Write;

use pprof::{protos::Message, ProfilerGuard};

pub fn pprof_start() -> ProfilerGuard<'static> {
pprof::ProfilerGuardBuilder::default()
.frequency(1000)
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
.build()
.expect("Failed to start profiler")
}

pub fn pprof_end(guard: ProfilerGuard<'static>, filename: &str) {
let report = guard
.report()
.build()
.expect("Failed to pprof build report");
let profile = report.pprof().expect("Failed to generate pprof profile");

let mut content = Vec::new();
profile
.encode(&mut content)
.expect("Failed to encode pprof profile");

let mut file = File::create(filename).expect("Failed to create pprof profile file");
file.write_all(&content)
.expect("Failed to write pprof profile data");
}
7 changes: 7 additions & 0 deletions macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use proc_macro::TokenStream;
mod pprof;

#[proc_macro_attribute]
pub fn profile(attr: TokenStream, input: TokenStream) -> TokenStream {
pprof::derive(attr, input)
}
40 changes: 40 additions & 0 deletions macro/src/pprof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{parse_macro_input, Ident, ItemFn, LitStr};

pub fn derive(attr: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let vis = input_fn.vis;
let sig = input_fn.sig;
let block = input_fn.block;

let file_name = if attr.is_empty() {
let function_name = format!("{}.pb", sig.ident);
LitStr::new(&function_name, Span::call_site())
} else {
parse_macro_input!(attr as LitStr)
};

let found_crate = crate_name("nexus-profiler").expect("profiler is not in `Cargo.toml`");

let profiler = match found_crate {
FoundCrate::Itself => quote!(crate::profiler),
FoundCrate::Name(name) => {
let ident = Ident::new(&name, Span::call_site());
quote!( #ident::profiler )
}
};

let output: proc_macro2::TokenStream = quote! {
#vis #sig {
let guard = #profiler::pprof_start();
let result = (|| #block)();
#profiler::pprof_end(guard, #file_name);
result
}
};

output.into()
}
4 changes: 4 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ categories = { workspace = true }
serde.workspace = true

nexus-core = { path = "../core", features = ["prover_nova", "prover_jolt", "prover_hypernova"] }
nexus-macro = { path = "../macro" }
postcard = { version = "1.0.8", features = ["alloc"] }
uuid = { version = "1.9.1", features = ["v4", "fast-rng"] }
thiserror = "1.0.61"

[dev-dependencies]
nexus-profiler = { path = "../macro/profiler" }

[lib]
doctest = false
3 changes: 3 additions & 0 deletions sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ pub mod compile;

/// Contains error types for SDK-specific interfaces.
pub mod error;

/// Development macros for for zkVM host functions.
pub use nexus_macro;

0 comments on commit 15bfc3f

Please sign in to comment.