vitest icon indicating copy to clipboard operation
vitest copied to clipboard

Vitest bench bug: NaNx faster than [alternative]

Open dalisoft opened this issue 3 months ago • 6 comments

Describe the bug

Problem

Shows NaNx faster than ... which is wrong

Result

 RERUN  matchit/native/index.js x11 
        Filename pattern: .


 ✓ static.bench.js > matching [zero] 40967ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            30,019,509.04  0.0000  0.0458  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.08%  15009755
   · fast-path-parse[-runtime]  30,418,133.33  0.0000  0.0507  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.10%  15209067
   · fast-path-parse[-aot]      30,350,035.33  0.0000  0.0383  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.08%  15175018
   · hono-regex-match           30,830,529.52  0.0000  1.0265  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.42%  15415266
   · hono-trie-match             5,601,067.35  0.0000  0.7059  0.0002  0.0002  0.0005  0.0005  0.0015  ±1.61%   2800534
   · find-my-way                21,003,598.28  0.0000  0.6267  0.0000  0.0000  0.0003  0.0003  0.0005  ±1.86%  10501800
   · wired                      29,853,646.52  0.0000  1.2337  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.62%  14926824
   · extreme-router             30,935,569.82  0.0000  0.2301  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.15%  15467786
   · matchit-native              7,451,079.27  0.0000  0.5605  0.0001  0.0001  0.0004  0.0004  0.0006  ±0.39%   3725540
   · matchit-wasm               20,943,997.97  0.0000  0.0403  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.06%  10471999

 ✓ static.bench.js > parse [zero] 34738ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            31,905,545.74  0.0000  0.2185  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.14%  15952773
   · fast-path-parse[-runtime]  30,474,755.27  0.0000  0.4835  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.34%  15237378
   · fast-path-parse[-aot]      30,550,115.28  0.0000  0.5365  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.43%  15275058
   · hono-regex-match           30,748,886.77  0.0000  0.0305  0.0000  0.0000  0.0000  0.0000  0.0000  ±0.06%  15374444
   · hono-trie-match             5,450,266.45  0.0000  0.5379  0.0002  0.0002  0.0004  0.0005  0.0009  ±1.48%   2725134
   · find-my-way                19,740,240.53  0.0000  0.5548  0.0001  0.0000  0.0002  0.0003  0.0005  ±1.67%   9870125
   · extreme-router             31,241,234.06  0.0000  0.0358  0.0000  0.0000  0.0000  0.0000  0.0000  ±0.07%  15620618
   · matchit-native              6,151,373.16  0.0000  0.5451  0.0002  0.0002  0.0005  0.0005  0.0008  ±1.13%   3075687
   · matchit-wasm                9,976,179.86  0.0000  0.5622  0.0001  0.0001  0.0002  0.0003  0.0005  ±0.79%   4988090

 ✓ static.bench.js > matching [zero] 38387ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            28,124,198.59  0.0000  1.8666  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.99%  14062100
   · fast-path-parse[-runtime]  31,349,635.64  0.0000  0.0574  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.10%  15674818
   · fast-path-parse[-aot]      30,679,080.65  0.0000  0.4049  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.24%  15339541
   · hono-regex-match           31,473,079.80  0.0000  0.0644  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.12%  15736541
   · hono-trie-match             5,039,334.34  0.0000  0.5752  0.0002  0.0002  0.0005  0.0005  0.0015  ±1.66%   2519668
   · find-my-way                16,754,992.33  0.0000  0.5524  0.0001  0.0000  0.0003  0.0003  0.0005  ±1.48%   8382641
   · wired                      15,222,495.72  0.0000  0.5142  0.0001  0.0001  0.0001  0.0001  0.0037  ±0.28%   7611249
   · extreme-router             23,429,667.77  0.0000  0.5260  0.0000  0.0000  0.0001  0.0002  0.0003  ±0.38%  11714834
   · matchit-native              7,768,851.08  0.0000  0.5821  0.0001  0.0001  0.0005  0.0006  0.0009  ±0.99%   3884426
   · matchit-wasm               18,727,100.69  0.0000  0.0460  0.0001  0.0001  0.0001  0.0001  0.0001  ±0.08%   9363551

 ✓ static.bench.js > matching [easy] 31077ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            27,382,696.63  0.0000  0.5385  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.45%  13691349
   · fast-path-parse[-runtime]  20,995,865.16  0.0000  3.7429  0.0000  0.0000  0.0001  0.0001  0.0003  ±1.96%  10497933
   · fast-path-parse[-aot]      21,158,889.41  0.0000  0.6327  0.0000  0.0000  0.0001  0.0001  0.0003  ±1.67%  10579445
   · hono-regex-match           11,774,569.36  0.0000  0.7902  0.0001  0.0001  0.0002  0.0003  0.0005  ±1.53%   5891585
   · hono-trie-match             2,302,971.61  0.0002  0.6820  0.0004  0.0004  0.0009  0.0010  0.0031  ±1.53%   1151486
   · find-my-way                12,918,306.94  0.0000  0.6637  0.0001  0.0001  0.0003  0.0003  0.0005  ±1.55%   6459154
   · wired                      18,126,992.84  0.0000  0.0683  0.0001  0.0000  0.0001  0.0001  0.0036  ±0.23%   9063497
   · extreme-router             25,975,278.12  0.0000  0.6053  0.0000  0.0000  0.0000  0.0001  0.0003  ±1.37%  12987640
   · matchit-native              7,970,656.72  0.0000  0.6431  0.0001  0.0001  0.0005  0.0006  0.0009  ±0.79%   3988258
   · matchit-wasm               17,297,253.31  0.0000  0.0595  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.13%   8648627

 ✓ static.bench.js > matching [easy] 33384ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            27,961,393.90  0.0000  0.5879  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.63%  13980697
   · fast-path-parse[-runtime]  20,679,474.78  0.0000  0.6838  0.0000  0.0000  0.0001  0.0001  0.0003  ±1.24%  10339738
   · fast-path-parse[-aot]      23,113,812.51  0.0000  0.4921  0.0000  0.0000  0.0001  0.0001  0.0003  ±0.61%  11556907
   · hono-regex-match           13,817,366.20  0.0000  0.6592  0.0001  0.0001  0.0001  0.0002  0.0004  ±1.68%   6908684
   · hono-trie-match             3,532,856.34  0.0001  0.8187  0.0003  0.0003  0.0005  0.0006  0.0019  ±1.76%   1766429
   · find-my-way                13,694,773.15  0.0000  0.6684  0.0001  0.0001  0.0003  0.0003  0.0005  ±1.92%   6847387
   · wired                      30,541,689.90  0.0000  0.4797  0.0000  0.0000  0.0000  0.0000  0.0002  ±0.21%  15270845
   · extreme-router             21,367,267.06  0.0000  0.6366  0.0000  0.0000  0.0001  0.0002  0.0003  ±1.22%  10683634
   · matchit-native              8,110,694.23  0.0000  0.5349  0.0001  0.0001  0.0005  0.0006  0.0008  ±0.82%   4056958
   · matchit-wasm               17,752,789.22  0.0000  0.0355  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.08%   8876395

 ✓ static.bench.js > matching [medium] 25891ms
     name                                  hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · fast-node-parse            27,174,588.37  0.0000  0.5482  0.0000  0.0000  0.0000  0.0001  0.0003  ±0.29%  13587295
   · fast-path-parse[-runtime]  14,788,582.43  0.0000  0.7382  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.66%   7394292
   · fast-path-parse[-aot]      17,220,880.60  0.0000  0.6600  0.0001  0.0001  0.0001  0.0002  0.0004  ±1.30%   8610441
   · hono-regex-match           12,172,305.03  0.0000  0.6217  0.0001  0.0001  0.0002  0.0003  0.0005  ±0.91%   6086153
   · hono-trie-match             1,362,665.99  0.0005  0.7682  0.0007  0.0007  0.0014  0.0035  0.0091  ±1.18%   1000000
   · find-my-way                 5,730,973.65  0.0000  5.2088  0.0002  0.0002  0.0005  0.0006  0.0018  ±2.89%   2865487
   · wired                      11,709,312.53  0.0000  0.6307  0.0001  0.0001  0.0001  0.0002  0.0039  ±0.33%   5854657
   · extreme-router             21,637,816.40  0.0000  0.6980  0.0000  0.0000  0.0001  0.0002  0.0004  ±1.38%  10818909
   · matchit-native              6,738,257.77  0.0000  0.7877  0.0001  0.0001  0.0006  0.0008  0.0011  ±1.40%   3369129
   · matchit-wasm               11,742,427.86  0.0000  0.1590  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.17%   5871395

 BENCH  Summary

  extreme-router - static.bench.js > matching [zero]
    1.00x faster than hono-regex-match
    1.02x faster than fast-path-parse[-runtime]
    1.02x faster than fast-path-parse[-aot]
    1.03x faster than fast-node-parse
    1.04x faster than wired
    1.47x faster than find-my-way
    1.48x faster than matchit-wasm
    4.15x faster than matchit-native
    5.52x faster than hono-trie-match

  fast-node-parse - static.bench.js > parse [zero]
    1.02x faster than extreme-router
    1.04x faster than hono-regex-match
    1.04x faster than fast-path-parse[-aot]
    1.05x faster than fast-path-parse[-runtime]
    1.62x faster than find-my-way
    3.20x faster than matchit-wasm
    5.19x faster than matchit-native
    5.85x faster than hono-trie-match

  extreme-router - static.bench.js > lookup [zero]
    1.04x faster than hono-regex-match
    1.04x faster than fast-node-parse
    2.07x faster than find-my-way
    6.00x faster than hono-trie-match
    NaNx faster than wired
    8.00x faster than matchit-wasm
    11.21x faster than matchit-native

  hono-regex-match - static.bench.js > matching [zero]
    1.00x faster than fast-path-parse[-runtime]
    1.03x faster than fast-path-parse[-aot]
    1.12x faster than fast-node-parse
    1.34x faster than extreme-router
    1.68x faster than matchit-wasm
    1.88x faster than find-my-way
    2.07x faster than wired
    4.05x faster than matchit-native
    6.25x faster than hono-trie-match

  hono-regex-match - static.bench.js > parse [zero]
    1.03x faster than fast-node-parse
    1.03x faster than fast-path-parse[-runtime]
    1.06x faster than fast-path-parse[-aot]
    1.89x faster than find-my-way
    2.82x faster than matchit-wasm
    5.25x faster than matchit-native
    6.01x faster than hono-trie-match
    NaNx faster than extreme-router

  fast-node-parse - static.bench.js > lookup [zero]
    1.03x faster than hono-regex-match
    2.28x faster than find-my-way
    6.64x faster than hono-trie-match
    NaNx faster than wired
    NaNx faster than extreme-router
    7.37x faster than matchit-wasm
    10.25x faster than matchit-native

  fast-node-parse - static.bench.js > matching [easy]
    1.05x faster than extreme-router
    1.29x faster than fast-path-parse[-aot]
    1.30x faster than fast-path-parse[-runtime]
    1.51x faster than wired
    1.58x faster than matchit-wasm
    2.12x faster than find-my-way
    2.33x faster than hono-regex-match
    3.44x faster than matchit-native
    11.89x faster than hono-trie-match

  fast-node-parse - static.bench.js > parse [easy]
    1.07x faster than fast-path-parse[-runtime]
    1.14x faster than fast-path-parse[-aot]
    2.21x faster than find-my-way
    2.27x faster than hono-regex-match
    11.67x faster than hono-trie-match
    NaNx faster than extreme-router
    16.92x faster than matchit-wasm
    18.46x faster than matchit-native

  fast-node-parse - static.bench.js > lookup [easy]
    2.51x faster than hono-regex-match
    2.70x faster than find-my-way
    12.46x faster than hono-trie-match
    NaNx faster than wired
    NaNx faster than extreme-router
    22.86x faster than matchit-wasm
    25.71x faster than matchit-native

  wired - static.bench.js > matching [easy]
    1.09x faster than fast-node-parse
    1.32x faster than fast-path-parse[-aot]
    1.43x faster than extreme-router
    1.48x faster than fast-path-parse[-runtime]
    1.72x faster than matchit-wasm
    2.21x faster than hono-regex-match
    2.23x faster than find-my-way
    3.77x faster than matchit-native
    8.65x faster than hono-trie-match

  fast-node-parse - static.bench.js > parse [easy]
    1.06x faster than fast-path-parse[-aot]
    1.16x faster than fast-path-parse[-runtime]
    2.29x faster than find-my-way
    2.39x faster than hono-regex-match
    9.02x faster than hono-trie-match
    NaNx faster than extreme-router
    19.14x faster than matchit-wasm
    21.62x faster than matchit-native

  fast-node-parse - static.bench.js > lookup [easy]
    2.35x faster than hono-regex-match
    2.71x faster than find-my-way
    9.10x faster than hono-trie-match
    NaNx faster than wired
    NaNx faster than extreme-router
    22.82x faster than matchit-wasm
    26.51x faster than matchit-native

  fast-node-parse - static.bench.js > matching [medium]
    1.26x faster than extreme-router
    1.58x faster than fast-path-parse[-aot]
    1.84x faster than fast-path-parse[-runtime]
    2.23x faster than hono-regex-match
    2.31x faster than matchit-wasm
    2.32x faster than wired
    4.03x faster than matchit-native
    4.74x faster than find-my-way
    19.94x faster than hono-trie-match

  fast-node-parse - static.bench.js > parse [medium]
    1.45x faster than fast-path-parse[-aot]
    1.79x faster than fast-path-parse[-runtime]
    2.73x faster than hono-regex-match
    4.36x faster than find-my-way
    19.77x faster than hono-trie-match
    NaNx faster than extreme-router
    31.77x faster than matchit-wasm
    32.65x faster than matchit-native

  fast-node-parse - static.bench.js > lookup [medium]
    2.64x faster than hono-regex-match
    4.57x faster than find-my-way
    18.83x faster than hono-trie-match
    NaNx faster than wired
    NaNx faster than extreme-router
    34.58x faster than matchit-wasm
    37.60x faster than matchit-native

 PASS  Waiting for file changes...
       press h to show help, press q to quit

