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.
4 files+64−80/4 viewed
| 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 | + | } |
| 1 | 1 | import type { Route } from "./+types/code"; | |
| 2 | 2 | import { TreeView } from "../../components/repo-view"; | |
| 3 | 3 | import { page } from "../../lib/meta"; | |
| 4 | + | import { lastCommitsFor } from "../../lib/last-commits.server"; | |
| 4 | 5 | import { repos } from "../../lib/services.server"; | |
| 5 | 6 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 6 | 7 | ||
| 17 | 18 | repos.branches(path, viewer).catch(() => null), | |
| 18 | 19 | ]); | |
| 19 | 20 | // 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, ""); | |
| 24 | 22 | return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null, lastCommits }; | |
| 25 | 23 | } | |
| 26 | 24 |
| 2 | 2 | import { page } from "../../lib/meta"; | |
| 3 | 3 | import { TreeView } from "../../components/repo-view"; | |
| 4 | 4 | import { redirectIfBranchRenamed } from "../../lib/branch-redirect.server"; | |
| 5 | + | import { lastCommitsFor } from "../../lib/last-commits.server"; | |
| 5 | 6 | import { repos } from "../../lib/services.server"; | |
| 6 | 7 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 7 | 8 | ||
| 21 | 22 | // A branch that was renamed: the same folder on its new name. | |
| 22 | 23 | if (!tree.ok && tree.error.code === "not_found") await redirectIfBranchRenamed(request, path, viewer, params.ref); | |
| 23 | 24 | // 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["*"] ?? ""); | |
| 28 | 26 | return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null, lastCommits }; | |
| 29 | 27 | } | |
| 30 | 28 |
| 16 | 16 | ||
| 17 | 17 | /// How far back the history is walked. | |
| 18 | 18 | 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; | |
| 19 | 22 | ||
| 20 | 23 | /// Reads trees, remembering those already read: commits share most of them. | |
| 21 | 24 | struct Trees<'a, R: GitRepo> { | |
| 24 | 27 | } | |
| 25 | 28 | ||
| 26 | 29 | 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 | + | ||
| 27 | 58 | async fn get(&mut self, hash: &str) -> Result<Option<Vec<TreeEntry>>> { | |
| 28 | 59 | if let Some(found) = self.read.get(hash) { | |
| 29 | 60 | return Ok(found.clone()); | |
| 73 | 104 | if open.is_empty() { | |
| 74 | 105 | break; | |
| 75 | 106 | } | |
| 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 | + | } | |
| 76 | 111 | let Some(parent) = history.get(index + 1) else { | |
| 77 | 112 | // The oldest commit read. If it is the first commit there is, | |
| 78 | 113 | // what is left was added by it. |