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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Drive the home rail from the events table

20442b5948ea72c5e84b6cf2ff922f3cc987ac13 · Keenan Nemetz · 10d ago · parent bca9ec24

Drive the home rail from the events table

The landing page still built every visible repository's whole feed in
memory to show the newest ten events across the instance. It now asks the
events table for one bounded page, with the repository names attached, so
the rail costs a page instead of the history.

The cards are separated from the feed:

- counts come from the index (issue and change totals);
- the newest event time per card is one grouped query (`LastEventTimes`)
  instead of deriving it from the feed that was built anyway;
- the release chip comes from the newest release event (`LastReleases`)
  instead of listing tags from git per card;
- the head commit and language still come from git, which is the
  remaining per-repository work on the page.

`visibleRepos` now builds handles with `Meta` rather than `Open`, which
removes a `symbolic-ref` per repository from the landing page and the
instance activity page. The all-git card builder stays as the fallback
for a Web without an index.

Load harness at concurrency 4 with three repositories: home goes from 178
to 243 req/s, p50 from 21 ms to 16 ms.

server/web/home.go +118−9

@@ -96,7 +96,15 @@ func (w *Web) IndexPage(rw http.ResponseWriter, r *http.Request) {
96 96 }
97 97
98 98 var events []activityEvent
99 data.Cards, events = w.repoCards(memoOf(r), entries)
99 memo := memoOf(r)
100 if w.Index != nil {
101 data.Cards = w.repoCards(memo, entries)
102 events = w.homeEvents(memo, entries)
103 } else {
104 // A Web without an index (a test, or a CLI-shaped Web) builds the cards
105 // and the rail from git, as every page used to.
106 data.Cards, events = w.repoCardsFromGit(memo, entries)
107 }
100 108 if query != "" {
101 109 sort.SliceStable(data.Cards, func(i, j int) bool { return data.Cards[i].Name < data.Cards[j].Name })
102 110 } else {
@@ -167,7 +175,9 @@ func (w *Web) visibleRepos(r *http.Request, query string) ([]index.Entry, error)
167 175 needle := strings.ToLower(strings.TrimSpace(query))
168 176 var visible []index.Entry
169 177 for _, e := range entries {
170 m, err := w.repo(memoOf(r), e.Name)
178 // Meta rather than Open: visibility needs the id and the ACL, not the
179 // default branch, so do not spend a git call per repository on it.
180 m, err := w.Repos.Meta(e.Name)
171 181 if err != nil {
172 182 continue
173 183 }
@@ -184,10 +194,112 @@ func (w *Web) visibleRepos(r *http.Request, query string) ([]index.Entry, error)
184 194 return visible, nil
185 195 }
186 196
187 // repoCards reads each repository once and returns its card plus its events with
188 // the repository named on them. It replaces the separate card and feed passes so
189 // the landing page does not walk each repository twice.
190 func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, []activityEvent) {
197 // repoSummary is the card's data: the counts the index knows and the history the
198 // card shows. The rail's events are a separate query, so building a card does not
199 // walk the repository's issues and comments.
200 type repoSummary struct {
201 LastCommit *git.Commit
202 OpenIssues int
203 OpenChanges int
204 }
205
206 func (w *Web) repoSummary(memo *requestMemo, m *repo.Meta) repoSummary {
207 var s repoSummary
208 if err := w.ensureIndex(memo, m); err == nil {
209 s.OpenIssues, _ = w.Index.IssueCount(m.ID, "open")
210 s.OpenChanges, _ = w.Index.RequestCount(m.ID, "open")
211 }
212 g := m.Git()
213 if commit, _ := g.RevParse(m.DefaultBranch); commit != "" {
214 if commits, err := g.Log(commit, 1); err == nil && len(commits) > 0 {
215 s.LastCommit = &commits[0]
216 }
217 }
218 return s
219 }
220
221 // repoCards builds one card per repository from the index and the repository's
222 // head. The newest event time comes from the events table for the whole page in
223 // one grouped query.
224 func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) []repoCard {
225 type item struct {
226 entry index.Entry
227 meta *repo.Meta
228 sum repoSummary
229 }
230 items := make([]item, 0, len(entries))
231 ids := make([]string, 0, len(entries))
232 for _, e := range entries {
233 m, err := w.repo(memo, e.Name)
234 if err != nil {
235 continue
236 }
237 items = append(items, item{entry: e, meta: m, sum: w.repoSummary(memo, m)})
238 ids = append(ids, e.ID)
239 }
240 updated, _ := w.Index.LastEventTimes(ids)
241 releases, _ := w.Index.LastReleases(ids)
242 cards := make([]repoCard, 0, len(items))
243 for _, it := range items {
244 card := repoCard{
245 Name: it.entry.Name, Description: it.entry.Description, Branch: it.meta.DefaultBranch,
246 Private: true, Commit: it.sum.LastCommit,
247 OpenIssues: it.sum.OpenIssues, OpenChanges: it.sum.OpenChanges,
248 }
249 if name, ok := releases[it.entry.ID]; ok {
250 card.LastTag = &git.Tag{Name: name}
251 }
252 if it.sum.LastCommit != nil {
253 if tree, err := it.meta.Git().LsTreeR(it.sum.LastCommit.SHA); err == nil {
254 card.Language, card.LangClass = topLanguage(tree)
255 }
256 }
257 // Visibility is the same ACL rule the repository header uses, so the
258 // lock/globe on a card cannot disagree with whether an anonymous clone
259 // works.
260 if acl, err := it.meta.ACL(); err == nil {
261 card.Private = !acl.Can("", access.Read)
262 }
263 if t, ok := updated[it.entry.ID]; ok {
264 card.Updated = t
265 } else if it.sum.LastCommit != nil {
266 card.Updated = time.Unix(it.sum.LastCommit.When, 0).UTC()
267 }
268 cards = append(cards, card)
269 }
270 return cards
271 }
272
273 // homeEvents is the landing page rail: the newest events across every visible
274 // repository, one events-table query, bounded to what fits in the rail.
275 func (w *Web) homeEvents(memo *requestMemo, entries []index.Entry) []activityEvent {
276 repoIDs := make([]string, 0, len(entries))
277 names := make(map[string]string, len(entries))
278 for _, e := range entries {
279 m, err := w.Repos.Meta(e.Name)
280 if err != nil {
281 continue
282 }
283 if err := w.ensureIndex(memo, m); err != nil {
284 continue
285 }
286 repoIDs = append(repoIDs, e.ID)
287 names[e.ID] = e.Name
288 }
289 rows, err := w.Index.Events(repoIDs, "", "", "", homeEventLimit, 0)
290 if err != nil {
291 return nil
292 }
293 out := make([]activityEvent, 0, len(rows))
294 for _, row := range rows {
295 out = append(out, w.activityEventFromRow(row, names[row.RepoID], true))
296 }
297 return out
298 }
299
300 // repoCardsFromGit is the fallback for a Web without an index: one pass per
301 // repository builds the card and its events from git.
302 func (w *Web) repoCardsFromGit(memo *requestMemo, entries []index.Entry) ([]repoCard, []activityEvent) {
191 303 cards := make([]repoCard, 0, len(entries))
192 304 var events []activityEvent
193 305 for _, e := range entries {
@@ -206,9 +318,6 @@ func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, [
206 318 card.Language, card.LangClass = topLanguage(tree)
207 319 }
208 320 }
209 // Visibility is the same ACL rule the repository header uses, so the
210 // lock/globe on a card cannot disagree with whether an anonymous clone
211 // works.
212 321 if acl, err := m.ACL(); err == nil {
213 322 card.Private = !acl.Can("", access.Read)
214 323 }