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.

Store the overview's summaries when a push lands

2a8f7cef47240564179a45126863d1ccd8d9d362 · Keenan Nemetz · 9d ago · parent 8dc56e4e

Store the overview's summaries when a push lands

The repository overview walked git on every read: `rev-parse` for the
tip, `log` for the last five commits, `for-each-ref` for tags, `ls-tree`
plus `shortlog` for languages and contributors, and a `cat-file` for the
README. All of those describe a ref except the README, and a ref moves
only when a push lands, so the push now records them.

- Schema v10 adds `repo_languages`, `repo_commits`, `repo_contributors`
  and `repo_tags`.
- `RefreshHead` stores the tip's newest commits and its language
  breakdown from one `ls-tree -r -l`, and returns whether the tip moved.
- `RefreshContributors` stores the top authors; the hook calls it only
  when the tip changed, never on a metadata or tag push.
- `RefreshTags` stores the tag rows beside the release events it already
  rewrote, and `IndexRepo` now refreshes tags and contributors too, so a
  rebuild restores everything a push would have written.
- The overview reads those rows when the recorded head is the branch tip
  (`RefCommit` is a loose-ref file read) and falls back to git when it is
  not, so a missed hook shows the correct page rather than a stale one.
  If one stored section is empty on a repository with commits, that
  section alone is re-read from git.
- The sidebar asks SQL for five open issues and changes instead of
  sorting the whole open queue in memory.
- The rendered README is cached by repository, commit and ref under a
  byte budget, so the overview's last git spawn is gone after the first
  read of a commit.
- The language fallback reads one `ls-tree -r -l` instead of `ls-tree`
  followed by a `cat-file --batch-check`.
- `Remove` deletes every derived row for a repository, not only its row.

Load harness at concurrency 4: overview goes from 473 to about 740 req/s,
p50 from 8 ms to 5 ms, and the server runs no git child process for the
warm requests.

server/web/cache.go +57−2

@@ -14,9 +14,11 @@ package web
14 14 // a thousand branches cannot turn it into a leak.
15 15
16 16 import (
17 "html/template"
17 18 "sync"
18 19
19 20 "github.com/rodent-software/vole/core/git"
21 "github.com/rodent-software/vole/internal/lang"
20 22 )
21 23
22 24 // commitCacheEntries is how many commits one cache remembers. A repository page
@@ -105,6 +107,59 @@ func (w *Web) contributors(g *git.Repo, rev string) []contributorView {
105 107 return list
106 108 }
107 109
110 // The README is the overview's largest read that is not a count: a cat-file
111 // spawn for the blob and a markdown render, for a file that changes only when
112 // its commit does. Relative links resolve against the branch the reader is on,
113 // so the ref is part of the key; the render is byte-bounded rather than counted,
114 // because one 1 MiB README is not the same as one 1 KiB one.
115
116 const (
117 readmeCacheBytes = 8 << 20
118 readmeCacheEntries = 512
119 )
120
121 type readmeAnswer struct {
122 html template.HTML
123 name string
124 }
125
126 type readmeCache struct {
127 mu sync.Mutex
128 entries map[string]readmeAnswer
129 order []string
130 bytes int
131 }
132
133 func (c *readmeCache) get(key string) (readmeAnswer, bool) {
134 c.mu.Lock()
135 defer c.mu.Unlock()
136 v, ok := c.entries[key]
137 return v, ok
138 }
139
140 func (c *readmeCache) put(key string, v readmeAnswer) {
141 size := len(v.html) + len(v.name) + 2
142 c.mu.Lock()
143 defer c.mu.Unlock()
144 if _, ok := c.entries[key]; ok {
145 return
146 }
147 if c.entries == nil {
148 c.entries = make(map[string]readmeAnswer, readmeCacheEntries)
149 }
150 c.entries[key] = v
151 c.order = append(c.order, key)
152 c.bytes += size
153 for len(c.order) > 1 && (c.bytes > readmeCacheBytes || len(c.order) > readmeCacheEntries) {
154 old := c.order[0]
155 c.order = c.order[1:]
156 if gone, ok := c.entries[old]; ok {
157 c.bytes -= len(gone.html) + len(gone.name) + 2
158 delete(c.entries, old)
159 }
160 }
161 }
162
108 163 // languages describes what rev is made of, by extension. It reads the tree and
109 164 // every blob size once, so the overview remembers the grouping rather than the
110 165 // file list: the answer is small and the tree is not.
@@ -116,11 +171,11 @@ func (w *Web) languages(g *git.Repo, rev string) []languageShare {
116 171 if list, ok := w.commitLanguages.get(key); ok {
117 172 return list
118 173 }
119 blobs, err := g.TreeBlobs(rev)
174 sizes, err := g.LsTreeSizes(rev)
120 175 if err != nil {
121 176 return nil
122 177 }
123 list := languageShares(blobs)
178 list := languageShares(lang.Breakdown(sizes))
124 179 w.commitLanguages.put(key, list)
125 180 return list
126 181 }