From 9964169b9cd2daed7bf815737d5bdfa26206e802 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Thu, 8 Jan 2026 14:06:06 +0100 Subject: [PATCH 01/10] fix: include package name for duplicate bench names [#264] * add test output --- .../go_fiber_duplicate_names_output.txt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/data/extract/go_fiber_duplicate_names_output.txt diff --git a/test/data/extract/go_fiber_duplicate_names_output.txt b/test/data/extract/go_fiber_duplicate_names_output.txt new file mode 100644 index 000000000..d686b064c --- /dev/null +++ b/test/data/extract/go_fiber_duplicate_names_output.txt @@ -0,0 +1,27 @@ +PASS +ok github.com/gofiber/fiber/v3/log 0.173s +PASS +ok github.com/gofiber/fiber/v3/middleware/adaptor 0.184s +PASS +ok github.com/gofiber/fiber/v3/middleware/basicauth 0.173s +goos: darwin +goarch: arm64 +pkg: github.com/gofiber/fiber/v3/middleware/cache +BenchmarkAppendMsgitem +BenchmarkAppendMsgitem-12 63634455 19.01 ns/op 3103.57 MB/s 0 B/op 0 allocs/op +BenchmarkAppendMsgitem-12 66411781 18.42 ns/op 3202.97 MB/s 0 B/op 0 allocs/op +PASS +ok github.com/gofiber/fiber/v3/middleware/cache 2.649s +PASS +ok github.com/gofiber/fiber/v3/middleware/compress 0.219s +PASS +ok github.com/gofiber/fiber/v3/middleware/cors 0.188s +goos: darwin +goarch: arm64 +pkg: github.com/gofiber/fiber/v3/middleware/csrf +BenchmarkAppendMsgitem +BenchmarkAppendMsgitem-12 1000000000 0.2926 ns/op 3417.23 MB/s 0 B/op 0 allocs/op +BenchmarkAppendMsgitem-12 1000000000 0.2883 ns/op 3468.54 MB/s 0 B/op 0 allocs/op +PASS +ok github.com/gofiber/fiber/v3/middleware/csrf 0.842s +PASS From c4a19323cdf1698661f221e7d7d7b02a172978c9 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 21:18:09 +0100 Subject: [PATCH 02/10] fix: include package name for duplicate bench names [#264] * add test file with a snapshot * add test output --- test/__snapshots__/extract.spec.ts.snap | 157 ++++++++++++++++++ .../go_fiber_duplicate_names_output.txt | 20 +-- test/extract.spec.ts | 4 + 3 files changed, 171 insertions(+), 10 deletions(-) diff --git a/test/__snapshots__/extract.spec.ts.snap b/test/__snapshots__/extract.spec.ts.snap index 720bfa5f5..6666d6549 100644 --- a/test/__snapshots__/extract.spec.ts.snap +++ b/test/__snapshots__/extract.spec.ts.snap @@ -524,6 +524,163 @@ exports[`extractResult() extracts benchmark output from customSmallerIsBetter - } `; +exports[`extractResult() extracts benchmark output from go - go_fiber_duplicate_names_output.txt 1`] = ` +{ + "benches": [ + { + "extra": "21228057 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache)", + "unit": "ns/op 2833.37 MB/s 0 B/op 0 allocs/op", + "value": 55.76, + }, + { + "extra": "21228057 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache) - ns/op", + "unit": "ns/op", + "value": 55.76, + }, + { + "extra": "21228057 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache) - MB/s", + "unit": "MB/s", + "value": 2833.37, + }, + { + "extra": "21228057 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache) - B/op", + "unit": "B/op", + "value": 0, + }, + { + "extra": "21228057 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache) - allocs/op", + "unit": "allocs/op", + "value": 0, + }, + { + "extra": "6855655 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/cache)", + "unit": "ns/op 907.83 MB/s 0 B/op 0 allocs/op", + "value": 174, + }, + { + "extra": "6855655 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/cache) - ns/op", + "unit": "ns/op", + "value": 174, + }, + { + "extra": "6855655 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/cache) - MB/s", + "unit": "MB/s", + "value": 907.83, + }, + { + "extra": "6855655 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/cache) - B/op", + "unit": "B/op", + "value": 0, + }, + { + "extra": "6855655 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/cache) - allocs/op", + "unit": "allocs/op", + "value": 0, + }, + { + "extra": "1000000000 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf)", + "unit": "ns/op 2678.46 MB/s 0 B/op 0 allocs/op", + "value": 0.3733, + }, + { + "extra": "1000000000 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf) - ns/op", + "unit": "ns/op", + "value": 0.3733, + }, + { + "extra": "1000000000 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf) - MB/s", + "unit": "MB/s", + "value": 2678.46, + }, + { + "extra": "1000000000 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf) - B/op", + "unit": "B/op", + "value": 0, + }, + { + "extra": "1000000000 times +4 procs", + "name": "BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf) - allocs/op", + "unit": "allocs/op", + "value": 0, + }, + { + "extra": "275056135 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/csrf)", + "unit": "ns/op 229.35 MB/s 0 B/op 0 allocs/op", + "value": 4.36, + }, + { + "extra": "275056135 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/csrf) - ns/op", + "unit": "ns/op", + "value": 4.36, + }, + { + "extra": "275056135 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/csrf) - MB/s", + "unit": "MB/s", + "value": 229.35, + }, + { + "extra": "275056135 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/csrf) - B/op", + "unit": "B/op", + "value": 0, + }, + { + "extra": "275056135 times +4 procs", + "name": "BenchmarkUnmarshalitem (github.com/gofiber/fiber/v3/middleware/csrf) - allocs/op", + "unit": "allocs/op", + "value": 0, + }, + ], + "commit": { + "author": null, + "committer": null, + "id": "123456789abcdef", + "message": "this is dummy", + "timestamp": "dummy timestamp", + "url": "https://github.com/dummy/repo", + }, + "date": 1712131503296, + "tool": "go", +} +`; + exports[`extractResult() extracts benchmark output from go - go_fiber_output.txt 1`] = ` { "benches": [ diff --git a/test/data/extract/go_fiber_duplicate_names_output.txt b/test/data/extract/go_fiber_duplicate_names_output.txt index d686b064c..343e04d37 100644 --- a/test/data/extract/go_fiber_duplicate_names_output.txt +++ b/test/data/extract/go_fiber_duplicate_names_output.txt @@ -4,24 +4,24 @@ PASS ok github.com/gofiber/fiber/v3/middleware/adaptor 0.184s PASS ok github.com/gofiber/fiber/v3/middleware/basicauth 0.173s -goos: darwin -goarch: arm64 +goos: linux +goarch: amd64 pkg: github.com/gofiber/fiber/v3/middleware/cache -BenchmarkAppendMsgitem -BenchmarkAppendMsgitem-12 63634455 19.01 ns/op 3103.57 MB/s 0 B/op 0 allocs/op -BenchmarkAppendMsgitem-12 66411781 18.42 ns/op 3202.97 MB/s 0 B/op 0 allocs/op +cpu: AMD EPYC 7763 64-Core Processor +BenchmarkAppendMsgitem-4 21228057 55.76 ns/op 2833.37 MB/s 0 B/op 0 allocs/op +BenchmarkUnmarshalitem-4 6855655 174.0 ns/op 907.83 MB/s 0 B/op 0 allocs/op PASS ok github.com/gofiber/fiber/v3/middleware/cache 2.649s PASS ok github.com/gofiber/fiber/v3/middleware/compress 0.219s PASS ok github.com/gofiber/fiber/v3/middleware/cors 0.188s -goos: darwin -goarch: arm64 +goos: linux +goarch: amd64 pkg: github.com/gofiber/fiber/v3/middleware/csrf -BenchmarkAppendMsgitem -BenchmarkAppendMsgitem-12 1000000000 0.2926 ns/op 3417.23 MB/s 0 B/op 0 allocs/op -BenchmarkAppendMsgitem-12 1000000000 0.2883 ns/op 3468.54 MB/s 0 B/op 0 allocs/op +cpu: AMD EPYC 7763 64-Core Processor +BenchmarkAppendMsgitem-4 1000000000 0.3733 ns/op 2678.46 MB/s 0 B/op 0 allocs/op +BenchmarkUnmarshalitem-4 275056135 4.360 ns/op 229.35 MB/s 0 B/op 0 allocs/op PASS ok github.com/gofiber/fiber/v3/middleware/csrf 0.842s PASS diff --git a/test/extract.spec.ts b/test/extract.spec.ts index f07a81d53..8184b4a8d 100644 --- a/test/extract.spec.ts +++ b/test/extract.spec.ts @@ -95,6 +95,10 @@ describe('extractResult()', function () { tool: 'go', file: 'go_fiber_output.txt', }, + { + tool: 'go', + file: 'go_fiber_duplicate_names_output.txt', + }, { tool: 'benchmarkjs', file: 'benchmarkjs_output.txt', From 177bfcb90d0e58f86c749083857037cdc064fa98 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 21:22:15 +0100 Subject: [PATCH 03/10] fix: include package name for duplicate bench names [#264] * add test for backwards compatibility * add test output --- test/__snapshots__/extract.spec.ts.snap | 31 +++++++++++++++++++ .../data/extract/go_single_package_output.txt | 9 ++++++ test/extract.spec.ts | 4 +++ 3 files changed, 44 insertions(+) create mode 100644 test/data/extract/go_single_package_output.txt diff --git a/test/__snapshots__/extract.spec.ts.snap b/test/__snapshots__/extract.spec.ts.snap index 6666d6549..9f54fb7f8 100644 --- a/test/__snapshots__/extract.spec.ts.snap +++ b/test/__snapshots__/extract.spec.ts.snap @@ -963,6 +963,37 @@ exports[`extractResult() extracts benchmark output from go - go_output.txt 1`] = } `; +exports[`extractResult() extracts benchmark output from go - go_single_package_output.txt 1`] = ` +{ + "benches": [ + { + "extra": "5000000 times +8 procs", + "name": "BenchmarkFib10", + "unit": "ns/op", + "value": 325, + }, + { + "extra": "30000 times +8 procs", + "name": "BenchmarkFib20", + "unit": "ns/op", + "value": 40537, + }, + ], + "commit": { + "author": null, + "committer": null, + "id": "123456789abcdef", + "message": "this is dummy", + "timestamp": "dummy timestamp", + "url": "https://github.com/dummy/repo", + }, + "date": 1712131503296, + "tool": "go", +} +`; + exports[`extractResult() extracts benchmark output from googlecpp - googlecpp_output.json 1`] = ` { "benches": [ diff --git a/test/data/extract/go_single_package_output.txt b/test/data/extract/go_single_package_output.txt new file mode 100644 index 000000000..d276db6ef --- /dev/null +++ b/test/data/extract/go_single_package_output.txt @@ -0,0 +1,9 @@ +goos: darwin +goarch: arm64 +pkg: github.com/example/mypackage +BenchmarkFib10 +BenchmarkFib10-8 5000000 325 ns/op +BenchmarkFib20 +BenchmarkFib20-8 30000 40537 ns/op +PASS +ok github.com/example/mypackage 3.614s diff --git a/test/extract.spec.ts b/test/extract.spec.ts index 8184b4a8d..631b64f43 100644 --- a/test/extract.spec.ts +++ b/test/extract.spec.ts @@ -99,6 +99,10 @@ describe('extractResult()', function () { tool: 'go', file: 'go_fiber_duplicate_names_output.txt', }, + { + tool: 'go', + file: 'go_single_package_output.txt', + }, { tool: 'benchmarkjs', file: 'benchmarkjs_output.txt', From b92ae9032819fdc6e0d42ae197e594185f5e1828 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 21:27:08 +0100 Subject: [PATCH 04/10] fix: include package name for duplicate bench names [#264] * initial implementation * add test output --- package.json | 1 + src/extract.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a2c9472cb..fe40e2973 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "format": "prettier -w **", "format:check": "prettier -c **", "test": "jest", + "test-s": "jest --silent", "test:watch": "jest --watch", "coverage": "jest --coverage", "coverage:open": "jest --coverage && open ./coverage/lcov-report/index.html" diff --git a/src/extract.ts b/src/extract.ts index 60736e61c..163504f13 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -359,7 +359,28 @@ function extractGoResult(output: string): BenchmarkResult[] { const reExtract = /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; + // First pass: detect all unique packages + // This is used to determine if we need to append package names to benchmark names for disambiguation + const packageRegex = /^pkg:\s+(.+)$/; + const packages = new Set(); for (const line of lines) { + const pkgMatch = line.match(packageRegex); + if (pkgMatch) { + packages.add(pkgMatch[1]); + } + } + const hasMultiplePackages = packages.size > 1; + + // Second pass: extract benchmarks with package context + let currentPackage = ''; + for (const line of lines) { + // Track current package from "pkg:" lines + const pkgMatch = line.match(packageRegex); + if (pkgMatch) { + currentPackage = pkgMatch[1]; + continue; + } + const m = line.match(reExtract); if (m?.groups) { const procs = m.groups.procs !== undefined ? m.groups.procs.slice(1) : null; @@ -374,6 +395,10 @@ function extractGoResult(output: string): BenchmarkResult[] { pieces.unshift(pieces[0], remainder.slice(remainder.indexOf(pieces[1]))); } + // Build base benchmark name with optional package suffix for disambiguation + const baseName = + hasMultiplePackages && currentPackage ? `${m.groups.name} (${currentPackage})` : m.groups.name; + for (let i = 0; i < pieces.length; i = i + 2) { let extra = `${times} times`.replace(/\s\s+/g, ' '); if (procs !== null) { @@ -383,9 +408,9 @@ function extractGoResult(output: string): BenchmarkResult[] { const unit = pieces[i + 1]; let name; if (i > 0) { - name = m.groups.name + ' - ' + unit; + name = baseName + ' - ' + unit; } else { - name = m.groups.name; + name = baseName; } ret.push({ name, value, unit, extra }); } From b4980ac91af4e93414baef5ec0d8c41a8e2070b4 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 21:43:15 +0100 Subject: [PATCH 05/10] fix: include package name for duplicate bench names [#264] * add more granular tests --- src/extract.ts | 2 +- test/extractGoResult.spec.ts | 254 +++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 test/extractGoResult.spec.ts diff --git a/src/extract.ts b/src/extract.ts index 163504f13..f36eed6c2 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -343,7 +343,7 @@ function extractCargoResult(output: string): BenchmarkResult[] { return ret; } -function extractGoResult(output: string): BenchmarkResult[] { +export function extractGoResult(output: string): BenchmarkResult[] { const lines = output.split(/\r?\n/g); const ret = []; // Example: diff --git a/test/extractGoResult.spec.ts b/test/extractGoResult.spec.ts new file mode 100644 index 000000000..f2e2a9eae --- /dev/null +++ b/test/extractGoResult.spec.ts @@ -0,0 +1,254 @@ +import { extractGoResult } from '../src/extract'; +import dedent from 'dedent'; + +describe('extractGoResult()', () => { + describe('basic benchmark extraction', () => { + it('extracts a simple benchmark result', () => { + const output = `BenchmarkFib10-8 5000000 325 ns/op`; + + const results = extractGoResult(output); + + expect(results).toHaveLength(1); + expect(results[0]).toEqual({ + name: 'BenchmarkFib10', + value: 325, + unit: 'ns/op', + extra: '5000000 times\n8 procs', + }); + }); + + it('extracts benchmark without processor count', () => { + const output = `BenchmarkFib10 5000000 325 ns/op`; + + const results = extractGoResult(output); + + expect(results).toHaveLength(1); + expect(results[0]).toEqual({ + name: 'BenchmarkFib10', + value: 325, + unit: 'ns/op', + extra: '5000000 times', + }); + }); + + it('extracts multiple benchmarks', () => { + const output = dedent` + BenchmarkFib10-8 5000000 325 ns/op + BenchmarkFib20-8 30000 40537 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFib10'); + expect(results[1].name).toBe('BenchmarkFib20'); + }); + + it('handles benchmarks with special characters in name', () => { + const output = `BenchmarkFib/my/tabled/benchmark_-_20,var1=13,var2=14-8 5000000 325 ns/op`; + + const results = extractGoResult(output); + + expect(results).toHaveLength(1); + expect(results[0].name).toBe('BenchmarkFib/my/tabled/benchmark_-_20,var1=13,var2=14'); + }); + }); + + describe('multiple metrics per benchmark', () => { + it('extracts all metrics from a benchmark with multiple values', () => { + const output = `BenchmarkAlloc-8 1000000 1024 ns/op 512 B/op 8 allocs/op`; + + const results = extractGoResult(output); + + expect(results).toHaveLength(4); + // First entry is the combined metrics (backward compatibility) + expect(results[0].name).toBe('BenchmarkAlloc'); + expect(results[0].unit).toContain('ns/op'); + // Second entry is ns/op metric + expect(results[1].name).toBe('BenchmarkAlloc - ns/op'); + expect(results[1].value).toBe(1024); + expect(results[1].unit).toBe('ns/op'); + // Third entry is B/op metric + expect(results[2].name).toBe('BenchmarkAlloc - B/op'); + expect(results[2].value).toBe(512); + expect(results[2].unit).toBe('B/op'); + // Fourth entry is allocs/op metric + expect(results[3].name).toBe('BenchmarkAlloc - allocs/op'); + expect(results[3].value).toBe(8); + expect(results[3].unit).toBe('allocs/op'); + }); + }); + + describe('single package (backward compatibility)', () => { + it('does not add package suffix when only one package exists', () => { + const output = dedent` + goos: darwin + goarch: arm64 + pkg: github.com/example/mypackage + BenchmarkFib10 + BenchmarkFib10-8 5000000 325 ns/op + BenchmarkFib20 + BenchmarkFib20-8 30000 40537 ns/op + PASS + ok github.com/example/mypackage 3.614s + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFib10'); + expect(results[1].name).toBe('BenchmarkFib20'); + }); + + it('does not add package suffix when no pkg lines exist', () => { + const output = dedent` + BenchmarkFib10-8 5000000 325 ns/op + BenchmarkFib20-8 30000 40537 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFib10'); + expect(results[1].name).toBe('BenchmarkFib20'); + }); + }); + + describe('multiple packages (issue #264)', () => { + it('adds package suffix when multiple packages have benchmarks', () => { + const output = dedent` + goos: darwin + goarch: arm64 + pkg: github.com/example/package1 + BenchmarkFoo + BenchmarkFoo-8 5000000 100 ns/op + pkg: github.com/example/package2 + BenchmarkBar + BenchmarkBar-8 3000000 200 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFoo (github.com/example/package1)'); + expect(results[1].name).toBe('BenchmarkBar (github.com/example/package2)'); + }); + + it('disambiguates benchmarks with the same name in different packages', () => { + const output = dedent` + goos: darwin + goarch: arm64 + pkg: github.com/gofiber/fiber/v3/middleware/cache + BenchmarkAppendMsgitem + BenchmarkAppendMsgitem-12 63634455 19.01 ns/op + pkg: github.com/gofiber/fiber/v3/middleware/csrf + BenchmarkAppendMsgitem + BenchmarkAppendMsgitem-12 1000000000 0.2926 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/cache)'); + expect(results[0].value).toBe(19.01); + expect(results[1].name).toBe('BenchmarkAppendMsgitem (github.com/gofiber/fiber/v3/middleware/csrf)'); + expect(results[1].value).toBe(0.2926); + }); + + it('applies package suffix to all metrics when multiple packages exist', () => { + const output = dedent` + pkg: github.com/example/pkg1 + BenchmarkAlloc-8 1000000 100 ns/op 512 B/op 4 allocs/op + pkg: github.com/example/pkg2 + BenchmarkAlloc-8 2000000 200 ns/op 256 B/op 2 allocs/op + `; + + const results = extractGoResult(output); + + // Each benchmark produces 4 entries (combined + ns/op + B/op + allocs/op) + expect(results).toHaveLength(8); + + // First package benchmarks + expect(results[0].name).toBe('BenchmarkAlloc (github.com/example/pkg1)'); + expect(results[1].name).toBe('BenchmarkAlloc (github.com/example/pkg1) - ns/op'); + expect(results[2].name).toBe('BenchmarkAlloc (github.com/example/pkg1) - B/op'); + expect(results[3].name).toBe('BenchmarkAlloc (github.com/example/pkg1) - allocs/op'); + + // Second package benchmarks + expect(results[4].name).toBe('BenchmarkAlloc (github.com/example/pkg2)'); + expect(results[5].name).toBe('BenchmarkAlloc (github.com/example/pkg2) - ns/op'); + expect(results[6].name).toBe('BenchmarkAlloc (github.com/example/pkg2) - B/op'); + expect(results[7].name).toBe('BenchmarkAlloc (github.com/example/pkg2) - allocs/op'); + }); + }); + + describe('edge cases', () => { + it('handles benchmarks before any pkg line in multi-package output', () => { + const output = dedent` + BenchmarkOrphan-8 1000000 50 ns/op + pkg: github.com/example/pkg1 + BenchmarkFoo-8 5000000 100 ns/op + pkg: github.com/example/pkg2 + BenchmarkBar-8 3000000 200 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(3); + // Orphan benchmark has no package context, so no suffix even though multiple packages exist + expect(results[0].name).toBe('BenchmarkOrphan'); + expect(results[1].name).toBe('BenchmarkFoo (github.com/example/pkg1)'); + expect(results[2].name).toBe('BenchmarkBar (github.com/example/pkg2)'); + }); + + it('returns empty array for output with no benchmarks', () => { + const output = dedent` + goos: darwin + goarch: arm64 + PASS + ok github.com/example/mypackage 0.001s + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(0); + }); + + it('returns empty array for empty input', () => { + const results = extractGoResult(''); + + expect(results).toHaveLength(0); + }); + + it('handles Windows line endings', () => { + const output = + 'pkg: github.com/example/pkg1\r\n' + + 'BenchmarkFoo-8 5000000 100 ns/op\r\n' + + 'pkg: github.com/example/pkg2\r\n' + + 'BenchmarkBar-8 3000000 200 ns/op'; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFoo (github.com/example/pkg1)'); + expect(results[1].name).toBe('BenchmarkBar (github.com/example/pkg2)'); + }); + + it('handles multiple benchmarks within the same package', () => { + const output = dedent` + pkg: github.com/example/pkg1 + BenchmarkFoo-8 5000000 100 ns/op + BenchmarkBar-8 3000000 150 ns/op + pkg: github.com/example/pkg2 + BenchmarkBaz-8 2000000 200 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(3); + expect(results[0].name).toBe('BenchmarkFoo (github.com/example/pkg1)'); + expect(results[1].name).toBe('BenchmarkBar (github.com/example/pkg1)'); + expect(results[2].name).toBe('BenchmarkBaz (github.com/example/pkg2)'); + }); + }); +}); From 9e2838db76da7cfd564e7a0393574362c9da6441 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 22:27:36 +0100 Subject: [PATCH 06/10] refactor: add chunkPairs helper for cleaner Go result extraction Replace index arithmetic (i * 2, i * 2 + 1) with explicit chunking using a purpose-built chunkPairs function. This makes the [value, unit] pair structure obvious through destructuring and isolates the index math. --- src/extract.ts | 90 ++++++++++++------------------------ test/extractGoResult.spec.ts | 2 +- 2 files changed, 31 insertions(+), 61 deletions(-) diff --git a/src/extract.ts b/src/extract.ts index f36eed6c2..69d7c73db 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -344,48 +344,27 @@ function extractCargoResult(output: string): BenchmarkResult[] { } export function extractGoResult(output: string): BenchmarkResult[] { - const lines = output.split(/\r?\n/g); - const ret = []; - // Example: - // BenchmarkFib20-8 30000 41653 ns/op - // BenchmarkDoWithConfigurer1-8 30000000 42.3 ns/op - - // Example if someone has used the ReportMetric function to add additional metrics to each benchmark: - // BenchmarkThing-16 1 95258906556 ns/op 64.02 UnitsForMeasure2 31.13 UnitsForMeasure3 - - // reference, "Proposal: Go Benchmark Data Format": https://go.googlesource.com/proposal/+/master/design/14313-benchmark-format.md - // "A benchmark result line has the general form: [ ...]" - // "The fields are separated by runs of space characters (as defined by unicode.IsSpace), so the line can be parsed with strings.Fields. The line must have an even number of fields, and at least four." - const reExtract = + const benchmarkRegex = /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; - // First pass: detect all unique packages - // This is used to determine if we need to append package names to benchmark names for disambiguation - const packageRegex = /^pkg:\s+(.+)$/; - const packages = new Set(); - for (const line of lines) { - const pkgMatch = line.match(packageRegex); - if (pkgMatch) { - packages.add(pkgMatch[1]); - } - } - const hasMultiplePackages = packages.size > 1; + // Split into sections by "pkg:" lines, keeping package name with each section + const sections = output.split(/^pkg:\s+/m).map((section, index) => { + if (index === 0) return { pkg: '', lines: section.split(/\r?\n/g) }; + const [pkg, ...rest] = section.split(/\r?\n/g); + return { pkg, lines: rest }; + }); - // Second pass: extract benchmarks with package context - let currentPackage = ''; - for (const line of lines) { - // Track current package from "pkg:" lines - const pkgMatch = line.match(packageRegex); - if (pkgMatch) { - currentPackage = pkgMatch[1]; - continue; - } + const hasMultiplePackages = sections.filter((s) => s.pkg).length > 1; - const m = line.match(reExtract); - if (m?.groups) { - const procs = m.groups.procs !== undefined ? m.groups.procs.slice(1) : null; - const times = m.groups.times; - const remainder = m.groups.remainder; + // Process each section and flatten results + return sections.flatMap(({ pkg, lines }) => + lines.flatMap((line) => { + const match = line.match(benchmarkRegex); + if (!match?.groups) return []; + + const { name, procs: procsRaw, times, remainder } = match.groups; + const procs = procsRaw?.slice(1) ?? null; + const extra = procs !== null ? `${times} times\n${procs} procs` : `${times} times`; const pieces = remainder.split(/[ \t]+/); @@ -395,29 +374,20 @@ export function extractGoResult(output: string): BenchmarkResult[] { pieces.unshift(pieces[0], remainder.slice(remainder.indexOf(pieces[1]))); } - // Build base benchmark name with optional package suffix for disambiguation - const baseName = - hasMultiplePackages && currentPackage ? `${m.groups.name} (${currentPackage})` : m.groups.name; - - for (let i = 0; i < pieces.length; i = i + 2) { - let extra = `${times} times`.replace(/\s\s+/g, ' '); - if (procs !== null) { - extra += `\n${procs} procs`; - } - const value = parseFloat(pieces[i]); - const unit = pieces[i + 1]; - let name; - if (i > 0) { - name = baseName + ' - ' + unit; - } else { - name = baseName; - } - ret.push({ name, value, unit, extra }); - } - } - } + const baseName = hasMultiplePackages && pkg ? `${name} (${pkg})` : name; + // Chunk into [value, unit] pairs and map to results + return chunkPairs(pieces).map(([valueStr, unit], i) => ({ + name: i > 0 ? `${baseName} - ${unit}` : baseName, + value: parseFloat(valueStr), + unit, + extra, + })); + }), + ); +} - return ret; +function chunkPairs(arr: string[]): Array<[string, string]> { + return Array.from({ length: Math.floor(arr.length / 2) }, (_, i) => [arr[i * 2], arr[i * 2 + 1]]); } function extractBenchmarkJsResult(output: string): BenchmarkResult[] { diff --git a/test/extractGoResult.spec.ts b/test/extractGoResult.spec.ts index f2e2a9eae..59f4efd67 100644 --- a/test/extractGoResult.spec.ts +++ b/test/extractGoResult.spec.ts @@ -1,5 +1,5 @@ -import { extractGoResult } from '../src/extract'; import dedent from 'dedent'; +import { extractGoResult } from '../src/extract'; describe('extractGoResult()', () => { describe('basic benchmark extraction', () => { From 306885425b58067d3214d5ef103deb77aa6cd995 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 22:31:59 +0100 Subject: [PATCH 07/10] get closer to original structure --- src/extract.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/extract.ts b/src/extract.ts index 69d7c73db..7cb9687db 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -344,9 +344,6 @@ function extractCargoResult(output: string): BenchmarkResult[] { } export function extractGoResult(output: string): BenchmarkResult[] { - const benchmarkRegex = - /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; - // Split into sections by "pkg:" lines, keeping package name with each section const sections = output.split(/^pkg:\s+/m).map((section, index) => { if (index === 0) return { pkg: '', lines: section.split(/\r?\n/g) }; @@ -356,10 +353,23 @@ export function extractGoResult(output: string): BenchmarkResult[] { const hasMultiplePackages = sections.filter((s) => s.pkg).length > 1; + // Example: + // BenchmarkFib20-8 30000 41653 ns/op + // BenchmarkDoWithConfigurer1-8 30000000 42.3 ns/op + + // Example if someone has used the ReportMetric function to add additional metrics to each benchmark: + // BenchmarkThing-16 1 95258906556 ns/op 64.02 UnitsForMeasure2 31.13 UnitsForMeasure3 + + // reference, "Proposal: Go Benchmark Data Format": https://go.googlesource.com/proposal/+/master/design/14313-benchmark-format.md + // "A benchmark result line has the general form: [ ...]" + // "The fields are separated by runs of space characters (as defined by unicode.IsSpace), so the line can be parsed with strings.Fields. The line must have an even number of fields, and at least four." + const reExtractRegexp = + /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; + // Process each section and flatten results return sections.flatMap(({ pkg, lines }) => lines.flatMap((line) => { - const match = line.match(benchmarkRegex); + const match = line.match(reExtractRegexp); if (!match?.groups) return []; const { name, procs: procsRaw, times, remainder } = match.groups; From a7f001dd9bfc99a63cfb4645db77bb4097ba0493 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 22:35:48 +0100 Subject: [PATCH 08/10] use chained flatMap instead of nested --- src/extract.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/extract.ts b/src/extract.ts index 7cb9687db..6876752c4 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -366,9 +366,10 @@ export function extractGoResult(output: string): BenchmarkResult[] { const reExtractRegexp = /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; - // Process each section and flatten results - return sections.flatMap(({ pkg, lines }) => - lines.flatMap((line) => { + // Flatten sections into lines with package context, then process each line + return sections + .flatMap(({ pkg, lines }) => lines.map((line) => ({ pkg, line }))) + .flatMap(({ pkg, line }) => { const match = line.match(reExtractRegexp); if (!match?.groups) return []; @@ -392,8 +393,7 @@ export function extractGoResult(output: string): BenchmarkResult[] { unit, extra, })); - }), - ); + }); } function chunkPairs(arr: string[]): Array<[string, string]> { From 61241b4ea8dba866d5f9c97db6fa38ae743f77e2 Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Fri, 30 Jan 2026 22:46:51 +0100 Subject: [PATCH 09/10] disable doc string pre merge check --- .coderabbit.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..4ef516b11 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,4 @@ +reviews: + pre_merge_checks: + docstrings: + mode: "off" From c68fc65dcf7676eb8497e35785a9302e3d6c002b Mon Sep 17 00:00:00 2001 From: Chris Trzesniewski Date: Thu, 5 Feb 2026 13:40:52 +0100 Subject: [PATCH 10/10] fix: improve package uniqueness detection and add tests for edge cases --- src/extract.ts | 3 ++- test/extractGoResult.spec.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/extract.ts b/src/extract.ts index 6876752c4..dbac3e46c 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -351,7 +351,8 @@ export function extractGoResult(output: string): BenchmarkResult[] { return { pkg, lines: rest }; }); - const hasMultiplePackages = sections.filter((s) => s.pkg).length > 1; + const uniquePackages = new Set(sections.map((s) => s.pkg).filter(Boolean)); + const hasMultiplePackages = uniquePackages.size > 1; // Example: // BenchmarkFib20-8 30000 41653 ns/op diff --git a/test/extractGoResult.spec.ts b/test/extractGoResult.spec.ts index 59f4efd67..7f73ae9db 100644 --- a/test/extractGoResult.spec.ts +++ b/test/extractGoResult.spec.ts @@ -250,5 +250,41 @@ describe('extractGoResult()', () => { expect(results[1].name).toBe('BenchmarkBar (github.com/example/pkg1)'); expect(results[2].name).toBe('BenchmarkBaz (github.com/example/pkg2)'); }); + + it('does not add package suffix when same package appears multiple times', () => { + const output = dedent` + pkg: github.com/example/pkg1 + BenchmarkFoo-8 5000000 100 ns/op + pkg: github.com/example/pkg1 + BenchmarkBar-8 3000000 200 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('BenchmarkFoo'); + expect(results[1].name).toBe('BenchmarkBar'); + }); + + it('adds package suffix when multiple packages exist even with duplicate sections', () => { + const output = dedent` + pkg: github.com/example/pkg1 + BenchmarkFoo-8 5000000 100 ns/op + pkg: github.com/example/pkg2 + BenchmarkBar-8 3000000 200 ns/op + pkg: github.com/example/pkg1 + BenchmarkBaz-8 2000000 300 ns/op + pkg: github.com/example/pkg2 + BenchmarkFoo-8 2000000 400 ns/op + `; + + const results = extractGoResult(output); + + expect(results).toHaveLength(4); + expect(results[0].name).toBe('BenchmarkFoo (github.com/example/pkg1)'); + expect(results[1].name).toBe('BenchmarkBar (github.com/example/pkg2)'); + expect(results[2].name).toBe('BenchmarkBaz (github.com/example/pkg1)'); + expect(results[3].name).toBe('BenchmarkFoo (github.com/example/pkg2)'); + }); }); });