Skip to content

Commit 2ab4b29

Browse files
author
merge-queue-bot
committed
Merge PR #767: perf: audit against high-performance-go.md, fix 3 confirmed hot paths
2 parents 8948a7c + e8d7647 commit 2ab4b29

7 files changed

Lines changed: 189 additions & 25 deletions

File tree

internal/rules/build/alloc_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package build
2+
3+
import "testing"
4+
5+
// hasReservedDeviceNameBudget pins docs/development/high-performance-go.md's
6+
// "compile regexes at package scope" sibling pattern for case folding: gate
7+
// the strings.ToUpper allocation behind a cheap length check instead of
8+
// converting every path segment. reservedDeviceNames only holds 3-4 byte
9+
// entries (CON, PRN, COM1..9, LPT1..9), so a segment outside that length
10+
// range can never match and must not pay for the case-fold copy. Measured
11+
// baseline on a 3-path, 7-segment fixture: 10 allocs/op before the length
12+
// gate, 5 after (the two segments that do fall in the 3-4 byte range still
13+
// allocate, which is correct — only the always-allocate-on-every-segment
14+
// behavior regressed).
15+
const hasReservedDeviceNameAllocBudget = 5
16+
17+
// hasReservedDeviceNameFixturePaths mirrors a small set of real repo-style
18+
// paths: a mix of segment lengths, most outside the reserved-name range.
19+
var hasReservedDeviceNameFixturePaths = []string{
20+
"docs/readme.md",
21+
"internal/rules/foo.go",
22+
"scripts/build.sh",
23+
}
24+
25+
// TestHasReservedDeviceName_AllocBudget pins the allocation regression gate
26+
// under a normal `go test` run (not only `-bench`), matching the project's
27+
// paragraphstructure.TestCheckAllocBudget convention.
28+
func TestHasReservedDeviceName_AllocBudget(t *testing.T) {
29+
if testing.Short() {
30+
t.Skip("alloc gate skipped in -short mode")
31+
}
32+
33+
allocs := testing.AllocsPerRun(200, func() {
34+
for _, p := range hasReservedDeviceNameFixturePaths {
35+
_ = hasReservedDeviceName(p)
36+
}
37+
})
38+
t.Logf("hasReservedDeviceName allocs/op over %d paths = %.1f (budget = %d)",
39+
len(hasReservedDeviceNameFixturePaths), allocs, hasReservedDeviceNameAllocBudget)
40+
if allocs > float64(hasReservedDeviceNameAllocBudget) {
41+
t.Fatalf("hasReservedDeviceName allocs/op = %.1f, budget = %d; "+
42+
"the length gate before strings.ToUpper may have regressed",
43+
allocs, hasReservedDeviceNameAllocBudget)
44+
}
45+
}
46+
47+
// TestHasReservedDeviceName_Correctness pins that the length gate does not
48+
// change which paths are flagged: reserved names at every valid length
49+
// (3 and 4 bytes) still match, in any case, and non-reserved segments of
50+
// any length (including 3-4 byte look-alikes) do not.
51+
func TestHasReservedDeviceName_Correctness(t *testing.T) {
52+
cases := []struct {
53+
path string
54+
want bool
55+
}{
56+
{"CON", true},
57+
{"con", true},
58+
{"dir/NUL.txt", true},
59+
{"COM1.log", true},
60+
{"com9", true},
61+
{"LPT9", true},
62+
{"CONSOLE.md", false},
63+
{"docs/readme.md", false},
64+
{"foo", false},
65+
{"bar/baz.md", false},
66+
}
67+
for _, c := range cases {
68+
if got := hasReservedDeviceName(c.path); got != c.want {
69+
t.Errorf("hasReservedDeviceName(%q) = %v, want %v", c.path, got, c.want)
70+
}
71+
}
72+
}
73+
74+
// BenchmarkHasReservedDeviceName reports allocs/op alongside ns/op so a
75+
// regression shows up in `go test -bench` output too.
76+
func BenchmarkHasReservedDeviceName(b *testing.B) {
77+
b.ReportAllocs()
78+
for i := 0; i < b.N; i++ {
79+
for _, p := range hasReservedDeviceNameFixturePaths {
80+
_ = hasReservedDeviceName(p)
81+
}
82+
}
83+
}

