somatize_core/fact.rs
1//! What happened while a plan ran: the vocabulary of level 1, the engine's.
2//!
3//! A fact and not a judgement. Whether 400 ms is slow or a gradient is dying is
4//! an opinion about the record, and the invariant is that the opinion has to be
5//! reproducible from the record without running again.
6//!
7//! An enum because the set is closed and the engine knows it. What the original
8//! got wrong was not the number of variants but putting three vocabularies in
9//! one — a fact beside an opinion about facts. Here each level keeps its own:
10//! this is the engine's, a training run's is Python's, a study's is a record on
11//! disk. **They do not meet in Rust, they meet in the record**, and
12//! [`Fact::flattened`] is that meeting: a name and text-to-text pairs.
13//!
14//! Every measurement is a [`Duration`] and never an instant. A duration from
15//! another machine is worth reading; two wall clocks disagree. **When** it was
16//! written down is the store's business, and it stamps it.
17
18use crate::{Device, Host, Key, NodeId};
19use std::time::Duration;
20
21/// One thing the engine saw.
22#[derive(Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Fact {
25 /// A node was advanced, and answered.
26 Ran {
27 /// Which one.
28 node: NodeId,
29 /// How long after this run started it began. An offset into a slice
30 /// is a fact about the slice, so one that ran elsewhere counts from its
31 /// own start and a timeline adds the [`Fact::Left`] it arrived under.
32 began: Duration,
33 /// How long its `forward` took. Whatever it did in there is inside
34 /// that number: the engine does not look inside a node.
35 took: Duration,
36 /// Where it was told to run, if it was told.
37 device: Option<Device>,
38 },
39 /// A node was advanced and did not answer. Emitted **before** the run
40 /// stops, so a watcher learns which node while it is happening.
41 Failed {
42 /// Which one.
43 node: NodeId,
44 /// What it said.
45 why: String,
46 },
47 /// A node was not run because nobody needed what it makes.
48 ///
49 /// A fact and not an absence: a node missing from a record cannot be told
50 /// from one that was never in the graph.
51 Spared {
52 /// Which one.
53 node: NodeId,
54 },
55 /// A node was not advanced at all: what it would have produced was already
56 /// kept under that name.
57 Recalled {
58 /// Which one.
59 node: NodeId,
60 /// The name it was found under.
61 key: Key,
62 },
63 /// A node ran and what it produced was written down.
64 Kept {
65 /// Which one.
66 node: NodeId,
67 /// The name it was written under.
68 key: Key,
69 },
70 /// A node that maps over its items, item by item — it runs the new ones
71 /// and reads the rest back, so one number would not say what happened.
72 Items {
73 /// Which one.
74 node: NodeId,
75 /// How many items it was given.
76 of: usize,
77 /// How many of them did not have to be computed.
78 recalled: usize,
79 },
80 /// A slice of the plan crossed to another machine, and came back. `took`
81 /// is the whole round trip, which is not the sum of what happened there.
82 Left {
83 /// Whose machine.
84 host: Host,
85 /// How long after this run started it left.
86 began: Duration,
87 /// How long the round trip took.
88 took: Duration,
89 },
90 /// And this is what happened over there. Recursive, so a slice that
91 /// carried on to a third host still says where each thing happened and
92 /// nothing that travelled is rewritten; flattening turns it into a `host`.
93 Elsewhere {
94 /// Whose machine.
95 host: Host,
96 /// What it saw there.
97 saw: Box<Fact>,
98 },
99 /// A level that is **not** the engine had something to say, already flat.
100 ///
101 /// The carrier and not the vocabulary: the core does not learn what a load
102 /// average is, only that other levels exist and one may be speaking from
103 /// another machine. Not for level 2, whose loss is computed where the
104 /// notebook is and goes straight into the record.
105 Said {
106 /// What kind of thing it is, which is what it will be written down as.
107 kind: String,
108 /// And its fields, text to text, already in the written form.
109 pairs: Vec<(String, String)>,
110 },
111 /// The whole thing is over. Emitted by [`Executor::run`](crate::Executor::run)
112 /// and not by [`resume`](crate::Executor::resume): a slice is not a
113 /// `forward`. It is what tells a writer where one record ends.
114 Finished {
115 /// How long all of it took.
116 took: Duration,
117 },
118 /// ...or it is over because of this: the other terminal fact, so a record
119 /// is closed either way. A node that failed said so as [`Fact::Failed`].
120 Broke {
121 /// What stopped it.
122 why: String,
123 },
124}
125
126impl Fact {
127 /// This fact as a name and text-to-text fields: **how it is written down**,
128 /// which is not how it is emitted.
129 ///
130 /// [`Fact::Elsewhere`] does not survive as a name — it becomes a `host`
131 /// field on whatever it wrapped, so a reader gets columns and not a tree.
132 pub fn flattened(&self) -> (&str, Vec<(String, String)>) {
133 match self {
134 Self::Ran {
135 node,
136 began,
137 took,
138 device,
139 } => {
140 let mut said = vec![
141 ("node".into(), node.to_string()),
142 began_us(began),
143 took_us(took),
144 ];
145 if let Some(device) = device {
146 said.push(("device".into(), device.to_string()));
147 }
148 ("ran", said)
149 }
150 Self::Failed { node, why } => (
151 "failed",
152 vec![
153 ("node".into(), node.to_string()),
154 ("why".into(), why.clone()),
155 ],
156 ),
157 Self::Spared { node } => ("spared", vec![("node".into(), node.to_string())]),
158 Self::Recalled { node, key } => (
159 "recalled",
160 vec![
161 ("node".into(), node.to_string()),
162 ("key".into(), key.to_string()),
163 ],
164 ),
165 Self::Kept { node, key } => (
166 "kept",
167 vec![
168 ("node".into(), node.to_string()),
169 ("key".into(), key.to_string()),
170 ],
171 ),
172 Self::Items { node, of, recalled } => (
173 "items",
174 vec![
175 ("node".into(), node.to_string()),
176 ("of".into(), of.to_string()),
177 ("recalled".into(), recalled.to_string()),
178 ],
179 ),
180 Self::Left { host, began, took } => (
181 "left",
182 vec![
183 ("host".into(), host.to_string()),
184 began_us(began),
185 took_us(took),
186 ],
187 ),
188 // Already flat, and reshaping it here would be this crate deciding
189 // something about a vocabulary it does not know.
190 Self::Said { kind, pairs } => (kind.as_str(), pairs.clone()),
191 Self::Elsewhere { host, saw } => {
192 let (kind, mut said) = saw.flattened();
193 // Last, so that a fact which crossed two machines keeps the
194 // nearest host last and the reader sees the route in order.
195 said.push(("host".into(), host.to_string()));
196 (kind, said)
197 }
198 Self::Finished { took } => ("finished", vec![took_us(took)]),
199 Self::Broke { why } => ("broke", vec![("why".into(), why.clone())]),
200 }
201 }
202
203 /// Whether this fact ends a run, whichever way. Asked by whoever writes
204 /// records so it does not have to know the vocabulary.
205 pub fn ends_a_run(&self) -> bool {
206 matches!(self, Self::Finished { .. } | Self::Broke { .. })
207 }
208}
209
210/// A duration as whole microseconds — an integer, because this is text somebody
211/// reads with `cat` and something else parses.
212fn took_us(took: &Duration) -> (String, String) {
213 ("took_us".into(), took.as_micros().to_string())
214}
215
216/// And where it sat on the run's own timeline, which is what makes a picture of
217/// *what ran when* possible at all.
218fn began_us(began: &Duration) -> (String, String) {
219 ("began_us".into(), began.as_micros().to_string())
220}