feat(aura-engine): Source::resident_records — probe ring residency via the trait
The residency probe was an inherent method on the concrete M1FieldSource, so a Box<dyn Source> consumer could not read it without downcasting (the adjacent friction noted in #95). Hoist it onto the Source trait as resident_records(&self) -> Option<usize> with a default of None, mirroring bounds()'s Option-for-unknown idiom: None = "does not report" (the eager VecSource, which has no streaming ring), distinct from Some(0) = "reports, holds none" (e.g. exhausted). M1FieldSource overrides it to return Some(chunk len). Callers updated (streaming_seam, the two deep-dive fieldtest bins). Tests: a hermetic VecSource default-is-None check, and a &dyn Source probe in the gated residency test proving M1FieldSource residency is readable without a downcast. refs #95
This commit is contained in:
@@ -70,6 +70,18 @@ pub trait Source {
|
||||
/// open-ended. Cursor-independent: a property of the data extent, not the
|
||||
/// read position.
|
||||
fn bounds(&self) -> Option<(Timestamp, Timestamp)>;
|
||||
|
||||
/// Records this source holds resident right now — the streaming-ring probe
|
||||
/// (#95). `None` (the default) means the source does not report a residency:
|
||||
/// the eager [`VecSource`] materializes its whole stream and has no bounded
|
||||
/// ring. A lazy chunk source overrides this to return `Some(len)` of the chunk
|
||||
/// it currently borrows, so a `Box<dyn Source>` consumer can budget memory
|
||||
/// without downcasting. Mirrors [`bounds`](Source::bounds)' Option idiom:
|
||||
/// `None` = "not reported," distinct from `Some(0)` = "reports, holds none"
|
||||
/// (e.g. exhausted). Probes the aura source ring only, not whole-process RSS.
|
||||
fn resident_records(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Source` over a fully materialized stream — the cycle-0011 eager shape,
|
||||
@@ -493,6 +505,16 @@ mod tests {
|
||||
points.iter().map(|&(t, v)| (Timestamp(t), Scalar::f64(v))).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec_source_reports_no_residency_by_default() {
|
||||
// The eager VecSource has no streaming ring, so it takes the Source trait
|
||||
// default: residency is "not reported" (None), distinct from Some(0). The
|
||||
// probe is reachable through `&dyn Source` without downcasting (#95).
|
||||
let v = VecSource::new(f64_stream(&[(1, 1.0), (2, 2.0)]));
|
||||
let probe: &dyn Source = &v;
|
||||
assert_eq!(probe.resident_records(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeded_source_same_seed_same_stream() {
|
||||
// Same seed -> bit-identical stream (C1/C12 determinism).
|
||||
|
||||
@@ -182,7 +182,7 @@ fn decode(field: M1Field, bar: &M1Parsed) -> (Timestamp, Scalar) {
|
||||
/// at most one `Arc` chunk (a zero-copy clone of the cache's chunk) and a cursor;
|
||||
/// constructs each `Scalar` per-pull; refills via `next_chunk()` when the chunk
|
||||
/// drains. The **source**'s resident footprint is O(one chunk), independent of
|
||||
/// window length (see [`resident_records`](Self::resident_records)) — a per-source
|
||||
/// window length (see [`resident_records`](aura_engine::Source::resident_records)) — a per-source
|
||||
/// bound, not whole-process RSS: the data-server cache below retains the loaded
|
||||
/// window's parsed chunks for the pass (the replay-many sharing model, #95).
|
||||
pub struct M1FieldSource {
|
||||
@@ -237,18 +237,20 @@ impl M1FieldSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Records resident in this source right now: the current chunk's length (or
|
||||
/// 0 at exhaustion). The residency probe — bounded by one chunk length by
|
||||
/// construction (the source holds at most one `Arc` chunk and no accumulating
|
||||
/// field), so it can never grow with window length. This probes the **source
|
||||
/// ring** only; the data-server `FileCache` below retains the loaded window's
|
||||
/// parsed chunks for the pass and is not counted here (#95).
|
||||
pub fn resident_records(&self) -> usize {
|
||||
self.chunk.as_ref().map_or(0, |c| c.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl aura_engine::Source for M1FieldSource {
|
||||
/// Records resident in this source right now: the current chunk's length (or
|
||||
/// 0 at exhaustion). The ring-residency probe (#95) — bounded by one chunk
|
||||
/// length by construction (the source holds at most one `Arc` chunk and no
|
||||
/// accumulating field), so it can never grow with window length; always `Some`
|
||||
/// (this source reports). Probes the **source ring** only; the data-server
|
||||
/// `FileCache` below retains the loaded window's parsed chunks for the pass and
|
||||
/// is not counted here.
|
||||
fn resident_records(&self) -> Option<usize> {
|
||||
Some(self.chunk.as_ref().map_or(0, |c| c.len()))
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<Timestamp> {
|
||||
self.head.map(|(t, _)| t)
|
||||
}
|
||||
|
||||
@@ -150,12 +150,19 @@ fn residency_is_bounded_by_one_chunk_independent_of_window_length() {
|
||||
let mut src = M1FieldSource::open(&server, SYMBOL, None, None, M1Field::Close)
|
||||
.expect("AAPL.US has archived data");
|
||||
|
||||
// #95 adjacent friction resolved: residency is probeable through the trait —
|
||||
// a `&dyn Source` reads it without downcasting to the concrete M1FieldSource.
|
||||
assert!(
|
||||
(&src as &dyn Source).resident_records().is_some(),
|
||||
"M1FieldSource residency must be probeable via &dyn Source (no downcast)",
|
||||
);
|
||||
|
||||
// Drive the source the way `run` does (peek to pick, next to pop), sampling
|
||||
// resident records at every pull.
|
||||
let mut peak = 0usize;
|
||||
let mut total = 0usize;
|
||||
while src.peek().is_some() {
|
||||
peak = peak.max(src.resident_records());
|
||||
peak = peak.max(src.resident_records().expect("M1FieldSource reports residency"));
|
||||
Source::next(&mut src);
|
||||
total += 1;
|
||||
}
|
||||
|
||||
@@ -59,18 +59,18 @@ fn main() {
|
||||
// four field sources to exhaustion with no harness and no sinks. Per the docs
|
||||
// each keeps O(one chunk) resident — peak resident_records should stay tiny.
|
||||
{
|
||||
// NOTE: resident_records() is an INHERENT method on the concrete
|
||||
// M1FieldSource, NOT on the public Source trait (the streaming_seam test
|
||||
// calls it on a concrete source). A consumer driving boxed `dyn Source`s
|
||||
// therefore cannot read residency — recorded as a finding. We use the
|
||||
// concrete type here, per field, exactly as the docs' residency test does.
|
||||
// resident_records() is now on the public Source trait (#95 follow-up): a
|
||||
// consumer driving boxed `dyn Source`s can read residency without a
|
||||
// downcast. We use the concrete type here, per field, exactly as the docs'
|
||||
// residency test does; the value is Option (None = a source that does not
|
||||
// report, e.g. the eager VecSource), unwrapped to 0 for the max.
|
||||
let fields = [M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close];
|
||||
let mut peak_resident = 0usize;
|
||||
let mut total = 0u64;
|
||||
for f in fields {
|
||||
let mut s = M1FieldSource::open(&server, SYMBOL, Some(from_ms), Some(to_ms), f).unwrap();
|
||||
while s.peek().is_some() {
|
||||
peak_resident = peak_resident.max(s.resident_records());
|
||||
peak_resident = peak_resident.max(s.resident_records().unwrap_or(0));
|
||||
Source::next(&mut s);
|
||||
total += 1;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ fn drive(label: &str, from_ms: i64, to_ms: i64) {
|
||||
let mut total = 0u64;
|
||||
let mut peak_resident = 0usize;
|
||||
while s.peek().is_some() {
|
||||
peak_resident = peak_resident.max(s.resident_records());
|
||||
peak_resident = peak_resident.max(s.resident_records().unwrap_or(0));
|
||||
Source::next(&mut s);
|
||||
total += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user