Reproduction

// eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair
/* eslint-disable max-lines */
import FindMyWay from 'find-my-way';
import { RegExpRouter } from 'hono/router/reg-exp-router';
import { TrieRouter } from 'hono/router/trie-router';
import { CallbackNode, RootNode } from './nodes.js';
import fppMatchR from 'fast-path-parse/runtime/match';
import fppMatchA from 'fast-path-parse/aot/match';
import fppParseR from 'fast-path-parse/runtime/parse';
import fppParseA from 'fast-path-parse/aot/parse';
import { describe, bench } from 'vitest';
import { Wired } from './wired.js';
import Extreme, { param, wildcard } from 'extreme-router';

/**
 * DO NOT TOUCH
 * THIS IS TESTING CONSTANT
 */
/** @type {import('vitest').BenchOptions} */
const globalBenchConfig = {
  iterations: 1_000_000,
  warmupIterations: 1_000,
  now: process.now
};

const modes = [
  {
    path: '/foo',
    complexity: 'zero',
    validate: '/foo'
  },
  {
    path: '/foo/bar',
    complexity: 'zero',
    validate: '/foo/bar'
  },
  {
    path: '/foo/:bar',
    complexity: 'easy',
    validate: '/foo/bar'
  },
  {
    path: '/foo/*',
    complexity: 'easy',
    validate: '/foo/bar'
  },
  {
    path: '/user/:id/edit/:page',
    complexity: 'medium',
    validate: '/user/1234/edit/weather'
  }
];

