feat: implement capacity estimate (workers → supportable jobs)#29
feat: implement capacity estimate (workers → supportable jobs)#29lukas-frystak-sonarsource wants to merge 12 commits into
Conversation
Sonar CAG skill added by the sonar CLI
Spec 031 introduces an experimental --estimate-workers flag on `analyze bgtasks` (and `run bgtasks` for parity) that inverts the existing capacity-demand analysis. Given a planned worker count, it estimates how many project analyses per hour that capacity can support, broken down by size category (XXS–XXXL) with a ±20% error margin. Key design decisions captured in the spec: - Pure estimation functions in estimate.go; no new data sources - ISSUE_SYNC tasks excluded from all sums to avoid reindex skew - Mode on 0.1s-rounded execution times as the representative per-job cost - Every calculation step logged at DEBUG; headline results at INFO - Report output is byte-for-byte identical when the flag is omitted - Algorithm fully specified with guards for empty categories and zero totals
Adds an experimental --estimate-workers flag to `analyze bgtasks` (and `run bgtasks` for parity). Given a comma-separated list of worker counts the estimator computes expected project-analysis throughput per hour (with ±20% margin) broken down by size category (XXS…XXXL). Key changes: - internal/analyzer/bgtasks/estimate.go: pure estimation functions; EstimateCapacity performs the 5-step algorithm (ISSUE_SYNC exclusion, REPORT isolation, report-share, per-category mode cost, per-worker projection). Named constants for rounding granularity, cost floor, margin, and seconds-per-hour. - internal/analyzer/bgtasks/estimate_test.go: unit tests for every acceptance criterion — happy path, ISSUE_SYNC exclusion, category shares summing to 1, mode floor, tie-breaking, duplicate/unsorted worker inputs, XXXL label, empty-category div-by-zero safety, and proportional scaling across worker counts. - internal/analyzer/bgtasks.go: AnalyzeBgTasks gains an estimateWorkers []int parameter; logCapacityEstimate is called after the report is written — report generation is never affected. All algorithm steps are logged: DEBUG for intermediate arithmetic, INFO for headline results. - cmd/analyze.go: --estimate-workers IntSlice flag with validation (values must be >= 1); withEstimateWorkers analyzeOption threads it through to AnalyzeBgTasks. - cmd/run.go: same flag for parity. The estimate result is not added to AnalysisResults and is not passed to report.go — HTML output is byte-for-byte identical when the flag is omitted. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ng in logger Co-authored-by: Lukas Frystak <95630751+lukas-frystak-sonarsource@users.noreply.github.com>
…lower bound Co-authored-by: Lukas Frystak <95630751+lukas-frystak-sonarsource@users.noreply.github.com>
Co-authored-by: Lukas Frystak <95630751+lukas-frystak-sonarsource@users.noreply.github.com>
Adds two integration tests in bgtasks_test.go that exercise the logCapacityEstimate code path which had zero coverage: - TestAnalyzeBgTasks_WithEstimateWorkers: runs AnalyzeBgTasks with --estimate-workers 4,8 and task data that includes a REPORT task (M bucket), a sub-50ms REPORT task (XXS, triggers the mode-floor branch), and an ISSUE_SYNC task (verifies exclusion). Uses a DEBUG-level logger so all step-by-step log branches execute. - TestAnalyzeBgTasks_EstimateSkipped_NoReport: runs with only a non-REPORT task so the estimator skips gracefully; verifies the report is still written. Both tests verify that the HTML report is produced unchanged regardless of the estimate path taken. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
| func TestAnalyzeBgTasks_WithEstimateWorkers(t *testing.T) { | ||
| dir := t.TempDir() | ||
| bgtasksDir := writeBgtasksDir(t, dir) | ||
| if err := os.WriteFile(filepath.Join(bgtasksDir, "page-0001.json"), []byte(estimateTaskJSON), 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| reportDir := t.TempDir() | ||
| // Use a debug logger to exercise all log branches in logCapacityEstimate. | ||
| debugLogger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) | ||
| if err := AnalyzeBgTasks(dir, reportDir, "myreport", nil, nil, debugLogger, []int{4, 8}); err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| // Report must still be written despite the estimate running. | ||
| if _, err := os.Stat(filepath.Join(reportDir, "myreport.html")); os.IsNotExist(err) { | ||
| t.Error("expected report file myreport.html to be created") |
There was a problem hiding this comment.
💡 Quality: Estimate integration tests assert only report creation
TestAnalyzeBgTasks_WithEstimateWorkers and TestAnalyzeBgTasks_EstimateSkipped_NoReport only assert that myreport.html is created — the same assertion already made by TestAnalyzeBgTasks_HappyPath. Neither test captures or inspects the logger output, so they don't actually verify that the capacity estimate ran, produced expected per-category throughput, applied the cost floor (the comment claims '20ms → floor applied'), excluded the ISSUE_SYNC task, or skipped gracefully with the expected reason. As written, these tests would still pass even if logCapacityEstimate did nothing. The pure algorithm is covered in estimate_test.go, so this is low impact, but capturing the debug logger output (e.g. into a bytes.Buffer via slog.NewTextHandler) and asserting on key lines would make these tests meaningfully exercise the wiring rather than duplicating the happy-path check.
Capture logger output and assert the estimate actually ran.:
var buf bytes.Buffer
debugLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
if err := AnalyzeBgTasks(dir, reportDir, "myreport", nil, nil, debugLogger, []int{4, 8}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(buf.String(), "capacity estimate") {
t.Errorf("expected capacity estimate to be logged, got:
%s", buf.String())
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
Amends shipped spec 031. Replaces two of its design choices that this refinement supersedes: - Representative per-job cost switches from mode to mean, removing the 0.1s rounding and tie-break. The total then reduces to the provable identity totalJobs = reportCapacity / averageCost, so the size-category split is unbiased. (Mode sat below the mean for right-skewed execution times and over-estimated throughput, worst for the wide buckets.) - The flat +/-20% band is replaced by a demand profile: REPORT arrivals are binned by clock-hour (from SubmittedAt) to report average and peak (p95) hourly demand, and each worker count gets a plain-language verdict on whether its capacity covers average and peak load. Also specifies always listing all eight size categories (empties marked "no tasks") and a doc comment for the capacity -> jobs chain. Log-only, report output unchanged. draft-status: draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review fixes to spec 032 (capacity estimate mean + demand profile): - Fix illustrative output: empty XXXL shows "> 180s" label, not a numeric upper bound (a numeric max implies a task in XXXL, which contradicts "no tasks"). Matches unchanged 031 labeling + the backwards-range guard. - Point percentile reuse at mathutil.CalculatePercentile (already used by metrics.go/capacity.go); note it interpolates linearly, not nearest-rank, and takes 0.95. - Define averageReportCost explicitly where the mean identity is stated. - Qualify the mean-identity acceptance criterion: exact only when no category hits the cost floor; update matching test scenario. - Set draft-status: ready (no blockers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Lukas Frystak <95630751+lukas-frystak-sonarsource@users.noreply.github.com>
Code Review 👍 Approved with suggestions 4 resolved / 5 findingsImplements capacity estimation for background task workers using mean-based costing and demand profiles. Consider adding specific assertions for estimate output values in the integration tests to move beyond verifying report creation only. 💡 Quality: Estimate integration tests assert only report creation📄 internal/analyzer/bgtasks_test.go:283-297
Capture logger output and assert the estimate actually ran.✅ 4 resolved✅ Quality: Step-5 arithmetic duplicated between estimator and logger
✅ Quality: XXXL label is nonsensical when no task exceeds 180s
✅ Quality: CLI validateEstimateWorkers has no unit test
✅ Quality: Stray tags at end of spec file
🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|



Summary
--estimate-workersflag onanalyze bgtasks(andrun bgtasksfor parity)Key changes
internal/analyzer/bgtasks/estimate.gointernal/analyzer/bgtasks/estimate_test.gointernal/analyzer/bgtasks.goAnalyzeBgTasksgainsestimateWorkers []int;logCapacityEstimateis called after the report is written — report generation is never affected. DEBUG for intermediate arithmetic, INFO for headline results.cmd/analyze.go--estimate-workersIntSlice flag with>=1validation;withEstimateWorkersoption threads it through.cmd/run.goNotable decisions
EstimateCapacityis pure (no I/O, no globals); intermediate values are returned in the result struct and logged at the call site inlogCapacityEstimate.(maxObservedMs + 999) / 1000so the label always shows a whole-second value ≥ the actual max.Testing notes
estimate_test.godirectly cover each acceptance criterionsonar analyze agentic --stagedandgolangci-lint runboth report 0 issuessonar-insights -v analyze bgtasks --estimate-workers 4,8,16) was not run — no live instance available in CI; the flag wiring and logging logic are covered by the unit and integration tests inbgtasks_test.go🤖 Generated with Claude Code
Summary by Gitar
specs/032-IMP-capacity-estimate-mean-and-demand.mdto refine the capacity estimation logic, introducing mean-based costing and a demand-profile analysis.internal/analyzer/bgtasks/estimate.goto use the mean of execution times as the representative cost, removing arbitrary ±20% margins and rounding-based mode selection.DemandProfilecalculation to compare throughput against peak and average analysis arrival rates.internal/analyzer/bgtasks.goto report per-worker verdict lines based on demand vs. capacity, replacing uncertainty margins with operational guidance.internal/analyzer/bgtasks/estimate_test.gowith tests covering the new mean-based calculations, demand profiles, and verdict logic.This will update automatically on new commits.