Skip to content

Commit 2c83ee3

Browse files
authored
perf(lexers): build the registry from generated metadata instead of parsing XML at init (#1356)
Importing the lexers package decodes the `<config>` element of all 285 embedded XML definitions with encoding/xml at init. This generates that metadata into Go literals with a new go:generate tool (internal/metadatagen, output printed with repr), so building the registry parses no XML at all; each lexer's rules are still loaded lazily from its XML on first use, as before. A test verifies the generated metadata matches the XML and fails with a hint to re-run go generate when they drift. Analyser regexes are now compiled on the first AnalyseText call, with their validity checked at generation time instead. Registering a fresh lexer also no longer scans the whole registry slice, which was quadratic over a full registration pass. | `GODEBUG=inittrace=1` | init clock | init heap | |---|---|---| | before | 9.4 ms | 3.0 MB / 34,714 allocs | | after | 0.92 ms | 463 KB / 2,341 allocs | Part of a series reducing the cost of importing chroma (lexers + styles + formatters) from ~15.4 ms to ~1.1 ms.
1 parent 3af0c4c commit 2c83ee3

8 files changed

Lines changed: 4319 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ jobs:
1212
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
1313
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
1414
- run: go test ./...
15+
check-generated:
16+
name: Check generated code
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
20+
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
21+
- run: just check-generated
1522
lint:
1623
name: Lint
1724
runs-on: ubuntu-latest

Justfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ tokentype-string:
2121
format-js:
2222
biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js
2323

24+
# Regenerate lexers/lexers_gen.go from the embedded lexer definitions
25+
lexer-metadata:
26+
GOOS= GOARCH= go generate ./lexers
27+
28+
# Check that generated code is in sync with the embedded lexer definitions
29+
check-generated: lexer-metadata
30+
#!/usr/bin/env bash
31+
git diff --exit-code lexers/lexers_gen.go || {
32+
echo "lexers/lexers_gen.go is stale; run 'just lexer-metadata' and commit the result." >&2
33+
exit 1
34+
}
35+
2436
# Tidy Go modules
2537
tidy:
2638
find . -name 'go.mod' -execdir go mod tidy \;

internal/metadatagen/main.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Command metadatagen generates a Go source file containing the <config>
2+
// section of every embedded lexer XML definition, so that the lexer registry
3+
// can be constructed without parsing any XML at init time.
4+
package main
5+
6+
import (
7+
"bytes"
8+
"flag"
9+
"fmt"
10+
"go/format"
11+
"io/fs"
12+
"log"
13+
"os"
14+
15+
"github.com/alecthomas/chroma/v3"
16+
"github.com/alecthomas/repr"
17+
"github.com/dlclark/regexp2/v2"
18+
)
19+
20+
func main() {
21+
log.SetFlags(0)
22+
log.SetPrefix("metadatagen: ")
23+
dir := flag.String("dir", "embedded", "directory containing lexer XML definitions")
24+
out := flag.String("out", "lexers_gen.go", "output file")
25+
flag.Parse()
26+
27+
fsys := os.DirFS(*dir)
28+
paths, err := fs.Glob(fsys, "*.xml")
29+
if err != nil {
30+
log.Fatal(err)
31+
}
32+
33+
buf := &bytes.Buffer{}
34+
fmt.Fprintf(buf, `// Code generated by "go generate" (internal/metadatagen); DO NOT EDIT.
35+
36+
package lexers
37+
38+
import (
39+
"github.com/alecthomas/chroma/v3"
40+
)
41+
42+
// embeddedLexers mirrors the <config> element of every XML lexer definition
43+
// under embedded/, so that the registry can be built without parsing XML at
44+
// init time. TestEmbeddedLexerMetadata verifies it is up to date.
45+
var embeddedLexers = []struct {
46+
path string
47+
config chroma.Config
48+
}{
49+
`)
50+
for _, path := range paths {
51+
lexer, err := chroma.NewXMLLexer(fsys, path)
52+
if err != nil {
53+
log.Fatalf("%s: %s", path, err)
54+
}
55+
config := lexer.Config()
56+
validateAnalysers(path, config)
57+
fmt.Fprintf(buf, "\t{%q, %s},\n", "embedded/"+path, repr.String(*config, repr.Indent("\t")))
58+
}
59+
fmt.Fprintf(buf, "}\n")
60+
61+
source, err := format.Source(buf.Bytes())
62+
if err != nil {
63+
log.Fatalf("formatting generated source: %s", err)
64+
}
65+
if err := os.WriteFile(*out, source, 0o600); err != nil {
66+
log.Fatal(err)
67+
}
68+
}
69+
70+
// validateAnalysers compiles the analyser regexes so that an invalid pattern
71+
// fails at generation time; at runtime they are compiled lazily.
72+
func validateAnalysers(path string, config *chroma.Config) {
73+
if config.Analyse == nil {
74+
return
75+
}
76+
flags := regexp2.None
77+
if config.CaseInsensitive {
78+
flags = regexp2.IgnoreCase
79+
}
80+
for _, ra := range config.Analyse.Regexes {
81+
if _, err := regexp2.Compile(ra.Pattern, flags); err != nil {
82+
log.Fatalf("%s: %q is not a valid analyser regex: %s", path, ra.Pattern, err)
83+
}
84+
}
85+
}

lexers/lexers.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,29 @@ package lexers
22

33
import (
44
"embed"
5-
"io/fs"
65

76
"github.com/alecthomas/chroma/v3"
87
)
98

9+
//go:generate go run github.com/alecthomas/chroma/v3/internal/metadatagen -dir embedded -out lexers_gen.go
10+
1011
//go:embed embedded
1112
var embedded embed.FS
1213

1314
// GlobalLexerRegistry is the global LexerRegistry of Lexers.
15+
//
16+
// It is populated from generated metadata mirroring the embedded XML
17+
// definitions, so that registering every lexer does not require parsing any
18+
// XML; each lexer's rules are still loaded from its XML on first use.
1419
var GlobalLexerRegistry = func() *chroma.LexerRegistry {
1520
reg := chroma.NewLexerRegistry()
16-
paths, err := fs.Glob(embedded, "embedded/*.xml")
17-
if err != nil {
18-
panic(err)
19-
}
20-
for _, path := range paths {
21-
reg.Register(chroma.MustNewXMLLexer(embedded, path))
21+
for i := range embeddedLexers {
22+
entry := &embeddedLexers[i]
23+
lexer, err := chroma.NewXMLLexerFromConfig(&entry.config, embedded, entry.path)
24+
if err != nil {
25+
panic(err)
26+
}
27+
reg.Register(lexer)
2228
}
2329
return reg
2430
}()

0 commit comments

Comments
 (0)