Skip to content

Commit dba714d

Browse files
author
merge-queue-bot
committed
Merge PR #765: perf: fix top 5 high-performance-go.md violations (struct layout + byte scans)
2 parents f86dc1a + fbc7cd5 commit dba714d

12 files changed

Lines changed: 217 additions & 59 deletions

File tree

docs/reference/cli.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,13 @@ path (`····^`) pointing to the exact column.
154154
[
155155
{
156156
"file": "README.md",
157-
"line": 10,
158-
"column": 81,
159157
"rule": "MDS001",
160158
"name": "line-length",
161159
"severity": "error",
162160
"message": "line too long (120 > 80)",
163161
"source_lines": ["Previous line.", "Another context.", "The long line...", "Next.", "Another."],
162+
"line": 10,
163+
"column": 81,
164164
"source_start_line": 8
165165
}
166166
]

internal/archetype/gensection/engine.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,14 @@ func ReplaceContent(f *lint.File, mp MarkerPair, content string) []byte {
147147
return result
148148
}
149149

150-
// SplitLines splits source into lines (like bytes.Split but returns [][]byte).
150+
// SplitLines splits source into lines on []byte instead of
151+
// hand-rolling a byte-by-byte scan: bytes.Split uses the same
152+
// SIMD-accelerated IndexByte the standard library relies on
153+
// everywhere else in mdsmith, and it pre-sizes the result in one
154+
// pass instead of growing it via repeated append. See
155+
// docs/development/high-performance-go.md "Strings and bytes".
151156
func SplitLines(source []byte) [][]byte {
152-
var lines [][]byte
153-
start := 0
154-
for i, b := range source {
155-
if b == '\n' {
156-
lines = append(lines, source[start:i])
157-
start = i + 1
158-
}
159-
}
160-
lines = append(lines, source[start:])
161-
return lines
157+
return bytes.Split(source, []byte{'\n'})
162158
}
163159

164160
// EnsureTrailingNewline appends \n if s does not already end with \n.

internal/archetype/gensection/engine_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,60 @@ func TestSplitLines_Empty(t *testing.T) {
491491
require.Len(t, lines, 1, "expected 1 line, got %d", len(lines))
492492
}
493493

