@@ -335,37 +335,64 @@ fn path_string(p: &Value) -> Result<String, Crash> {
335 Ok(parts.join("/"))
335 Ok(parts.join("/"))
338+/// entries of one snapshot, keyed by path components
339+type SnapMap = BTreeMap<Vec<String>, Value>;
338fn touched_paths(change: &Value) -> Result<Vec<(String, char, bool)>, Crash> {
341fn touched_paths(change: &Value) -> Result<Vec<(String, char, bool)>, Crash> {
339− // (path, mark, unresolved)
340 let from = snapshot_map_of(&change.field("from")?)?;
342 let from = snapshot_map_of(&change.field("from")?)?;
341 let to = snapshot_map_of(&change.field("to")?)?;
343 let to = snapshot_map_of(&change.field("to")?)?;
342− let mut paths: BTreeSet<Vec<String>> = BTreeSet::new();
344+ Ok(touched_in_maps(&from, &to)?
343− paths.extend(from.keys().cloned());
345+ .into_iter()
344− paths.extend(to.keys().cloned());
346+ .map(|(p, mark, unresolved)| (p.join("/"), mark, unresolved))
350+/// The changed paths between two snapshots that have already been keyed, as
351+/// (path components, mark, unresolved). Both maps are sorted, so the union is
352+/// a merge — the previous version collected every key of both sides into a
353+/// `BTreeSet`, cloning each one.
354+fn touched_in_maps(from: &SnapMap, to: &SnapMap) -> Result<Vec<(Vec<String>, char, bool)>, Crash> {
345 let mut out = Vec::new();
355 let mut out = Vec::new();
346− for p in paths {
356+ let mut fi = from.iter().peekable();
347− let f = from.get(&p);
357+ let mut ti = to.iter().peekable();
348− let t = to.get(&p);
358+ loop {
359+ let ord = match (fi.peek(), ti.peek()) {
360+ (None, None) => break,
361+ (Some(_), None) => std::cmp::Ordering::Less,
362+ (None, Some(_)) => std::cmp::Ordering::Greater,
363+ (Some((fp, _)), Some((tp, _))) => fp.cmp(tp),
349− let mark = match (f, t) {
365+ let (path, mark, t) = match ord {
366+ std::cmp::Ordering::Less => {
367+ let (p, _) = fi.next().unwrap();
350− (None, Some(_)) => '+',
368+ (p, '−', None)
370+ std::cmp::Ordering::Greater => {
371+ let (p, v) = ti.next().unwrap();
351− (Some(_), None) => '−',
372+ (p, '+', Some(v))
352− (Some(a), Some(b)) => {
373+ }
374+ std::cmp::Ordering::Equal => {
375+ let (p, a) = fi.next().unwrap();
376+ let (_, b) = ti.next().unwrap();
353 if crate::value::value_eq(a, b)? {
377 if crate::value::value_eq(a, b)? {
354 continue;
378 continue;
356− '~'
380+ (p, '~', Some(b))
358− (None, None) => continue,
360 let unresolved = t
383 let unresolved = t
361 .map(|b| matches!(b, Value::Blob(bl) if bl.is_unresolved()))
384 .map(|b| matches!(b, Value::Blob(bl) if bl.is_unresolved()))
362 .unwrap_or(false);
385 .unwrap_or(false);
363− out.push((p.join("/"), if unresolved { '✖' } else { mark }, unresolved));
388+ if unresolved { '✖' } else { mark },
368−fn snapshot_map_of(snap: &Value) -> Result<BTreeMap<Vec<String>, Value>, Crash> {
395+fn snapshot_map_of(snap: &Value) -> Result<SnapMap, Crash> {
369 let mut m = BTreeMap::new();
396 let mut m = BTreeMap::new();
370 for e in snap.as_list()?.iter() {
397 for e in snap.as_list()?.iter() {
371 let path = e
398 let path = e
@@ -998,6 +1025,24 @@ impl CommitInfo {
1028+/// A parent commit's snapshot as seen by its children: the files value, plus
1029+/// its path-keyed map built at most once and shared by every child's diff.
1030+/// Keying a snapshot allocates a `Vec<String>` per entry, so rebuilding it for
1031+/// each child dominated `treeFull`/`treeData` on a large history.
1032+struct ParentSnap<'a> {
1034+ map: &'a std::cell::OnceCell<SnapMap>,
1037+impl ParentSnap<'_> {
1038+ fn map(&self) -> Result<&SnapMap, Crash> {
1039+ if self.map.get().is_none() {
1040+ let _ = self.map.set(snapshot_map_of(self.files)?);
1042+ Ok(self.map.get().expect("just set"))
1001#[allow(clippy::too_many_arguments)]
1046#[allow(clippy::too_many_arguments)]
1002fn build_info(
1047fn build_info(
1003 interp: &mut Interp,
1048 interp: &mut Interp,
@@ -1007,7 +1052,7 @@ fn build_info(
1007 immutable: &BTreeSet<String>,
1052 immutable: &BTreeSet<String>,
1008 focus_id: &str,
1053 focus_id: &str,
1009 anc: &BTreeSet<String>,
1054 anc: &BTreeSet<String>,
1010− parent_files: Option<&Value>,
1055+ parent_files: Option<&ParentSnap<'_>>,
1011 with_focus: bool,
1056 with_focus: bool,
1012 parent_is_ancestor: bool,
1057 parent_is_ancestor: bool,
1013 parent_is_focus: bool,
1058 parent_is_focus: bool,
@@ -1049,38 +1094,39 @@ fn build_info(
1051 let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1096 let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1097+ // this commit's own snapshot map, built on first use and shared with every
1098+ // child that diffs against it
1099+ let my_map: std::cell::OnceCell<SnapMap> = std::cell::OnceCell::new();
1100+ let my_snap = files.as_ref().map(|f| ParentSnap {
1052 let (empty, size, detail_marks) = match parent_files {
1104 let (empty, size, detail_marks) = match parent_files {
1053− Some(pf) => {
1105+ Some(parent) => {
1106+ let pf = parent.files;
1054 let empty = match backend_empty {
1107 let empty = match backend_empty {
1055 Some(e) => e,
1108 Some(e) => e,
1056 None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1109 None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1058 if want_diff {
1111 if want_diff {
1112+ // both maps are built at most once per commit and reused by
1113+ // this commit's children, so a diff costs one keying, not four
1114+ let from_map = parent.map()?;
1059− let files = files.as_ref().expect("files loaded");
1115+ let to_map = my_snap.as_ref().expect("files loaded").map()?;
1060− let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
1116+ let marks = touched_in_maps(from_map, to_map)?;
1061− let marks = touched_paths(&change)?;
1062 // size: lines added+removed against the parent
1117 // size: lines added+removed against the parent
1063− let from_map = snapshot_map_of(pf)?;
1064− let to_map = snapshot_map_of(files)?;
1065 let mut lines = 0usize;
1118 let mut lines = 0usize;
1066− for (p, _, _) in &marks {
1119+ for (key, _, _) in &marks {
1067− let key: Vec<String> = p.split('/').map(|s| s.to_string()).collect();
1120+ let count = |m: &SnapMap| -> usize {
1068− let count = |m: &BTreeMap<Vec<String>, Value>| -> usize {
1069− match m.get(&key) {
1121+ match m.get(key) {
1070− Some(Value::Blob(b)) => {
1122+ Some(Value::Blob(b)) => b.line_count().max(1),
1073− .map(|v| String::from_utf8_lossy(&v).lines().count())
1080− lines += count(&from_map) + count(&to_map);
1126+ lines += count(from_map) + count(to_map);
1082 let marks2: Vec<(String, char)> =
1128 let marks2: Vec<(String, char)> =
1083− marks.iter().map(|(p, m, _)| (p.clone(), *m)).collect();
1129+ marks.iter().map(|(p, m, _)| (p.join("/"), *m)).collect();
1084 (marks.is_empty(), Some(lines), marks2)
1130 (marks.is_empty(), Some(lines), marks2)
1085 } else {
1131 } else {
1086 (empty, None, Vec::new())
1132 (empty, None, Vec::new())
@@ -1114,7 +1160,7 @@ fn build_info(
1114 immutable,
1160 immutable,
1115 focus_id,
1161 focus_id,
1117− files.as_ref(),
1163+ my_snap.as_ref(),
1118 with_focus,
1164 with_focus,
1119 is_ancestor_of_focus,
1165 is_ancestor_of_focus,
1120 is_focus_pre,
1166 is_focus_pre,
@@ -1258,11 +1304,7 @@ fn top_id(repo: &Value) -> Result<String, Crash> {
1258fn compute_trunk(interp: &mut Interp, repo: &Value) -> Result<BTreeSet<String>, Crash> {
1304fn compute_trunk(interp: &mut Interp, repo: &Value) -> Result<BTreeSet<String>, Crash> {
1259 let mut set = BTreeSet::new();
1305 let mut set = BTreeSet::new();
1260 set.insert(top_id(repo)?);
1306 set.insert(top_id(repo)?);
1261− let trunk_fn = interp
1307+ let v = interp.apply_cached_revset("trunk", repo)?;
1264− .ok_or_else(|| Crash::new("`trunk` is not defined"))?;
1265− let v = interp.apply(trunk_fn, repo.clone())?;
1266 let mut named: Vec<String> = Vec::new();
1308 let mut named: Vec<String> = Vec::new();
1267 for idv in v.as_list()?.iter() {
1309 for idv in v.as_list()?.iter() {
1268 match idv {
1310 match idv {