Skip to content

Commit 94a6d83

Browse files
committed
perf(octicons): cache embedded data URIs on demand
1 parent c36e4e4 commit 94a6d83

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

pkg/octicons/octicons.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/base64"
99
"fmt"
1010
"strings"
11+
"sync"
1112

1213
"github.com/modelcontextprotocol/go-sdk/mcp"
1314
)
@@ -18,6 +19,47 @@ var iconsFS embed.FS
1819
//go:embed required_icons.txt
1920
var requiredIconsTxt string
2021

22+
type dataURIKey struct {
23+
name string
24+
theme Theme
25+
}
26+
27+
type dataURICache struct {
28+
mu sync.RWMutex
29+
values map[dataURIKey]string
30+
}
31+
32+
// This covers the current embedded variants without encoding them at startup.
33+
const initialDataURICacheCapacity = 64
34+
35+
func (c *dataURICache) load(name string, theme Theme) string {
36+
key := dataURIKey{name: name, theme: theme}
37+
c.mu.RLock()
38+
cached, ok := c.values[key]
39+
c.mu.RUnlock()
40+
if ok {
41+
return cached
42+
}
43+
44+
dataURI := readDataURI(name, theme)
45+
if dataURI == "" {
46+
return ""
47+
}
48+
49+
c.mu.Lock()
50+
defer c.mu.Unlock()
51+
if cached, ok := c.values[key]; ok {
52+
return cached
53+
}
54+
if c.values == nil {
55+
c.values = make(map[dataURIKey]string, initialDataURICacheCapacity)
56+
}
57+
c.values[key] = dataURI
58+
return dataURI
59+
}
60+
61+
var dataURIs dataURICache
62+
2163
// RequiredIcons returns the list of icon names from required_icons.txt.
2264
// This is the single source of truth for which icons should be embedded.
2365
func RequiredIcons() []string {
@@ -50,6 +92,10 @@ const (
5092
// - ThemeDark: light icons for dark backgrounds
5193
// If the icon is not found in the embedded filesystem, it returns an empty string.
5294
func DataURI(name string, theme Theme) string {
95+
return dataURIs.load(name, theme)
96+
}
97+
98+
func readDataURI(name string, theme Theme) string {
5399
filename := fmt.Sprintf("icons/%s-%s.png", name, theme)
54100
data, err := iconsFS.ReadFile(filename)
55101
if err != nil {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package octicons
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
var benchmarkDataURISink string
11+
var benchmarkIconsSink [][]mcp.Icon
12+
13+
func BenchmarkDataURICache(b *testing.B) {
14+
b.Run("cold", func(b *testing.B) {
15+
b.ReportAllocs()
16+
for b.Loop() {
17+
var cache dataURICache
18+
benchmarkDataURISink = cache.load("repo", ThemeLight)
19+
}
20+
})
21+
22+
b.Run("warm", func(b *testing.B) {
23+
var cache dataURICache
24+
benchmarkDataURISink = cache.load("repo", ThemeLight)
25+
b.ReportAllocs()
26+
b.ResetTimer()
27+
for b.Loop() {
28+
benchmarkDataURISink = cache.load("repo", ThemeLight)
29+
}
30+
})
31+
}
32+
33+
// Run each ColdStart inventory separately with -benchtime=1x in a fresh
34+
// `go test` process so package-level cache state cannot cross-contaminate it.
35+
func BenchmarkIconsRegistrationColdStart(b *testing.B) {
36+
benchmarkIconsRegistration(b, false)
37+
}
38+
39+
func BenchmarkIconsRegistrationWarm(b *testing.B) {
40+
benchmarkIconsRegistration(b, true)
41+
}
42+
43+
func benchmarkIconsRegistration(b *testing.B, warm bool) {
44+
inventories := map[string][]string{
45+
"narrow": {"repo"},
46+
"default": RequiredIcons(),
47+
}
48+
for name, inventory := range inventories {
49+
b.Run(name, func(b *testing.B) {
50+
if warm {
51+
for _, icon := range inventory {
52+
_ = Icons(icon)
53+
}
54+
} else {
55+
dataURIs.mu.Lock()
56+
dataURIs.values = nil
57+
dataURIs.mu.Unlock()
58+
}
59+
60+
batch := make([][]mcp.Icon, len(inventory))
61+
b.ReportAllocs()
62+
b.ResetTimer()
63+
for b.Loop() {
64+
for index, icon := range inventory {
65+
batch[index] = Icons(icon)
66+
}
67+
}
68+
b.StopTimer()
69+
benchmarkIconsSink = batch
70+
runtime.KeepAlive(batch)
71+
})
72+
}
73+
}

pkg/octicons/octicons_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package octicons
22

33
import (
4+
"io/fs"
45
"strings"
6+
"sync"
57
"testing"
68

79
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -53,6 +55,65 @@ func TestDataURI(t *testing.T) {
5355
}
5456
}
5557

58+
func TestDataURIForEveryEmbeddedIcon(t *testing.T) {
59+
paths, err := fs.Glob(iconsFS, "icons/*.png")
60+
assert.NoError(t, err)
61+
assert.NotEmpty(t, paths)
62+
63+
for _, path := range paths {
64+
filename := strings.TrimSuffix(strings.TrimPrefix(path, "icons/"), ".png")
65+
separator := strings.LastIndexByte(filename, '-')
66+
if separator <= 0 {
67+
t.Errorf("cannot parse embedded icon path %q", path)
68+
continue
69+
}
70+
name := filename[:separator]
71+
theme := Theme(filename[separator+1:])
72+
t.Run(filename, func(t *testing.T) {
73+
assert.True(t, strings.HasPrefix(DataURI(name, theme), "data:image/png;base64,"))
74+
})
75+
}
76+
}
77+
78+
func TestDataURICacheOnlyStoresSuccessfulReads(t *testing.T) {
79+
var cache dataURICache
80+
missingKey := dataURIKey{name: "nonexistent-icon", theme: ThemeLight}
81+
82+
assert.Empty(t, cache.load(missingKey.name, missingKey.theme))
83+
assert.Nil(t, cache.values, "missing icons must not initialize the cache")
84+
_, found := cache.values[missingKey]
85+
assert.False(t, found, "missing icons must not be cached")
86+
87+
validKey := dataURIKey{name: "repo", theme: ThemeLight}
88+
assert.NotEmpty(t, cache.load(validKey.name, validKey.theme))
89+
_, found = cache.values[validKey]
90+
assert.True(t, found, "successful reads should be cached")
91+
}
92+
93+
func TestDataURICacheConcurrentFirstUse(t *testing.T) {
94+
var cache dataURICache
95+
want := readDataURI("repo", ThemeLight)
96+
assert.NotEmpty(t, want)
97+
98+
const workers = 64
99+
start := make(chan struct{})
100+
results := make(chan string, workers)
101+
var wg sync.WaitGroup
102+
for range workers {
103+
wg.Go(func() {
104+
<-start
105+
results <- cache.load("repo", ThemeLight)
106+
})
107+
}
108+
109+
close(start)
110+
wg.Wait()
111+
close(results)
112+
for result := range results {
113+
assert.Equal(t, want, result)
114+
}
115+
}
116+
56117
func TestIcons(t *testing.T) {
57118
tests := []struct {
58119
name string
@@ -99,6 +160,23 @@ func TestIcons(t *testing.T) {
99160
}
100161
}
101162

163+
func TestIconsReturnsFreshSlice(t *testing.T) {
164+
lightSource := DataURI("repo", ThemeLight)
165+
darkSource := DataURI("repo", ThemeDark)
166+
167+
icons := Icons("repo")
168+
icons[0] = mcp.Icon{}
169+
icons[1].Source = "mutated"
170+
171+
fresh := Icons("repo")
172+
assert.Equal(t, lightSource, fresh[0].Source)
173+
assert.Equal(t, darkSource, fresh[1].Source)
174+
assert.Equal(t, "image/png", fresh[0].MIMEType)
175+
assert.Equal(t, "image/png", fresh[1].MIMEType)
176+
assert.Equal(t, mcp.IconThemeLight, fresh[0].Theme)
177+
assert.Equal(t, mcp.IconThemeDark, fresh[1].Theme)
178+
}
179+
102180
func TestThemeConstants(t *testing.T) {
103181
assert.Equal(t, Theme("light"), ThemeLight)
104182
assert.Equal(t, Theme("dark"), ThemeDark)

0 commit comments

Comments
 (0)