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" 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..dbac3e46c 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -343,9 +343,17 @@ function extractCargoResult(output: string): BenchmarkResult[] { return ret; } -function extractGoResult(output: string): BenchmarkResult[] { - const lines = output.split(/\r?\n/g); - const ret = []; +export function extractGoResult(output: string): BenchmarkResult[] { + // 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 }; + }); + + const uniquePackages = new Set(sections.map((s) => s.pkg).filter(Boolean)); + const hasMultiplePackages = uniquePackages.size > 1; + // Example: // BenchmarkFib20-8 30000 41653 ns/op // BenchmarkDoWithConfigurer1-8 30000000 42.3 ns/op @@ -356,15 +364,19 @@ function extractGoResult(output: string): BenchmarkResult[] { // 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 reExtractRegexp = /^(?Benchmark\w+[\w()$%^&*-=|,[\]{}"#]*?)(?-\d+)?\s+(?\d+)\s+(?.+)$/; - for (const line of lines) { - 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; + // 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 []; + + 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]+/); @@ -374,25 +386,19 @@ function extractGoResult(output: string): BenchmarkResult[] { pieces.unshift(pieces[0], remainder.slice(remainder.indexOf(pieces[1]))); } - 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 = m.groups.name + ' - ' + unit; - } else { - name = m.groups.name; - } - 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/__snapshots__/extract.spec.ts.snap b/test/__snapshots__/extract.spec.ts.snap index 720bfa5f5..9f54fb7f8 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": [ @@ -806,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_fiber_duplicate_names_output.txt b/test/data/extract/go_fiber_duplicate_names_output.txt new file mode 100644 index 000000000..343e04d37 --- /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: linux +goarch: amd64 +pkg: github.com/gofiber/fiber/v3/middleware/cache +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: linux +goarch: amd64 +pkg: github.com/gofiber/fiber/v3/middleware/csrf +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/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 f07a81d53..631b64f43 100644 --- a/test/extract.spec.ts +++ b/test/extract.spec.ts @@ -95,6 +95,14 @@ describe('extractResult()', function () { tool: 'go', file: 'go_fiber_output.txt', }, + { + tool: 'go', + file: 'go_fiber_duplicate_names_output.txt', + }, + { + tool: 'go', + file: 'go_single_package_output.txt', + }, { tool: 'benchmarkjs', file: 'benchmarkjs_output.txt', diff --git a/test/extractGoResult.spec.ts b/test/extractGoResult.spec.ts new file mode 100644 index 000000000..7f73ae9db --- /dev/null +++ b/test/extractGoResult.spec.ts @@ -0,0 +1,290 @@ +import dedent from 'dedent'; +import { extractGoResult } from '../src/extract'; + +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)'); + }); + + 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)'); + }); + }); +});