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 | } | ||