///|
/// Cache policy for repeated waveform queries.
pub enum TraceCachePolicy {
NoCache
LastValue
SignalSnapshots
WindowResults
} derive(Debug, Eq)
///|
/// A cached value lookup with source provenance.
pub struct TraceCacheEntry {
signal_id : String
time : Int64
value : Value?
source_time : Int64?
hits : Int
} derive(Debug)
///|
/// Reusable cache for signal-at-time and snapshot queries.
pub struct TraceCache {
policy : TraceCachePolicy
entries : Array[TraceCacheEntry]
lookups : Int
hits : Int
misses : Int
capacity : Int
} derive(Debug)
///|
/// List cache policies.
pub fn trace_cache_policy_catalog() -> Array[TraceCachePolicy] {
[NoCache, LastValue, SignalSnapshots, WindowResults]
}
///|
/// Create an empty bounded cache.
pub fn trace_cache(policy : TraceCachePolicy, capacity : Int) -> TraceCache {
{
policy,
entries: [],
lookups: 0,
hits: 0,
misses: 0,
capacity: Int::max(1, capacity),
}
}
///|
/// Look up a signal value and remember the result.
pub fn TraceCache::value_at(
self : TraceCache,
trace : Trace,
signal_id : String,
time : Int64,
) -> (TraceCache, Value?) {
let lookups = self.lookups + 1
if self.policy is NoCache {
return (
{ ..self, lookups, misses: self.misses + 1 },
value_at(trace, signal_id, time),
)
}
for index, entry in self.entries {
if entry.signal_id == signal_id && entry.time == time {
let entries = self.entries.copy()
entries[index] = { ..entry, hits: entry.hits + 1 }
return ({ ..self, entries, lookups, hits: self.hits + 1 }, entry.value)
}
}
let value = value_at(trace, signal_id, time)
let source_time = trace.changes
.filter(fn(change) { change.id == signal_id && change.time <= time })
.fold(init=None, fn(current, item) {
match current {
Some(previous) if previous >= item.time => Some(previous)
_ => Some(item.time)
}
})
let entries = self.entries.copy()
if entries.length() >= self.capacity {
let _ = entries.remove(0)
}
entries.push({ signal_id, time, value, source_time, hits: 0 })
({ ..self, entries, lookups, misses: self.misses + 1 }, value)
}
///|
/// Precompute a cache for every change timestamp.
pub fn TraceCache::warm_signal(
self : TraceCache,
trace : Trace,
signal_id : String,
) -> TraceCache {
let mut cache = self
for change in trace.changes {
if change.id == signal_id {
let result = cache.value_at(trace, signal_id, change.time)
cache = result.0
}
}
cache
}
///|
/// Remove all entries for one signal.
pub fn TraceCache::invalidate_signal(
self : TraceCache,
signal_id : String,
) -> TraceCache {
{
..self,
entries: self.entries.filter(fn(item) { item.signal_id != signal_id }),
}
}
///|
/// Remove every cached entry.
pub fn TraceCache::clear(self : TraceCache) -> TraceCache {
{ ..self, entries: [] }
}
///|
/// Return the cache hit ratio.
pub fn TraceCache::hit_ratio(self : TraceCache) -> Double {
if self.lookups == 0 {
0.0
} else {
self.hits.to_double() / self.lookups.to_double()
}
}
///|
/// Return a stable cache summary.
pub fn TraceCache::summary(self : TraceCache) -> String {
"policy=" +
trace_cache_policy_name(self.policy) +
" entries=" +
self.entries.length().to_string() +
" lookups=" +
self.lookups.to_string() +
" hits=" +
self.hits.to_string() +
" misses=" +
self.misses.to_string() +
" hit_ratio=" +
self.hit_ratio().to_string()
}
///|
/// Export cache entries as CSV.
pub fn trace_cache_entries_csv(entries : Array[TraceCacheEntry]) -> String {
let out = StringBuilder()
out.write_string("signal,time,value,source_time,hits\n")
for entry in entries {
out.write_string(
escape_text(entry.signal_id) + "," + entry.time.to_string() + ",",
)
out.write_string(
match entry.value {
Some(value) => escape_text(value.display())
None => ""
},
)
out.write_string(
"," +
trace_cache_optional_i64(entry.source_time) +
"," +
entry.hits.to_string() +
"\n",
)
}
out.to_string()
}
///|
/// Name a cache policy.
pub fn trace_cache_policy_name(policy : TraceCachePolicy) -> String {
match policy {
NoCache => "none"
LastValue => "last-value"
SignalSnapshots => "signal-snapshots"
WindowResults => "window-results"
}
}
///|
/// Encode an optional timestamp.
fn trace_cache_optional_i64(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}