on master help sign in
Make commit file lists lazy; answer conflict/empty from tree ids
2c66fc1iko
src/domain.rs+10
@@ -42,6 +42,16 @@ pub trait Backend {
42 fn meta(&self, id: &str) -> Result<MetaInfo, Crash>;
42 fn meta(&self, id: &str) -> Result<MetaInfo, Crash>;
43 /// True if the change id denotes a merge commit (2+ parents).
43 /// True if the change id denotes a merge commit (2+ parents).
44 fn is_merge(&self, id: &str) -> bool;
44 fn is_merge(&self, id: &str) -> bool;
45+ /// True if the commit's tree contains a conflict, if the backend can say
46+ /// without reading files (jj: O(1)); None means "unknown, compare files".
47+ fn has_conflict(&self, _id: &str) -> Option<bool> {
48+ None
49+ }
50+ /// True if the commit's tree equals its first parent's tree, if the
51+ /// backend can say without reading files; None means "compare files".
52+ fn is_empty(&self, _id: &str) -> Option<bool> {
53+ None
54+ }
45 /// All ancestors (inclusive) of the given change ids.
55 /// All ancestors (inclusive) of the given change ids.
46 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String>;
56 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String>;
47
57
src/jj.rs+33−7
@@ -1763,6 +1763,22 @@ impl Backend for JjBackend {
1763 .unwrap_or(false)
1763 .unwrap_or(false)
1764 }
1764 }
1765
1765
1766+ fn has_conflict(&self, id: &str) -> Option<bool> {
1767+ let vis = self.inner.visible.lock().unwrap().clone();
1768+ // O(1): a commit is conflicted iff its tree id is an unresolved merge
1769+ vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
1770+ }
1771+
1772+ fn is_empty(&self, id: &str) -> Option<bool> {
1773+ let vis = self.inner.visible.lock().unwrap().clone();
1774+ // O(1): empty iff the tree id equals the first parent's tree id
1775+ vis.and_then(|v| {
1776+ let rec = v.commits.get(id)?;
1777+ let parent = v.commits.get(&rec.first_parent)?;
1778+ Some(rec.commit.tree_ids() == parent.commit.tree_ids())
1779+ })
1780+ }
1781+
1766 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
1782 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
1767 let vis = self.inner.visible.lock().unwrap().clone();
1783 let vis = self.inner.visible.lock().unwrap().clone();
1768 let Some(vis) = vis else {
1784 let Some(vis) = vis else {
@@ -1941,8 +1957,8 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
1941/// files are unchanged between commits, so inflating each blob once (instead
1957/// files are unchanged between commits, so inflating each blob once (instead
1942/// of once per commit that references it) keeps loading linear in the
1958/// of once per commit that references it) keeps loading linear in the
1943/// repository's unique data rather than commits × files
1959/// repository's unique data rather than commits × files
1944−#[derive(Default)]
1960+#[derive(Clone, Default)]
1945−struct BlobCache(RefCell<HashMap<FileId, Rc<Vec<u8>>>>);
1961+struct BlobCache(Rc<RefCell<HashMap<FileId, Rc<Vec<u8>>>>>);
1946
1962
1947impl BlobCache {
1963impl BlobCache {
1948 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
1964 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
@@ -1956,8 +1972,8 @@ impl BlobCache {
1956/// whole file *entries* ({path, content}) keyed by (path, blob identity),
1972/// whole file *entries* ({path, content}) keyed by (path, blob identity),
1957/// shared across the whole repo load: an unchanged file is the same Value in
1973/// shared across the whole repo load: an unchanged file is the same Value in
1958/// every commit, so it is built once and cloned (an Rc bump) thereafter
1974/// every commit, so it is built once and cloned (an Rc bump) thereafter
1959−#[derive(Default)]
1975+#[derive(Clone, Default)]
1960−struct EntryCache(RefCell<HashMap<String, Value>>);
1976+struct EntryCache(Rc<RefCell<HashMap<String, Value>>>);
1961
1977
1962impl EntryCache {
1978impl EntryCache {
1963 fn get(&self, key: &str) -> Option<Value> {
1979 fn get(&self, key: &str) -> Option<Value> {
@@ -2191,8 +2207,18 @@ fn build_subtree(
2191 cache: &BlobCache,
2207 cache: &BlobCache,
2192 entries: &EntryCache,
2208 entries: &EntryCache,
2193) -> Result<Value, OpenError> {
2209) -> Result<Value, OpenError> {
2194− let files =
2210+ // the files list is lazy (§7.2): most commits are never inspected, so
2195− block_on(tree_to_files(store, &rec.commit.tree(), cache, entries)).map_err(|c| (2, c.msg))?;
2211+ // their file entries are materialized only on first `field("files")`
2212+ let files_thunk = {
2213+ let store = store.clone();
2214+ let tree = rec.commit.tree();
2215+ let cache = cache.clone();
2216+ let entries = entries.clone();
2217+ Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
2218+ let files = block_on(tree_to_files(&store, &tree, &cache, &entries))?;
2219+ Ok(Value::list(files))
2220+ })))
2221+ };
2196 let labels: Vec<Value> = vis
2222 let labels: Vec<Value> = vis
2197 .labels
2223 .labels
2198 .get(&rec.change_id)
2224 .get(&rec.change_id)
@@ -2202,7 +2228,7 @@ fn build_subtree(
2202 .map(Value::text)
2228 .map(Value::text)
2203 .collect();
2229 .collect();
2204 let commit_v = Value::record(&[
2230 let commit_v = Value::record(&[
2205− ("files", Value::list(files)),
2231+ ("files", files_thunk),
2206 ("id", Value::Id(Rc::new(rec.change_id.clone()))),
2232 ("id", Value::Id(Rc::new(rec.change_id.clone()))),
2207 ("labels", Value::list(labels)),
2233 ("labels", Value::list(labels)),
2208 ("message", Value::text(rec.commit.description())),
2234 ("message", Value::text(rec.commit.description())),
src/render.rs+30−12
@@ -191,7 +191,9 @@ fn display_id(interp: &Interp, id: &str, pal: &Palette) -> String {
191}
191}
192
192
193fn display_line(interp: &Interp, v: &Value, pal: &Palette) -> Result<String, Crash> {
193fn display_line(interp: &Interp, v: &Value, pal: &Palette) -> Result<String, Crash> {
194+ let v = &v.forced()?;
194 match v {
195 match v {
196+ Value::Thunk(_) => unreachable!("forced never returns a thunk"),
195 Value::Int(n) => Ok(n.to_string()),
197 Value::Int(n) => Ok(n.to_string()),
196 Value::Bool(b) => Ok(b.to_string()),
198 Value::Bool(b) => Ok(b.to_string()),
197 Value::Text(t) => {
199 Value::Text(t) => {
@@ -463,7 +465,9 @@ fn display_block(
463 out: &mut String,
465 out: &mut String,
464) -> Result<(), Crash> {
466) -> Result<(), Crash> {
465 let pad = " ".repeat(indent);
467 let pad = " ".repeat(indent);
468+ let v = &v.forced()?;
466 match v {
469 match v {
470+ Value::Thunk(_) => unreachable!("forced never returns a thunk"),
467 Value::Text(t) => {
471 Value::Text(t) => {
468 out.push_str(t);
472 out.push_str(t);
469 return Ok(());
473 return Ok(());
@@ -1016,11 +1020,13 @@ fn build_info(
1016 .iter()
1020 .iter()
1017 .map(|l| l.as_text().map(|s| s.to_string()))
1021 .map(|l| l.as_text().map(|s| s.to_string()))
1018 .collect::<Result<_, _>>()?;
1022 .collect::<Result<_, _>>()?;
1019− let conflict = has_conflict(commit)?;
1023+ // conflict and empty are O(1) backend queries when the backend can answer
1020− let files = commit.field("files")?;
1024+ // them from tree ids (jj), without touching the file list at all; the
1021− let nfiles = files.as_list()?.len();
1025+ // in-memory backend cannot, so fall back to walking the files
1022− // `empty` (no change against the parent) is needed at every detail level;
1026+ let conflict = match interp.backend.has_conflict(&id) {
1023− // compute it by id-based snapshot equality, which never forces blob bytes.
1027+ Some(c) => c,
1028+ None => has_conflict(commit)?,
1029+ };
1024 // `size` (a line count) and `detail_marks` are only rendered for detail ≥
1030 // `size` (a line count) and `detail_marks` are only rendered for detail ≥
1025 // 2 and for the focus and its neighbours, so skip the per-commit line
1031 // 2 and for the focus and its neighbours, so skip the per-commit line
1026 // counting elsewhere — it dominates render time on large histories.
1032 // counting elsewhere — it dominates render time on large histories.
@@ -1030,16 +1036,29 @@ fn build_info(
1030 matches!(c.field("root").and_then(|r| r.field("id")), Ok(Value::Id(i)) if *i == focus_id)
1036 matches!(c.field("root").and_then(|r| r.field("id")), Ok(Value::Id(i)) if *i == focus_id)
1031 });
1037 });
1032 let want_diff = opts.detail >= 2 || is_focus_pre || child_is_focus || parent_is_focus;
1038 let want_diff = opts.detail >= 2 || is_focus_pre || child_is_focus || parent_is_focus;
1039+ let backend_empty = interp.backend.is_empty(&id);
1040+ // the files list is only materialized when something needs it: the diff
1041+ // (detail/focus), the files data column, or the emptiness fallback
1042+ let need_files = want_diff || opts.files || backend_empty.is_none();
1043+ let files = if need_files {
1044+ Some(commit.field("files")?)
1045+ } else {
1046+ None
1047+ };
1048+ let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1033 let (empty, size, detail_marks) = match parent_files {
1049 let (empty, size, detail_marks) = match parent_files {
1034 Some(pf) => {
1050 Some(pf) => {
1035− // empty ⟺ the snapshot equals the parent's (id-based, no forcing)
1051+ let empty = match backend_empty {
1052+ Some(e) => e,
1036− let empty = crate::value::value_eq(&files, pf)?;
1053+ None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1054+ };
1037 if want_diff {
1055 if want_diff {
1056+ let files = files.as_ref().expect("files loaded");
1038 let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
1057 let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
1039 let marks = touched_paths(&change)?;
1058 let marks = touched_paths(&change)?;
1040 // size: lines added+removed against the parent
1059 // size: lines added+removed against the parent
1041 let from_map = snapshot_map_of(pf)?;
1060 let from_map = snapshot_map_of(pf)?;
1042− let to_map = snapshot_map_of(&files)?;
1061+ let to_map = snapshot_map_of(files)?;
1043 let mut lines = 0usize;
1062 let mut lines = 0usize;
1044 for (p, _, _) in &marks {
1063 for (p, _, _) in &marks {
1045 let key: Vec<String> = p.split('/').map(|s| s.to_string()).collect();
1064 let key: Vec<String> = p.split('/').map(|s| s.to_string()).collect();
@@ -1065,7 +1084,7 @@ fn build_info(
1065 }
1084 }
1066 }
1085 }
1067 None => {
1086 None => {
1068− let n = files.as_list()?.len();
1087+ let n = nfiles;
1069 (n == 0 && id != ROOT_ID, None, Vec::new())
1088 (n == 0 && id != ROOT_ID, None, Vec::new())
1070 }
1089 }
1071 };
1090 };
@@ -1082,13 +1101,12 @@ fn build_info(
1082 immutable,
1101 immutable,
1083 focus_id,
1102 focus_id,
1084 anc,
1103 anc,
1085− Some(&files),
1104+ files.as_ref(),
1086 with_focus,
1105 with_focus,
1087 is_ancestor_of_focus,
1106 is_ancestor_of_focus,
1088 is_focus_pre,
1107 is_focus_pre,
1089 )?);
1108 )?);
1090− }
1091− let immutable_flag = immutable.contains(&id);
1109+ } let immutable_flag = immutable.contains(&id);
1092 Ok(CommitInfo {
1110 Ok(CommitInfo {
1093 id,
1111 id,
1094 message,
1112 message,
src/show.rs+4
@@ -14,6 +14,10 @@ pub fn show(interp: &Interp, v: &Value) -> String {
14
14
15fn render(interp: &Interp, v: &Value) -> String {
15fn render(interp: &Interp, v: &Value) -> String {
16 match v {
16 match v {
17+ Value::Thunk(t) => match t.force() {
18+ Ok(v) => render(interp, &v),
19+ Err(_) => "<lazy>".to_string(),
20+ },
17 Value::Int(n) => {
21 Value::Int(n) => {
18 if n.sign() == num_bigint::Sign::Minus {
22 if n.sign() == num_bigint::Sign::Minus {
19 format!("(0 - {})", n.magnitude())
23 format!("(0 - {})", n.magnitude())
src/value.rs+73−1
@@ -18,6 +18,66 @@ pub enum Value {
18 Id(Rc<String>),
18 Id(Rc<String>),
19 Blob(Rc<BlobVal>),
19 Blob(Rc<BlobVal>),
20 Shape(Rc<ShapeVal>),
20 Shape(Rc<ShapeVal>),
21+ /// a value computed on first use (§2): currently used for a commit's
22+ /// `files` list, so loading a repo does not materialize every commit's
23+ /// file list. Forced transparently by `Value::field` and `value_eq`.
24+ Thunk(Rc<ThunkVal>),
25+}
26+
27+pub struct ThunkVal {
28+ state: std::cell::RefCell<ThunkState>,
29+}
30+
31+enum ThunkState {
32+ Pending(Box<dyn FnOnce() -> Result<Value, Crash>>),
33+ Ready(Value),
34+}
35+
36+impl std::fmt::Debug for ThunkVal {
37+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+ match &*self.state.borrow() {
39+ ThunkState::Pending(_) => write!(f, "Thunk(pending)"),
40+ ThunkState::Ready(v) => write!(f, "Thunk({:?})", v),
41+ }
42+ }
43+}
44+
45+impl ThunkVal {
46+ pub fn new(f: impl FnOnce() -> Result<Value, Crash> + 'static) -> Self {
47+ ThunkVal {
48+ state: std::cell::RefCell::new(ThunkState::Pending(Box::new(f))),
49+ }
50+ }
51+
52+ /// the value, computing it on first call and memoizing
53+ pub fn force(&self) -> Result<Value, Crash> {
54+ if let ThunkState::Ready(v) = &*self.state.borrow() {
55+ return Ok(v.clone());
56+ }
57+ let thunk = {
58+ let mut st = self.state.borrow_mut();
59+ match std::mem::replace(&mut *st, ThunkState::Ready(Value::Bool(false))) {
60+ ThunkState::Pending(t) => t,
61+ ThunkState::Ready(v) => {
62+ *st = ThunkState::Ready(v.clone());
63+ return Ok(v);
64+ }
65+ }
66+ };
67+ let v = thunk()?;
68+ *self.state.borrow_mut() = ThunkState::Ready(v.clone());
69+ Ok(v)
70+ }
71+}
72+
73+impl Value {
74+ /// force a thunk; every other value is returned unchanged
75+ pub fn forced(&self) -> Result<Value, Crash> {
76+ match self {
77+ Value::Thunk(t) => t.force(),
78+ _ => Ok(self.clone()),
79+ }
80+ }
21}
81}
22
82
23#[derive(Clone, Debug, PartialEq, Eq)]
83#[derive(Clone, Debug, PartialEq, Eq)]
@@ -266,6 +326,9 @@ impl Value {
266 Value::Id(_) => "Id",
326 Value::Id(_) => "Id",
267 Value::Blob(_) => "Blob",
327 Value::Blob(_) => "Blob",
268 Value::Shape(_) => "Shape",
328 Value::Shape(_) => "Shape",
329+ // a thunk stands in for its eventual value (currently a list of
330+ // file entries); kind_name is used in error messages
331+ Value::Thunk(_) => "list",
269 }
332 }
270 }
333 }
271
334
@@ -282,7 +345,8 @@ impl Value {
282 Value::Record(m) => m
345 Value::Record(m) => m
283 .get(name)
346 .get(name)
284 .cloned()
347 .cloned()
285− .ok_or_else(|| Crash::new(format!("record has no field `{}`", name))),
348+ .ok_or_else(|| Crash::new(format!("record has no field `{}`", name)))
349+ .and_then(|v| v.forced()),
286 _ => Err(Crash::new(format!(
350 _ => Err(Crash::new(format!(
287 "cannot select field `{}` from a {}",
351 "cannot select field `{}` from a {}",
288 name,
352 name,
@@ -533,6 +597,13 @@ impl Env {
533// ----------------------------------------------------------------------
597// ----------------------------------------------------------------------
534
598
535pub fn value_eq(a: &Value, b: &Value) -> Result<bool, Crash> {
599pub fn value_eq(a: &Value, b: &Value) -> Result<bool, Crash> {
600+ // force thunks at the boundary so callers never see them
601+ if let Value::Thunk(_) = a {
602+ return value_eq(&a.forced()?, b);
603+ }
604+ if let Value::Thunk(_) = b {
605+ return value_eq(a, &b.forced()?);
606+ }
536 Ok(match (a, b) {
607 Ok(match (a, b) {
537 (Value::Int(x), Value::Int(y)) => x == y,
608 (Value::Int(x), Value::Int(y)) => x == y,
538 (Value::Text(x), Value::Text(y)) => x == y,
609 (Value::Text(x), Value::Text(y)) => x == y,
@@ -596,6 +667,7 @@ impl std::fmt::Debug for Value {
596 Value::Fun(_) => write!(f, "Fun(..)"),
667 Value::Fun(_) => write!(f, "Fun(..)"),
597 Value::Blob(b) => write!(f, "Blob({:?})", b),
668 Value::Blob(b) => write!(f, "Blob({:?})", b),
598 Value::Shape(s) => write!(f, "Shape({})", s.name),
669 Value::Shape(s) => write!(f, "Shape({})", s.name),
670+ Value::Thunk(t) => write!(f, "{:?}", t),
599 }
671 }
600 }
672 }
601}
673}