@fuzdev/fuz_util 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- The MIT License (MIT)
1
+ MIT License
2
2
 
3
3
  Copyright (c) fuz.dev
4
4
 
@@ -135,9 +135,24 @@ export declare class Benchmark {
135
135
  table(options?: BenchmarkFormatTableOptions): string;
136
136
  /**
137
137
  * Format results as a Markdown table.
138
+ * @param options - Formatting options (groups for organized output with optional baselines)
138
139
  * @returns Formatted markdown string
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // Standard table
144
+ * console.log(bench.markdown());
145
+ *
146
+ * // Grouped by category with custom baseline
147
+ * console.log(bench.markdown({
148
+ * groups: [
149
+ * { name: 'Format', filter: (r) => r.name.startsWith('format/'), baseline: 'format/prettier' },
150
+ * { name: 'Parse', filter: (r) => r.name.startsWith('parse/') },
151
+ * ]
152
+ * }));
153
+ * ```
139
154
  */
140
- markdown(): string;
155
+ markdown(options?: BenchmarkFormatTableOptions): string;
141
156
  /**
142
157
  * Format results as JSON.
143
158
  * @param options - Formatting options (pretty, include_timings)
@@ -150,6 +165,33 @@ export declare class Benchmark {
150
165
  * @returns Array of benchmark results
151
166
  */
152
167
  results(): Array<BenchmarkResult>;
168
+ /**
169
+ * Check if the benchmark has been run and has results.
170
+ * @returns True if results are available
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * if (bench.has_results) {
175
+ * console.log(bench.table());
176
+ * }
177
+ * ```
178
+ */
179
+ get has_results(): boolean;
180
+ /**
181
+ * Get results as a map for convenient lookup by task name.
182
+ * Returns a new Map each call to prevent external mutation.
183
+ * @returns Map of task name to benchmark result
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const results_map = bench.results_by_name();
188
+ * const slugify_result = results_map.get('slugify');
189
+ * if (slugify_result) {
190
+ * console.log(`slugify: ${slugify_result.stats.ops_per_second} ops/sec`);
191
+ * }
192
+ * ```
193
+ */
194
+ results_by_name(): Map<string, BenchmarkResult>;
153
195
  /**
154
196
  * Reset the benchmark results.
155
197
  * Keeps tasks intact so benchmarks can be rerun.
@@ -1 +1 @@
1
- {"version":3,"file":"benchmark.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAMN,KAAK,0BAA0B,EAC/B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACX,eAAe,EACf,aAAa,EACb,eAAe,EACf,2BAA2B,EAC3B,MAAM,sBAAsB,CAAC;AAgD9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC5B,IAAI,MAAM,OAAO,EACjB,YAAY,MAAM,EAClB,aAAa,OAAO,KAClB,OAAO,CAAC,OAAO,CAyBjB,CAAC;AAEF;;GAEG;AACH,qBAAa,SAAS;;gBAMT,MAAM,GAAE,eAAoB;IAcxC;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,GAAG,IAAI;IAC1C,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAkB9B;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAS1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASxB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASxB;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAqH5C;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,MAAM;IAMpD;;;OAGG;IACH,QAAQ,IAAI,MAAM;IAIlB;;;;OAIG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,MAAM;IAIlD;;;;OAIG;IACH,OAAO,IAAI,KAAK,CAAC,eAAe,CAAC;IAIjC;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAKb;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,MAAM;CA+BjB"}
1
+ {"version":3,"file":"benchmark.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAON,KAAK,0BAA0B,EAC/B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACX,eAAe,EACf,aAAa,EACb,eAAe,EACf,2BAA2B,EAC3B,MAAM,sBAAsB,CAAC;AAgD9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC5B,IAAI,MAAM,OAAO,EACjB,YAAY,MAAM,EAClB,aAAa,OAAO,KAClB,OAAO,CAAC,OAAO,CAyBjB,CAAC;AAEF;;GAEG;AACH,qBAAa,SAAS;;gBAMT,MAAM,GAAE,eAAoB;IAcxC;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,GAAG,IAAI;IAC1C,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAkB9B;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAS1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASxB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASxB;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAqH5C;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,MAAM;IAMpD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,MAAM;IAMvD;;;;OAIG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,MAAM;IAIlD;;;;OAIG;IACH,OAAO,IAAI,KAAK,CAAC,eAAe,CAAC;IAIjC;;;;;;;;;;OAUG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC;IAI/C;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAKb;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,MAAM;CA+BjB"}
package/dist/benchmark.js CHANGED
@@ -21,7 +21,7 @@
21
21
  import { is_promise, wait } from './async.js';
22
22
  import { BenchmarkStats } from './benchmark_stats.js';
23
23
  import { timer_default, time_unit_detect_best, time_format } from './time.js';
24
- import { benchmark_format_table, benchmark_format_table_grouped, benchmark_format_markdown, benchmark_format_json, benchmark_format_number, } from './benchmark_format.js';
24
+ import { benchmark_format_table, benchmark_format_table_grouped, benchmark_format_markdown, benchmark_format_markdown_grouped, benchmark_format_json, benchmark_format_number, } from './benchmark_format.js';
25
25
  // Default configuration values
26
26
  const DEFAULT_DURATION_MS = 1000;
27
27
  const DEFAULT_WARMUP_ITERATIONS = 10;
@@ -328,10 +328,27 @@ export class Benchmark {
328
328
  }
329
329
  /**
330
330
  * Format results as a Markdown table.
331
+ * @param options - Formatting options (groups for organized output with optional baselines)
331
332
  * @returns Formatted markdown string
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * // Standard table
337
+ * console.log(bench.markdown());
338
+ *
339
+ * // Grouped by category with custom baseline
340
+ * console.log(bench.markdown({
341
+ * groups: [
342
+ * { name: 'Format', filter: (r) => r.name.startsWith('format/'), baseline: 'format/prettier' },
343
+ * { name: 'Parse', filter: (r) => r.name.startsWith('parse/') },
344
+ * ]
345
+ * }));
346
+ * ```
332
347
  */
333
- markdown() {
334
- return benchmark_format_markdown(this.#results);
348
+ markdown(options) {
349
+ return options?.groups
350
+ ? benchmark_format_markdown_grouped(this.#results, options.groups)
351
+ : benchmark_format_markdown(this.#results);
335
352
  }
336
353
  /**
337
354
  * Format results as JSON.
@@ -349,6 +366,37 @@ export class Benchmark {
349
366
  results() {
350
367
  return [...this.#results];
351
368
  }
369
+ /**
370
+ * Check if the benchmark has been run and has results.
371
+ * @returns True if results are available
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * if (bench.has_results) {
376
+ * console.log(bench.table());
377
+ * }
378
+ * ```
379
+ */
380
+ get has_results() {
381
+ return this.#results.length > 0;
382
+ }
383
+ /**
384
+ * Get results as a map for convenient lookup by task name.
385
+ * Returns a new Map each call to prevent external mutation.
386
+ * @returns Map of task name to benchmark result
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * const results_map = bench.results_by_name();
391
+ * const slugify_result = results_map.get('slugify');
392
+ * if (slugify_result) {
393
+ * console.log(`slugify: ${slugify_result.stats.ops_per_second} ops/sec`);
394
+ * }
395
+ * ```
396
+ */
397
+ results_by_name() {
398
+ return new Map(this.#results.map((r) => [r.name, r]));
399
+ }
352
400
  /**
353
401
  * Reset the benchmark results.
354
402
  * Keeps tasks intact so benchmarks can be rerun.
@@ -11,7 +11,7 @@ import { type BenchmarkComparison } from './benchmark_stats.js';
11
11
  export declare const BenchmarkBaselineEntry: z.ZodObject<{
12
12
  name: z.ZodString;
13
13
  mean_ns: z.ZodNumber;
14
- median_ns: z.ZodNumber;
14
+ p50_ns: z.ZodNumber;
15
15
  std_dev_ns: z.ZodNumber;
16
16
  min_ns: z.ZodNumber;
17
17
  max_ns: z.ZodNumber;
@@ -35,7 +35,7 @@ export declare const BenchmarkBaseline: z.ZodObject<{
35
35
  entries: z.ZodArray<z.ZodObject<{
36
36
  name: z.ZodString;
37
37
  mean_ns: z.ZodNumber;
38
- median_ns: z.ZodNumber;
38
+ p50_ns: z.ZodNumber;
39
39
  std_dev_ns: z.ZodNumber;
40
40
  min_ns: z.ZodNumber;
41
41
  max_ns: z.ZodNumber;
@@ -10,14 +10,14 @@ import { git_info_get } from './git.js';
10
10
  import { benchmark_stats_compare, } from './benchmark_stats.js';
11
11
  import { stats_confidence_interval_from_summary } from './stats.js';
12
12
  // Version for forward compatibility - increment when schema changes
13
- const BASELINE_VERSION = 1;
13
+ const BASELINE_VERSION = 2;
14
14
  /**
15
15
  * Schema for a single benchmark entry in the baseline.
16
16
  */
17
17
  export const BenchmarkBaselineEntry = z.object({
18
18
  name: z.string(),
19
19
  mean_ns: z.number(),
20
- median_ns: z.number(),
20
+ p50_ns: z.number(),
21
21
  std_dev_ns: z.number(),
22
22
  min_ns: z.number(),
23
23
  max_ns: z.number(),
@@ -48,7 +48,7 @@ const results_to_entries = (results) => {
48
48
  return results.map((r) => ({
49
49
  name: r.name,
50
50
  mean_ns: r.stats.mean_ns,
51
- median_ns: r.stats.median_ns,
51
+ p50_ns: r.stats.p50_ns,
52
52
  std_dev_ns: r.stats.std_dev_ns,
53
53
  min_ns: r.stats.min_ns,
54
54
  max_ns: r.stats.max_ns,
@@ -3,36 +3,63 @@ import type { BenchmarkResult, BenchmarkGroup } from './benchmark_types.js';
3
3
  * Format results as an ASCII table with percentiles, min/max, and relative performance.
4
4
  * All times use the same unit for easy comparison.
5
5
  * @param results - Array of benchmark results
6
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
6
7
  * @returns Formatted table string with enhanced metrics
7
8
  *
8
9
  * @example
9
10
  * ```ts
10
11
  * console.log(benchmark_format_table(results));
11
- * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
12
- * // │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
13
- * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
14
- * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
15
- * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
16
- * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
12
+ * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
13
+ * // │ Task Name │ ops/sec │ p50 (μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
14
+ * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
15
+ * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
16
+ * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
17
+ * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
17
18
  * ```
18
19
  */
19
- export declare const benchmark_format_table: (results: Array<BenchmarkResult>) => string;
20
+ export declare const benchmark_format_table: (results: Array<BenchmarkResult>, baseline?: string) => string;
20
21
  /**
21
22
  * Format results as a Markdown table with key metrics.
22
23
  * All times use the same unit for easy comparison.
23
24
  * @param results - Array of benchmark results
25
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
24
26
  * @returns Formatted markdown table string
25
27
  *
26
28
  * @example
27
29
  * ```ts
28
30
  * console.log(benchmark_format_markdown(results));
29
- * // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
30
- * // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
31
- * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
32
- * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
31
+ * // | Task Name | ops/sec | p50 (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
32
+ * // |------------|------------|----------|----------|----------|----------|----------|----------|----------|----------|
33
+ * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
34
+ * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
33
35
  * ```
34
36
  */
35
- export declare const benchmark_format_markdown: (results: Array<BenchmarkResult>) => string;
37
+ export declare const benchmark_format_markdown: (results: Array<BenchmarkResult>, baseline?: string) => string;
38
+ /**
39
+ * Format results as grouped Markdown tables with headers between groups.
40
+ * @param results - Array of benchmark results
41
+ * @param groups - Array of group definitions
42
+ * @returns Formatted markdown string with group headers and tables
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const groups = [
47
+ * { name: 'Fast Paths', filter: (r) => r.name.includes('fast'), baseline: 'fast/reference' },
48
+ * { name: 'Slow Paths', filter: (r) => r.name.includes('slow') },
49
+ * ];
50
+ * console.log(benchmark_format_markdown_grouped(results, groups));
51
+ * // ### Fast Paths
52
+ * // | Task Name | ops/sec | ... | vs fast/reference |
53
+ * // |-----------|---------|-----|-------------------|
54
+ * // | ... | ... | ... | ... |
55
+ * //
56
+ * // ### Slow Paths
57
+ * // | Task Name | ops/sec | ... | vs Best |
58
+ * // |-----------|---------|-----|---------|
59
+ * // | ... | ... | ... | ... |
60
+ * ```
61
+ */
62
+ export declare const benchmark_format_markdown_grouped: (results: Array<BenchmarkResult>, groups: Array<BenchmarkGroup>) => string;
36
63
  export interface BenchmarkFormatJsonOptions {
37
64
  /** Whether to pretty-print (default: true) */
38
65
  pretty?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"benchmark_format.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAE,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAK1E;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,KAAK,CAAC,eAAe,CAAC,KAAG,MAiFxE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,KAAK,CAAC,eAAe,CAAC,KAAG,MA4E3E,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,UAAU,0BAA0B,KAClC,MA6BF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,QAAQ,KAAK,CAAC,cAAc,CAAC,KAC3B,MA2BF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,0CAAgB,CAAC"}
1
+ {"version":3,"file":"benchmark_format.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAE,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAK1E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,WAAW,MAAM,KACf,MAiGF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,WAAW,MAAM,KACf,MA4FF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,QAAQ,KAAK,CAAC,cAAc,CAAC,KAC3B,MA2BF,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,UAAU,0BAA0B,KAClC,MA6BF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,QAAQ,KAAK,CAAC,cAAc,CAAC,KAC3B,MA2BF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,0CAAgB,CAAC"}
@@ -5,46 +5,61 @@ import { format_number } from './maths.js';
5
5
  * Format results as an ASCII table with percentiles, min/max, and relative performance.
6
6
  * All times use the same unit for easy comparison.
7
7
  * @param results - Array of benchmark results
8
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
8
9
  * @returns Formatted table string with enhanced metrics
9
10
  *
10
11
  * @example
11
12
  * ```ts
12
13
  * console.log(benchmark_format_table(results));
13
- * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
14
- * // │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
15
- * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
16
- * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
17
- * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
18
- * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
14
+ * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
15
+ * // │ Task Name │ ops/sec │ p50 (μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
16
+ * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
17
+ * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
18
+ * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
19
+ * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
19
20
  * ```
20
21
  */
21
- export const benchmark_format_table = (results) => {
22
+ export const benchmark_format_table = (results, baseline) => {
22
23
  if (results.length === 0)
23
24
  return '(no results)';
24
25
  // Detect best unit for all results
25
26
  const mean_times = results.map((r) => r.stats.mean_ns);
26
27
  const unit = time_unit_detect_best(mean_times);
27
28
  const unit_str = TIME_UNIT_DISPLAY[unit];
28
- // Find fastest for relative comparison
29
- const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
29
+ // Find baseline for relative comparison
30
+ let baseline_ops;
31
+ let vs_column_header;
32
+ if (baseline !== undefined) {
33
+ const baseline_result = results.find((r) => r.name === baseline);
34
+ if (!baseline_result) {
35
+ const available = results.map((r) => r.name).join(', ');
36
+ throw new Error(`Baseline task "${baseline}" not found in results. Available tasks: ${available}`);
37
+ }
38
+ baseline_ops = baseline_result.stats.ops_per_second;
39
+ vs_column_header = `vs ${baseline}`;
40
+ }
41
+ else {
42
+ baseline_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
43
+ vs_column_header = 'vs Best';
44
+ }
30
45
  const rows = [];
31
46
  // Header with unit
32
47
  rows.push([
33
48
  'Task Name',
34
49
  'ops/sec',
35
- `median (${unit_str})`,
50
+ `p50 (${unit_str})`,
36
51
  `p75 (${unit_str})`,
37
52
  `p90 (${unit_str})`,
38
53
  `p95 (${unit_str})`,
39
54
  `p99 (${unit_str})`,
40
55
  `min (${unit_str})`,
41
56
  `max (${unit_str})`,
42
- 'vs Best',
57
+ vs_column_header,
43
58
  ]);
44
59
  // Data rows - all use same unit
45
60
  results.forEach((r) => {
46
61
  const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
47
- const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
62
+ const p50 = time_format(r.stats.p50_ns, unit, 2).replace(unit_str, '').trim();
48
63
  const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
49
64
  const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
50
65
  const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
@@ -52,9 +67,9 @@ export const benchmark_format_table = (results) => {
52
67
  const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
53
68
  const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
54
69
  // Calculate relative performance
55
- const ratio = fastest_ops / r.stats.ops_per_second;
56
- const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
57
- rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
70
+ const ratio = baseline_ops / r.stats.ops_per_second;
71
+ const vs_baseline = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
72
+ rows.push([r.name, ops_sec, p50, p75, p90, p95, p99, min, max, vs_baseline]);
58
73
  });
59
74
  // Calculate column widths (using display width for proper emoji handling)
60
75
  const widths = rows[0].map((_, col_i) => {
@@ -91,44 +106,59 @@ export const benchmark_format_table = (results) => {
91
106
  * Format results as a Markdown table with key metrics.
92
107
  * All times use the same unit for easy comparison.
93
108
  * @param results - Array of benchmark results
109
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
94
110
  * @returns Formatted markdown table string
95
111
  *
96
112
  * @example
97
113
  * ```ts
98
114
  * console.log(benchmark_format_markdown(results));
99
- * // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
100
- * // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
101
- * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
102
- * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
115
+ * // | Task Name | ops/sec | p50 (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
116
+ * // |------------|------------|----------|----------|----------|----------|----------|----------|----------|----------|
117
+ * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
118
+ * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
103
119
  * ```
104
120
  */
105
- export const benchmark_format_markdown = (results) => {
121
+ export const benchmark_format_markdown = (results, baseline) => {
106
122
  if (results.length === 0)
107
123
  return '(no results)';
108
124
  // Detect best unit for all results
109
125
  const mean_times = results.map((r) => r.stats.mean_ns);
110
126
  const unit = time_unit_detect_best(mean_times);
111
127
  const unit_str = TIME_UNIT_DISPLAY[unit];
112
- // Find fastest for relative comparison
113
- const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
128
+ // Find baseline for relative comparison
129
+ let baseline_ops;
130
+ let vs_column_header;
131
+ if (baseline !== undefined) {
132
+ const baseline_result = results.find((r) => r.name === baseline);
133
+ if (!baseline_result) {
134
+ const available = results.map((r) => r.name).join(', ');
135
+ throw new Error(`Baseline task "${baseline}" not found in results. Available tasks: ${available}`);
136
+ }
137
+ baseline_ops = baseline_result.stats.ops_per_second;
138
+ vs_column_header = `vs ${baseline}`;
139
+ }
140
+ else {
141
+ baseline_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
142
+ vs_column_header = 'vs Best';
143
+ }
114
144
  const rows = [];
115
145
  // Header with unit
116
146
  rows.push([
117
147
  'Task Name',
118
148
  'ops/sec',
119
- `median (${unit_str})`,
149
+ `p50 (${unit_str})`,
120
150
  `p75 (${unit_str})`,
121
151
  `p90 (${unit_str})`,
122
152
  `p95 (${unit_str})`,
123
153
  `p99 (${unit_str})`,
124
154
  `min (${unit_str})`,
125
155
  `max (${unit_str})`,
126
- 'vs Best',
156
+ vs_column_header,
127
157
  ]);
128
158
  // Data rows - all use same unit
129
159
  results.forEach((r) => {
130
160
  const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
131
- const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
161
+ const p50 = time_format(r.stats.p50_ns, unit, 2).replace(unit_str, '').trim();
132
162
  const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
133
163
  const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
134
164
  const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
@@ -136,9 +166,9 @@ export const benchmark_format_markdown = (results) => {
136
166
  const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
137
167
  const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
138
168
  // Calculate relative performance
139
- const ratio = fastest_ops / r.stats.ops_per_second;
140
- const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
141
- rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
169
+ const ratio = baseline_ops / r.stats.ops_per_second;
170
+ const vs_baseline = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
171
+ rows.push([r.name, ops_sec, p50, p75, p90, p95, p99, min, max, vs_baseline]);
142
172
  });
143
173
  // Calculate column widths
144
174
  const widths = rows[0].map((_, col_i) => {
@@ -168,6 +198,54 @@ export const benchmark_format_markdown = (results) => {
168
198
  }
169
199
  return lines.join('\n');
170
200
  };
201
+ /**
202
+ * Format results as grouped Markdown tables with headers between groups.
203
+ * @param results - Array of benchmark results
204
+ * @param groups - Array of group definitions
205
+ * @returns Formatted markdown string with group headers and tables
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * const groups = [
210
+ * { name: 'Fast Paths', filter: (r) => r.name.includes('fast'), baseline: 'fast/reference' },
211
+ * { name: 'Slow Paths', filter: (r) => r.name.includes('slow') },
212
+ * ];
213
+ * console.log(benchmark_format_markdown_grouped(results, groups));
214
+ * // ### Fast Paths
215
+ * // | Task Name | ops/sec | ... | vs fast/reference |
216
+ * // |-----------|---------|-----|-------------------|
217
+ * // | ... | ... | ... | ... |
218
+ * //
219
+ * // ### Slow Paths
220
+ * // | Task Name | ops/sec | ... | vs Best |
221
+ * // |-----------|---------|-----|---------|
222
+ * // | ... | ... | ... | ... |
223
+ * ```
224
+ */
225
+ export const benchmark_format_markdown_grouped = (results, groups) => {
226
+ if (results.length === 0)
227
+ return '(no results)';
228
+ const sections = [];
229
+ for (const group of groups) {
230
+ const group_results = results.filter(group.filter);
231
+ if (group_results.length === 0)
232
+ continue;
233
+ // Add group header and table
234
+ const header = group.description
235
+ ? `### ${group.name}\n\n${group.description}\n`
236
+ : `### ${group.name}\n`;
237
+ sections.push(header);
238
+ sections.push(benchmark_format_markdown(group_results, group.baseline));
239
+ }
240
+ // Handle ungrouped results (those that don't match any group)
241
+ const grouped_names = new Set(groups.flatMap((g) => results.filter(g.filter).map((r) => r.name)));
242
+ const ungrouped = results.filter((r) => !grouped_names.has(r.name));
243
+ if (ungrouped.length > 0) {
244
+ sections.push('### Other\n');
245
+ sections.push(benchmark_format_markdown(ungrouped));
246
+ }
247
+ return sections.join('\n');
248
+ };
171
249
  /**
172
250
  * Format results as JSON.
173
251
  * @param results - Array of benchmark results
@@ -191,7 +269,7 @@ export const benchmark_format_json = (results, options) => {
191
269
  total_time_ms: r.total_time_ms,
192
270
  ops_per_second: r.stats.ops_per_second,
193
271
  mean_ns: r.stats.mean_ns,
194
- median_ns: r.stats.median_ns,
272
+ p50_ns: r.stats.p50_ns,
195
273
  std_dev_ns: r.stats.std_dev_ns,
196
274
  min_ns: r.stats.min_ns,
197
275
  max_ns: r.stats.max_ns,
@@ -248,7 +326,7 @@ export const benchmark_format_table_grouped = (results, groups) => {
248
326
  ? `\nšŸ“¦ ${group.name}\n ${group.description}`
249
327
  : `\nšŸ“¦ ${group.name}`;
250
328
  sections.push(header);
251
- sections.push(benchmark_format_table(group_results));
329
+ sections.push(benchmark_format_table(group_results, group.baseline));
252
330
  }
253
331
  // Handle ungrouped results (those that don't match any group)
254
332
  const grouped_names = new Set(groups.flatMap((g) => results.filter(g.filter).map((r) => r.name)));
@@ -53,8 +53,8 @@ export interface BenchmarkCompareOptions {
53
53
  export declare class BenchmarkStats {
54
54
  /** Mean (average) time in nanoseconds */
55
55
  readonly mean_ns: number;
56
- /** Median time in nanoseconds */
57
- readonly median_ns: number;
56
+ /** 50th percentile (median) time in nanoseconds */
57
+ readonly p50_ns: number;
58
58
  /** Standard deviation in nanoseconds */
59
59
  readonly std_dev_ns: number;
60
60
  /** Minimum time in nanoseconds */
@@ -1 +1 @@
1
- {"version":3,"file":"benchmark_stats.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_stats.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,mFAAmF;IACnF,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,CAAC;IAC5B,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,WAAW,EAAE,OAAO,CAAC;IACrB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,gBAAgB,EAAE,eAAe,CAAC;IAClC,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,qBAAa,cAAc;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wCAAwC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,sDAAsD;IACtD,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sEAAsE;IACtE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBAEvB,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;IAiErC;;OAEG;IACH,QAAQ,IAAI,MAAM;CAGlB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,GACnC,GAAG,wBAAwB,EAC3B,GAAG,wBAAwB,EAC3B,UAAU,uBAAuB,KAC/B,mBA6GF,CAAC"}
1
+ {"version":3,"file":"benchmark_stats.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_stats.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,mFAAmF;IACnF,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,CAAC;IAC5B,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,WAAW,EAAE,OAAO,CAAC;IACrB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,gBAAgB,EAAE,eAAe,CAAC;IAClC,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,qBAAa,cAAc;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,sDAAsD;IACtD,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sEAAsE;IACtE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBAEvB,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;IAiErC;;OAEG;IACH,QAAQ,IAAI,MAAM;CAGlB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,GACnC,GAAG,wBAAwB,EAC3B,GAAG,wBAAwB,EAC3B,UAAU,uBAAuB,KAC/B,mBA6GF,CAAC"}
@@ -13,8 +13,8 @@ import { stats_mean, stats_median, stats_std_dev, stats_percentile, stats_cv, st
13
13
  export class BenchmarkStats {
14
14
  /** Mean (average) time in nanoseconds */
15
15
  mean_ns;
16
- /** Median time in nanoseconds */
17
- median_ns;
16
+ /** 50th percentile (median) time in nanoseconds */
17
+ p50_ns;
18
18
  /** Standard deviation in nanoseconds */
19
19
  std_dev_ns;
20
20
  /** Minimum time in nanoseconds */
@@ -62,7 +62,7 @@ export class BenchmarkStats {
62
62
  // If no valid timings, return empty stats
63
63
  if (valid_timings.length === 0) {
64
64
  this.mean_ns = NaN;
65
- this.median_ns = NaN;
65
+ this.p50_ns = NaN;
66
66
  this.std_dev_ns = NaN;
67
67
  this.min_ns = NaN;
68
68
  this.max_ns = NaN;
@@ -86,7 +86,7 @@ export class BenchmarkStats {
86
86
  this.sample_size = cleaned.length;
87
87
  // Calculate statistics on cleaned data
88
88
  this.mean_ns = stats_mean(cleaned);
89
- this.median_ns = stats_median(sorted_cleaned);
89
+ this.p50_ns = stats_median(sorted_cleaned);
90
90
  this.std_dev_ns = stats_std_dev(cleaned, this.mean_ns);
91
91
  const { min, max } = stats_min_max(sorted_cleaned);
92
92
  this.min_ns = min;
@@ -170,5 +170,11 @@ export interface BenchmarkGroup {
170
170
  description?: string;
171
171
  /** Filter function to determine which results belong to this group */
172
172
  filter: (result: BenchmarkResult) => boolean;
173
+ /**
174
+ * Task name to use as baseline for the "vs" column.
175
+ * When specified, ratios are computed against this task instead of the fastest.
176
+ * If the baseline task is not found in the group, falls back to "vs Best" with a warning.
177
+ */
178
+ baseline?: string;
173
179
  }
174
180
  //# sourceMappingURL=benchmark_types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"benchmark_types.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAC,KAAK,EAAC,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAEjF;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnF;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IAEb,wEAAwE;IACxE,EAAE,EAAE,MAAM,OAAO,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtC;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IAEb,4CAA4C;IAC5C,KAAK,EAAE,cAAc,CAAC;IAEtB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IAEnB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C;;OAEG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,sEAAsE;IACtE,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC;CAC7C"}
1
+ {"version":3,"file":"benchmark_types.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAC,KAAK,EAAC,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAEjF;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnF;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IAEb,wEAAwE;IACxE,EAAE,EAAE,MAAM,OAAO,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtC;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IAEb,4CAA4C;IAC5C,KAAK,EAAE,cAAc,CAAC;IAEtB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IAEnB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C;;OAEG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,sEAAsE;IACtE,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC;IAE7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -33,7 +33,6 @@ export declare const PackageJson: z.ZodObject<{
33
33
  name: z.ZodString;
34
34
  version: z.ZodString;
35
35
  private: z.ZodOptional<z.ZodBoolean>;
36
- public: z.ZodOptional<z.ZodBoolean>;
37
36
  description: z.ZodOptional<z.ZodString>;
38
37
  motto: z.ZodOptional<z.ZodString>;
39
38
  glyph: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"package_json.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/package_json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAMtB,eAAO,MAAM,qBAAqB;;;;mBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,eAAO,MAAM,iBAAiB;;;;mBAO5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,kBAAkB;;;mBAM7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAapE,eAAO,MAAM,WAAW,yEAAsB,CAAC;AAC/C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,kJAI7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
1
+ {"version":3,"file":"package_json.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/package_json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAMtB,eAAO,MAAM,qBAAqB;;;;mBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,eAAO,MAAM,iBAAiB;;;;mBAO5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,kBAAkB;;;mBAM7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAapE,eAAO,MAAM,WAAW,yEAAsB,CAAC;AAC/C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,kJAI7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6DtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
@@ -54,12 +54,6 @@ export const PackageJson = z.looseObject({
54
54
  .boolean()
55
55
  .meta({ description: 'disallow publishing to the configured registry' })
56
56
  .optional(),
57
- public: z
58
- .boolean()
59
- .meta({
60
- description: 'a Gro extension that enables publishing `.well-known/package.json` and `.well-known/src`',
61
- })
62
- .optional(),
63
57
  description: z.string().optional(),
64
58
  motto: z
65
59
  .string()
package/dist/process.d.ts CHANGED
@@ -74,4 +74,8 @@ export interface RestartableProcess {
74
74
  * handling many concurrent `restart` calls gracefully.
75
75
  */
76
76
  export declare const spawn_restartable_process: (command: string, args?: ReadonlyArray<string>, options?: SpawnOptions) => RestartableProcess;
77
+ /**
78
+ * Check if a PID is still running.
79
+ */
80
+ export declare const process_is_pid_running: (pid: number) => boolean;
77
81
  //# sourceMappingURL=process.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/process.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAIxC,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAID,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAI,GAAG,MAAM,UAAU,CAAC,OAAO,aAAa,CAAC,KAAG,OAAO,CAAC,WAAW,CACvD,CAAC;AAE/B,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,OAAO,CAAC,UAAU,CAYpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACzB,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,cAUF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,YAAY,KAAG,MACwC,CAAC;AAEnG;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,GAAG,CAAC,YAAY,CAAa,CAAC;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,YAAY,KAAG,CAAC,MAAM,IAAI,CAWtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,YAAY,KAAG,OAAO,CAAC,WAAW,CAShE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAO,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CACQ,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GACzC,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,MAAM,GAAG,IAAI,EACtF,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,MAAM,GAAG,IAAI,EACtF,eAAc,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,IACtD,KACd,IAYF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,WAAW,KAAG,MAKxD,CAAC;AAGF,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,kBAuBF,CAAC"}
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/process.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAIxC,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAID,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAI,GAAG,MAAM,UAAU,CAAC,OAAO,aAAa,CAAC,KAAG,OAAO,CAAC,WAAW,CACvD,CAAC;AAE/B,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,OAAO,CAAC,UAAU,CAYpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACzB,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,cAUF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,YAAY,KAAG,MACwC,CAAC;AAEnG;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,GAAG,CAAC,YAAY,CAAa,CAAC;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,YAAY,KAAG,CAAC,MAAM,IAAI,CAWtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,YAAY,KAAG,OAAO,CAAC,WAAW,CAShE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAO,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CACQ,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GACzC,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,MAAM,GAAG,IAAI,EACtF,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,MAAM,GAAG,IAAI,EACtF,eAAc,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,uBAAuB,KAAK,IACtD,KACd,IAYF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,WAAW,KAAG,MAKxD,CAAC;AAGF,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,MAAM,EACf,OAAM,aAAa,CAAC,MAAM,CAAM,EAChC,UAAU,YAAY,KACpB,kBAuBF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,OASpD,CAAC"}
package/dist/process.js CHANGED
@@ -146,3 +146,17 @@ export const spawn_restartable_process = (command, args = [], options) => {
146
146
  void restart();
147
147
  return { restart, kill };
148
148
  };
149
+ /**
150
+ * Check if a PID is still running.
151
+ */
152
+ export const process_is_pid_running = (pid) => {
153
+ try {
154
+ // Sending signal 0 doesn't actually send a signal, just checks if process exists
155
+ process.kill(pid, 0);
156
+ return true;
157
+ }
158
+ catch (err) {
159
+ // ESRCH = no such process, EPERM = exists but no permission
160
+ return err.code === 'EPERM';
161
+ }
162
+ };
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_util",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "utility belt for JS",
5
5
  "glyph": "šŸ¦•",
6
6
  "logo": "logo.svg",
7
7
  "logo_alt": "a green sauropod wearing a brown utility belt",
8
8
  "motto": "ancient not extinct",
9
- "public": true,
10
9
  "license": "MIT",
11
10
  "homepage": "https://util.fuz.dev/",
12
11
  "author": {
@@ -26,6 +26,7 @@ import {
26
26
  benchmark_format_table,
27
27
  benchmark_format_table_grouped,
28
28
  benchmark_format_markdown,
29
+ benchmark_format_markdown_grouped,
29
30
  benchmark_format_json,
30
31
  benchmark_format_number,
31
32
  type BenchmarkFormatJsonOptions,
@@ -407,10 +408,27 @@ export class Benchmark {
407
408
 
408
409
  /**
409
410
  * Format results as a Markdown table.
411
+ * @param options - Formatting options (groups for organized output with optional baselines)
410
412
  * @returns Formatted markdown string
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * // Standard table
417
+ * console.log(bench.markdown());
418
+ *
419
+ * // Grouped by category with custom baseline
420
+ * console.log(bench.markdown({
421
+ * groups: [
422
+ * { name: 'Format', filter: (r) => r.name.startsWith('format/'), baseline: 'format/prettier' },
423
+ * { name: 'Parse', filter: (r) => r.name.startsWith('parse/') },
424
+ * ]
425
+ * }));
426
+ * ```
411
427
  */
412
- markdown(): string {
413
- return benchmark_format_markdown(this.#results);
428
+ markdown(options?: BenchmarkFormatTableOptions): string {
429
+ return options?.groups
430
+ ? benchmark_format_markdown_grouped(this.#results, options.groups)
431
+ : benchmark_format_markdown(this.#results);
414
432
  }
415
433
 
416
434
  /**
@@ -431,6 +449,39 @@ export class Benchmark {
431
449
  return [...this.#results];
432
450
  }
433
451
 
452
+ /**
453
+ * Check if the benchmark has been run and has results.
454
+ * @returns True if results are available
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * if (bench.has_results) {
459
+ * console.log(bench.table());
460
+ * }
461
+ * ```
462
+ */
463
+ get has_results(): boolean {
464
+ return this.#results.length > 0;
465
+ }
466
+
467
+ /**
468
+ * Get results as a map for convenient lookup by task name.
469
+ * Returns a new Map each call to prevent external mutation.
470
+ * @returns Map of task name to benchmark result
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * const results_map = bench.results_by_name();
475
+ * const slugify_result = results_map.get('slugify');
476
+ * if (slugify_result) {
477
+ * console.log(`slugify: ${slugify_result.stats.ops_per_second} ops/sec`);
478
+ * }
479
+ * ```
480
+ */
481
+ results_by_name(): Map<string, BenchmarkResult> {
482
+ return new Map(this.#results.map((r) => [r.name, r]));
483
+ }
484
+
434
485
  /**
435
486
  * Reset the benchmark results.
436
487
  * Keeps tasks intact so benchmarks can be rerun.
@@ -18,7 +18,7 @@ import {
18
18
  import {stats_confidence_interval_from_summary} from './stats.js';
19
19
 
20
20
  // Version for forward compatibility - increment when schema changes
21
- const BASELINE_VERSION = 1;
21
+ const BASELINE_VERSION = 2;
22
22
 
23
23
  /**
24
24
  * Schema for a single benchmark entry in the baseline.
@@ -26,7 +26,7 @@ const BASELINE_VERSION = 1;
26
26
  export const BenchmarkBaselineEntry = z.object({
27
27
  name: z.string(),
28
28
  mean_ns: z.number(),
29
- median_ns: z.number(),
29
+ p50_ns: z.number(),
30
30
  std_dev_ns: z.number(),
31
31
  min_ns: z.number(),
32
32
  max_ns: z.number(),
@@ -137,7 +137,7 @@ const results_to_entries = (results: Array<BenchmarkResult>): Array<BenchmarkBas
137
137
  return results.map((r) => ({
138
138
  name: r.name,
139
139
  mean_ns: r.stats.mean_ns,
140
- median_ns: r.stats.median_ns,
140
+ p50_ns: r.stats.p50_ns,
141
141
  std_dev_ns: r.stats.std_dev_ns,
142
142
  min_ns: r.stats.min_ns,
143
143
  max_ns: r.stats.max_ns,
@@ -7,20 +7,24 @@ import {format_number} from './maths.js';
7
7
  * Format results as an ASCII table with percentiles, min/max, and relative performance.
8
8
  * All times use the same unit for easy comparison.
9
9
  * @param results - Array of benchmark results
10
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
10
11
  * @returns Formatted table string with enhanced metrics
11
12
  *
12
13
  * @example
13
14
  * ```ts
14
15
  * console.log(benchmark_format_table(results));
15
- * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
16
- * // │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
17
- * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
18
- * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
19
- * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
20
- * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
16
+ * // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
17
+ * // │ Task Name │ ops/sec │ p50 (μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
18
+ * // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
19
+ * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
20
+ * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
21
+ * // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
21
22
  * ```
22
23
  */
23
- export const benchmark_format_table = (results: Array<BenchmarkResult>): string => {
24
+ export const benchmark_format_table = (
25
+ results: Array<BenchmarkResult>,
26
+ baseline?: string,
27
+ ): string => {
24
28
  if (results.length === 0) return '(no results)';
25
29
 
26
30
  // Detect best unit for all results
@@ -28,8 +32,24 @@ export const benchmark_format_table = (results: Array<BenchmarkResult>): string
28
32
  const unit = time_unit_detect_best(mean_times);
29
33
  const unit_str = TIME_UNIT_DISPLAY[unit];
30
34
 
31
- // Find fastest for relative comparison
32
- const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
35
+ // Find baseline for relative comparison
36
+ let baseline_ops: number;
37
+ let vs_column_header: string;
38
+
39
+ if (baseline !== undefined) {
40
+ const baseline_result = results.find((r) => r.name === baseline);
41
+ if (!baseline_result) {
42
+ const available = results.map((r) => r.name).join(', ');
43
+ throw new Error(
44
+ `Baseline task "${baseline}" not found in results. Available tasks: ${available}`,
45
+ );
46
+ }
47
+ baseline_ops = baseline_result.stats.ops_per_second;
48
+ vs_column_header = `vs ${baseline}`;
49
+ } else {
50
+ baseline_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
51
+ vs_column_header = 'vs Best';
52
+ }
33
53
 
34
54
  const rows: Array<Array<string>> = [];
35
55
 
@@ -37,20 +57,20 @@ export const benchmark_format_table = (results: Array<BenchmarkResult>): string
37
57
  rows.push([
38
58
  'Task Name',
39
59
  'ops/sec',
40
- `median (${unit_str})`,
60
+ `p50 (${unit_str})`,
41
61
  `p75 (${unit_str})`,
42
62
  `p90 (${unit_str})`,
43
63
  `p95 (${unit_str})`,
44
64
  `p99 (${unit_str})`,
45
65
  `min (${unit_str})`,
46
66
  `max (${unit_str})`,
47
- 'vs Best',
67
+ vs_column_header,
48
68
  ]);
49
69
 
50
70
  // Data rows - all use same unit
51
71
  results.forEach((r) => {
52
72
  const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
53
- const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
73
+ const p50 = time_format(r.stats.p50_ns, unit, 2).replace(unit_str, '').trim();
54
74
  const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
55
75
  const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
56
76
  const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
@@ -59,10 +79,10 @@ export const benchmark_format_table = (results: Array<BenchmarkResult>): string
59
79
  const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
60
80
 
61
81
  // Calculate relative performance
62
- const ratio = fastest_ops / r.stats.ops_per_second;
63
- const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
82
+ const ratio = baseline_ops / r.stats.ops_per_second;
83
+ const vs_baseline = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
64
84
 
65
- rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
85
+ rows.push([r.name, ops_sec, p50, p75, p90, p95, p99, min, max, vs_baseline]);
66
86
  });
67
87
 
68
88
  // Calculate column widths (using display width for proper emoji handling)
@@ -107,18 +127,22 @@ export const benchmark_format_table = (results: Array<BenchmarkResult>): string
107
127
  * Format results as a Markdown table with key metrics.
108
128
  * All times use the same unit for easy comparison.
109
129
  * @param results - Array of benchmark results
130
+ * @param baseline - Optional task name to use as baseline for comparison (defaults to fastest)
110
131
  * @returns Formatted markdown table string
111
132
  *
112
133
  * @example
113
134
  * ```ts
114
135
  * console.log(benchmark_format_markdown(results));
115
- * // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
116
- * // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
117
- * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
118
- * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
136
+ * // | Task Name | ops/sec | p50 (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
137
+ * // |------------|------------|----------|----------|----------|----------|----------|----------|----------|----------|
138
+ * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
139
+ * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
119
140
  * ```
120
141
  */
121
- export const benchmark_format_markdown = (results: Array<BenchmarkResult>): string => {
142
+ export const benchmark_format_markdown = (
143
+ results: Array<BenchmarkResult>,
144
+ baseline?: string,
145
+ ): string => {
122
146
  if (results.length === 0) return '(no results)';
123
147
 
124
148
  // Detect best unit for all results
@@ -126,8 +150,24 @@ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): stri
126
150
  const unit = time_unit_detect_best(mean_times);
127
151
  const unit_str = TIME_UNIT_DISPLAY[unit];
128
152
 
129
- // Find fastest for relative comparison
130
- const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
153
+ // Find baseline for relative comparison
154
+ let baseline_ops: number;
155
+ let vs_column_header: string;
156
+
157
+ if (baseline !== undefined) {
158
+ const baseline_result = results.find((r) => r.name === baseline);
159
+ if (!baseline_result) {
160
+ const available = results.map((r) => r.name).join(', ');
161
+ throw new Error(
162
+ `Baseline task "${baseline}" not found in results. Available tasks: ${available}`,
163
+ );
164
+ }
165
+ baseline_ops = baseline_result.stats.ops_per_second;
166
+ vs_column_header = `vs ${baseline}`;
167
+ } else {
168
+ baseline_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
169
+ vs_column_header = 'vs Best';
170
+ }
131
171
 
132
172
  const rows: Array<Array<string>> = [];
133
173
 
@@ -135,20 +175,20 @@ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): stri
135
175
  rows.push([
136
176
  'Task Name',
137
177
  'ops/sec',
138
- `median (${unit_str})`,
178
+ `p50 (${unit_str})`,
139
179
  `p75 (${unit_str})`,
140
180
  `p90 (${unit_str})`,
141
181
  `p95 (${unit_str})`,
142
182
  `p99 (${unit_str})`,
143
183
  `min (${unit_str})`,
144
184
  `max (${unit_str})`,
145
- 'vs Best',
185
+ vs_column_header,
146
186
  ]);
147
187
 
148
188
  // Data rows - all use same unit
149
189
  results.forEach((r) => {
150
190
  const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
151
- const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
191
+ const p50 = time_format(r.stats.p50_ns, unit, 2).replace(unit_str, '').trim();
152
192
  const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
153
193
  const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
154
194
  const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
@@ -157,10 +197,10 @@ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): stri
157
197
  const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
158
198
 
159
199
  // Calculate relative performance
160
- const ratio = fastest_ops / r.stats.ops_per_second;
161
- const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
200
+ const ratio = baseline_ops / r.stats.ops_per_second;
201
+ const vs_baseline = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
162
202
 
163
- rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
203
+ rows.push([r.name, ops_sec, p50, p75, p90, p95, p99, min, max, vs_baseline]);
164
204
  });
165
205
 
166
206
  // Calculate column widths
@@ -196,6 +236,62 @@ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): stri
196
236
  return lines.join('\n');
197
237
  };
198
238
 
239
+ /**
240
+ * Format results as grouped Markdown tables with headers between groups.
241
+ * @param results - Array of benchmark results
242
+ * @param groups - Array of group definitions
243
+ * @returns Formatted markdown string with group headers and tables
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * const groups = [
248
+ * { name: 'Fast Paths', filter: (r) => r.name.includes('fast'), baseline: 'fast/reference' },
249
+ * { name: 'Slow Paths', filter: (r) => r.name.includes('slow') },
250
+ * ];
251
+ * console.log(benchmark_format_markdown_grouped(results, groups));
252
+ * // ### Fast Paths
253
+ * // | Task Name | ops/sec | ... | vs fast/reference |
254
+ * // |-----------|---------|-----|-------------------|
255
+ * // | ... | ... | ... | ... |
256
+ * //
257
+ * // ### Slow Paths
258
+ * // | Task Name | ops/sec | ... | vs Best |
259
+ * // |-----------|---------|-----|---------|
260
+ * // | ... | ... | ... | ... |
261
+ * ```
262
+ */
263
+ export const benchmark_format_markdown_grouped = (
264
+ results: Array<BenchmarkResult>,
265
+ groups: Array<BenchmarkGroup>,
266
+ ): string => {
267
+ if (results.length === 0) return '(no results)';
268
+
269
+ const sections: Array<string> = [];
270
+
271
+ for (const group of groups) {
272
+ const group_results = results.filter(group.filter);
273
+ if (group_results.length === 0) continue;
274
+
275
+ // Add group header and table
276
+ const header = group.description
277
+ ? `### ${group.name}\n\n${group.description}\n`
278
+ : `### ${group.name}\n`;
279
+ sections.push(header);
280
+ sections.push(benchmark_format_markdown(group_results, group.baseline));
281
+ }
282
+
283
+ // Handle ungrouped results (those that don't match any group)
284
+ const grouped_names = new Set(groups.flatMap((g) => results.filter(g.filter).map((r) => r.name)));
285
+ const ungrouped = results.filter((r) => !grouped_names.has(r.name));
286
+
287
+ if (ungrouped.length > 0) {
288
+ sections.push('### Other\n');
289
+ sections.push(benchmark_format_markdown(ungrouped));
290
+ }
291
+
292
+ return sections.join('\n');
293
+ };
294
+
199
295
  export interface BenchmarkFormatJsonOptions {
200
296
  /** Whether to pretty-print (default: true) */
201
297
  pretty?: boolean;
@@ -229,7 +325,7 @@ export const benchmark_format_json = (
229
325
  total_time_ms: r.total_time_ms,
230
326
  ops_per_second: r.stats.ops_per_second,
231
327
  mean_ns: r.stats.mean_ns,
232
- median_ns: r.stats.median_ns,
328
+ p50_ns: r.stats.p50_ns,
233
329
  std_dev_ns: r.stats.std_dev_ns,
234
330
  min_ns: r.stats.min_ns,
235
331
  max_ns: r.stats.max_ns,
@@ -292,7 +388,7 @@ export const benchmark_format_table_grouped = (
292
388
  ? `\nšŸ“¦ ${group.name}\n ${group.description}`
293
389
  : `\nšŸ“¦ ${group.name}`;
294
390
  sections.push(header);
295
- sections.push(benchmark_format_table(group_results));
391
+ sections.push(benchmark_format_table(group_results, group.baseline));
296
392
  }
297
393
 
298
394
  // Handle ungrouped results (those that don't match any group)
@@ -72,8 +72,8 @@ export interface BenchmarkCompareOptions {
72
72
  export class BenchmarkStats {
73
73
  /** Mean (average) time in nanoseconds */
74
74
  readonly mean_ns: number;
75
- /** Median time in nanoseconds */
76
- readonly median_ns: number;
75
+ /** 50th percentile (median) time in nanoseconds */
76
+ readonly p50_ns: number;
77
77
  /** Standard deviation in nanoseconds */
78
78
  readonly std_dev_ns: number;
79
79
  /** Minimum time in nanoseconds */
@@ -124,7 +124,7 @@ export class BenchmarkStats {
124
124
  // If no valid timings, return empty stats
125
125
  if (valid_timings.length === 0) {
126
126
  this.mean_ns = NaN;
127
- this.median_ns = NaN;
127
+ this.p50_ns = NaN;
128
128
  this.std_dev_ns = NaN;
129
129
  this.min_ns = NaN;
130
130
  this.max_ns = NaN;
@@ -151,7 +151,7 @@ export class BenchmarkStats {
151
151
 
152
152
  // Calculate statistics on cleaned data
153
153
  this.mean_ns = stats_mean(cleaned);
154
- this.median_ns = stats_median(sorted_cleaned);
154
+ this.p50_ns = stats_median(sorted_cleaned);
155
155
  this.std_dev_ns = stats_std_dev(cleaned, this.mean_ns);
156
156
 
157
157
  const {min, max} = stats_min_max(sorted_cleaned);
@@ -194,4 +194,11 @@ export interface BenchmarkGroup {
194
194
 
195
195
  /** Filter function to determine which results belong to this group */
196
196
  filter: (result: BenchmarkResult) => boolean;
197
+
198
+ /**
199
+ * Task name to use as baseline for the "vs" column.
200
+ * When specified, ratios are computed against this task instead of the fastest.
201
+ * If the baseline task is not found in the group, falls back to "vs Best" with a warning.
202
+ */
203
+ baseline?: string;
197
204
  }
@@ -71,13 +71,6 @@ export const PackageJson = z.looseObject({
71
71
  .boolean()
72
72
  .meta({description: 'disallow publishing to the configured registry'})
73
73
  .optional(),
74
- public: z
75
- .boolean()
76
- .meta({
77
- description:
78
- 'a Gro extension that enables publishing `.well-known/package.json` and `.well-known/src`',
79
- })
80
- .optional(),
81
74
  description: z.string().optional(),
82
75
  motto: z
83
76
  .string()
@@ -205,3 +205,17 @@ export const spawn_restartable_process = (
205
205
  void restart();
206
206
  return {restart, kill};
207
207
  };
208
+
209
+ /**
210
+ * Check if a PID is still running.
211
+ */
212
+ export const process_is_pid_running = (pid: number): boolean => {
213
+ try {
214
+ // Sending signal 0 doesn't actually send a signal, just checks if process exists
215
+ process.kill(pid, 0);
216
+ return true;
217
+ } catch (err: any) {
218
+ // ESRCH = no such process, EPERM = exists but no permission
219
+ return err.code === 'EPERM';
220
+ }
221
+ };