OXYGENHUB

Cache each file's last change and its blame by state and path, and serve ?plain=1 without computing either 75d611ab99d1

refs #14
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-28 14:12 UTC
Signature
✔ signed by Andy McCall
Hash
75d611ab99d1a8f74e35cfdb963c0c083d16815d921194e980bbfc48aa757fda
Depends on
6ae41081ccd3 736f6813543d 9ff27da517fe d23464c0ac60
Depended on by
b19606f28101
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
6 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/hub/src/repos/view.rs +55 −6
…
610 unchanged lines
611 611
        FileBody::Text(String::from_utf8_lossy(&bytes).into_owned())
612 612
    };
613 613
    let labels = head_labels(repo)?;
614 -
    let last_change = repo
615 -
        .log_for_path_in(path, &target.heads)
616 -
        .ok()
617 -
        .and_then(|l| l.into_iter().next())
618 -
        .map(|e| change_view(repo, &e.hash, &e.change, e.is_head, &labels));
614 +
    let last_change = last_change_of(repo, &target.heads, path)
615 +
        .and_then(|h| loaded.state.get(&h).map(|meta| change_view(repo, &h, meta, loaded.state.heads().contains(&h), &labels)));
619 616
    let name = path.rsplit('/').next().unwrap_or(path).to_string();
620 617
    Ok(FileView {
621 618
        target,
…
25 unchanged lines
647 644
        },
648 645
    };
649 646
    Ok((path.rsplit('/').next().unwrap_or(path).to_string(), bytes))
647 +
}
648 +
649 +
/// Facts about a path in a state never change, so they are cached by the
650 +
/// state's heads and the path (hub spec §16.3).
651 +
fn state_path_key(tag: &[u8], heads: &[ChangeHash], path: &str) -> [u8; 32] {
652 +
    let mut sorted: Vec<&ChangeHash> = heads.iter().collect();
653 +
    sorted.sort();
654 +
    let joined: Vec<u8> = sorted.iter().flat_map(|h| h.to_hex().into_bytes()).collect();
655 +
    crate::render::cache::key(&[tag, &joined, path.as_bytes()])
650 656
}
651 657
658 +
struct Last(Option<ChangeHash>);
659 +
660 +
impl crate::render::cache::Weigh for Last {
661 +
    fn weight(&self) -> usize {
662 +
        64
663 +
    }
664 +
}
665 +
666 +
/// The newest change touching `path` in the state, from a cache of 4 MiB.
667 +
pub fn last_change_of(repo: &Repo, heads: &[ChangeHash], path: &str) -> Option<ChangeHash> {
668 +
    static C: std::sync::OnceLock<crate::render::cache::Lru<Last>> = std::sync::OnceLock::new();
669 +
    let cache = C.get_or_init(|| crate::render::cache::Lru::new(4 << 20));
670 +
    let key = state_path_key(b"last1", heads, path);
671 +
    cache.get_or(key, || Last(repo.log_for_path_in(path, heads).ok().and_then(|l| l.into_iter().next()).map(|e| e.hash))).0
672 +
}
673 +
674 +
struct BlameLines(Vec<BlameLine>);
675 +
676 +
impl crate::render::cache::Weigh for BlameLines {
677 +
    fn weight(&self) -> usize {
678 +
        self.0.iter().map(|l| l.text.len() + l.author.len() + 128).sum::<usize>() + 32
679 +
    }
680 +
}
681 +
682 +
/// `Repo::blame_in`, cached by state and path (64 MiB).
683 +
fn blame_cached(repo: &Repo, heads: &[ChangeHash], path: &str) -> Result<Vec<BlameLine>> {
684 +
    static C: std::sync::OnceLock<crate::render::cache::Lru<BlameLines>> = std::sync::OnceLock::new();
685 +
    let cache = C.get_or_init(|| crate::render::cache::Lru::new(64 << 20));
686 +
    let key = state_path_key(b"blame1", heads, path);
687 +
    let mut failure = None;
688 +
    let lines = cache.get_or(key, || match repo.blame_in(path, heads) {
689 +
        Ok(l) => BlameLines(l),
690 +
        Err(e) => {
691 +
            failure = Some(e);
692 +
            BlameLines(Vec::new())
693 +
        }
694 +
    });
695 +
    if let Some(e) = failure {
696 +
        return Err(RepoError::Oxy(e));
697 +
    }
698 +
    Ok(lines.0.clone())
699 +
}
700 +
652 701
pub fn is_binary(bytes: &[u8]) -> bool {
653 702
    let head = &bytes[..bytes.len().min(8192)];
654 703
    head.contains(&0)
…
57 unchanged lines
712 761
pub fn blame(repo: &Repo, cache: &StateCache, target: RefTarget, path: &str, render_limit: u64) -> Result<Blame> {
713 762
    let heads = target.heads.clone();
714 763
    let file = file(repo, cache, target, path, render_limit)?;
715 -
    let lines = repo.blame_in(path, &heads)?;
764 +
    let lines = blame_cached(repo, &heads, path)?;
716 765
    let mut before = HashMap::new();
717 766
    for l in &lines {
718 767
        before.entry(l.hash).or_insert_with(|| repo.store().get_change(&l.hash).ok().and_then(|c| state_spec(&c.deps)));
…
415 unchanged lines
modified crates/hub/src/web/repo.rs +10 −0
…
259 unchanged lines
260 260
        return Ok(axum::response::Redirect::to(&format!("/{owner}/{name}/flow/at/{r}")).into_response());
261 261
    }
262 262
    let limit = app.config.limits.render_limit.0;
263 +
    if q.plain.is_some() {
264 +
        // Bytes only: no last change, sidebar or highlighting to compute.
265 +
        let (spec, p) = (r.clone(), path.clone());
266 +
        let (file_name, bytes) = app
267 +
            .repos
268 +
            .read(&repo, move |repo, cache| view::raw(repo, cache, &view::resolve(repo, cache, &spec)?, &p))
269 +
            .await
270 +
            .map_err(|e| Error::from_repo(shell.clone(), e))?;
271 +
        return Ok(super::raw(bytes, &file_name, true));
272 +
    }
263 273
    let want_blame = q.blame.is_some();
264 274
    let (spec, p) = (r.clone(), path.clone());
265 275
    let (f, blame) = app
…
217 unchanged lines