internal/rules/build/rule.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ func hasReservedDeviceName(p string) bool {
393393
if dot := strings.IndexByte(seg, '.'); dot >= 0 {
394394
seg = seg[:dot]
395395
}
396+
// Every reserved name is 3-4 bytes; gate the ToUpper allocation
397+
// behind a length check so an ordinary (usually longer) path
398+
// segment never pays for the case-fold copy.
399+
if len(seg) < 3 || len(seg) > 4 {
400+
continue
401+
}
396402
if setutil.Contains(reservedDeviceNames, strings.ToUpper(seg)) {
397403
return true
398404
}

internal/rules/githooksync/rule.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ const hookMaxReadBytes int64 = 1024 * 1024
2828
// hoisted to avoid re-converting the marker constant to []byte on every call.
2929
var preMergeMarkerBytes = []byte(githooks.PreMergeCommitMarker)
3030

31+
// resolveHooksDir is a package variable so tests can substitute a
32+
// counting stub and assert driftParts resolves the hooks directory
33+
// exactly once per cache miss, instead of once for peekHookSource and
34+
// again for preMergeCommitHookDrift (each a `git rev-parse` subprocess).
35+
var resolveHooksDir = githooks.ResolveHooksDir
36+
3137
func init() {
3238
rule.Register(&Rule{})
3339
}
@@ -181,9 +187,12 @@ const (
181187
)
182188

183189
// peekHookSource reports the current state of the pre-merge-commit
184-
// hook without parsing its contents.
185-
func peekHookSource(repoRoot string) hookSource {
186-
hookPath := filepath.Join(githooks.ResolveHooksDir(repoRoot), "pre-merge-commit")
190+
// hook without parsing its contents. hooksDir is the repo's resolved
191+
// hooks directory (githooks.ResolveHooksDir), passed in so a caller
192+
// that also needs it (driftParts) resolves it once rather than
193+
// spawning a second `git rev-parse` for the same repo.
194+
func peekHookSource(hooksDir string) hookSource {
195+
hookPath := filepath.Join(hooksDir, "pre-merge-commit")
187196
data, err := bytelimit.ReadFileLimited(hookPath, hookMaxReadBytes)
188197
if err != nil {
189198
if os.IsNotExist(err) {
@@ -213,7 +222,8 @@ func (r *Rule) driftParts(repoRoot string) []string {
213222
driftMu.Unlock()
214223

215224
hasDriver := githooks.HasMdsmithMergeDriver(repoRoot)
216-
hookState := peekHookSource(repoRoot)
225+
hooksDir := resolveHooksDir(repoRoot)
226+
hookState := peekHookSource(hooksDir)
217227
// Early-exit when the user has not opted in (no driver) and the hook
218228
// is not mdsmith-managed. hookSourceUnreadable (file too large, bad
219229
// perms) is included in the early-exit: we cannot verify the hook's
@@ -231,7 +241,7 @@ func (r *Rule) driftParts(repoRoot string) []string {
231241
if msg := r.mergeDriverDrift(repoRoot, hasDriver, expectedGlobs); msg != "" {
232242
parts = append(parts, msg)
233243
}
234-
if msg := r.preMergeCommitHookDrift(repoRoot); msg != "" {
244+
if msg := r.preMergeCommitHookDrift(hooksDir); msg != "" {
235245
parts = append(parts, msg)
236246
}
237247

@@ -301,9 +311,12 @@ func describeGlobs(patterns []string) string {
301311
// hook content. Returns an empty string if no hook is installed, the
302312
// hook is not mdsmith-managed, or the content matches. A non-ENOENT
303313
// read error is surfaced rather than silently passing so permission
304-
// or IO failures cannot mask real drift.
305-
func (r *Rule) preMergeCommitHookDrift(repoRoot string) string {
306-
hookPath := filepath.Join(githooks.ResolveHooksDir(repoRoot), "pre-merge-commit")
314+
// or IO failures cannot mask real drift. hooksDir is the repo's
315+
// resolved hooks directory, shared with peekHookSource by driftParts
316+
// so the two checks spawn only one `git rev-parse` per repo instead
317+
// of one each.
318+
func (r *Rule) preMergeCommitHookDrift(hooksDir string) string {
319+
hookPath := filepath.Join(hooksDir, "pre-merge-commit")
307320
data, err := bytelimit.ReadFileLimited(hookPath, hookMaxReadBytes)
308321
if err != nil {
309322
if os.IsNotExist(err) {

internal/rules/githooksync/rule_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,3 +1071,32 @@ func TestRule_Check_OversizedHookNoDriverSilent(t *testing.T) {
10711071
assert.Empty(t, diags,
10721072
"no driver registered: oversized third-party hook must not produce a diagnostic")
10731073
}
1074+
1075+
// TestDriftParts_ResolvesHooksDirOnce pins that a driftParts cache miss
1076+
// spawns `git rev-parse --git-path hooks` exactly once, shared between
1077+
// peekHookSource and preMergeCommitHookDrift, instead of once for each.
1078+
func TestDriftParts_ResolvesHooksDirOnce(t *testing.T) {
1079+
dir := t.TempDir()
1080+
initRepoWithDriver(t, dir)
1081+
installCanonicalHook(t, dir)
1082+
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitattributes"),
1083+
[]byte(canonicalManagedBlock()), 0o644))
1084+
1085+
driftMu.Lock()
1086+
delete(driftCache, dir)
1087+
driftMu.Unlock()
1088+
1089+
calls := 0
1090+
orig := resolveHooksDir
1091+
resolveHooksDir = func(repoRoot string) string {
1092+
calls++
1093+
return orig(repoRoot)
1094+
}
1095+
defer func() { resolveHooksDir = orig }()
1096+
1097+
r := &Rule{}
1098+
parts := r.driftParts(dir)
1099+
assert.Empty(t, parts, "canonical repo must report no drift")
1100+
assert.Equal(t, 1, calls,
1101+
"driftParts must resolve the hooks directory once, not once per drift check")
1102+
}

internal/rules/noreferencestyle/alloc_test.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,19 @@ func TestMayContainFootnote(t *testing.T) {
155155
assert.True(t, mayContainFootnote([]byte("[^note]: a definition.\n")))
156156
}
157157

158-
// TestCheckFootnotes_NoNeedle_SkipsBothRegexPasses benchmarks
159-
// checkFootnotes on prose with no footnote syntax to demonstrate the
160-
// gate's real effect: without it, footnoteRefRE and footnoteDefRE each
161-
// run a full FindAllSubmatchIndex over the whole file on every Check
162-
// call, unconditionally, even though this rule (MDS043) is opt-in and
163-
// so only runs for workspaces that enabled it. b.Fatalf pins a budget
164-
// so a future regression that removes the gate is caught in CI rather
165-
// than by a human re-running benchstat.
158+
// footnoteCheckBudgetNs pins the gate's real effect: without it,
159+
// footnoteRefRE and footnoteDefRE each run a full FindAllSubmatchIndex
160+
// over the whole file on every Check call. Gated: ~1us (one
161+
// bytes.Contains scan). Ungated: ~580us (two full regex passes over
162+
// ~28KB). The budget keeps roughly the same ~15-20x headroom over the
163+
// gated baseline that BenchmarkCheckCorpusSmall/Large use (see
164+
// internal/engine/bench_test.go), well above measurement noise, while
165+
// staying two orders of magnitude below the ungated cost.
166+
const footnoteCheckBudgetNs = 50_000
167+
168+
// BenchmarkCheckFootnotes_NoNeedle exercises checkFootnotes on prose with
169+
// no footnote syntax; benchstat-friendly (no assertion), consumed by
170+
// TestCheckFootnotes_NoNeedleBudget below for the enforced gate.
166171
func BenchmarkCheckFootnotes_NoNeedle(b *testing.B) {
167172
var src []byte
168173
for i := 0; i < 200; i++ {
@@ -175,18 +180,36 @@ func BenchmarkCheckFootnotes_NoNeedle(b *testing.B) {
175180
require.NoError(b, err)
176181
r := &Rule{}
177182

178-
b.ResetTimer()
179183
for i := 0; i < b.N; i++ {
180184
r.checkFootnotes(f)
181185
}
182-
perOp := float64(b.Elapsed().Nanoseconds()) / float64(b.N)
183-
// Gated: ~1µs (one bytes.Contains scan). Ungated: ~580µs (two full
184-
// regex passes over ~28KB). 50µs stays far above measurement noise
185-
// while catching a dropped gate by two orders of magnitude.
186-
const budgetNsPerOp = 50_000
187-
if perOp > budgetNsPerOp {
188-
b.Fatalf("checkFootnotes on a no-footnote file: %.0f ns/op, budget = %d; "+
186+
}
187+
188+
// TestCheckFootnotes_NoNeedleBudget pins the ns/op regression gate under
189+
// a normal `go test` run. CI's check-bench/markdown-bench jobs only run
190+
// `-bench` against internal/engine, pkg/markdown, internal/lsp, and
191+
// cue/cuelite (see .github/workflows/ci.yml) — a plain
192+
// BenchmarkCheckFootnotes_NoNeedle with an inline b.Fatalf would never
193+
// execute in CI and the assertion would be dead code. testing.Benchmark
194+
// runs the benchmark function programmatically so the assertion lands
195+
// in a Test that `go test ./...` (and therefore CI) actually runs,
196+
// matching paragraphstructure.TestCheckAllocBudget's rationale for its
197+
// own Benchmark/Test pair.
198+
func TestCheckFootnotes_NoNeedleBudget(t *testing.T) {
199+
if testing.Short() {
200+
t.Skip("perf gate skipped in -short mode")
201+
}
202+
if raceEnabled {
203+
t.Skip("perf gate skipped under -race; the race detector's " +
204+
"instrumentation overhead perturbs the ns/op measurement")
205+
}
206+
result := testing.Benchmark(BenchmarkCheckFootnotes_NoNeedle)
207+
perOp := float64(result.NsPerOp())
208+
t.Logf("checkFootnotes on a no-footnote file = %.0f ns/op (budget = %d)",
209+
perOp, footnoteCheckBudgetNs)
210+
if perOp > footnoteCheckBudgetNs {
211+
t.Fatalf("checkFootnotes on a no-footnote file: %.0f ns/op, budget = %d; "+
189212
"the mayContainFootnote gate may have been removed or bypassed",
190-
perOp, budgetNsPerOp)
213+
perOp, footnoteCheckBudgetNs)
191214
}
192215
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build !race
2+
3+
package noreferencestyle
4+
5+
const raceEnabled = false
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build race
2+
3+
package noreferencestyle
4+
5+
const raceEnabled = true

0 commit comments

Comments
 (0)