Commit

Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them

An uncached walk read one tree per round trip and ran past the page's 5 s stream timeout, which failed the whole page. Trees are now read ahead in parallel batches, and the page goes out without the column after 3 s while the walk finishes in the background into the cache.

syntaqxcommitted Parent1aa1479Browse files
4 files+64−80/4 viewed
+25−0
1+/**
2+ * Each entry's last commit for the file list, waited on only so long. An
3+ * answer not yet kept walks history, which can take seconds on a large or
4+ * busy repository; the page goes out without it then, the column empty,
5+ * while the walk finishes in the background (waitUntil) so the next view
6+ * has it from the cache. The page's stream never waits on it past its own
7+ * timeout.
8+ */
9+import { waitUntil } from "cloudflare:workers";
10+
11+import type { LastCommits, RepoPath, Viewer } from "@g1t/contracts";
12+
13+import { repos } from "./services.server";
14+
15+/** How long the page waits for the column. */
16+const WAIT_MS = 3_000;
17+
18+export function lastCommitsFor(path: RepoPath, viewer: Viewer, ref: string | null, treePath: string): Promise<LastCommits | null> {
19+ const walk = repos
20+ .lastCommits(path, viewer, ref, treePath)
21+ .then((found) => (found.ok ? found.value : null))
22+ .catch(() => null);
23+ waitUntil(walk);
24+ return Promise.race([walk, new Promise<null>((resolve) => setTimeout(() => resolve(null), WAIT_MS))]);
25+}
+2−4
11 import type { Route } from "./+types/code";
22 import { TreeView } from "../../components/repo-view";
33 import { page } from "../../lib/meta";
4+import { lastCommitsFor } from "../../lib/last-commits.server";
45 import { repos } from "../../lib/services.server";
56 import { getViewer, unwrap } from "../../lib/session.server";
67
1718 repos.branches(path, viewer).catch(() => null),
1819 ]);
1920 // Each entry's last commit walks history: streamed in after the list.
20− const lastCommits = repos
21− .lastCommits(path, viewer, null, "")
22− .then((found) => (found.ok ? found.value : null))
23− .catch(() => null);
21+ const lastCommits = lastCommitsFor(path, viewer, null, "");
2422 return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null, lastCommits };
2523 }
2624
+2−4
22 import { page } from "../../lib/meta";
33 import { TreeView } from "../../components/repo-view";
44 import { redirectIfBranchRenamed } from "../../lib/branch-redirect.server";
5+import { lastCommitsFor } from "../../lib/last-commits.server";
56 import { repos } from "../../lib/services.server";
67 import { getViewer, unwrap } from "../../lib/session.server";
78
2122 // A branch that was renamed: the same folder on its new name.
2223 if (!tree.ok && tree.error.code === "not_found") await redirectIfBranchRenamed(request, path, viewer, params.ref);
2324 // Each entry's last commit walks history: streamed in after the list.
24− const lastCommits = repos
25− .lastCommits(path, viewer, params.ref, params["*"] ?? "")
26− .then((found) => (found.ok ? found.value : null))
27− .catch(() => null);
25+ const lastCommits = lastCommitsFor(path, viewer, params.ref, params["*"] ?? "");
2826 return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null, lastCommits };
2927 }
3028
+35−0
1616
1717 /// How far back the history is walked.
1818 pub const MAX_COMMITS: u32 = 300;
19+/// Commits whose trees are read together, ahead of the walk: each read is a
20+/// round trip to the store, so reading them one by one is what is slow.
21+const READ_AHEAD: usize = 24;
1922
2023 /// Reads trees, remembering those already read: commits share most of them.
2124 struct Trees<'a, R: GitRepo> {
2427 }
2528
2629 impl<'a, R: GitRepo> Trees<'a, R> {
30+ /// Reads the trees not read yet, all at once.
31+ async fn prefetch(&mut self, hashes: impl IntoIterator<Item = String>) -> Result<()> {
32+ let mut wanted: Vec<String> = hashes.into_iter().filter(|hash| !self.read.contains_key(hash)).collect();
33+ wanted.sort();
34+ wanted.dedup();
35+ let found = futures_util::future::join_all(wanted.iter().map(|hash| self.repo.read_tree(hash))).await;
36+ for (hash, tree) in wanted.into_iter().zip(found) {
37+ self.read.insert(hash, tree?);
38+ }
39+ Ok(())
40+ }
41+
42+ /// Reads, level by level and each level at once, the trees on the way
43+ /// to `path` in each of `roots`, and the directory itself.
44+ async fn prefetch_dirs(&mut self, roots: Vec<String>, path: &str) -> Result<()> {
45+ let mut level = roots;
46+ for segment in path.split('/').filter(|segment| !segment.is_empty()) {
47+ self.prefetch(level.clone()).await?;
48+ level = level
49+ .iter()
50+ .filter_map(|hash| {
51+ self.read.get(hash)?.as_ref()?.iter().find(|entry| entry.name == segment && entry.kind == EntryKind::Tree).map(|entry| entry.hash.clone())
52+ })
53+ .collect();
54+ }
55+ self.prefetch(level).await
56+ }
57+
2758 async fn get(&mut self, hash: &str) -> Result<Option<Vec<TreeEntry>>> {
2859 if let Some(found) = self.read.get(hash) {
2960 return Ok(found.clone());
73104 if open.is_empty() {
74105 break;
75106 }
107+ if index % READ_AHEAD == 0 {
108+ let ahead = history.iter().skip(index + 1).take(READ_AHEAD).map(|commit| commit.tree_hash.clone()).collect();
109+ trees.prefetch_dirs(ahead, path).await?;
110+ }
76111 let Some(parent) = history.get(index + 1) else {
77112 // The oldest commit read. If it is the first commit there is,
78113 // what is left was added by it.