494+
// splitLinesAllocBudget pins SplitLines to a single allocation per
495+
// call. A hand-rolled append loop over each '\n' reallocates its
496+
// backing array as the line count grows past each capacity doubling;
497+
// bytes.Split pre-counts separators in one pass and allocates the
498+
// result slice exactly once. See
499+
// docs/development/high-performance-go.md "Strings and bytes".
500+
const splitLinesAllocBudget = 1
501+
502+
// representativeMarkdownSource is a 50-line body sized like a real
503+
// Markdown file passed through Engine.Fix — enough lines that a
504+
// growth-only append loop needs several reallocations, but not an
505+
// artificial stress size.
506+
func representativeMarkdownSource() []byte {
507+
var b strings.Builder
508+
for i := 0; i < 50; i++ {
509+
b.WriteString("this is a representative line of markdown prose\n")
510+
}
511+
return []byte(b.String())
512+
}
513+
514+
// TestSplitLines_AllocBudget pins the per-call allocation count so a
515+
// regression back to the manual append loop fails CI.
516+
func TestSplitLines_AllocBudget(t *testing.T) {
517+
if testing.Short() {
518+
t.Skip("alloc gate skipped in -short mode")
519+
}
520+
src := representativeMarkdownSource()
521+
allocs := testing.AllocsPerRun(100, func() {
522+
_ = SplitLines(src)
523+
})
524+
require.LessOrEqualf(t, allocs, float64(splitLinesAllocBudget),
525+
"SplitLines allocs/op = %.0f, budget = %d", allocs, splitLinesAllocBudget)
526+
}
527+
528+
// BenchmarkSplitLines reports allocs/op for SplitLines on a
529+
// representative 50-line body and fails the run if it regresses past
530+
// splitLinesAllocBudget, matching the project's "benchmarks that
531+
// always run" convention.
532+
func BenchmarkSplitLines(b *testing.B) {
533+
src := representativeMarkdownSource()
534+
b.ReportAllocs()
535+
for i := 0; i < b.N; i++ {
536+
_ = SplitLines(src)
537+
}
538+
b.StopTimer()
539+
540+
allocs := testing.AllocsPerRun(100, func() {
541+
_ = SplitLines(src)
542+
})
543+
if allocs > float64(splitLinesAllocBudget) {
544+
b.Fatalf("SplitLines allocs/op = %.0f, budget = %d", allocs, splitLinesAllocBudget)
545+
}
546+
}
547+
494548
func TestParseColumnConfig_Basic(t *testing.T) {
495549
raw := map[string]any{
496550
"desc": map[string]any{

internal/lsp/protocol.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,20 @@ const (
177177
)
178178

179179
// Diagnostic is the LSP wire shape produced by the server.
180+
//
181+
// Fields are ordered pointer-containing (string/slice/pointer/struct-
182+
// with-pointer-field) first, then scalar (int) last, matching
183+
// internal/lint.Diagnostic's layout. Go's GC computes a struct's
184+
// ptrdata as the offset through the last pointer-containing field;
185+
// this type is built once per diagnostic on the LSP's keystroke hot
186+
// path (every publishDiagnostics call), so keeping the scalar fields
187+
// out of that span matters here. See
188+
// docs/development/high-performance-go.md "Struct layout".
180189
type Diagnostic struct {
181-
Range Range `json:"range"`
182-
Severity DiagnosticSeverity `json:"severity,omitempty"`
183-
Code string `json:"code,omitempty"`
184-
Source string `json:"source,omitempty"`
185-
Message string `json:"message"`
186-
Data *diagnosticData `json:"data,omitempty"`
190+
Code string `json:"code,omitempty"`
191+
Source string `json:"source,omitempty"`
192+
Message string `json:"message"`
193+
Data *diagnosticData `json:"data,omitempty"`
187194
// RelatedInformation surfaces secondary locations (plan 230): for
188195
// MDS020, the proto.md / kind-file line that declares the violated
189196
// constraint, which the editor renders as a navigable entry.
@@ -194,6 +201,9 @@ type Diagnostic struct {
194201
// to its documentation. Href must be an http(s) URL per the LSP
195202
// spec; clients render it next to the code.
196203
CodeDescription *codeDescription `json:"codeDescription,omitempty"`
204+
205+
Range Range `json:"range"`
206+
Severity DiagnosticSeverity `json:"severity,omitempty"`
197207
}
198208

199209
// diagnosticRelatedInformation is one entry of Diagnostic.relatedInformation

internal/lsp/server_codeaction.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lsp
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"strings"
67

@@ -425,14 +426,11 @@ func documentEndPosition(source []byte) (int, int) {
425426
}
426427
if source[len(source)-1] == '\n' {
427428
// Count newlines; the position past the final \n is the
428-
// one-past-the-end line, character 0.
429-
nl := 0
430-
for _, b := range source {
431-
if b == '\n' {
432-
nl++
433-
}
434-
}
435-
return nl, 0
429+
// one-past-the-end line, character 0. bytes.Count uses the
430+
// same SIMD-accelerated scan as bytes.IndexByte instead of a
431+
// scalar per-byte Go loop — see
432+
// docs/development/high-performance-go.md "Strings and bytes".
433+
return bytes.Count(source, []byte{'\n'}), 0
436434
}
437435
// No trailing newline: end at last line's UTF-16 length. source
438436
// is non-empty here (checked above), so splitLines always yields

internal/lsp/server_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,39 @@ func TestDocumentEndPositionEmpty(t *testing.T) {
21242124
assert.Equal(t, 0, endChar)
21252125
}
21262126

2127+
// representativeLSPDocument is a 2000-line trailing-newline body sized
2128+
// like a real document passed through fullFileEdit on every "fix all"
2129+
// whole-document code action.
2130+
func representativeLSPDocument() []byte {
2131+
var b strings.Builder
2132+
for i := 0; i < 2000; i++ {
2133+
b.WriteString("this is a representative line of markdown prose\n")
2134+
}
2135+
return []byte(b.String())
2136+
}
2137+
2138+
// BenchmarkDocumentEndPosition pins documentEndPosition to zero
2139+
// allocations on the trailing-newline path so a regression back to an
2140+
// allocating implementation fails CI. The newline count itself uses
2141+
// bytes.Count's SIMD-accelerated scan instead of a scalar per-byte Go
2142+
// loop; see docs/development/high-performance-go.md "Strings and
2143+
// bytes".
2144+
func BenchmarkDocumentEndPosition(b *testing.B) {
2145+
src := representativeLSPDocument()
2146+
b.ReportAllocs()
2147+
for i := 0; i < b.N; i++ {
2148+
_, _ = documentEndPosition(src)
2149+
}
2150+
b.StopTimer()
2151+
2152+
allocs := testing.AllocsPerRun(100, func() {
2153+
_, _ = documentEndPosition(src)
2154+
})
2155+
if allocs > 0 {
2156+
b.Fatalf("documentEndPosition allocs/op = %.0f, budget = 0", allocs)
2157+
}
2158+
}
2159+
21272160
func TestReloadConfigEmptyRoot(t *testing.T) {
21282161
t.Parallel()
21292162
s := New(Options{Reader: nil, Writer: io.Discard})

internal/lsp/structlayout_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package lsp
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/jeduden/mdsmith/internal/structlayout"
8+
)
9+
10+
func TestDiagnostic_PointerFieldsPrecedeScalars(t *testing.T) {
11+
structlayout.AssertPointerFieldsFirst(t, reflect.TypeOf(Diagnostic{}))
12+
}

internal/output/json.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,45 @@ import (
1010
// JSONFormatter outputs diagnostics as a JSON array.
1111
type JSONFormatter struct{}
1212

13+
// Fields are ordered pointer-containing (string/slice/pointer) first,
14+
// then scalar (int/bool) last, matching internal/lint.Diagnostic's
15+
// layout. Go's GC computes a struct's ptrdata as the offset through
16+
// the last pointer-containing field; one of these is built per
17+
// diagnostic on every `--format json` run. See
18+
// docs/development/high-performance-go.md "Struct layout".
1319
type jsonDiagnostic struct {
14-
File string `json:"file"`
15-
Line int `json:"line"`
16-
Column int `json:"column"`
17-
Rule string `json:"rule"`
18-
Name string `json:"name"`
19-
Severity string `json:"severity"`
20-
Message string `json:"message"`
21-
SourceLines []string `json:"source_lines,omitempty"`
22-
SourceStartLine int `json:"source_start_line,omitempty"`
23-
Explanation *jsonExplanation `json:"explanation,omitempty"`
24-
// Deprecated and ReplacedBy mirror lint.Diagnostic's plan-136
25-
// fields so CI scripts can route a deprecation warning without
26-
// scanning the message body. Both are omitempty so non-
27-
// deprecation diagnostics stay unchanged on the wire.
28-
Deprecated bool `json:"deprecated,omitempty"`
20+
File string `json:"file"`
21+
Rule string `json:"rule"`
22+
Name string `json:"name"`
23+
Severity string `json:"severity"`
24+
Message string `json:"message"`
25+
SourceLines []string `json:"source_lines,omitempty"`
26+
Explanation *jsonExplanation `json:"explanation,omitempty"`
27+
// ReplacedBy mirrors lint.Diagnostic's plan-136 field so CI
28+
// scripts can route a deprecation warning without scanning the
29+
// message body. omitempty so non-deprecation diagnostics stay
30+
// unchanged on the wire.
2931
ReplacedBy string `json:"replaced_by,omitempty"`
3032
// RelatedLocations mirrors lint.Diagnostic's plan-230 field so CI
3133
// scripts can read the schema-constraint location without parsing
3234
// the message. omitempty so diagnostics that carry none stay
3335
// unchanged on the wire. The rule-doc URL is not emitted here — it
3436
// is derivable from the `rule` field and is an editor (LSP) concern.
3537
RelatedLocations []jsonRelatedLocation `json:"related_locations,omitempty"`
38+
39+
Line int `json:"line"`
40+
Column int `json:"column"`
41+
SourceStartLine int `json:"source_start_line,omitempty"`
42+
Deprecated bool `json:"deprecated,omitempty"`
3643
}
3744

45+
// File and Message (pointer-containing) precede Line and Column
46+
// (scalar) for the same GC-ptrdata reason as jsonDiagnostic above.
3847
type jsonRelatedLocation struct {
3948
File string `json:"file,omitempty"`
49+
Message string `json:"message"`
4050
Line int `json:"line,omitempty"`
4151
Column int `json:"column,omitempty"`
42-
Message string `json:"message"`
4352
}
4453

4554
type jsonExplanation struct {

internal/output/json_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ func TestJSONFormatter_ExactOutput(t *testing.T) {
170170
expected := `[
171171
{
172172
"file": "README.md",
173-
"line": 10,
174-
"column": 5,
175173
"rule": "MDS001",
176174
"name": "line-length",
177175
"severity": "error",
178-
"message": "line too long (120 \u003e 80)"
176+
"message": "line too long (120 \u003e 80)",
177+
"line": 10,
178+
"column": 5
179179
}
180180
]
181181
`
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package output
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/jeduden/mdsmith/internal/structlayout"
8+
)
9+
10+
func TestJSONDiagnostic_PointerFieldsPrecedeScalars(t *testing.T) {
11+
structlayout.AssertPointerFieldsFirst(t, reflect.TypeOf(jsonDiagnostic{}))
12+
}
13+
14+
func TestJSONRelatedLocation_PointerFieldsPrecedeScalars(t *testing.T) {
15+
structlayout.AssertPointerFieldsFirst(t, reflect.TypeOf(jsonRelatedLocation{}))
16+
}

0 commit comments

Comments
 (0)