5 min readRunning it for real

Output that travels

Moving beyond the terminal with JSON, Markdown, and CSV reporters to make performance data shareable and archivable.

A number in a terminal window persuades nobody in a pull request.

When you're arguing for a refactor, "it felt faster on my machine" isn't a technical argument; it's a vibe. To make performance data a part of your engineering process, that data needs to move. It needs to be a file that can be attached to a ticket, a table that can be pasted into a PR, or a JSON record that can be fed into a dashboard.

NBenchmark provides a system of stackable reporters that turn raw measurement results into durable assets.

More than just a print statement

Most people start with result.Print(), which is great for a quick check. But for professional work, you want the ConsoleReporter. While Print gives you a static snapshot, the ConsoleReporter provides a rich, formatted table with live progress updates as the engine samples your code.

It's important to note the packaging: the JSON, Markdown, and CSV reporters are built into the core package. The ConsoleReporter, however, lives in NBenchmark.Reporters.Console. This keeps the core library zero-dependency while allowing the terminal UI to use specialized formatting libraries.

Stacking reporters for different audiences

You rarely have only one audience for your results. You need a human-readable table for your teammates, and a machine-readable file for your archives. NBenchmark handles this by allowing you to stack multiple reporters in a single run.

When you configure a suite, you can add as many reporters as you need. The engine will pipe the results to all of them simultaneously.

using NBenchmark;
using NBenchmark.Reporters.Console;
using NBenchmark.Reporters;

var suite = new BenchmarkSuite()
    .Add(() => { /* implementation A */ })
    .Add(() => { /* implementation B */ })
    .WithReporter(new ConsoleReporter()) // For the dev running the test
    .WithReporter(new MarkdownReporter("results/benchmark")) // For the PR
    .WithReporter(new JsonReporter("results/archive")); // For the history log

suite.Run();

Choosing the right format

Each reporter serves a specific purpose in the development lifecycle.

Markdown: The PR standard

The MarkdownReporter generates a GitHub-flavored markdown table. This is the gold standard for pull requests. Instead of a screenshot of a terminal (which is unsearchable and hard to read), you paste a real table. Your reviewers can see the Median, Ratio, and Significance columns clearly, making the "why" of your refactor immediately obvious.

CSV: The spreadsheet power-user

If you're doing a massive sweep of parameters and need to generate a trend line or a scatter plot, the CSVReporter is your best tool. It exports the results into a format that Excel or Google Sheets can ingest instantly, allowing you to perform your own secondary analysis on the data.

JSON: The source of truth

The JsonReporter is the most important reporter in the set. While Markdown and CSV are for humans, JSON is for the system. It carries the full record of the run, including:

  • Every raw sample collected.
  • The full histogram of the distribution.
  • The exact runtime knobs and profile applied.
  • The auto-tune diagnostic data (warmup stop reasons, CI half-width).

If you ever need to re-analyze an old run or build a custom performance dashboard, you start with the JSON file.

Controlling the detail

Not every report needs to be a data dump. NBenchmark provides several ways to control the level of detail in your output.

You can set the detail level per reporter, per suite, or via the --detail CLI flag. The Simple level gives you the core ten columns (Median, Mean, Error, etc.), while Advanced adds the deep-dive statistics: quartiles, fences, shape statistics (skewness, kurtosis), and the exact number of samples trimmed by the outlier fence.

One exception: JSON always carries the full record. Because JSON is intended as the archive, it ignores detail settings and saves everything.

Managing the payload

Because the raw sample sets can become quite large - especially in long-running suites - NBenchmark provides flags to trim the JSON payload without losing the primary metrics.

  • --no-raw-samples: Removes the individual sample timings but keeps the calculated statistics.
  • --full-raw-samples: Ensures every single timing is captured, even if the engine usually caps the reservoir.
  • --no-histogram: Removes the distribution buckets to save space.

Single-result extensions

If you aren't using a suite and just have a single BenchmarkResult from a Benchmark.Run call, you don't need to instantiate a full reporter. NBenchmark provides a set of async extension methods for quick exports:

var result = Benchmark.Run(() => { /* ... */ });

await result.ToJsonAsync("result.json");
await result.ToMarkdownAsync("result.md");
await result.ToCsvAsync("result.csv");

These extensions are perfect for small scripts or integration tests where you just need a quick file on disk to verify a result.


Go deeper: Now that your results can travel, let's look at how to make those results meaningful across different inputs. Most code doesn't have a single "speed" - it has a scaling curve.

Benchmarks that take arguments →