for (const { path, complexity, validate } of modes) {
  const root = new RootNode('/');
  const foo = new CallbackNode(
    'GET',
    () => {
      return 1 + (1 * 2) / 2 + 1;
    },
    'foo'
  );
  const fastPathParseMatchRuntime = fppMatchR(path);
  const fastPathParseMatchAoT = fppMatchA(path);
  const fastPathParseParseRuntime = fppParseR(path);
  const fastPathParseParseAoT = fppParseA(path);
  const wire = new Wired();
  const extreme = new Extreme();

  extreme.use(param).use(wildcard);

  root.push(foo);
  root.optimize(['method', 'callback']);

  const route = FindMyWay({
    defaultRoute() {
      return false;
    }
  });
  route.on('GET', path, () => () => {
    return 1 + (1 * 2) / 2 + 1;
  });

  const honoRouteRegEx = new RegExpRouter();
  honoRouteRegEx.add('GET', path, () => {
    return 1 + (1 * 2) / 2 + 1;
  });
  const honoTrieRoute = new TrieRouter();
  honoTrieRoute.add('GET', path, () => {
    return 1 + (1 * 2) / 2 + 1;
  });

  wire.add('GET', path, () => {
    return 1 + (1 * 2) / 2 + 1;
  });

  extreme.register('/foo').handler = () => {
    return 1 + (1 * 2) / 2 + 1;
  };

  describe(`matching [${complexity}]`, () => {
    bench(
      'fast-node-parse',
      () => {
        return root.match(validate);
      },
      globalBenchConfig
    );
    bench(
      'fast-path-parse[-runtime]',
      () => {
        return fastPathParseMatchRuntime(validate);
      },
      globalBenchConfig
    );
    bench(
      'fast-path-parse[-aot]',
      () => {
        return fastPathParseMatchAoT(validate);
      },
      globalBenchConfig
    );
    bench(
      'hono-regex-match',
      () => {
        return honoRouteRegEx.match('GET', validate);
      },
      globalBenchConfig
    );
    bench(
      'hono-trie-match',
      () => {
        return honoTrieRoute.match('GET', validate);
      },
      globalBenchConfig
    );
    bench(
      'find-my-way',
      () => {
        return route.find('GET', validate);
      },
      globalBenchConfig
    );
    bench(
      'wired',
      () => {
        return wire.has('GET', validate);
      },
      globalBenchConfig
    );
    bench(
      'extreme-router',
      () => {
        return extreme.match(validate);
      },
      globalBenchConfig
    );
  });

  // Suite
  describe(`parse [${complexity}]`, () => {
    bench(
      'fast-node-parse',
      () => {
        return root.parse(validate);
      },
      globalBenchConfig
    );
    bench(
      'fast-path-parse[-runtime]',
      () => {
        return fastPathParseParseRuntime(validate);
      },
      globalBenchConfig
    );
    bench(
      'fast-path-parse[-aot]',
      () => {
        return fastPathParseParseAoT(validate);
      },
      globalBenchConfig
    );
    bench(
      'hono-regex-match',
      () => {
        const [res] = honoRouteRegEx.match('GET', validate);
        const params = res[0][1];

        return params;
      },
      globalBenchConfig
    );
    bench(
      'hono-trie-match',
      () => {
        const [res] = honoTrieRoute.match('GET', validate);
        const params = res[0][1];

        return params;
      },
      globalBenchConfig
    );
    bench(
      'find-my-way',
      () => {
        return route.find('GET', validate);
      },
      globalBenchConfig
    );
    bench(
      'extreme-router',
      () => {
        return extreme.match(validate).params;
      },
      globalBenchConfig
    );
  });

  // Suite
  describe(`lookup [${complexity}]`, () => {
    bench(
      'fast-node-parse',
      () => {
        return root.lookup(validate);
      },
      globalBenchConfig
    );
    bench(
      'hono-regex-match',
      () => {
        const [res] = honoRouteRegEx.match('GET', validate);
        const [callback, params] = res[0];

        return callback(params);
      },
      globalBenchConfig
    );
    bench(
      'hono-trie-match',
      () => {
        const [res] = honoTrieRoute.match('GET', validate);
        const [callback, params] = res[0];

        return callback(params);
      },
      globalBenchConfig
    );
    bench(
      'find-my-way',
      () => {
        return route.lookup({ method: 'GET', url: validate }, {});
      },
      globalBenchConfig
    );
    bench(
      'wired',
      () => {
        return wire.lookup('GET', validate)();
      },
      globalBenchConfig
    );
    bench(
      'extreme-router',
      () => {
        const { handler, params } = extreme.match(validate);

        return handler(params);
      },
      globalBenchConfig
    );
  });
}

