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:
+145
-3
@@ -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>| {
|
||||
|
||||
Reference in New Issue
Block a user