| … | … | @@ -32,8 +32,16 @@ function dirSize (p) { |
| 32 | 32 | /** |
| 33 | 33 | * Scan a directory for bare git repos. |
| 34 | 34 | * Returns a Map of repo name → info. |
| 35 | * |
| 36 | * `opts.previous` is the Map from the last scan. Repos whose footprint key |
| 37 | * is unchanged carry their packSize forward instead of re-walking the tree, |
| 38 | * which is ~89% of the scan's cost and blocks the event loop while it runs. |
| 39 | * Omit it for a full, unconditional scan — the state stays with the caller, |
| 40 | * so this function has no hidden memo of its own and repeat calls without |
| 41 | * `previous` behave exactly as before. |
| 35 | 42 | */ |
| 36 | | function scanRepos (dir) { |
| 43 | function scanRepos (dir, opts = {}) { |
| 44 | const previous = opts.previous instanceof Map ? opts.previous : null; |
| 37 | 45 | const repos = new Map(); |
| 38 | 46 | let entries; |
| 39 | 47 | |
| … | … | @@ -56,7 +64,7 @@ function scanRepos (dir) { |
| 56 | 64 | continue; |
| 57 | 65 | } |
| 58 | 66 | |
| 59 | | const info = repoInfo(repoPath, name); |
| 67 | const info = repoInfo(repoPath, name, previous ? previous.get(name) : null); |
| 60 | 68 | if (info) repos.set(name, info); |
| 61 | 69 | } |
| 62 | 70 | |
| … | … | @@ -112,7 +120,7 @@ function newestRefMtime (dir) { |
| 112 | 120 | * HEAD to the first available branch — the SAME branch repoInfo |
| 113 | 121 | * picks for the .head field, so downstream readers stay consistent. |
| 114 | 122 | */ |
| 115 | | function repoInfo (repoPath, name) { |
| 123 | function repoInfo (repoPath, name, cached = null) { |
| 116 | 124 | const refs = readRefs(repoPath); |
| 117 | 125 | const branches = Object.keys(refs); |
| 118 | 126 | const headBranch = refs.master ? 'master' |
| … | … | @@ -121,13 +129,43 @@ function repoInfo (repoPath, name) { |
| 121 | 129 | const head = headBranch ? refs[headBranch] : null; |
| 122 | 130 | if (headBranch) repointHeadIfDangling(repoPath, refs, headBranch); |
| 123 | 131 | |
| 132 | // Computed once and used twice below: as the newest-activity signal, and |
| 133 | // as part of the footprint key. |
| 134 | const refsMtime = newestRefMtime(path.join(repoPath, 'refs')); |
| 135 | |
| 136 | // Cheap fingerprint of the repo's on-disk footprint. Three stats stand in |
| 137 | // for a full tree walk: the newest ref write (a push), objects/ (new |
| 138 | // loose-object subdirs), and objects/pack/ (a repack or gc). Together |
| 139 | // those cover every way a repo normally gains or loses bytes. |
| 140 | let footprint = refsMtime || 0; |
| 141 | for (const rel of [['objects'], ['objects', 'pack']]) { |
| 142 | try { |
| 143 | const st = fs.statSync(path.join(repoPath, ...rel)); |
| 144 | if (st.mtimeMs > footprint) footprint = st.mtimeMs; |
| 145 | } catch {} |
| 146 | } |
| 147 | |
| 124 | 148 | // Real bare-repo footprint: walk the whole dir. Counts loose objects |
| 125 | 149 | // under objects/xx/, packs + idx + bitmap + rev, refs/, packed-refs, |
| 126 | 150 | // info/, hooks/, etc. Previously only `objects/pack/*.pack` bytes were |
| 127 | 151 | // counted — that misses freshly-fetched repos where content is still |
| 128 | 152 | // loose, and produces 0 for many real repos (which then renders blank |
| 129 | 153 | // on /repos because of `truthy ?` template check). |
| 130 | | const size = dirSize(repoPath); |
| 154 | // |
| 155 | // That walk is ~89% of a scan (364ms of 409ms across 147 live repos, |
| 156 | // 7.9GB), and scanRepos is synchronous — so it stalls the daemon's event |
| 157 | // loop, and the repos-dir watcher re-runs it on every push. An untouched |
| 158 | // repo cannot have changed size, so carry the previous value forward when |
| 159 | // the footprint key matches. |
| 160 | // |
| 161 | // Deliberately not exhaustive: a gc that only prunes loose objects inside |
| 162 | // an existing objects/xx/ subdir, touching no ref and no pack, does not |
| 163 | // move the key, leaving a slightly stale byte count until the repo next |
| 164 | // changes. packSize is display-only, so that is the right trade against |
| 165 | // stalling the event loop on every scan. |
| 166 | const size = (cached && cached.footprint === footprint && typeof cached.packSize === 'number') |
| 167 | ? cached.packSize |
| 168 | : dirSize(repoPath); |
| 131 | 169 | |
| 132 | 170 | // Last-activity signal, as the newest of three independent sources. Each |
| 133 | 171 | // is probed separately so a missing one cannot suppress the others — the |
| … | … | @@ -149,7 +187,7 @@ function repoInfo (repoPath, name) { |
| 149 | 187 | |
| 150 | 188 | try { bump(fs.statSync(path.join(repoPath, 'HEAD')).mtimeMs); } catch {} |
| 151 | 189 | try { bump(fs.statSync(path.join(repoPath, 'packed-refs')).mtimeMs); } catch {} |
| 152 | | bump(newestRefMtime(path.join(repoPath, 'refs'))); |
| 190 | bump(refsMtime); |
| 153 | 191 | |
| 154 | 192 | return { |
| 155 | 193 | name, |
| … | … | @@ -158,7 +196,8 @@ function repoInfo (repoPath, name) { |
| 158 | 196 | branchCount: branches.length, |
| 159 | 197 | head, |
| 160 | 198 | packSize: size, |
| 161 | | lastUpdated |
| 199 | lastUpdated, |
| 200 | footprint |
| 162 | 201 | }; |
| 163 | 202 | } |
| 164 | 203 | |