use std::{any::Any, borrow::Cow, cell::RefCell, panic::Location, rc::Rc, sync::Arc};
use crate::{
Error, NumEntries, Scope,
circuit::{
GlobalNodeId,
circuit_builder::StreamId,
metadata::{
ALLOCATED_MEMORY_BYTES, MetaItem, OperatorLocation, OperatorMeta, STATE_RECORDS_COUNT,
USED_MEMORY_BYTES,
},
operator_traits::{Operator, OperatorName, SinkOperator},
},
circuit_cache_key,
trace::{Batch, Spine, Trace},
};
use size_of::SizeOf;
circuit_cache_key!(RecorderId<B: Batch>(StreamId => RecorderHandle<B>));
circuit_cache_key!(RecorderControlId(StreamId => Rc<dyn RecorderControl>));
pub trait RecorderControl {
fn start_recording(&self);
fn is_recording(&self) -> bool;
fn stop_recording_any(&self) -> Option<Box<dyn Any>>;
}
impl<B> RecorderControl for RecorderHandle<B>
where
B: Batch,
{
fn start_recording(&self) {
RecorderHandle::start_recording(self)
}
fn is_recording(&self) -> bool {
RecorderHandle::is_recording(self)
}
fn stop_recording_any(&self) -> Option<Box<dyn Any>> {
self.stop_recording()
.map(|spine| Box::new(spine) as Box<dyn Any>)
}
}
enum RecordingState<B>
where
B: Batch,
{
Disabled,
Recording(Option<Spine<B>>),
}
struct RecorderInner<B>
where
B: Batch,
{
factories: B::Factories,
name: Arc<String>,
state: RecordingState<B>,
}
pub struct RecorderHandle<B>(Rc<RefCell<RecorderInner<B>>>)
where
B: Batch;
impl<B> Clone for RecorderHandle<B>
where
B: Batch,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<B> RecorderHandle<B>
where
B: Batch,
{
pub fn start_recording(&self) {
let mut inner = self.0.borrow_mut();
if matches!(inner.state, RecordingState::Disabled) {
inner.state =
RecordingState::Recording(Some(Spine::new(&inner.factories, inner.name.clone())));
}
}
pub fn stop_recording(&self) -> Option<Spine<B>> {
let mut inner = self.0.borrow_mut();
match std::mem::replace(&mut inner.state, RecordingState::Disabled) {
RecordingState::Disabled => None,
RecordingState::Recording(Some(trace)) => Some(trace),
RecordingState::Recording(None) => {
panic!("recorder lost its contents: an evaluation was cancelled mid-insert")
}
}
}
pub fn is_recording(&self) -> bool {
matches!(self.0.borrow().state, RecordingState::Recording(_))
}
fn take_trace(&self) -> Option<Spine<B>> {
match &mut self.0.borrow_mut().state {
RecordingState::Disabled | RecordingState::Recording(None) => None,
RecordingState::Recording(trace @ Some(_)) => trace.take(),
}
}
fn put_trace(&self, trace: Spine<B>) {
let mut inner = self.0.borrow_mut();
debug_assert!(matches!(inner.state, RecordingState::Recording(None)));
inner.state = RecordingState::Recording(Some(trace));
}
}
pub struct Recorder<B>
where
B: Batch,
{
name: OperatorName,
location: &'static Location<'static>,
state: RecorderHandle<B>,
}
impl<B> Recorder<B>
where
B: Batch,
{
pub fn new(factories: &B::Factories, location: &'static Location<'static>) -> Self {
let name = OperatorName::new("Recorder");
Self {
state: RecorderHandle(Rc::new(RefCell::new(RecorderInner {
factories: factories.clone(),
name: name.get(),
state: RecordingState::Disabled,
}))),
name,
location,
}
}
pub fn handle(&self) -> RecorderHandle<B> {
self.state.clone()
}
}
impl<B> Operator for Recorder<B>
where
B: Batch,
{
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed("Recorder")
}
fn init(&mut self, global_id: &GlobalNodeId) {
self.name.init(global_id);
self.state.0.borrow_mut().name = self.name.get();
}
fn location(&self) -> OperatorLocation {
Some(self.location)
}
fn metadata(&self, meta: &mut OperatorMeta) {
let inner = self.state.0.borrow();
let trace = match &inner.state {
RecordingState::Disabled | RecordingState::Recording(None) => None,
RecordingState::Recording(Some(trace)) => Some(trace),
};
let total_size = trace.map(|trace| trace.num_entries_deep()).unwrap_or(0);
let bytes = trace.map(|trace| trace.size_of()).unwrap_or_default();
meta.extend(metadata! {
STATE_RECORDS_COUNT => MetaItem::Count(total_size),
ALLOCATED_MEMORY_BYTES => MetaItem::bytes(bytes.total_bytes()),
USED_MEMORY_BYTES => MetaItem::bytes(bytes.used_bytes()),
});
if let Some(trace) = trace {
trace.metadata(meta);
}
}
fn fixedpoint(&self, _scope: Scope) -> bool {
true
}
fn clear_state(&mut self) -> Result<(), Error> {
self.state.0.borrow_mut().state = RecordingState::Disabled;
Ok(())
}
}
impl<B> SinkOperator<B> for Recorder<B>
where
B: Batch,
{
async fn eval(&mut self, batch: &B) {
if batch.is_empty() || !self.state.is_recording() {
return;
}
self.eval_owned(batch.clone()).await
}
async fn eval_owned(&mut self, batch: B) {
if batch.is_empty() {
return;
}
let Some(mut trace) = self.state.take_trace() else {
return;
};
trace.insert(batch).await;
self.state.put_trace(trace);
}
}
#[cfg(test)]
mod test {
use super::{Recorder, RecorderId};
use crate::{
Circuit, RootCircuit, Runtime, ZWeight,
algebra::OrdZSet,
circuit::{
CircuitConfig, OwnershipPreference, circuit_builder::CircuitBase,
operator_traits::Operator, schedule::util::ownership_constraints,
},
dynamic::{DowncastTrait, DynData},
trace::{BatchReaderFactories, Trace, test::test_batch::batch_to_tuples},
};
use std::panic::Location;
fn zset_contents(batch: &OrdZSet<DynData>) -> Vec<(u64, ZWeight)> {
batch_to_tuples(batch)
.into_iter()
.map(|((k, _v, ()), r)| {
(
*k.downcast_checked::<u64>(),
*r.downcast_checked::<ZWeight>(),
)
})
.collect()
}
#[test]
fn test_recorder() {
Runtime::run(CircuitConfig::with_workers(1), move |_parker| {
let (circuit, (input, recorder)) = RootCircuit::build(|circuit| {
let (stream, input) = circuit.add_input_zset::<u64>();
let _trace = stream.integrate_trace();
let recorder = circuit
.cache_get(&RecorderId::<OrdZSet<DynData>>::new(
stream.inner().stream_id(),
))
.expect("registering a replay source attaches a recorder");
Ok((input, recorder))
})
.unwrap();
assert!(!recorder.is_recording());
input.push(1, 1);
circuit.transaction().unwrap();
assert!(recorder.stop_recording().is_none());
recorder.start_recording();
assert!(recorder.is_recording());
input.push(2, 1);
input.push(3, 2);
circuit.transaction().unwrap();
input.push(2, -1);
input.push(4, 1);
circuit.transaction().unwrap();
circuit.transaction().unwrap();
let trace = recorder
.stop_recording()
.expect("a recording recorder returns its contents");
assert!(!recorder.is_recording());
let consolidated = trace.consolidate().expect("recorded data is non-empty");
assert_eq!(zset_contents(&consolidated), vec![(3, 2), (4, 1)]);
input.push(5, 1);
circuit.transaction().unwrap();
assert!(recorder.stop_recording().is_none());
recorder.start_recording();
input.push(6, 1);
circuit.transaction().unwrap();
let trace = recorder.stop_recording().unwrap();
let consolidated = trace.consolidate().unwrap();
assert_eq!(zset_contents(&consolidated), vec![(6, 1)]);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_recorder_clear_state() {
Runtime::run(CircuitConfig::with_workers(1), move |_parker| {
let factories = <<OrdZSet<DynData> as crate::trace::BatchReader>::Factories>::new::<
u64,
(),
ZWeight,
>();
let mut recorder = Recorder::<OrdZSet<DynData>>::new(&factories, Location::caller());
let handle = recorder.handle();
handle.start_recording();
assert!(handle.is_recording());
recorder.clear_state().unwrap();
assert!(!handle.is_recording());
assert!(handle.stop_recording().is_none());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_recorder_scheduled_before_owned_consumers() {
Runtime::run(CircuitConfig::with_workers(1), move |_parker| {
RootCircuit::build(|circuit| {
let (stream, _input) = circuit.add_input_zset::<u64>();
let _trace = stream.integrate_trace();
let (recorder_node, owned_consumer) = {
let edges = circuit.edges();
let recorder_edge = edges
.iter()
.find(|edge| {
edge.ownership_preference == Some(OwnershipPreference::YIELD_OWNERSHIP)
})
.expect("the recorder consumes with YIELD_OWNERSHIP");
let owned_edge = edges
.iter()
.find(|edge| {
edge.stream_id() == recorder_edge.stream_id()
&& edge
.ownership_preference
.is_some_and(|pref| pref >= OwnershipPreference::PREFER_OWNED)
})
.expect("the integral's appender prefers owned input");
(recorder_edge.to, owned_edge.to)
};
let constraints = ownership_constraints(circuit).unwrap();
assert!(
constraints.contains(&(recorder_node, owned_consumer)),
"missing recorder-before-appender constraint in {constraints:?}"
);
Ok(())
})
.unwrap();
})
.unwrap()
.join()
.unwrap();
}
}