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.
Stop repeating the index check and the history summaries
73ec800beafe53eea186fbc1cebec3306cc49da0 · Keenan Nemetz · 10d ago · parent 5d440e37
Stop repeating the index check and the history summaries The request memo covered the repository, the open counts and the change list, but two more reads were repeated: - The index refresh (issue rows, filters, `#42` numbers) ran on every surface that shows one, and each run started by resolving the metadata ref. A page calls it from the header, the number lookup and its own view, so it now happens once per request, and the resolved commit is shared with the change-request read instead of being resolved twice. - The overview walked the repository's whole history on every load: `shortlog` for contributors, `rev-list --count` for the code page's commit count, and the tree grouping for languages. Those are properties of a commit, and a commit id is content-addressed, so they cannot go stale: they are remembered by commit id in a bounded per-process cache that evicts oldest-first. On the fixture (30 change requests, 30 issues) the overview drops from 19 git calls to 9 on a repeated load, the code page from 12 to 11, and every page loses one `rev-parse` for the metadata ref.
server/web/memo.go +69−8
| @@ -37,18 +37,30 @@ type memoRequests struct { | |||
| 37 | 37 | err error | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | // memoHead is the metadata branch's commit as resolved for this request. The | ||
| 41 | // index and the request list both need it, and it cannot move under a read | ||
| 42 | // request without a push arriving mid-render. | ||
| 43 | type memoHead struct { | ||
| 44 | sha string | ||
| 45 | err error | ||
| 46 | } | ||
| 47 | |||
| 40 | 48 | type requestMemo struct { | |
| 41 | mu sync.Mutex | ||
| 42 | repos map[string]*repo.Meta | ||
| 43 | counts map[string]memoCounts | ||
| 44 | lists map[string]memoRequests | ||
| 49 | mu sync.Mutex | ||
| 50 | repos map[string]*repo.Meta | ||
| 51 | counts map[string]memoCounts | ||
| 52 | lists map[string]memoRequests | ||
| 53 | ensured map[string]error | ||
| 54 | heads map[string]memoHead | ||
| 45 | 55 | } | |
| 46 | 56 | ||
| 47 | 57 | func newRequestMemo() *requestMemo { | |
| 48 | 58 | return &requestMemo{ | |
| 49 | repos: map[string]*repo.Meta{}, | ||
| 50 | counts: map[string]memoCounts{}, | ||
| 51 | lists: map[string]memoRequests{}, | ||
| 59 | repos: map[string]*repo.Meta{}, | ||
| 60 | counts: map[string]memoCounts{}, | ||
| 61 | lists: map[string]memoRequests{}, | ||
| 62 | ensured: map[string]error{}, | ||
| 63 | heads: map[string]memoHead{}, | ||
| 52 | 64 | } | |
| 53 | 65 | } | |
| 54 | 66 | ||
| @@ -93,6 +105,51 @@ func (w *Web) repo(memo *requestMemo, name string) (*repo.Meta, error) { | |||
| 93 | 105 | return m, nil | |
| 94 | 106 | } | |
| 95 | 107 | ||
| 108 | // metadataHead resolves the metadata branch once per request. Both the index | ||
| 109 | // refresh and the change-request read start by resolving it, and a page needs | ||
| 110 | // both. | ||
| 111 | func (w *Web) metadataHead(memo *requestMemo, m *repo.Meta) (string, error) { | ||
| 112 | if memo == nil { | ||
| 113 | return m.Git().RevParse(metadata.Ref) | ||
| 114 | } | ||
| 115 | memo.mu.Lock() | ||
| 116 | h, ok := memo.heads[m.Name] | ||
| 117 | memo.mu.Unlock() | ||
| 118 | if ok { | ||
| 119 | return h.sha, h.err | ||
| 120 | } | ||
| 121 | sha, err := m.Git().RevParse(metadata.Ref) | ||
| 122 | memo.mu.Lock() | ||
| 123 | memo.heads[m.Name] = memoHead{sha: sha, err: err} | ||
| 124 | memo.mu.Unlock() | ||
| 125 | return sha, err | ||
| 126 | } | ||
| 127 | |||
| 128 | // ensureIndex refreshes the rows the repository derives from its metadata branch | ||
| 129 | // at most once per request. Every surface that shows a number or a count asks | ||
| 130 | // for it, and the refresh starts with a git call to see whether the branch | ||
| 131 | // moved: the header, the issue list and the number in a URL should not each pay | ||
| 132 | // for the same check on one page. | ||
| 133 | func (w *Web) ensureIndex(memo *requestMemo, m *repo.Meta) error { | ||
| 134 | if memo == nil { | ||
| 135 | return w.Index.Ensure(m) | ||
| 136 | } | ||
| 137 | memo.mu.Lock() | ||
| 138 | err, ok := memo.ensured[m.Name] | ||
| 139 | memo.mu.Unlock() | ||
| 140 | if ok { | ||
| 141 | return err | ||
| 142 | } | ||
| 143 | head, err := w.metadataHead(memo, m) | ||
| 144 | if err == nil { | ||
| 145 | err = w.Index.EnsureAt(m, head) | ||
| 146 | } | ||
| 147 | memo.mu.Lock() | ||
| 148 | memo.ensured[m.Name] = err | ||
| 149 | memo.mu.Unlock() | ||
| 150 | return err | ||
| 151 | } | ||
| 152 | |||
| 96 | 153 | // requestsAt returns the change requests on the metadata branch, reading them | |
| 97 | 154 | // once per request. The list is the whole branch, so the header's open count and | |
| 98 | 155 | // the page's rows share one read rather than paying for the tree twice. | |
| @@ -106,7 +163,11 @@ func (w *Web) requestsAt(memo *requestMemo, m *repo.Meta) ([]*metadata.Request, | |||
| 106 | 163 | if ok { | |
| 107 | 164 | return entry.list, entry.err | |
| 108 | 165 | } | |
| 109 | list, err := metadata.LoadRequests(m.Git(), metadata.Ref) | ||
| 166 | head, err := w.metadataHead(memo, m) | ||
| 167 | var list []*metadata.Request | ||
| 168 | if err == nil { | ||
| 169 | list, err = metadata.LoadRequestsAt(m.Git(), head) | ||
| 170 | } | ||
| 110 | 171 | memo.mu.Lock() | |
| 111 | 172 | memo.lists[m.Name] = memoRequests{list: list, err: err} | |
| 112 | 173 | memo.mu.Unlock() | |