Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/cache.go

Line attribution on main · 156 line(s)

73ec800b
Keenan Nemetz
10d ago
1 package web
2
3 // Answers that cannot change while a commit id stays the same.
4 //
5 // The overview summarises a repository by walking its history: how many commits
a32a859a
Keenan Nemetz
4d ago
6 // it has and what it is made of. Those are properties of the commit graph, and a
7 // commit is content-addressed — its ancestors are part of its identity — so
8 // remembering them by commit id is not a stale-data risk. It is a cheaper second
9 // read of the same bytes, which matters because a repository page is read far
10 // more often than its history is written.
73ec800b
Keenan Nemetz
10d ago
11 //
12 // The cache belongs to the process, not to an account or a request: the values
13 // depend on nothing about the viewer. It is bounded and evicts oldest-first, so
14 // a thousand branches cannot turn it into a leak.
15
16 import (
2a8f7cef
Keenan Nemetz
9d ago
17 "html/template"
73ec800b
Keenan Nemetz
10d ago
18 "sync"
19
d37d301f
Keenan Nemetz
15h ago
20 "rodent.software/vole/core/git"
21 "rodent.software/vole/internal/lang"
73ec800b
Keenan Nemetz
10d ago
22 )
23
24 // commitCacheEntries is how many commits one cache remembers. A repository page
25 // is usually read at its tip, so a few hundred covers an instance's active
26 // repositories; past that the oldest entry is evicted, which only costs the
27 // next reader one git call.
28 const commitCacheEntries = 256
29
30 type commitCache[T any] struct {
31 mu sync.Mutex
32 entries map[string]T
33 order []string
34 }
35
36 func (c *commitCache[T]) get(key string) (T, bool) {
37 c.mu.Lock()
38 defer c.mu.Unlock()
39 v, ok := c.entries[key]
40 return v, ok
41 }
42
43 func (c *commitCache[T]) put(key string, v T) {
44 c.mu.Lock()
45 defer c.mu.Unlock()
46 if c.entries == nil {
47 c.entries = make(map[string]T, commitCacheEntries)
48 }
49 if _, ok := c.entries[key]; ok {
50 return
51 }
52 c.entries[key] = v
53 c.order = append(c.order, key)
54 if len(c.order) > commitCacheEntries {
55 delete(c.entries, c.order[0])
56 c.order = c.order[1:]
57 }
58 }
59
60 // commitKey names a value by repository and commit. The directory separates two
61 // repositories' identical commits, which is cheap and enough: an id is unique
62 // per repository, and the same content in two repositories should not share a
63 // slot by accident.
64 func commitKey(gitDir, commit string) string { return gitDir + "\x00" + commit }
65
66 // countCommits returns how many commits rev is made of. On a large repository
67 // this walks the whole history, which is the expensive part of the code page
68 // header, so it is worth remembering by commit.
69 func (w *Web) countCommits(g *git.Repo, rev string) (int, error) {
70 if rev == "" {
71 return 0, nil
72 }
73 key := commitKey(g.Dir, rev)
74 if n, ok := w.commitCounts.get(key); ok {
75 return n, nil
76 }
77 n, err := g.CountCommits(rev)
78 if err != nil {
79 return 0, err
80 }
81 w.commitCounts.put(key, n)
82 return n, nil
83 }
84
2a8f7cef
Keenan Nemetz
9d ago
85 // The README is the overview's largest read that is not a count: a cat-file
86 // spawn for the blob and a markdown render, for a file that changes only when
87 // its commit does. Relative links resolve against the branch the reader is on,
88 // so the ref is part of the key; the render is byte-bounded rather than counted,
89 // because one 1 MiB README is not the same as one 1 KiB one.
90
91 const (
92 readmeCacheBytes = 8 << 20
93 readmeCacheEntries = 512
94 )
95
96 type readmeAnswer struct {
97 html template.HTML
98 name string
99 }
100
101 type readmeCache struct {
102 mu sync.Mutex
103 entries map[string]readmeAnswer
104 order []string
105 bytes int
106 }
107
108 func (c *readmeCache) get(key string) (readmeAnswer, bool) {
109 c.mu.Lock()
110 defer c.mu.Unlock()
111 v, ok := c.entries[key]
112 return v, ok
113 }
114
115 func (c *readmeCache) put(key string, v readmeAnswer) {
116 size := len(v.html) + len(v.name) + 2
117 c.mu.Lock()
118 defer c.mu.Unlock()
119 if _, ok := c.entries[key]; ok {
120 return
121 }
122 if c.entries == nil {
123 c.entries = make(map[string]readmeAnswer, readmeCacheEntries)
124 }
125 c.entries[key] = v
126 c.order = append(c.order, key)
127 c.bytes += size
128 for len(c.order) > 1 && (c.bytes > readmeCacheBytes || len(c.order) > readmeCacheEntries) {
129 old := c.order[0]
130 c.order = c.order[1:]
131 if gone, ok := c.entries[old]; ok {
132 c.bytes -= len(gone.html) + len(gone.name) + 2
133 delete(c.entries, old)
134 }
135 }
136 }
137
73ec800b
Keenan Nemetz
10d ago
138 // languages describes what rev is made of, by extension. It reads the tree and
139 // every blob size once, so the overview remembers the grouping rather than the
140 // file list: the answer is small and the tree is not.
141 func (w *Web) languages(g *git.Repo, rev string) []languageShare {
142 if rev == "" {
143 return nil
144 }
145 key := commitKey(g.Dir, rev)
146 if list, ok := w.commitLanguages.get(key); ok {
147 return list
148 }
2a8f7cef
Keenan Nemetz
9d ago
149 sizes, err := g.LsTreeSizes(rev)
73ec800b
Keenan Nemetz
10d ago
150 if err != nil {
151 return nil
152 }
2a8f7cef
Keenan Nemetz
9d ago
153 list := languageShares(lang.Breakdown(sizes))
73ec800b
Keenan Nemetz
10d ago
154 w.commitLanguages.put(key, list)
155 return list
156 }