System Info

System:
    OS: macOS 15.7
    CPU: (10) arm64 Apple M1 Max
    Memory: 2.81 GB / 64.00 GB
    Shell: 4.0.8 - /opt/homebrew/bin/fish
  Binaries:
    Node: 22.19.0 - ~/.local/state/fnm_multishells/49780_1758898988127/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 10.9.3 - ~/.local/state/fnm_multishells/49780_1758898988127/bin/npm
    pnpm: 10.17.1 - /opt/homebrew/bin/pnpm
    bun: 1.2.22 - /opt/homebrew/bin/bun
    Watchman: 2025.09.15.00 - /opt/homebrew/bin/watchman
  Browsers:
    Safari: 26.0.1
  npmPackages:
    vitest: ^3.2.4 => 3.2.4

Used Package Manager

npm

Validations

dalisoft avatar Sep 26 '25 15:09 dalisoft

I've started seeing this too.

Lewenhaupt avatar Oct 21 '25 09:10 Lewenhaupt

This happened to me when exceptions were being thrown inside the benchmarks. Unfortunately, Vitest didn’t report them. After fixing the errors, the speedup displayed correctly.

pioluk avatar Nov 24 '25 15:11 pioluk

As a workaround, you can use { throws: true } option for bench while you author the benchmark:

bench("my test", () => {
  [].sort()
  throw new Error("bad")
}, { throws: true })

But yes, it took a while to track this down so I would have preferred throws: true as the default.

pcattori avatar Dec 02 '25 19:12 pcattori

https://github.com/vitest-dev/vitest/issues/8628#issuecomment-3603691588

This solution worked, thank you @pcattori

dalisoft avatar Dec 05 '25 05:12 dalisoft

@dalisoft glad it helped!

That said, I feel like this issue should stay open since my comment is really a workaround. The core is still exists: users get no feedback about what is causing the NaNx faster than in normal test development.

pcattori avatar Dec 05 '25 18:12 pcattori

@pcattori I reopen for now them

dalisoft avatar Dec 05 '25 20:12 dalisoft