Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sandbox

The Sandbox is a temporary directory used for a single benchmark run. It is created before the benchmark-specific setup and deleted after the benchmark-specific teardown. If setup or teardown are not present, the benchmark function still runs inside the sandbox.

The same Sandbox type is used by library and binary benchmarks. Binary benchmarks use BinaryBenchmarkConfig::sandbox instead of LibraryBenchmarkConfig::sandbox, but the isolation and fixture behavior is the same.

Why Using a Sandbox?

A Sandbox can help mitigating differences in benchmark results on different machines. As long as $TMPDIR is unset or consistently set to /tmp, the temporary directory has a constant length on unix machines (except android which uses /data/local/tmp). The directory itself is created with a constant length but random name like /tmp/.a23sr8fk.

It is not implausible that code has different event counts just because the directory it is executed in has a different length. For example, if a member of your project has set up the project in /home/bob/workspace/our-project running the benchmarks in this directory, and the ci runs the benchmarks in /runner/our-project, the event counts might differ. If possible, the benchmarks should be run in a constant environment. For example clearing the environment variables is also such a measure.

Other good reasons for using a Sandbox are convenience, e.g. if you create files during setup, the benchmark function, or teardown and do not want to delete all files manually. Or, maybe more importantly, if benchmarked code is destructive and deletes files, it is usually safer to run such code in a temporary directory where it cannot cause damage to your or other file systems.

The Sandbox is deleted after the benchmark, regardless of whether the benchmark run was successful or not. The latter is not guaranteed if you only rely on teardown, as teardown is only executed if the benchmark returns without error.

extern crate gungraun;
mod my_lib { pub fn count_bytes(path: &str) -> u64 { path.len() as u64 } }
use gungraun::prelude::*;
use gungraun::Sandbox;

use std::hint::black_box;

fn create_file(path: &str) -> String {
    std::fs::write(path, "some content").unwrap();
    path.to_owned()
}

#[library_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = LibraryBenchmarkConfig::default().sandbox(Sandbox::new(true)),
    setup = create_file
)]
fn bench_library(path: String) -> u64 {
    black_box(my_lib::count_bytes(black_box(&path)))
}

library_benchmark_group!(name = my_group, benchmarks = bench_library);
fn main() {
main!(library_benchmark_groups = my_group);
}

In this example, as part of the setup, the create_file function with the argument foo.txt is executed in the Sandbox before the benchmark function is executed. The benchmark function is executed in the same Sandbox and therefore the file foo.txt with the content some content exists thanks to the setup. After the execution of the benchmark, the Sandbox is completely removed, deleting all files created during setup, the benchmark function, and teardown if it had been present in this example.

Fixtures

Since setup is run in the sandbox, you can’t copy fixtures from your project’s workspace into the sandbox that easily anymore. The Sandbox can be configured to copy fixtures into the temporary directory with Sandbox::fixtures:

extern crate gungraun;
mod my_lib { pub fn count_bytes(path: &str) -> u64 { path.len() as u64 } }
use gungraun::prelude::*;
use gungraun::Sandbox;

use std::hint::black_box;

#[library_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = LibraryBenchmarkConfig::default()
        .sandbox(Sandbox::new(true)
            .fixtures(["benches/foo.txt"])),
)]
fn bench_library(path: &str) -> u64 {
    black_box(my_lib::count_bytes(black_box(path)))
}

library_benchmark_group!(name = my_group, benchmarks = bench_library);
fn main() {
main!(library_benchmark_groups = my_group);
}

The above will copy the fixture file foo.txt in the benches directory into the sandbox root as foo.txt. Relative paths in Sandbox::fixtures are interpreted relative to the workspace root. In a multi-crate workspace this is the directory with the top-level Cargo.toml file. Paths in Sandbox::fixtures are not limited to files, they can be directories, too.

If you have more complex demands, you can access the workspace root via the environment variable _WORKSPACE_ROOT in setup and teardown. Suppose, there is a fixture located in /home/the_project/foo_crate/benches/fixtures/foo.txt with the_project being the workspace root and foo_crate a workspace member. If the benchmark is expected to create a file bar.json, which needs further inspection after the benchmarks have run, you can copy it into a temporary directory tmp (which may or may not exist) in foo_crate:

extern crate gungraun;
mod my_lib {
    pub fn create_output(path: &str) {
        std::fs::write(path, "{}").unwrap();
    }
}
use gungraun::prelude::*;
use gungraun::Sandbox;

use std::path::PathBuf;

fn copy_fixture(path: &str) -> String {
    let workspace_root = PathBuf::from(std::env::var_os("_WORKSPACE_ROOT").unwrap());
    std::fs::copy(
        workspace_root
            .join("foo_crate")
            .join("benches")
            .join("fixtures")
            .join(path),
        path,
    )
    .unwrap();
    path.to_owned()
}

// This function will fail if `bar.json` does not exist, which is fine as this
// file is expected to be created by the benchmarked code. So, if this file does
// not exist, an error will occur and the benchmark will fail. Although
// benchmarks are not expected to test the correctness of the application, the
// `teardown` can be used to check postconditions for a successful run.
fn copy_back(path: &str) {
    let workspace_root = PathBuf::from(std::env::var_os("_WORKSPACE_ROOT").unwrap());
    let dest_dir = workspace_root.join("foo_crate").join("tmp");
    if !dest_dir.exists() {
        std::fs::create_dir(&dest_dir).unwrap();
    }
    std::fs::copy(path, dest_dir.join(path)).unwrap();
}

#[library_benchmark]
#[bench::foo(
    args = ("foo.txt"),
    config = LibraryBenchmarkConfig::default().sandbox(Sandbox::new(true)),
    setup = copy_fixture,
    teardown = copy_back
)]
fn bench_library(_path: String) -> &'static str {
    my_lib::create_output("bar.json");
    "bar.json"
}

library_benchmark_group!(name = my_group, benchmarks = bench_library);
fn main() {
main!(library_benchmark_groups = my_group);
}

Current Directory

By default, a benchmark with sandboxing enabled runs in the sandbox root. Without sandboxing, the benchmark uses the directory set by cargo bench. LibraryBenchmarkConfig::current_dir changes this working directory. If you use a relative current_dir with sandboxing enabled, it must point inside the sandbox, which is often useful together with copied fixture directories:

extern crate gungraun;
mod my_lib { pub fn count_bytes(path: &str) -> u64 { path.len() as u64 } }
use gungraun::prelude::*;
use gungraun::Sandbox;

use std::hint::black_box;

#[library_benchmark(
    config = LibraryBenchmarkConfig::default()
        .sandbox(Sandbox::new(true).fixtures(["benches/fixtures"]))
        .current_dir("fixtures")
)]
#[bench::foo("foo.txt")]
fn bench_library(path: &str) -> u64 {
    black_box(my_lib::count_bytes(black_box(path)))
}

library_benchmark_group!(name = my_group, benchmarks = bench_library);
fn main() {
main!(library_benchmark_groups = my_group);
}