mitata

repository·master·Indexed 25 days ago

https://github.com/evanwashere/mitata

A high-performance benchmarking tool for JavaScript and C++ developers. It provides insights into execution time, memory usage, garbage collection impact, and hardware-level CPU statistics. Features include ASCII visualizations (barplot, boxplot, lineplot), support for asynchronous benchmarks with concurrency, and a dedicated @mitata/counters extension for tracking IPC, L1 cache usage, and cycle counts on macOS and Linux.

Tokens
7.8K
Snippets
17
Records
47
Agent score
82%

What's inside mitata

  1. Prevent dead code elimination in benchmarks

    master

    JavaScript JIT compilers can detect and eliminate code that has no observable side effects. To ensure your benchmarked code is actually executed and not optimized away, use the do_not_optimize(value) function. This function emits code that forces the engine to treat the value as having observable side effects.

    import { do_not_optimize } from 'mitata';
    
    bench(function* () {
      // ❌ Bad: jit can see that function has zero side-effects
      yield () => new Array(0);
      // will get optimized to:
      /*
        yield () => {};
      */
    
      // ✅ Good: do_not_optimize(value) emits code that causes side-effects
      yield () => do_not_optimize(new Array(0));
    });
  2. Quick Start with mitata (JavaScript and C++)

    master

    Mitata provides high-performance benchmarking for both JavaScript and C++.

    In JavaScript, use run, bench, boxplot, and summary to define and execute benchmarks. You can use generators to pass parameters to benchmarks.

    In C++, use the mitata::runner class to manage benchmarks and summaries.

    import { run, bench, boxplot, summary } from 'mitata';
    
    function fibonacci(n) {
      if (n <= 1) return n;
      return fibonacci(n - 1) + fibonacci(n - 2);
    }
    
    bench('fibonacci(40)', () => fibonacci(40));
    
    boxplot(() => {
      summary(() => {
        bench('Array.from($size)', function* (state) {
          const size = state.get('size');
          yield () => Array.from({ length: size });
        }).range('size', 1, 1024);
      });
    });
    
    await run();
    #include "src/mitata.hpp"
    
    int fibonacci(int n) {
      if (n <= 1) return n;
      return fibonacci(n - 1) + fibonacci(n - 2);
    }
    
    int main() {
      mitata::runner runner;
      runner.bench("noop", []() { });
    
      runner.summary([&]() {
        runner.bench("empty fn", []() { });
        runner.bench("fibonacci", []() { fibonacci(20); });
      });
    
      auto stats = runner.run();
    }
  3. Visualize benchmarks with ASCII plots

    master

    Mitata provides built-in ASCII rendering for terminal-based visualizations. You can wrap your bench() calls in visualization scopes to generate barplots, boxplots, lineplots, and histograms. These scopes can be synchronous or asynchronous.

    Available visualization functions:

    • barplot(() => { ... }): Displays bar-style comparisons.
    • boxplot(async () => { ... }): Displays box-and-whisker plots (supports async).
    • lineplot(() => { ... }): Displays line plots.
    • summary(() => { ... }): Displays a text-based summary of results.

    You can nest visualizations, such as placing a summary inside a lineplot. Note that bench() calls made inside a nested scope (like summary) will not be included in the parent scope's visualization.

    import { summary, barplot, boxplot, lineplot } from 'mitata';
    
    // Wrap bench() calls in visualization scope
    barplot(() => {
      bench('task', () => {
        // ...
      });
    });
    
    // Scopes can be async
    await boxplot(async () => {
      // ...
    });
    
    // Combine multiple visualizations
    lineplot(() => {
      summary(() => {
        // ...
      });
    
      // bench() calls here wont be part of summary
    });
  4. Enable hardware counters with @mitata/counters

    master

    To view CPU statistics like IPC (instructions per cycle), L1 cache usage, and cycle counts, install the @mitata/counters extension.

    Installation: bun add @mitata/counters or npm install @mitata/counters

    Platform Support:

    • macos (apple silicon)
    • linux (amd64, aarch64)

    Linux Requirements:

    • /proc/sys/kernel/perf_event_paranoid must be set to 2 or lower.
    • Some VM systems may have the PMU disabled by the hypervisor.

    macOS Requirements:

    • Xcode must be installed.
    • Instruments.app (CPU Counters) must be closed during benchmarking.
    • Note: Corrupted Xcode/Command Line Tools installs can result in kernel panics.
    bun add @mitata/counters
    # or
    npm install @mitata/counters
  5. Prevent loop invariant code motion with computed parameters

    master

    JavaScript engines may optimize away repeated computations by hoisting them out of loops or caching results (Loop Invariant Code Motion). To prevent the JIT from optimizing away computations that depend on parameters, use mitata's computed parameters feature. This moves the parameter computation outside the benchmark loop, ensuring the engine cannot treat the values as constants or loop-invariant.

    bench(function* (ctx) {
      const str = 'abc';
    
      // ❌ Bad: JIT sees that both str and 'c' search value are constants/comptime-known
      yield () => str.includes('c');
      // will get optimized to:
      /*
        yield () => true;
      */
    
      // ❌ Bad: JIT sees that computation doesn't depend on anything inside loop
      const substr = ctx.get('substr');
      yield () => str.includes(substr);
      // will get optimized to:
      /*
        const $0 = str.includes(substr);
        yield () => $0;
      */
    
      // ✅ Good: using computed parameters prevents jit from performing any loop optimizations
      yield {
        [0]() {
          return str;
        },
    
        [1]() {
          return substr;
        },
    
        bench(str, substr) {
          return do_not_optimize(str.includes(substr));
        },
      };
    }).args('substr', ['c']);
  6. Control garbage collection pressure in benchmarks

    master

    When benchmarking code that performs significant memory allocations, unpredictable garbage collection (GC) pauses can skew results. You can use the .gc() method to control when GC occurs. Using .gc('inner') runs garbage collection before each (batch-)iteration to improve consistency.

    // ❌ Bad: unpredictable gc pauses
    bench(() => {
      const bigArray = new Array(1000000);
    });
    
    // ✅ Good: gc before each (batch-)iteration
    bench(() => {
      const bigArray = new Array(1000000);
    }).gc('inner'); // run gc before each iteration
  7. How to use mitata with engine CLIs

    master
    Mitata is designed to work with various JavaScript engines (d8, jsc, graaljs, spidermonkey, etc.). It automatically attempts to use the most accurate high-resolution timer (now) and garbage collection (gc) methods available in the global scope of the running environment.
  8. Configure mitata execution options

    master

    When calling run(), you can pass an options object to customize the output and execution behavior.

    JavaScript options:

    • format: Set the output format (e.g., 'json').
    • filter: A regex to only run benchmarks matching the pattern.
    • throw: If true, mitata will throw errors immediately instead of handling them quietly.
    • format.mitata.name: Set a fixed length for the benchmark name column.

    C++ options:

    • .colors: Boolean to enable/disable colors.
    • .format: String for the output format (e.g., "json").
    • .filter: A std::regex to filter benchmarks.
    import { run } from 'mitata';
    
    await run({ format: 'json' }) // output json
    await run({ filter: /new Array.*/ }) // only run benchmarks that match regex filter
    await run({ throw: true }); // will immediately throw instead of handling error quietly
    await run({ format: { mitata: { name: 'fixed' } } }); // benchmarks name column is fixed length
    auto stats = runner.run({ .colors = true, .format = "json", .filter = std::regex(".*") });
  9. Use computed parameters for unique iteration values

    master

    If you need a unique copy of a value for every single iteration (to avoid side effects or shared state), you can use computed parameters. These are defined within a yielded object and do not count towards the benchmark timing results.

    Note: There is no guarantee regarding the recompute time, order, or exact call count of these parameters.

    bench('deleting $keys from object', function* (state) {
      const keys = state.get('keys');
    
      const obj = {};
      for (let i = 0; i < keys; i++) obj[i] = i;
    
      yield {
        [0]() {
          return { ...obj };
        },
    
        bench(p0) {
          for (let i = 0; i < keys; i++) delete p0[i];
        },
      };
    }).args('keys', [1, 10, 100]);
  10. Run asynchronous benchmarks with concurrency

    master

    You can test the scalability and performance of asynchronous code by enabling the concurrency option. This allows you to see how functions perform under different levels of parallel execution.

    There are two ways to set concurrency:

    1. Inherited from arguments: Use .args('concurrency', [1, 5, 10]) to pass concurrency levels as parameters.
    2. Manual setting: Return an object from your generator that explicitly sets the concurrency property.
    // concurrency inherited from arguments
    bench('sleepAsync(1000) x $concurrency', function* () {
      yield async () => await sleepAsync(1000);
    }).args('concurrency', [1, 5, 10]);
    
    // concurrency is set manually
    bench('sleepAsync(1000) x 5', function* () {
      yield {
        concurrency: 5,
    
        async bench() {
          await sleepAsync(1000);
        },
      };
    });