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.

Read what a page needs once per request

5d440e37f16d06790ce87e2f8b7dcc7d9bf0532c · Keenan Nemetz · 10d ago · parent 34cbc99f

Read what a page needs once per request

A page and its header are built from the same facts, and each of them was
read separately: the repository (twice, each resolving HEAD's branch with
a git call), the open change requests (once for the header's count, once
for the page), and a change request looked up by reading every request.
On a repository with 30 change requests the overview paid 21 git
invocations after the metadata batching, and the requests list still paid
8.

Give each read request one memo, installed by the middleware, and have the
handlers and the header read through it. It lasts one request and covers
reads only: a request that writes metadata gets no memo, so there is no
TTL, no cross-request cache, and no invalidation protocol, and a push is
still visible on the next page load.

The header now takes the page it is rendering (`nav .Page "issues"`)
instead of six fields the view already carries, which is also why the memo
can reach it at all. Templates are unchanged in what they output.

server/web/page.go +38−14

@@ -10,14 +10,16 @@ import (
10 10
11 11 "github.com/rodent-software/vole/forge/access"
12 12 "github.com/rodent-software/vole/forge/repo"
13 "github.com/rodent-software/vole/metadata"
14 13 )
15 14
16 15 // Page is embedded by every view. Exported so html/template can promote its
17 16 // fields; an unexported embedded type is not reachable from a template.
18 17 type Page struct {
19 Title string
20 Error string
18 Title string
19 Error string
20 // RepoName is the repository the path names, empty on instance-wide pages.
21 // The header reads it instead of parsing the URL again.
22 RepoName string
21 23 LoggedIn bool
22 24 User string
23 25 AccountName string
@@ -46,6 +48,10 @@ type Page struct {
46 48 // Section is the global destination the current page belongs to, used by the
47 49 // rail to mark the current item. It is presentation only.
48 50 Section string
51 // memo is what this request has already read. It is unexported because no
52 // template may depend on it: the header only needs it to stop repeating work
53 // the handler did.
54 memo *requestMemo
49 55 }
50 56
51 57 // pageSection maps a request path to the global destination it belongs to. A
@@ -84,10 +90,14 @@ func (w *Web) who(name string) string {
84 90 // advertise. Visibility comes from the same ACL rule the handlers use, so the
85 91 // lock/globe icon cannot disagree with whether an anonymous clone works.
86 92 // Templates call it as
87 // {{template "repohead" (nav .Name "issues" .CanAdmin .CloneURL .SSHURL .Site)}}.
88 func (w *Web) repoNav(name, tab string, canAdmin bool, cloneURL, sshURL, site string) repoNavData {
89 n := repoNavData{Name: name, Tab: tab, Private: true, CanAdmin: canAdmin, CloneURL: cloneURL, SSHURL: sshURL, Site: site}
90 m, err := w.Repos.Open(name)
93 // {{template "repohead" (nav .Page "issues")}}.
94 func (w *Web) repoNav(p Page, tab string) repoNavData {
95 n := repoNavData{Name: p.RepoName, Tab: tab, Private: true, CanAdmin: p.CanAdmin,
96 CloneURL: p.CloneURL, SSHURL: p.SSHURL, Site: p.Site}
97 if p.RepoName == "" {
98 return n
99 }
100 m, err := w.repo(p.memo, p.RepoName)
91 101 if err != nil {
92 102 return n
93 103 }
@@ -95,16 +105,25 @@ func (w *Web) repoNav(name, tab string, canAdmin bool, cloneURL, sshURL, site st
95 105 if acl, err := m.ACL(); err == nil {
96 106 n.Private = !acl.Can("", access.Read)
97 107 }
98 n.OpenIssues, n.OpenRequests = w.openCounts(m)
108 n.OpenIssues, n.OpenRequests = w.openCounts(p.memo, m)
99 109 return n
100 110 }
101 111
102 112 // openCounts returns the counts the repository tabs show. They describe the
103 // whole repository, not a filtered page: a badge that changed when you searched
104 // would be a lie. A read that fails leaves a zero rather than failing the page
105 // the reader asked for; the tab still links to the list, which reports its own
106 // error if the same read fails there.
107 func (w *Web) openCounts(m *repo.Meta) (issues, requests int) {
113 // whole repository, not a filtered page: an "open" badge that changes when you
114 // searched would be a lie. A read that fails leaves a zero rather than failing
115 // the page the reader asked for; the tab still links to the list, which reports
116 // its own error if the same read fails there. The counts are remembered for the
117 // rest of the request, because the page asked for the same list.
118 func (w *Web) openCounts(memo *requestMemo, m *repo.Meta) (issues, requests int) {
119 if memo != nil {
120 memo.mu.Lock()
121 c, ok := memo.counts[m.Name]
122 memo.mu.Unlock()
123 if ok {
124 return c.issues, c.requests
125 }
126 }
108 127 if w.Index != nil {
109 128 if err := w.Index.Ensure(m); err == nil {
110 129 if n, err := w.Index.IssueCount(m.ID, "open"); err == nil {
@@ -112,13 +131,18 @@ func (w *Web) openCounts(m *repo.Meta) (issues, requests int) {
112 131 }
113 132 }
114 133 }
115 if list, err := metadata.LoadRequests(m.Git(), metadata.Ref); err == nil {
134 if list, err := w.requestsAt(memo, m); err == nil {
116 135 for _, req := range list {
117 136 if req.State == "open" {
118 137 requests++
119 138 }
120 139 }
121 140 }
141 if memo != nil {
142 memo.mu.Lock()
143 memo.counts[m.Name] = memoCounts{issues: issues, requests: requests}
144 memo.mu.Unlock()
145 }
122 146 return issues, requests
123 147 }
124 148