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/memo.go +129

@@ -0,0 +1,129 @@
1 package web
2
3 // What one page reads twice.
4 //
5 // A page and its header are built from the same facts: the repository, how many
6 // issues and change requests are open, and the change requests themselves. Each
7 // of those reads is git work, and the metadata branch grows with the number of
8 // issues and changes, so asking for the same list twice is the difference
9 // between a page that costs one read and one that costs two.
10 //
11 // The memo is per request and only for reads. A request that writes metadata
12 // answers with a redirect, so it has nothing to memoise and cannot be served a
13 // list it just invalidated; the next request sees the write. Nothing is cached
14 // across requests, which is why a push is visible on the next page load without
15 // a hook, a TTL, or an invalidation protocol.
16
17 import (
18 "context"
19 "fmt"
20 "net/http"
21 "sync"
22
23 "github.com/rodent-software/vole/forge/repo"
24 "github.com/rodent-software/vole/metadata"
25 )
26
27 // memoKey is the context key for the request's memo. It is a private type so no
28 // other package can install a value under it.
29 type memoKey struct{}
30
31 type memoCounts struct {
32 issues, requests int
33 }
34
35 type memoRequests struct {
36 list []*metadata.Request
37 err error
38 }
39
40 type requestMemo struct {
41 mu sync.Mutex
42 repos map[string]*repo.Meta
43 counts map[string]memoCounts
44 lists map[string]memoRequests
45 }
46
47 func newRequestMemo() *requestMemo {
48 return &requestMemo{
49 repos: map[string]*repo.Meta{},
50 counts: map[string]memoCounts{},
51 lists: map[string]memoRequests{},
52 }
53 }
54
55 // withMemo gives a read request one memo. A mutating request gets none: it may
56 // have just changed what it would be reading.
57 func withMemo(r *http.Request) *http.Request {
58 if r.Method != http.MethodGet && r.Method != http.MethodHead {
59 return r
60 }
61 return r.WithContext(context.WithValue(r.Context(), memoKey{}, newRequestMemo()))
62 }
63
64 // memoOf returns the request's memo, or nil when the request has none. Callers
65 // treat nil as "read it directly", so a helper can be used from a write handler
66 // that has no memo.
67 func memoOf(r *http.Request) *requestMemo {
68 memo, _ := r.Context().Value(memoKey{}).(*requestMemo)
69 return memo
70 }
71
72 // repo opens a repository by name, once per request. Opening it resolves HEAD's
73 // branch with a git call, and a page opens the same repository for its header,
74 // its body and its ACL: the same fact does not need three processes.
75 func (w *Web) repo(memo *requestMemo, name string) (*repo.Meta, error) {
76 if memo != nil {
77 memo.mu.Lock()
78 m, ok := memo.repos[name]
79 memo.mu.Unlock()
80 if ok {
81 return m, nil
82 }
83 }
84 m, err := w.Repos.Open(name)
85 if err != nil {
86 return nil, err
87 }
88 if memo != nil {
89 memo.mu.Lock()
90 memo.repos[name] = m
91 memo.mu.Unlock()
92 }
93 return m, nil
94 }
95
96 // requestsAt returns the change requests on the metadata branch, reading them
97 // once per request. The list is the whole branch, so the header's open count and
98 // the page's rows share one read rather than paying for the tree twice.
99 func (w *Web) requestsAt(memo *requestMemo, m *repo.Meta) ([]*metadata.Request, error) {
100 if memo == nil {
101 return metadata.LoadRequests(m.Git(), metadata.Ref)
102 }
103 memo.mu.Lock()
104 entry, ok := memo.lists[m.Name]
105 memo.mu.Unlock()
106 if ok {
107 return entry.list, entry.err
108 }
109 list, err := metadata.LoadRequests(m.Git(), metadata.Ref)
110 memo.mu.Lock()
111 memo.lists[m.Name] = memoRequests{list: list, err: err}
112 memo.mu.Unlock()
113 return list, err
114 }
115
116 // requestAt returns one change request from the request's list. It is the
117 // memoised form of metadata.GetRequest, which reads every request to find one.
118 func (w *Web) requestAt(memo *requestMemo, m *repo.Meta, id string) (*metadata.Request, error) {
119 list, err := w.requestsAt(memo, m)
120 if err != nil {
121 return nil, err
122 }
123 for _, req := range list {
124 if req.ID == id {
125 return req, nil
126 }
127 }
128 return nil, fmt.Errorf("requests: no request %s", id)
129 }