///|
/// Exchange artifact support for stable trace export/import.
///|
pub(all) suberror TraceArtifactError {
InvalidArtifact(String)
MissingField(String)
InvalidField(String, String)
} derive(Debug)
///|
pub(all) enum TraceArtifactFormat {
IsoTraceTextV1
} derive(Eq, Debug)
///|
pub fn TraceArtifactFormat::label(self : TraceArtifactFormat) -> String {
match self {
IsoTraceTextV1 => "isotrace-text-v1"
}
}
///|
pub(all) struct TraceArtifactSidecar {
schema : String
pcapng_route : String
pcapng_blocker : String?
seed : Int
event_count : Int
rng_steps : Int
digest_state : Int64
portable_digest_state : Int64
clock_domains : Array[String]
backend_labels : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct TraceArtifact {
format : TraceArtifactFormat
sidecar : TraceArtifactSidecar
trace_text : String
} derive(Eq, Debug)
///|
pub fn TraceArtifact::from_log(log : TraceLog, seed~ : Int) -> TraceArtifact {
let digest = log.digest(seed~)
let portable_digest = log.portable_digest(seed~)
{
format: IsoTraceTextV1,
sidecar: {
schema: "lockwire.trace.artifact.sidecar.v1",
pcapng_route: "pcapng-epb+custom-block-sidecar-v1",
pcapng_blocker: None,
seed,
event_count: log.len(),
rng_steps: digest.rng_steps,
digest_state: digest.state_digest,
portable_digest_state: portable_digest.state_digest,
clock_domains: collect_clock_domains(log),
backend_labels: collect_backend_labels(log),
},
trace_text: log.to_golden_text(),
}
}
///|
pub fn TraceLog::to_artifact(self : TraceLog, seed~ : Int) -> TraceArtifact {
TraceArtifact::from_log(self, seed~)
}
///|
pub fn TraceArtifact::import_log(
self : TraceArtifact,
) -> TraceLog raise TraceArtifactError {
if self.format != IsoTraceTextV1 {
raise TraceArtifactError::InvalidArtifact("unsupported artifact format")
}
let log = parse_trace_log_text(self.trace_text)
validate_artifact_sidecar(log, self.sidecar)
log
}
///|
pub fn TraceArtifact::replay_digest(
self : TraceArtifact,
) -> @core.SimDigest raise TraceArtifactError {
let log = self.import_log()
Replay::from_log(log).digest(seed=self.sidecar.seed)
}
///|
pub fn TraceArtifact::to_text(self : TraceArtifact) -> String {
let buf = StringBuilder::new()
buf.write_string("# lockwire-trace-artifact-v1\n")
buf.write_string("format=" + self.format.label() + "\n")
buf.write_string("schema=" + self.sidecar.schema + "\n")
buf.write_string("pcapng_route=" + self.sidecar.pcapng_route + "\n")
buf.write_string(
"pcapng_blocker=" +
string_option_to_artifact_text(self.sidecar.pcapng_blocker) +
"\n",
)
buf.write_string("seed=" + self.sidecar.seed.to_string() + "\n")
buf.write_string("event_count=" + self.sidecar.event_count.to_string() + "\n")
buf.write_string("rng_steps=" + self.sidecar.rng_steps.to_string() + "\n")
buf.write_string(
"digest_state=" + self.sidecar.digest_state.to_string() + "\n",
)
buf.write_string(
"portable_digest_state=" +
self.sidecar.portable_digest_state.to_string() +
"\n",
)
buf.write_string(
"clock_domains=" + self.sidecar.clock_domains.join(",") + "\n",
)
buf.write_string(
"backend_labels=" + self.sidecar.backend_labels.join(",") + "\n",
)
buf.write_string("-- trace --\n")
buf.write_string(self.trace_text)
buf.to_string()
}
///|
pub fn parse_trace_artifact_text(
text : String,
) -> TraceArtifact raise TraceArtifactError {
let lines = split_artifact_lines(text)
guard lines.get(0) is Some("# lockwire-trace-artifact-v1") else {
raise TraceArtifactError::InvalidArtifact("missing artifact header")
}
let fields : Array[(String, String)] = []
let trace_lines : Array[String] = []
let mut in_trace = false
for i in 1.. TraceLog raise TraceArtifactError {
let log = TraceLog::new()
let lines = split_artifact_lines(text)
for i, line in lines {
if !line[:].trim().is_empty() {
log.append(parse_trace_event_line(line, line_number=i + 1))
}
}
log
}
///|
fn validate_artifact_sidecar(
log : TraceLog,
sidecar : TraceArtifactSidecar,
) -> Unit raise TraceArtifactError {
let digest = log.digest(seed=sidecar.seed)
let portable_digest = log.portable_digest(seed=sidecar.seed)
if digest.event_count != sidecar.event_count {
raise TraceArtifactError::InvalidArtifact("event_count mismatch")
}
if digest.rng_steps != sidecar.rng_steps {
raise TraceArtifactError::InvalidArtifact("rng_steps mismatch")
}
if digest.state_digest != sidecar.digest_state {
raise TraceArtifactError::InvalidArtifact("digest_state mismatch")
}
if portable_digest.state_digest != sidecar.portable_digest_state {
raise TraceArtifactError::InvalidArtifact("portable_digest_state mismatch")
}
if collect_clock_domains(log) != sidecar.clock_domains {
raise TraceArtifactError::InvalidArtifact("clock_domains mismatch")
}
if collect_backend_labels(log) != sidecar.backend_labels {
raise TraceArtifactError::InvalidArtifact("backend_labels mismatch")
}
}
///|
fn parse_trace_event_line(
line : String,
line_number~ : Int,
) -> TraceEvent raise TraceArtifactError {
let fields = parse_trace_line_pairs(line, line_number~)
let event_id = parse_required_int(fields, "event_id")
let raw_ns = parse_required_int64(fields, "raw_ns")
let vtime_ns = parse_required_int64(fields, "vtime_ns")
TraceEvent::make(
event_id~,
parent_id=parse_int_option(required_trace_field(fields, "parent_id")),
vtime=@core.VTime::from_ns(vtime_ns),
clock_domain=unescape_artifact_text(
required_trace_field(fields, "clock_domain"),
),
raw_ns~,
node_id=unescape_artifact_text(required_trace_field(fields, "node_id")),
medium_id=unescape_artifact_text(required_trace_field(fields, "medium_id")),
channel_id=parse_channel_option(required_trace_field(fields, "channel_id")),
direction=parse_trace_direction(required_trace_field(fields, "direction")),
payload_digest=parse_int_option(
required_trace_field(fields, "payload_digest"),
),
rng_step=parse_required_int(fields, "rng_step"),
seed=parse_required_int(fields, "seed"),
backend=parse_backend_profile(required_trace_field(fields, "backend")),
label=unescape_artifact_text(required_trace_field(fields, "label")),
)
}
///|
fn parse_trace_line_pairs(
line : String,
line_number~ : Int,
) -> Array[(String, String)] raise TraceArtifactError {
let fields : Array[(String, String)] = []
for part in split_escaped(line, '|') {
match part.split_once("=") {
Some((key, value)) => fields.push((key.to_owned(), value.to_owned()))
None =>
raise TraceArtifactError::InvalidArtifact(
"invalid trace line " + line_number.to_string(),
)
}
}
fields
}
///|
fn parse_artifact_pair(
line : String,
) -> (String, String) raise TraceArtifactError {
match line.split_once("=") {
Some((key, value)) => (key.to_owned(), value.to_owned())
None => raise TraceArtifactError::InvalidArtifact("invalid sidecar line")
}
}
///|
fn required_artifact_field(
fields : Array[(String, String)],
name : String,
) -> String raise TraceArtifactError {
for field in fields {
let (key, value) = field
if key == name {
return value
}
}
raise TraceArtifactError::MissingField(name)
}
///|
fn required_trace_field(
fields : Array[(String, String)],
name : String,
) -> String raise TraceArtifactError {
required_artifact_field(fields, name)
}
///|
fn parse_artifact_int(
fields : Array[(String, String)],
name : String,
) -> Int raise TraceArtifactError {
parse_int_field(required_artifact_field(fields, name), name)
}
///|
fn parse_artifact_int64(
fields : Array[(String, String)],
name : String,
) -> Int64 raise TraceArtifactError {
parse_int64_field(required_artifact_field(fields, name), name)
}
///|
fn parse_required_int(
fields : Array[(String, String)],
name : String,
) -> Int raise TraceArtifactError {
parse_int_field(required_trace_field(fields, name), name)
}
///|
fn parse_required_int64(
fields : Array[(String, String)],
name : String,
) -> Int64 raise TraceArtifactError {
parse_int64_field(required_trace_field(fields, name), name)
}
///|
fn parse_int_field(
value : String,
name : String,
) -> Int raise TraceArtifactError {
@string.parse_int(value[:]) catch {
_ => raise TraceArtifactError::InvalidField(name, value)
}
}
///|
fn parse_int64_field(
value : String,
name : String,
) -> Int64 raise TraceArtifactError {
@string.parse_int64(value[:]) catch {
_ => raise TraceArtifactError::InvalidField(name, value)
}
}
///|
fn parse_int_option(value : String) -> Int? raise TraceArtifactError {
if value == "none" {
None
} else {
Some(parse_int_field(value, "int_option"))
}
}
///|
fn parse_channel_option(
value : String,
) -> @core.ChannelId? raise TraceArtifactError {
if value == "none" {
None
} else {
Some(@core.ChannelId(parse_int_field(value, "channel_id")))
}
}
///|
fn parse_trace_direction(
value : String,
) -> TraceDirection raise TraceArtifactError {
match value {
"tx" => Tx
"rx" => Rx
"fault" => Fault
"tick" => Tick
"charge" => Charge
"process" => Process
"resource" => Resource
"probe" => Probe
"meta" => Meta
other => raise TraceArtifactError::InvalidField("direction", other)
}
}
///|
fn parse_backend_profile(
value : String,
) -> @core.BackendProfile raise TraceArtifactError {
match value {
"sim-native" => @core.SimNative
"sim-wasm" => @core.SimWasm
"replay" => @core.Replay
"real-linux" => @core.RealLinux
"real-embedded" => @core.RealEmbedded
other => raise TraceArtifactError::InvalidField("backend", other)
}
}
///|
fn parse_artifact_format(
value : String,
) -> TraceArtifactFormat raise TraceArtifactError {
match value {
"isotrace-text-v1" => IsoTraceTextV1
other => raise TraceArtifactError::InvalidField("format", other)
}
}
///|
fn string_option_to_artifact_text(value : String?) -> String {
match value {
Some(text) => escape_text(text)
None => "none"
}
}
///|
fn parse_string_option_artifact_text(value : String) -> String? {
if value == "none" {
None
} else {
Some(unescape_artifact_text(value))
}
}
///|
fn collect_clock_domains(log : TraceLog) -> Array[String] {
let domains : Array[String] = []
for event in log.events {
push_unique(domains, event.clock_domain)
}
domains
}
///|
fn collect_backend_labels(log : TraceLog) -> Array[String] {
let labels : Array[String] = []
for event in log.events {
push_unique(labels, @core.backend_profile_label(event.backend))
}
labels
}
///|
fn push_unique(items : Array[String], value : String) -> Unit {
let mut found = false
for item in items {
if item == value {
found = true
}
}
if !found {
items.push(value)
}
}
///|
fn split_artifact_lines(text : String) -> Array[String] {
let lines : Array[String] = []
let buf = StringBuilder::new()
for c in text {
if c == '\n' {
lines.push(strip_trailing_cr(buf.to_string()))
buf.reset()
} else {
buf.write_char(c)
}
}
let remaining = strip_trailing_cr(buf.to_string())
if remaining.length() > 0 {
lines.push(remaining)
}
lines
}
///|
fn strip_trailing_cr(value : String) -> String {
if value.has_suffix("\r") {
value[:value.length() - 1].to_owned()
} else {
value
}
}
///|
fn join_lines(lines : Array[String]) -> String {
let buf = StringBuilder::new()
for i, line in lines {
if i > 0 {
buf.write_char('\n')
}
buf.write_string(line)
}
buf.to_string()
}
///|
fn split_csv(value : String) -> Array[String] {
if value == "" {
[]
} else {
[
for part in value.split(",") => part.to_owned()
]
}
}
///|
fn split_escaped(value : String, separator : Char) -> Array[String] {
let out : Array[String] = []
let buf = StringBuilder::new()
let mut escaped = false
for c in value {
if c == separator && !escaped {
out.push(buf.to_string())
buf.reset()
} else {
if c == '\\' && !escaped {
escaped = true
} else {
escaped = false
}
buf.write_char(c)
}
}
out.push(buf.to_string())
out
}
///|
fn unescape_artifact_text(value : String) -> String {
let buf = StringBuilder::new()
let mut escaped = false
for c in value {
if escaped {
match c {
'n' => buf.write_char('\n')
'r' => buf.write_char('\r')
'\\' => buf.write_char('\\')
'|' => buf.write_char('|')
other => buf.write_char(other)
}
escaped = false
} else if c == '\\' {
escaped = true
} else {
buf.write_char(c)
}
}
if escaped {
buf.write_char('\\')
}
buf.to_string()
}