Skip to content
Merged
345 changes: 296 additions & 49 deletions src/app/collection.rs

Large diffs are not rendered by default.

80 changes: 57 additions & 23 deletions src/app/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub(super) struct ReadAt {
fingerprint: String,
named: BTreeSet<String>,
roots: BTreeSet<String>,
draws_trees: bool,
speaks_until: Option<DateTime<Utc>>,
}

Expand All @@ -101,12 +102,14 @@ impl ReadAt {
fingerprint: &str,
named: &BTreeSet<String>,
roots: &BTreeSet<String>,
draws_trees: bool,
now: DateTime<Utc>,
) -> bool {
self.project == *project
&& self.fingerprint == fingerprint
&& self.named == *named
&& self.roots == *roots
&& self.draws_trees == draws_trees
&& self.speaks_until.is_none_or(|until| now < until)
}
}
Expand All @@ -124,16 +127,16 @@ fn speaks_until(beads: &[Arc<Bead>], read_at: DateTime<Utc>) -> Option<DateTime<
/// What one refresh of one project did.
pub(super) enum Refresh {
/// Nothing has moved since the read that is standing, so there is nothing
/// to replace it with.
Unchanged,
/// to replace it with. It speaks for the tracker as of `as_of`.
Unchanged { as_of: DateTime<Utc> },
/// What the tracker says now, and what it was read against. `None` where
/// the probe could not answer, which has every later refresh read in full
/// rather than compare against a state nobody established. `as_of` is
/// when what it says was last vouched for: when it was asked, unless the
/// tracker answered from what another process read.
///
/// `at` and `work` are behind a box because `Unchanged` is the usual
/// answer and carries nothing: a project that has not moved would
/// answer and carries only its date: a project that has not moved would
/// otherwise be handed back on the stack as the size of one that had.
/// `ReadAt` holds a whole `Project`, so it grows whenever a project entry
/// gains a field.
Expand Down Expand Up @@ -163,12 +166,16 @@ pub(super) enum Refresh {
///
/// The journal is read after `since` before the beads are, so every record
/// read has a bead read at least as new as it.
///
/// A project that `draws_trees` has its trees assembled. One read only for
/// what other projects' trees reach in it has none.
#[allow(clippy::too_many_arguments)]
pub(super) fn refresh_project(
trackers: &dyn Trackers,
project: &Project,
cfg: &Config,
panes: &[Pane],
draws_trees: bool,
standing: Option<&ReadAt>,
probing: bool,
since: u64,
Expand All @@ -186,20 +193,21 @@ pub(super) fn refresh_project(
let roots = roots_named(cfg, project);

if let (Some(fingerprint), Some(standing)) = (probed.as_deref(), standing) {
if standing.still_speaks_for(project, fingerprint, &named, &roots, now) {
return Ok(Refresh::Unchanged);
if standing.still_speaks_for(project, fingerprint, &named, &roots, draws_trees, now) {
return Ok(Refresh::Unchanged { as_of });
}
}

let journal = tracker.events(since);
let work = read_project(tracker.as_ref(), project, cfg, panes, now)?;
let work = read_project(tracker.as_ref(), project, cfg, panes, draws_trees, now)?;
let journal_answered = !matches!(journal, Some(Err(_)));
let at = probed.filter(|_| journal_answered).map(|fingerprint| {
Box::new(ReadAt {
project: project.clone(),
fingerprint,
named,
roots,
draws_trees,
speaks_until: work.speaks_until,
})
});
Expand All @@ -221,13 +229,14 @@ fn read_project(
project: &Project,
cfg: &Config,
panes: &[Pane],
draws_trees: bool,
now: DateTime<Utc>,
) -> Result<ProjectWork, RunFailure> {
let (beads, (ready, blocked_by)) = together(
|| tracker.all(),
|| together(|| tracker.ready(), || tracker.blocked()),
);
let beads: Vec<Arc<Bead>> = beads?.into_iter().map(Arc::new).collect();
let beads = beads?;

// An empty readiness set reads as "nothing here is ready", so a tracker
// that cannot answer must not leave one behind.
Expand All @@ -236,6 +245,27 @@ fn read_project(
blocked_by: blocked_by?,
};

Ok(ProjectWork {
readiness,
relations: edges::relations(&beads),
roots: if draws_trees {
trees(&beads, project, cfg, panes)
} else {
Vec::new()
},
speaks_until: speaks_until(&beads, now),
beads,
})
}

/// Each root's tree in one project's answer, in id order, or why it has
/// none.
fn trees(
beads: &[Arc<Bead>],
project: &Project,
cfg: &Config,
panes: &[Pane],
) -> Vec<(String, Result<Assembled, RootUnread>)> {
// Every bead this read of the tracker turned up, and the bead each one
// hangs under. A parent chain that leaves it has run off the end of what
// `bdi` read, and there is no tree to draw from where it went — so the
Expand All @@ -248,12 +278,12 @@ fn read_project(
// The edges are read before discovery rather than after it, because a
// bead with no parent is a root only where nothing nests it, and nothing
// outside the edges can say whether anything does.
let nesting = Nesting::of(&beads);
let nesting = Nesting::of(beads);

let mut roots = roots_named(cfg, project);
let mut ancestors: BTreeMap<String, Climbed> = BTreeMap::new();
let mut climbed: BTreeSet<Climbed> = BTreeSet::new();
for bead in unfinished(&beads) {
for bead in unfinished(beads) {
climbed.extend(root_of(bead, &parents, &mut ancestors));
}
for named in beads_named_here(panes, project, cfg) {
Expand Down Expand Up @@ -285,14 +315,7 @@ fn read_project(
.collect();
read.extend(what_no_root_reached(&nesting, &read));
read.sort_by(|(one, _), (two, _)| one.cmp(two));

Ok(ProjectWork {
readiness,
relations: edges::relations(&beads),
roots: read,
speaks_until: speaks_until(&beads, now),
beads,
})
read
}

/// The beads the discovered roots left off the screen, each drawn from the
Expand Down Expand Up @@ -669,7 +692,7 @@ dunwich = ["dun-4"]
let tracker = dunwich_tracker().also(beads(MAST_TREE)).also(beads(lost));

let before = nestings_on_this_thread();
let work = read_project(&tracker, &cfg.projects[0], &cfg, &[], now())
let work = read_project(&tracker, &cfg.projects[0], &cfg, &[], true, now())
.expect("the tracker answers every call");

let roots: Vec<&str> = work.roots.iter().map(|(root, _)| root.as_str()).collect();
Expand Down Expand Up @@ -987,6 +1010,7 @@ dunwich = ["dun-4"]
&before.projects[0],
&before,
&[],
true,
None,
true,
0,
Expand All @@ -1002,6 +1026,7 @@ dunwich = ["dun-4"]
&after.projects[0],
&after,
&[],
true,
at.as_deref(),
true,
0,
Expand Down Expand Up @@ -1037,7 +1062,7 @@ dunwich = ["dun-c3"]
.expect("the config parses");
let tracker = dunwich_holding(CHAIN_OF_PARENTLESS);

let work = read_project(&tracker, &cfg.projects[0], &cfg, &[], now())
let work = read_project(&tracker, &cfg.projects[0], &cfg, &[], true, now())
.expect("the tracker answers every call");

let roots: Vec<&str> = work.roots.iter().map(|(root, _)| root.as_str()).collect();
Expand Down Expand Up @@ -1383,9 +1408,18 @@ dunwich = ["dun-c3"]
);
let cfg = one_project();

let refreshed =
refresh_project(&trackers, &cfg.projects[0], &cfg, &[], None, true, 0, now())
.expect("the beads are read");
let refreshed = refresh_project(
&trackers,
&cfg.projects[0],
&cfg,
&[],
true,
None,
true,
0,
now(),
)
.expect("the beads are read");

let Refresh::Read { at, journal, .. } = refreshed else {
panic!("a project nothing has read is read in full")
Expand Down Expand Up @@ -1895,7 +1929,7 @@ dunwich = ["bdi-404"]
Tracker::fingerprint(&self.inner)
}

fn all(&self) -> Result<Vec<Bead>, RunFailure> {
fn all(&self) -> Result<Vec<Arc<Bead>>, RunFailure> {
self.at.arrive("all");
Tracker::all(&self.inner)
}
Expand Down
9 changes: 5 additions & 4 deletions src/collect/bd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::{Arc, Mutex};

use anyhow::Context;
use serde::Deserialize;
Expand Down Expand Up @@ -723,12 +723,13 @@ impl Tracker for Reader<'_> {
/// `--all` is load-bearing: without it bd answers about open beads only,
/// and a smaller correct-looking answer about a different population is
/// the kind of wrong that reads as right.
fn all(&self) -> Result<Vec<Bead>, RunFailure> {
if self.unfinished_work {
fn all(&self) -> Result<Vec<Arc<Bead>>, RunFailure> {
let beads = if self.unfinished_work {
self.every_bead_with_unfinished_text()
} else {
self.every_bead()
}
};
beads.map(|beads| beads.into_iter().map(Arc::new).collect())
}

/// bd computes readiness itself and treats it as a state of its own, so
Expand Down
24 changes: 20 additions & 4 deletions src/collect/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! is the line it stays behind.

use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

use chrono::{DateTime, Utc};
use serde_json::Value;
Expand All @@ -28,7 +29,7 @@ pub trait Tracker: Sync {
/// it depends on and the bead it hangs under. A tracker opened for a run
/// that shows unfinished work alone may leave out a finished bead's free
/// text.
fn all(&self) -> Result<Vec<Bead>, RunFailure>;
fn all(&self) -> Result<Vec<Arc<Bead>>, RunFailure>;

/// The ids the tracker itself considers ready to start.
fn ready(&self) -> Result<BTreeSet<String>, RunFailure>;
Expand Down Expand Up @@ -120,7 +121,7 @@ impl<T: Tracker + ?Sized> Tracker for &T {
(**self).fingerprint()
}

fn all(&self) -> Result<Vec<Bead>, RunFailure> {
fn all(&self) -> Result<Vec<Arc<Bead>>, RunFailure> {
(**self).all()
}

Expand Down Expand Up @@ -176,6 +177,7 @@ pub mod testing {
/// Every record the journal holds, where the tracker was opened with
/// one.
journal: Mutex<Option<Result<Vec<Value>, RunFailure>>>,
as_of: Option<DateTime<Utc>>,
asked: Mutex<Vec<Asked>>,
}

Expand All @@ -192,6 +194,7 @@ pub mod testing {
ready: Ok(BTreeSet::new()),
blocked: Ok(BTreeMap::new()),
journal: Mutex::new(None),
as_of: None,
asked: Mutex::new(Vec::new()),
}
}
Expand Down Expand Up @@ -248,6 +251,13 @@ pub mod testing {
self
}

/// The same tracker answering from what another process read, which
/// last vouched for it at `at`.
pub fn vouched_for(mut self, at: DateTime<Utc>) -> Self {
self.as_of = Some(at);
self
}

/// A tracker with nothing to fingerprint by, which is read in full
/// every time.
pub fn without_a_fingerprint(mut self) -> Self {
Expand Down Expand Up @@ -284,9 +294,11 @@ pub mod testing {
self.fingerprint.clone()
}

fn all(&self) -> Result<Vec<Bead>, RunFailure> {
fn all(&self) -> Result<Vec<Arc<Bead>>, RunFailure> {
self.note(Asked::All);
self.all.clone()
self.all
.clone()
.map(|beads| beads.into_iter().map(Arc::new).collect())
}

fn ready(&self) -> Result<BTreeSet<String>, RunFailure> {
Expand All @@ -299,6 +311,10 @@ pub mod testing {
self.blocked.clone()
}

fn as_of(&self) -> Option<DateTime<Utc>> {
self.as_of
}

fn events(&self, since: u64) -> Option<Result<Vec<Value>, RunFailure>> {
let journal = self.journal.lock().unwrap().clone()?;
self.note(Asked::Events(since));
Expand Down
Loading