feat: Specialize pipeline types for performance

Introduces type specialization for reactive pipeline nodes and their
data buffers. This eliminates the overhead of generic `Value` enums and
`SharedValueSeries` when dealing with known scalar or record types.

The architecture shifts buffer instantiation from the VM to the runtime
(RTL), leveraging type information from the AST. A new `out_type` field
is added to `BoundKind::Pipe` to store the static type of the pipeline's
output, determined by the type checker.

This enables the creation of specialized `RingBuffer<T>` and
`SharedRecordSeries` (for Struct-of-Arrays layout) when the output type
is known, significantly improving memory usage and processing speed for
time-series data, especially in financial analysis.
This commit is contained in:
Michael Schimmel
2026-03-02 22:03:24 +01:00
parent a4af142719
commit f7cb6655af
14 changed files with 295 additions and 58 deletions
+145 -3
View File
@@ -240,11 +240,14 @@ impl Observer for PipeStream {
pub struct SeriesPusher<T: crate::ast::rtl::series::ScalarValue> {
pub buffer: Rc<RefCell<crate::ast::rtl::series::RingBuffer<T>>>,
pub lookback: Option<usize>,
pub extractor: fn(Value) -> Option<T>,
}
impl<T: crate::ast::rtl::series::ScalarValue> Observer for SeriesPusher<T> {
fn notify(&mut self, _source_index: usize, _cycle_id: u64, _value: Value) {
// ... (Downcast and push logic)
fn notify(&mut self, _source_index: usize, _cycle_id: u64, value: Value) {
if let Some(v) = (self.extractor)(value) {
self.buffer.borrow_mut().push(v, self.lookback);
}
}
}
@@ -260,6 +263,137 @@ impl Observer for ValuePusher {
}
}
/// A specialized observer that splits a Record into its fields and pushes them into SoA buffers.
pub struct RecordPusher {
pub field_buffers: Vec<Rc<RefCell<dyn crate::ast::rtl::series::SeriesMember>>>,
pub lookback: Option<usize>,
}
impl Observer for RecordPusher {
fn notify(&mut self, _source_index: usize, _cycle_id: u64, value: Value) {
if let Value::Record(_, values) = value {
for (i, v) in values.iter().enumerate() {
if let Some(buf) = self.field_buffers.get(i) {
buf.borrow_mut().push_value(v.clone(), self.lookback);
}
}
}
}
}
/// Factory function to build a specialized pipeline node based on the output type.
/// This keeps the VM "dumb" and moves the buffer selection logic to the RTL.
pub fn build_pipeline_node(
inputs: Vec<Rc<dyn ObservableStream>>,
executor: Box<dyn FnMut(Vec<Value>) -> Value>,
out_type: &StaticType,
) -> Rc<PipelineNode> {
use crate::ast::rtl::series::*;
let pipe = Rc::new(RefCell::new(PipeStream::new(
"pipe".to_string(),
inputs.len(),
Some(executor),
)));
// Connect inputs to the pipe
for (i, input) in inputs.into_iter().enumerate() {
let adapter = Rc::new(RefCell::new(SourceAdapter {
target: pipe.clone(),
target_index: i,
}));
input.add_observer(adapter);
}
let series: Rc<dyn crate::ast::types::Series> = match out_type {
StaticType::Float => {
let buffer = Rc::new(RefCell::new(RingBuffer::<f64>::new()));
let pusher = Rc::new(RefCell::new(SeriesPusher {
buffer: buffer.clone(),
lookback: Some(100), // TODO: Configuration
extractor: |v| if let Value::Float(f) = v { Some(f) } else { None },
}));
pipe.borrow().add_observer(pusher);
Rc::new(SharedSeries {
buffer,
type_name: "SharedFloatSeries",
converter: |v| Value::Float(v),
})
}
StaticType::Int | StaticType::DateTime => {
let buffer = Rc::new(RefCell::new(RingBuffer::<i64>::new()));
let pusher = Rc::new(RefCell::new(SeriesPusher {
buffer: buffer.clone(),
lookback: Some(100),
extractor: |v| match v {
Value::Int(i) => Some(i),
Value::DateTime(i) => Some(i),
_ => None,
},
}));
pipe.borrow().add_observer(pusher);
Rc::new(SharedSeries {
buffer,
type_name: "SharedIntSeries",
converter: |v| Value::Int(v),
})
}
StaticType::Bool => {
let buffer = Rc::new(RefCell::new(RingBuffer::<bool>::new()));
let pusher = Rc::new(RefCell::new(SeriesPusher {
buffer: buffer.clone(),
lookback: Some(100),
extractor: |v| if let Value::Bool(b) = v { Some(b) } else { None },
}));
pipe.borrow().add_observer(pusher);
Rc::new(SharedSeries {
buffer,
type_name: "SharedBoolSeries",
converter: |v| Value::Bool(v),
})
}
StaticType::Record(layout) => {
let mut field_buffers = Vec::with_capacity(layout.fields.len());
for (_, ty) in &layout.fields {
let member: Rc<RefCell<dyn SeriesMember>> = match ty {
StaticType::Float => Rc::new(RefCell::new(ScalarSeries::<f64>::new("FloatSeries"))),
StaticType::Int | StaticType::DateTime => {
Rc::new(RefCell::new(ScalarSeries::<i64>::new("IntSeries")))
}
StaticType::Bool => Rc::new(RefCell::new(ScalarSeries::<bool>::new("BoolSeries"))),
_ => Rc::new(RefCell::new(ValueSeries::new())),
};
field_buffers.push(member);
}
let pusher = Rc::new(RefCell::new(RecordPusher {
field_buffers: field_buffers.clone(),
lookback: Some(100),
}));
pipe.borrow().add_observer(pusher);
Rc::new(SharedRecordSeries {
layout: layout.clone(),
field_buffers,
})
}
_ => {
let buffer = Rc::new(RefCell::new(RingBuffer::<Value>::new()));
let pusher = Rc::new(RefCell::new(ValuePusher {
buffer: buffer.clone(),
lookback: Some(100),
}));
pipe.borrow().add_observer(pusher);
Rc::new(SharedValueSeries { buffer })
}
};
Rc::new(PipelineNode {
stream: pipe,
series,
})
}
// ============================================================================
// Script Integration (RTL Registration)
// ============================================================================
@@ -271,11 +405,19 @@ pub fn register(env: &Environment) {
// (create-random-ohlc seed limit) -> StreamNode
let generators = env.pipeline_generators.clone();
// Define the OHLC layout for typing
let ohlc_layout = RecordLayout::get_or_create(vec![
(Keyword::intern("open"), StaticType::Float),
(Keyword::intern("high"), StaticType::Float),
(Keyword::intern("low"), StaticType::Float),
(Keyword::intern("close"), StaticType::Float),
]);
env.register_native_fn(
"create-random-ohlc",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]),
ret: StaticType::Any, // Returns a StreamNode
ret: StaticType::Series(Box::new(StaticType::Record(ohlc_layout.clone()))),
})),
Purity::Impure, // Modifies global generator registry
move |args: std::vec::Vec<Value>| {