///|
/// Trace package for M3-TRACE-REPLAY.
///|
pub fn package_id() -> String {
"lockwire/trace"
}
///|
pub(all) enum TraceDirection {
Tx
Rx
Fault
Tick
Charge
Process
Resource
Probe
Meta
} derive(Eq, Debug)
///|
pub fn TraceDirection::code(self : TraceDirection) -> Int {
match self {
Tx => 1
Rx => 2
Fault => 3
Tick => 4
Charge => 5
Process => 6
Resource => 7
Probe => 8
Meta => 9
}
}
///|
pub fn TraceDirection::label(self : TraceDirection) -> String {
match self {
Tx => "tx"
Rx => "rx"
Fault => "fault"
Tick => "tick"
Charge => "charge"
Process => "process"
Resource => "resource"
Probe => "probe"
Meta => "meta"
}
}
///|
pub(all) struct TraceEvent {
event_id : Int
parent_id : Int?
vtime : @core.VTime
clock_domain : String
raw_ns : Int64
node_id : String
medium_id : String
channel_id : @core.ChannelId?
direction : TraceDirection
payload_digest : Int?
rng_step : Int
seed : Int
backend : @core.BackendProfile
label : String
} derive(Eq, Debug)
///|
pub fn TraceEvent::make(
event_id~ : Int,
parent_id~ : Int?,
vtime~ : @core.VTime,
clock_domain~ : String,
raw_ns~ : Int64,
node_id~ : String,
medium_id~ : String,
channel_id~ : @core.ChannelId?,
direction~ : TraceDirection,
payload_digest~ : Int?,
rng_step~ : Int,
seed~ : Int,
backend~ : @core.BackendProfile,
label~ : String,
) -> TraceEvent {
{
event_id,
parent_id,
vtime,
clock_domain,
raw_ns,
node_id,
medium_id,
channel_id,
direction,
payload_digest,
rng_step,
seed,
backend,
label,
}
}
///|
pub fn payload_digest_placeholder(payload : Bytes) -> Int {
let mut digest = 17
for i in 0.. TraceEvent {
TraceEvent::make(
event_id=ev.id,
parent_id=ev.parent_id,
vtime=ev.vtime,
clock_domain~,
raw_ns=ev.vtime.ns(),
node_id~,
medium_id~,
channel_id=Some(ev.channel_id),
direction=Tx,
payload_digest=Some(payload_digest_placeholder(ev.frame.payload)),
rng_step~,
seed~,
backend~,
label="tx",
)
}
///|
pub fn rx_event_to_trace(
ev : @core.RxEvent,
seed~ : Int,
rng_step~ : Int,
clock_domain~ : String,
node_id~ : String,
medium_id~ : String,
backend~ : @core.BackendProfile,
) -> TraceEvent {
TraceEvent::make(
event_id=ev.id,
parent_id=Some(ev.parent_id),
vtime=ev.vtime,
clock_domain~,
raw_ns=ev.vtime.ns(),
node_id~,
medium_id~,
channel_id=Some(ev.channel_id),
direction=Rx,
payload_digest=Some(payload_digest_placeholder(ev.frame.payload)),
rng_step~,
seed~,
backend~,
label="rx",
)
}
///|
pub(all) struct TraceLog {
events : Array[TraceEvent]
} derive(Debug)
///|
pub fn TraceLog::new() -> TraceLog {
{ events: [] }
}
///|
pub fn TraceLog::append(self : TraceLog, event : TraceEvent) -> Unit {
self.events.push(event)
}
///|
pub fn TraceLog::len(self : TraceLog) -> Int {
self.events.length()
}
///|
pub fn TraceLog::digest(self : TraceLog, seed~ : Int) -> @core.SimDigest {
let mut digest = @core.SimDigest::empty(seed~)
let mut max_rng_step = 0
for event in self.events {
digest = mix_event(digest, event)
if event.rng_step > max_rng_step {
max_rng_step = event.rng_step
}
}
{
state_digest: digest.state_digest,
rng_steps: max_rng_step,
event_count: self.events.length(),
}
}
///|
pub fn TraceLog::portable_digest(
self : TraceLog,
seed~ : Int,
) -> @core.SimDigest {
let mut digest = @core.SimDigest::empty(seed~)
let mut max_rng_step = 0
for event in self.events {
digest = mix_portable_event(digest, event)
if event.rng_step > max_rng_step {
max_rng_step = event.rng_step
}
}
{
state_digest: digest.state_digest,
rng_steps: max_rng_step,
event_count: self.events.length(),
}
}
///|
pub fn TraceLog::to_golden_text(self : TraceLog) -> String {
let buf = StringBuilder::new()
for i, event in self.events {
if i > 0 {
buf.write_char('\n')
}
buf.write_string(event.to_golden_line())
}
buf.to_string()
}
///|
fn TraceLog::to_portable_benchmark_text(self : TraceLog) -> String {
let buf = StringBuilder::new()
for i, event in self.events {
if i > 0 {
buf.write_char('\n')
}
buf.write_string(event.to_portable_benchmark_line())
}
buf.to_string()
}
///|
pub(all) enum BenchmarkMetricStatus {
Measured
Estimated
Unsupported
Missing
} derive(Eq, Debug)
///|
pub fn BenchmarkMetricStatus::label(self : BenchmarkMetricStatus) -> String {
match self {
Measured => "measured"
Estimated => "estimated"
Unsupported => "unsupported"
Missing => "missing"
}
}
///|
pub(all) struct BenchmarkMetric {
name : String
status : BenchmarkMetricStatus
value : Int64?
unit : String
note : String
} derive(Eq, Debug)
///|
pub fn BenchmarkMetric::make(
name~ : String,
status~ : BenchmarkMetricStatus,
value~ : Int64?,
unit~ : String,
note~ : String,
) -> BenchmarkMetric {
{ name, status, value, unit, note }
}
///|
pub fn BenchmarkMetric::measured(
name~ : String,
value~ : Int64,
unit~ : String,
note~ : String,
) -> BenchmarkMetric {
BenchmarkMetric::make(name~, status=Measured, value=Some(value), unit~, note~)
}
///|
pub fn BenchmarkMetric::estimated(
name~ : String,
value~ : Int64,
unit~ : String,
note~ : String,
) -> BenchmarkMetric {
BenchmarkMetric::make(
name~,
status=Estimated,
value=Some(value),
unit~,
note~,
)
}
///|
pub fn BenchmarkMetric::unsupported(
name~ : String,
unit~ : String,
note~ : String,
) -> BenchmarkMetric {
BenchmarkMetric::make(name~, status=Unsupported, value=None, unit~, note~)
}
///|
pub fn BenchmarkMetric::missing(
name~ : String,
unit~ : String,
note~ : String,
) -> BenchmarkMetric {
BenchmarkMetric::make(name~, status=Missing, value=None, unit~, note~)
}
///|
pub fn BenchmarkMetric::to_text(self : BenchmarkMetric) -> String {
"metric=" +
escape_text(self.name) +
"|status=" +
self.status.label() +
"|value=" +
int64_option_to_text(self.value) +
"|unit=" +
escape_text(self.unit) +
"|note=" +
escape_text(self.note)
}
///|
pub(all) struct RuntimeBenchmarkReport {
scenario : String
seed : Int
backend : @core.BackendProfile
digest : @core.SimDigest
metrics : Array[BenchmarkMetric]
hard_realtime_claim : Bool
live_io_evidence : Bool
} derive(Eq, Debug)
///|
pub fn RuntimeBenchmarkReport::make(
scenario~ : String,
seed~ : Int,
backend~ : @core.BackendProfile,
digest~ : @core.SimDigest,
metrics~ : Array[BenchmarkMetric],
hard_realtime_claim~ : Bool,
live_io_evidence~ : Bool,
) -> RuntimeBenchmarkReport {
{
scenario,
seed,
backend,
digest,
metrics,
hard_realtime_claim,
live_io_evidence,
}
}
///|
pub fn RuntimeBenchmarkReport::metric_status(
self : RuntimeBenchmarkReport,
name : String,
) -> BenchmarkMetricStatus? {
for metric in self.metrics {
if metric.name == name {
return Some(metric.status)
}
}
None
}
///|
pub fn RuntimeBenchmarkReport::metric_value(
self : RuntimeBenchmarkReport,
name : String,
) -> Int64? {
for metric in self.metrics {
if metric.name == name {
return metric.value
}
}
None
}
///|
pub fn RuntimeBenchmarkReport::unsupported_count(
self : RuntimeBenchmarkReport,
) -> Int {
let mut count = 0
for metric in self.metrics {
if metric.status is Unsupported {
count += 1
}
}
count
}
///|
pub fn RuntimeBenchmarkReport::missing_count(
self : RuntimeBenchmarkReport,
) -> Int {
let mut count = 0
for metric in self.metrics {
if metric.status is Missing {
count += 1
}
}
count
}
///|
pub fn RuntimeBenchmarkReport::passes_offline_boundary(
self : RuntimeBenchmarkReport,
) -> Bool {
self.digest.event_count > 0 &&
self.metrics.length() >= 6 &&
!self.hard_realtime_claim &&
!self.live_io_evidence
}
///|
pub fn RuntimeBenchmarkReport::to_text(self : RuntimeBenchmarkReport) -> String {
let buf = StringBuilder::new()
buf.write_string("benchmark=")
buf.write_string(escape_text(self.scenario))
buf.write_string("|seed=")
buf.write_string(self.seed.to_string())
buf.write_string("|backend=")
buf.write_string(@core.backend_profile_label(self.backend))
buf.write_string("|digest=")
buf.write_string(self.digest.state_digest.to_string())
buf.write_string("|rng_steps=")
buf.write_string(self.digest.rng_steps.to_string())
buf.write_string("|event_count=")
buf.write_string(self.digest.event_count.to_string())
buf.write_string("|hard_realtime_claim=")
buf.write_string(bool_to_text(self.hard_realtime_claim))
buf.write_string("|live_io_evidence=")
buf.write_string(bool_to_text(self.live_io_evidence))
for metric in self.metrics {
buf.write_char('\n')
buf.write_string(metric.to_text())
}
buf.to_string()
}
///|
pub fn TraceLog::benchmark_report(
self : TraceLog,
scenario~ : String,
seed~ : Int,
backend~ : @core.BackendProfile,
copies_per_frame~ : Int,
) -> RuntimeBenchmarkReport {
let trace_text = self.to_portable_benchmark_text()
RuntimeBenchmarkReport::make(
scenario~,
seed~,
backend~,
digest=self.portable_digest(seed~),
metrics=[
BenchmarkMetric::measured(
name="events/run",
value=Int64::from_int(self.len()),
unit="events",
note="deterministic event count; not wall-clock throughput",
),
BenchmarkMetric::unsupported(
name="events/s",
unit="events/s",
note="wall-clock timer is not part of offline deterministic run",
),
BenchmarkMetric::measured(
name="trace-bytes/run",
value=Int64::from_int(trace_text.length()),
unit="bytes",
note="portable trace text size excluding backend label",
),
BenchmarkMetric::unsupported(
name="trace-throughput",
unit="bytes/s",
note="wall-clock timer is not part of offline deterministic run",
),
BenchmarkMetric::unsupported(
name="alloc/event",
unit="alloc/event",
note="MoonBit runtime allocation counter is unavailable here",
),
BenchmarkMetric::measured(
name="copy/frame",
value=Int64::from_int(copies_per_frame),
unit="copies/frame",
note="declared by the offline medium fixture",
),
],
hard_realtime_claim=false,
live_io_evidence=false,
)
}
///|
pub struct Recording {
priv log : TraceLog
} derive(Debug)
///|
pub fn Recording::new() -> Recording {
{ log: TraceLog::new() }
}
///|
pub fn Recording::record(self : Recording, event : TraceEvent) -> Unit {
self.log.append(event)
}
///|
pub fn Recording::record_tx(
self : Recording,
ev : @core.TxEvent,
seed~ : Int,
rng_step~ : Int,
clock_domain~ : String,
node_id~ : String,
medium_id~ : String,
backend~ : @core.BackendProfile,
) -> TraceEvent {
let event = tx_event_to_trace(
ev,
seed~,
rng_step~,
clock_domain~,
node_id~,
medium_id~,
backend~,
)
self.record(event)
event
}
///|
pub fn Recording::record_rx(
self : Recording,
ev : @core.RxEvent,
seed~ : Int,
rng_step~ : Int,
clock_domain~ : String,
node_id~ : String,
medium_id~ : String,
backend~ : @core.BackendProfile,
) -> TraceEvent {
let event = rx_event_to_trace(
ev,
seed~,
rng_step~,
clock_domain~,
node_id~,
medium_id~,
backend~,
)
self.record(event)
event
}
///|
pub fn Recording::get_log(self : Recording) -> TraceLog {
self.log
}
///|
pub fn Recording::digest(self : Recording, seed~ : Int) -> @core.SimDigest {
self.log.digest(seed~)
}
///|
pub struct Replay {
priv log : TraceLog
priv mut cursor : Int
} derive(Debug)
///|
pub fn Replay::from_log(log : TraceLog) -> Replay {
{ log, cursor: 0 }
}
///|
pub fn Replay::remaining(self : Replay) -> Int {
self.log.len() - self.cursor
}
///|
pub fn Replay::is_exhausted(self : Replay) -> Bool {
self.cursor >= self.log.len()
}
///|
pub fn Replay::peek(self : Replay) -> TraceEvent? {
self.log.events.get(self.cursor)
}
///|
pub fn Replay::next_event(self : Replay) -> TraceEvent? {
match self.peek() {
Some(event) => {
self.cursor += 1
Some(event)
}
None => None
}
}
///|
pub fn Replay::digest(self : Replay, seed~ : Int) -> @core.SimDigest {
self.log.digest(seed~)
}
///|
pub fn TraceEvent::to_golden_line(self : TraceEvent) -> String {
"event_id=" +
self.event_id.to_string() +
"|parent_id=" +
int_option_to_text(self.parent_id) +
"|vtime_ns=" +
self.vtime.ns().to_string() +
"|clock_domain=" +
escape_text(self.clock_domain) +
"|raw_ns=" +
self.raw_ns.to_string() +
"|node_id=" +
escape_text(self.node_id) +
"|medium_id=" +
escape_text(self.medium_id) +
"|channel_id=" +
channel_option_to_text(self.channel_id) +
"|direction=" +
self.direction.label() +
"|payload_digest=" +
int_option_to_text(self.payload_digest) +
"|rng_step=" +
self.rng_step.to_string() +
"|seed=" +
self.seed.to_string() +
"|backend=" +
@core.backend_profile_label(self.backend) +
"|label=" +
escape_text(self.label)
}
///|
fn TraceEvent::to_portable_benchmark_line(self : TraceEvent) -> String {
"event_id=" +
self.event_id.to_string() +
"|parent_id=" +
int_option_to_text(self.parent_id) +
"|vtime_ns=" +
self.vtime.ns().to_string() +
"|clock_domain=" +
escape_text(self.clock_domain) +
"|raw_ns=" +
self.raw_ns.to_string() +
"|node_id=" +
escape_text(self.node_id) +
"|medium_id=" +
escape_text(self.medium_id) +
"|channel_id=" +
channel_option_to_text(self.channel_id) +
"|direction=" +
self.direction.label() +
"|payload_digest=" +
int_option_to_text(self.payload_digest) +
"|rng_step=" +
self.rng_step.to_string() +
"|seed=" +
self.seed.to_string() +
"|label=" +
escape_text(self.label)
}
///|
fn mix_event(digest : @core.SimDigest, event : TraceEvent) -> @core.SimDigest {
let digest = digest
.mix(event.event_id)
.mix(event.vtime.ns().to_int())
.mix(event.raw_ns.to_int())
.mix(event.direction.code())
.mix(event.rng_step)
.mix(event.seed)
.mix(backend_code(event.backend))
let digest = mix_int_option(digest, event.parent_id)
let digest = mix_channel_option(digest, event.channel_id)
let digest = mix_int_option(digest, event.payload_digest)
let digest = mix_string(digest, event.clock_domain)
let digest = mix_string(digest, event.node_id)
let digest = mix_string(digest, event.medium_id)
mix_string(digest, event.label)
}
///|
fn mix_portable_event(
digest : @core.SimDigest,
event : TraceEvent,
) -> @core.SimDigest {
let digest = digest
.mix(event.event_id)
.mix(event.vtime.ns().to_int())
.mix(event.raw_ns.to_int())
.mix(event.direction.code())
.mix(event.rng_step)
.mix(event.seed)
let digest = mix_int_option(digest, event.parent_id)
let digest = mix_channel_option(digest, event.channel_id)
let digest = mix_int_option(digest, event.payload_digest)
let digest = mix_string(digest, event.clock_domain)
let digest = mix_string(digest, event.node_id)
let digest = mix_string(digest, event.medium_id)
mix_string(digest, event.label)
}
///|
fn backend_code(profile : @core.BackendProfile) -> Int {
match profile {
SimNative => 1
SimWasm => 2
Replay => 3
RealLinux => 4
RealEmbedded => 5
}
}
///|
fn mix_int_option(digest : @core.SimDigest, value : Int?) -> @core.SimDigest {
match value {
Some(v) => digest.mix(1).mix(v)
None => digest.mix(0)
}
}
///|
fn mix_channel_option(
digest : @core.SimDigest,
value : @core.ChannelId?,
) -> @core.SimDigest {
match value {
Some(v) => digest.mix(1).mix(v.value())
None => digest.mix(0)
}
}
///|
fn mix_string(digest : @core.SimDigest, value : String) -> @core.SimDigest {
let mut out = digest.mix(value.length())
for c in value {
out = out.mix(c.to_int())
}
out
}
///|
fn int_option_to_text(value : Int?) -> String {
match value {
Some(v) => v.to_string()
None => "none"
}
}
///|
fn int64_option_to_text(value : Int64?) -> String {
match value {
Some(v) => v.to_string()
None => "none"
}
}
///|
fn bool_to_text(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn channel_option_to_text(value : @core.ChannelId?) -> String {
match value {
Some(v) => v.value().to_string()
None => "none"
}
}
///|
fn escape_text(value : String) -> String {
let buf = StringBuilder::new()
for c in value {
match c {
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\\' => buf.write_string("\\\\")
'|' => buf.write_string("\\|")
_ => buf.write_char(c)
}
}
buf.to_string()
}