Skip to content

Optimize report generation performance (issue #123) - #127

Merged
ilmanzo merged 8 commits into
mainfrom
issue-123
Aug 7, 2026
Merged

Optimize report generation performance (issue #123)#127
ilmanzo merged 8 commits into
mainfrom
issue-123

Conversation

@ilmanzo

@ilmanzo ilmanzo commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the optimization plan from plan.md (issue #123: report generation gets slow as the number of traced binaries/libraries grows, observed on os-autoinst SUT VMs). All five identified bottlenecks addressed in cmd/report.go/cmd/funkoverage.go:

  • AscanLog: replaced regexp matching (FUNC/CALLED line parsing) with byte-prefix + bytes.IndexAny scanning.
  • B — HTML templates (detailedHTMLTemplateStr/aggregateHTMLTemplate) parsed once at package init instead of on every generateHTMLReport/generateAggregateHTMLReport call.
  • DgenerateXUnitReport no longer builds a throwaway single-entry map just to recompute totals it already has locally.
  • E — HTML/XML output now goes through a buffered writer (writeBuffered helper), flushed before close.
  • CanalyzeLogs scans log files concurrently (golang.org/x/sync/errgroup, bounded to GOMAXPROCS), merging each file's local coverage map sequentially afterward (avoids any shared-map locking). emitReport generates each image's HTML/XML concurrently, since each writes to a distinct file.
    • Library enumeration (EnumerateFunctions) was parallelized in an earlier draft and reverted — that's install/trace-time work, not report generation, out of scope here.

Also bumped github.com/ianlancetaylor/demangle to latest (cilium/ebpf and golang.org/x/sync were already at latest after rebasing onto main).

Local benchmarks

go test ./cmd/ -bench=. -benchmem (synthetic data, see cmd/report_bench_test.go):

Benchmark Before After Speedup
scanLog (20k-line log) 54.3ms 9.4ms 5.8x
analyzeLogs (50 images × 500 funcs × 4 called-log files) 114.7ms 28.6ms 4.0x
emitReport("html", ...) (200 images × 50 funcs) 78.5ms 9.6ms 8.2x
emitReport("xml", ...) (200 images × 50 funcs) 12.2ms 3.1ms 3.9x

Large-scale before/after benchmark

A synthetic corpus of 2000 images × 300 functions (600,000 total functions, 360,000 called, 94MB across 8000 log files — generated deterministically via go run tests/gen_report_bench_corpus.go), simulating "many binaries/libraries traced" per the issue. funkoverage report <corpus> <outdir> --formats html,xml,txt, 3 runs each:

Run Before (main) After (issue-123)
1 13.35s 3.06s
2 10.09s 3.08s
3 10.05s 3.07s
Average ~11.2s ~3.07s

~3.6x faster on realistic scale, with zero regression: Total Functions: 600000, Total Called: 360000, Average Coverage: 60.00% identical before/after; the full txt report is byte-for-byte identical, and per-image HTML/XML content is identical excluding timestamps. File counts match exactly (2000 XML, 2001 HTML including aggregate.html).

Test plan

  • go build ./..., go vet ./..., go test ./... pass (56 tests)
  • go test -race ./... clean, including the new concurrent code paths and their benchmarks
  • Local benchmark suite added (cmd/report_bench_test.go) and compared before/after each change
  • Large-scale benchmark: 3 runs each side, coverage counts and report content verified identical, only timing differs

ilmanzo added 8 commits August 7, 2026 17:51
scanLog used compiled regexes (FUNC/CALLED) against a fully-allocated
per-line string via scanner.Text(). Regexp.FindStringSubmatch on every
line is the single biggest cost in report generation as log size grows
(issue #123). Replace with scanner.Bytes() + bytes.HasPrefix +
bytes.IndexAny, converting to string only for the two substrings that
actually become map keys.

Benchmark (20k-line functions.log): scanLog 54.3ms -> 9.8ms (5.5x),
analyzeLogs (50 images x 500 funcs x 4 called-log files) 114.7ms ->
45.9ms (2.5x). Also adds report_bench_test.go with synthetic-corpus
benchmarks for scanLog/analyzeLogs/generateHTMLReport/generateXUnitReport
to track further optimizations in this series.
generateHTMLReport and generateAggregateHTMLReport re-parsed their
(compile-time-constant) template strings on every call. Parse once into
package-level *template.Template vars instead.

Also fixed the HTML/XUnit benchmarks: they used 1 image with a huge
function count, which doesn't exercise the per-call template-reparse
cost at all (that cost scales with number of images, not functions per
image, since generateHTMLReport/generateXUnitReport are called once per
image by emitReport). Switched to many images with modest per-image
function counts.

Benchmark (200 images x 50 functions): generateHTMLReport 150.8ms ->
121.1ms (-20%), allocs 244279 -> 194230. generateXUnitReport unaffected
(doesn't use html/template) - that's bottleneck D's job.
generateXUnitReport built a throwaway single-entry map just to call
summarizeCoverage() and recover totals it already had locally
(totalCount/calledCount/pct are mathematically identical to
summary.TotalFunctions/TotalCalled/AverageCoverage when there's exactly
one image). Use the local values directly.

Benchmark impact is small in practice (allocs 31436 -> 30446, ~1k fewer
per 200-image run; no measurable time change) - this was more a
correctness/clarity cleanup than a real bottleneck, unlike A/B.
generateXUnitReport, generateHTMLReport, and generateAggregateHTMLReport
all wrote to a raw *os.File, so html/template.Execute and xml.Encoder's
many small Write() calls each became a syscall. Added a writeBuffered
helper (bufio.Writer, flushed before close) used by all three.

Benchmark (200 images x 50 functions): generateHTMLReport 121.1ms ->
67.6ms (-44%). generateXUnitReport ~unchanged (its write volume per
call is small enough that syscall count wasn't the bottleneck there).
analyzeLogs: scan log files concurrently (golang.org/x/sync/errgroup,
bounded to GOMAXPROCS). Each file scans into its own local coverage map
(scanLog mutates a map in place, and Go maps reject concurrent writes
even to disjoint keys) which are merged sequentially once every scan
completes - avoids locking the shared map entirely.

emitReport: generate each image's HTML/XML report concurrently. Each
writes to a distinct output file, so this is embarrassingly parallel;
per-image errors are still logged individually rather than aborting the
batch, matching prior behavior.

Library enumeration (EnumerateFunctions) was also parallelized in an
earlier draft of this commit but reverted per review - it's install/
trace-time work, not report generation, and out of scope for what
issue #123 and this benchmark series are measuring.

Benchmark (200 images x 50 functions, 20 iterations):
  analyzeLogs (50 images x 500 funcs x 4 called-log files): ~46ms -> ~37ms
  emitReport("html", ...): 78.5ms -> 9.3ms (8.4x)
  emitReport("xml", ...):  12.2ms -> 3.7ms (3.3x)

Verified race-clean with `go test -race ./...` and the benchmarks
themselves under -race.
cilium/ebpf and golang.org/x/sync were already at their latest
available versions (v0.22.0 both) after rebasing onto main. demangle
had a newer pseudo-version available; bumped and reverified full test
suite under -race.
go run tests/gen_report_bench_corpus.go <outdir> <numImages>
<funcsPerImage> <calledFilesPerImage> writes a deterministic
_functions.log/_called.log corpus, simulating issue #123's scenario
(many binaries/libraries traced) without needing to actually install
and run hundreds of real binaries. Same args always produce
byte-identical output, so before/after report-generation timing runs
are directly comparable.
@ilmanzo
ilmanzo merged commit 10d21a8 into main Aug 7, 2026
2 checks passed
@ilmanzo
ilmanzo deleted the issue-123 branch August 7, 2026 16:01
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 76.38889% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.73%. Comparing base (f6eeaad) to head (34da9d2).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
cmd/report.go 77.58% 5 Missing and 8 partials ⚠️
cmd/funkoverage.go 71.42% 2 Missing and 2 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #127      +/-   ##
==========================================
+ Coverage   42.62%   43.73%   +1.10%     
==========================================
  Files          11       11              
  Lines        1499     1525      +26     
==========================================
+ Hits          639      667      +28     
+ Misses        763      762       -1     
+ Partials       97       96       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants