Skip to content

Commit 170b608

Browse files
committed
perf(lexers): build the registry from generated metadata instead of parsing XML at init
Importing the lexers package decoded the <config> element of all 285 embedded XML definitions with encoding/xml during init, which cost about 9.4 ms and 35k allocations before main starts. Generate that metadata into Go literals with a new go:generate tool (internal/metadatagen), 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 definitions and fails with a hint to re-run go generate when they drift. The analyser regexes are now compiled on the first call to AnalyseText rather than at registration time, with their validity checked at generation time instead. Registering a fresh lexer no longer scans the whole registry slice, which was quadratic across a full registration pass.
1 parent 5274f9a commit 170b608

6 files changed

Lines changed: 4300 additions & 37 deletions

File tree

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)