From f3ccb25d87f592a266a68c342734c94f71defd4a Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 12 Sep 2026 21:36:36 +0200 Subject: [PATCH 01/16] Write a marker value of zero instead of dropping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `write_tracing_csv` tested the optional fields for truth, so a marker value of `0` — and a span `deadline_us` or `value` of `0` — was written as an empty CSV cell. The column is meant to be empty only when the field is absent, and zero is a legitimate payload: a reason code, a count of nothing, a deadline at the activation instant. Downstream an empty cell is indistinguishable from a missing one. `diagram.py` coerces it to NaN and renders `—`, so the marker that reports "the controller skipped this cycle, reason 0" arrives on screen with its reason erased — the one value the marker existed to carry. The decoder was never at fault: it reads the field with `HasField`, which handles explicit presence correctly. The tests cover zero and absent separately for both markers and spans, because the two cases must produce different cells and only their equality was ever broken. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015X4gKfAJaHxNRqDhhRcwc3 --- python/CHANGELOG.md | 11 +++++++ python/src/execution_trace/decode.py | 10 +++++-- python/tests/test_decode.py | 44 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index a762e47..26b5dd7 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] + +### Fixed + +- `write_tracing_csv` wrote a marker value of `0` as an empty CSV cell, and likewise a span + `deadline_us` or `value` of `0`. The column is meant to be empty only when the field is + absent, but the writer tested the value for truth, and zero is a legitimate payload — a + reason code, a count of nothing, a deadline at the activation instant. Downstream the empty + cell reads as "no value": `diagram.py` renders it as `—`, hiding exactly the case the marker + was emitted to report. The decoder was always correct; only the writer dropped it. + ## [0.1.4] - 2026-05-24 ### Added diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index e2d7bdf..5146265 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -230,6 +230,12 @@ def write_tracing_csv( path = os.path.join(output_dir, filename) latest = os.path.join(output_dir, "trace_latest.csv") + # An optional column is empty only when the field is absent. Testing the value for + # truth instead would write 0 as empty, and zero is a legitimate payload: a reason + # code, a count of nothing, a deadline at the activation instant. + def _optional(field: float | int | None) -> float | int | str: + return "" if field is None else field + with open(path, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["name", "type", "start_us", "end_us", "priority", "deadline_us", "value"]) @@ -238,13 +244,13 @@ def write_tracing_csv( writer.writerow([ r.name, "marker", r.timestamp_us, r.timestamp_us, - 0, "", r.value or "", + 0, "", _optional(r.value), ]) else: writer.writerow([ r.name, r.type, r.start_us, r.end_us, - r.priority, r.deadline_us or "", r.value or "", + r.priority, _optional(r.deadline_us), _optional(r.value), ]) # Atomically replace the symlink so trace_latest.csv always points to newest. diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 2c4ea80..6d9a426 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -256,6 +256,50 @@ def test_marker_row_start_eq_end(self, tmp_path): assert rows[0]["start_us"] == rows[0]["end_us"] assert rows[0]["type"] == "marker" + def test_marker_value_of_zero_is_written(self, tmp_path): + # Zero is a legitimate payload — a reason code, or a count of nothing — and a + # truth test on the value would write it as empty, which reads downstream as + # "no value" and hides exactly the case the marker was emitted to report. + records = [MarkerRecord(name="ctl_skip", timestamp_us=1500.0, value=0)] + path = write_tracing_csv(records, output_dir=str(tmp_path)) + assert path is not None + with open(path) as f: + row = list(csv.DictReader(f))[0] + assert row["value"] == "0" + + def test_marker_without_value_is_written_empty(self, tmp_path): + records = [MarkerRecord(name="tick", timestamp_us=1500.0, value=None)] + path = write_tracing_csv(records, output_dir=str(tmp_path)) + assert path is not None + with open(path) as f: + row = list(csv.DictReader(f))[0] + assert row["value"] == "" + + def test_span_deadline_and_value_of_zero_are_written(self, tmp_path): + records = [ + TraceEvent( + name="t", type="task", start_us=0.0, end_us=1.0, + priority=1, deadline_us=0.0, value=0, + ), + ] + path = write_tracing_csv(records, output_dir=str(tmp_path)) + assert path is not None + with open(path) as f: + row = list(csv.DictReader(f))[0] + assert float(row["deadline_us"]) == pytest.approx(0.0) + assert row["value"] == "0" + + def test_span_without_deadline_or_value_is_written_empty(self, tmp_path): + records = [ + TraceEvent(name="t", type="task", start_us=0.0, end_us=1.0, priority=1), + ] + path = write_tracing_csv(records, output_dir=str(tmp_path)) + assert path is not None + with open(path) as f: + row = list(csv.DictReader(f))[0] + assert row["deadline_us"] == "" + assert row["value"] == "" + def test_symlink_updated(self, tmp_path): records = [ TraceEvent(name="t", type="task", start_us=0.0, end_us=1.0, priority=1), From 671c54fca6efaeb9c79d7fde19fba14e2cf9df62 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 21:39:09 +0200 Subject: [PATCH 02/16] Land the v2 trace wire format: interning, deltas, TraceStart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXEC-TRACE-002 increment 6, steps 2 and 3 of §18.5. The schema, the encoder and the Python decoder all move together, since v2 is not wire-compatible with v1 and nothing decodes a half-migrated stream. One flat `TraceFrame` carries every frame class, discriminated by `event_type`, rather than a oneof envelope: proto3 omits unset fields, so an event pays nothing for the dictionary fields it does not use where an envelope taxes every event with a tag and a length. Field numbers stay at or below 15 so every tag is one byte. Per-occurrence events now carry a dictionary id instead of a name and a delta instead of an absolute timestamp. The name, source type, priority and deadline are fixed by the name, so they go out once on a NAME_REGISTERED frame. Interning is keyed on content rather than on the `&'static str` pointer §5.5 proposed: by the time an event reaches the encoder the literal's pointer is gone, and the scan is linear over at most 64 entries, with a length check before any byte comparison, in the task that owns the wire. Measured steady state is 11-12 B/event over the delta range the firmware produces, against the 13 §18.1 predicted from candidate schemas — about a byte better, because the sequence and the delta both land in their two-varint-byte range more often than that estimate assumed. A snapshot test records the whole table and a budget test holds it against §18.1's figures. Two things the format forced, both recorded in tests: `decode_trace_frame` returns a `RawTraceFrame` rather than a `TraceEvent`, because in v2 an event is not recoverable from one frame. Name resolution and delta accumulation belong to the host decoder, which holds per-stream state; `TraceStreamState` is where that now lives on the Python side. Wrapping the sequence at 16 384 weakens reset detection. A restart at zero only looks backwards when the pre-reset counter was below half the modulus, so from high in the range a reboot reads as an ordinary gap. The TRACE_START frame is therefore the reliable reset signal, and a second header mid-stream now clears the dictionary — a stale entry would otherwise resolve a new id to the wrong name. `TraceEvent`, `TraceSink` and the channel element type are untouched, so the recording layer and its RAM footprint are unchanged; §5.10's reclaim stays in increment 8 where §9 put it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- README.md | 71 +- build.rs | 3 +- examples/simulate.rs | 161 +- proto/tracing.proto | 70 +- python/src/execution_trace/__init__.py | 8 +- python/src/execution_trace/_cli/decode.py | 6 +- .../src/execution_trace/_proto/tracing_pb2.py | 16 +- .../execution_trace/_proto/tracing_pb2.pyi | 41 +- python/src/execution_trace/decode.py | 261 +++- python/src/execution_trace/stream.py | 36 +- python/tests/conftest.py | 191 ++- python/tests/test_decode.py | 388 +++-- src/encode.rs | 1315 ++++++++++++----- src/lib.rs | 2 +- ...sizes_across_the_delta_range_snapshot.snap | 51 + ...ests__span_start_wire_format_snapshot.snap | 39 +- 16 files changed, 1944 insertions(+), 715 deletions(-) create mode 100644 src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap diff --git a/README.md b/README.md index 1b22fef..169a7d2 100644 --- a/README.md +++ b/README.md @@ -61,50 +61,93 @@ fn ukf_step(sink: &mut impl TraceSink) { } ``` -### 3. Encode with sequence tracking +### 3. Encode with `TraceEncoder` -Use [`SequenceEncoder`] when encoding events manually so the host can detect dropped frames: +[`TraceEncoder`] holds the three pieces of per-stream state the wire format needs: the name +dictionary, the timestamp base the deltas are taken against, and the sequence counter that +lets the host detect dropped frames. Emit the stream header once, then encode events: ```rust -use execution_trace::{SequenceEncoder, SourceType, TraceEvent, encode::MAX_TRACE_FRAME_SIZE}; +use execution_trace::{SourceType, TraceEncoder, TraceEvent}; +use execution_trace::encode::{MAX_TRACE_BURST_SIZE, TimeBase}; + +let mut enc = TraceEncoder::new(); +let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + +// Once, at startup: declares the tick unit and the active source mask. +if let Ok(n) = enc.encode_trace_start(0, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) { + let _ = &buf[..n]; +} let mut name = heapless::String::<32>::new(); name.push_str("my_task").unwrap(); let event = TraceEvent::SpanStart { - timestamp_ns: 0, + timestamp_ns: 1_000, name, source_type: SourceType::Task, sequence: 0, priority: 4, relative_deadline_ms: None, }; -let mut enc = SequenceEncoder::new(); -let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; -if let Ok(n) = enc.encode(&event, &mut buf) { - // forward buf[..n] over your transport (RTT, UART, USB, etc.) - let _ = &buf[..n]; +if let Ok(encoded) = enc.encode(&event, &mut buf) { + if encoded.name_registry_full { + // The dictionary is full: the event went out with the reserved "unknown" + // id. Count it — the stream stays decodable, but this name is lost. + } + // forward buf[..encoded.len] over your transport (RTT, UART, USB, etc.) + let _ = &buf[..encoded.len]; } ``` +The first sight of a name writes **two** frames — the dictionary entry, then the event — which +is why the buffer is [`encode::MAX_TRACE_BURST_SIZE`] rather than one frame. + ### 4. Decode on the host +A frame is not self-contained: its name is a dictionary id and its timestamp is a delta against +the previous frame. [`decode_trace_frame`] therefore returns a [`RawTraceFrame`], and the reader +resolves both from state it carries across the stream: + ```rust,ignore -use execution_trace::encode::decode_trace_frame; +use execution_trace::{FrameKind, encode::decode_trace_frame}; // raw_bytes arrives from your transport (RTT, UART, file, etc.) -let (event, consumed) = decode_trace_frame(raw_bytes).unwrap(); +let (frame, consumed) = decode_trace_frame(raw_bytes).unwrap(); + +// A dictionary entry or the stream header carries an absolute timestamp and +// re-establishes the origin; every other frame is a delta against it. +clock = match frame.kind { + FrameKind::NameRegistered | FrameKind::TraceStart => frame.timestamp_ticks, + _ => clock + frame.timestamp_ticks, +}; +if frame.kind == FrameKind::NameRegistered { + names.insert(frame.name_id, frame.name); +} ``` +`examples/simulate.rs` carries a complete worked reader; the `execution-trace` Python package +does the same job and renders a timing diagram from it. + ## Wire format Each frame is a standard protobuf length-delimited record: ```text -[ varint: payload byte count ][ protobuf-encoded TraceEvent ] +[ varint: payload byte count ][ protobuf-encoded TraceFrame ] ``` -Maximum frame size is [`encode::MAX_TRACE_FRAME_SIZE`] (128 bytes). Name strings are capped at 32 bytes; -longer names cause `record_*` to return [`TracingError::MessageDropped`] before sending. +One flat message carries every frame class, discriminated by its `event_type`: the three +per-occurrence events, the dictionary entry that assigns a name its id, and the stream header. +proto3 omits unset fields, so an event pays nothing for the dictionary fields it does not use. + +A steady-state event costs **11-13 bytes** — the name, the priority and the deadline are sent +once per name rather than on every occurrence, and the timestamp is a delta. + +Maximum frame size is [`encode::MAX_TRACE_FRAME_SIZE`] (128 bytes), and one `encode` call writes +at most [`encode::MAX_TRACE_BURST_SIZE`]. Name strings are capped at 32 bytes; longer names cause +`record_*` to return [`TracingError::MessageDropped`] before sending. The dictionary holds +[`encode::NAME_REGISTRY_CAPACITY`] distinct names, after which events fall back to a reserved +"unknown" id rather than to a wrong decode. ## Host-side tooling diff --git a/build.rs b/build.rs index 10aeefb..eb8a70e 100644 --- a/build.rs +++ b/build.rs @@ -19,8 +19,9 @@ fn main() { generator.add_protoc_arg("-Iproto"); // name carries the name of the source task/ISR (for spans) or the marker label. + // It appears only on NAME_REGISTERED frames, once per distinct name (§5.5). generator.configure( - ".tracing.TraceEvent.name", + ".tracing.TraceFrame.name", micropb_gen::Config::new() .string_type("heapless::String<$N>") .max_bytes(32), diff --git a/examples/simulate.rs b/examples/simulate.rs index 8f69b44..760c19d 100644 --- a/examples/simulate.rs +++ b/examples/simulate.rs @@ -7,26 +7,43 @@ // diagram from it, run the companion Python script: // python examples/visualize.py -use execution_trace::encode::{MAX_TRACE_FRAME_SIZE, decode_trace_frame}; +use execution_trace::encode::{MAX_TRACE_BURST_SIZE, TimeBase, decode_trace_frame}; use execution_trace::{ - SequenceEncoder, SourceType, TraceEvent, TraceSink, TraceTransport, TracingError, + FrameKind, SourceType, TraceEncoder, TraceEvent, TraceSink, TraceTransport, TracingError, }; +use std::collections::HashMap; // A sink that encodes each event into a byte buffer that can be written to a file or // forwarded over a transport (RTT, UART, USB). On real hardware this would wrap the // transport driver; here it wraps a Vec so we can write the bytes to disk. struct FileSink { buf: Vec, - encoder: SequenceEncoder, + encoder: TraceEncoder, pub tick_ns: u64, + pub registry_full_events: u32, } impl FileSink { fn new() -> Self { Self { buf: Vec::new(), - encoder: SequenceEncoder::new(), + encoder: TraceEncoder::new(), tick_ns: 0, + registry_full_events: 0, + } + } + + // Emits the stream header. On real hardware this runs once, from `init`. + fn write_header(&mut self, source_mask: u32) { + let mut frame = [0u8; MAX_TRACE_BURST_SIZE]; + if let Ok(n) = self.encoder.encode_trace_start( + self.tick_ns, + TimeBase::Nanoseconds, + 0, + source_mask, + &mut frame, + ) { + self.buf.extend_from_slice(&frame[..n]); } } @@ -37,12 +54,19 @@ impl FileSink { impl TraceTransport for FileSink { fn write_event(&mut self, event: TraceEvent) -> Result<(), TracingError> { - let mut frame = [0u8; MAX_TRACE_FRAME_SIZE]; - let n = self + // One call can write two frames: a first-sight name emits its dictionary + // entry ahead of the event, so the buffer is sized for the burst. + let mut frame = [0u8; MAX_TRACE_BURST_SIZE]; + let encoded = self .encoder .encode(&event, &mut frame) .map_err(|_| TracingError::MessageDropped)?; - self.buf.extend_from_slice(&frame[..n]); + if encoded.name_registry_full { + // On hardware this increments the `TracingNameRegistryFull` fault: + // the stream stays decodable, but this event's name is lost. + self.registry_full_events += 1; + } + self.buf.extend_from_slice(&frame[..encoded.len]); Ok(()) } } @@ -55,6 +79,8 @@ impl TraceSink for FileSink { fn main() -> std::io::Result<()> { let mut sink = FileSink::new(); + // Every group enabled; a real build passes the mask it was compiled with. + sink.write_header(0x1F); // --- Simulated timeline (nanosecond timestamps, two control-loop iterations) --- // @@ -133,82 +159,99 @@ fn main() -> std::io::Result<()> { ); // --- Decode and print each event (round-trip verification) --- + // + // v2 frames are not self-contained: a name is a dictionary id and a timestamp + // is a delta, so the reader carries the two pieces of per-stream state that + // resolve them. This is the same job the Python decoder does. println!("\nDecoded events:"); println!( "{:<6} {:<14} {:<12} {:<10} {:<8} {}", "seq", "name", "type", "source", "ts_ms", "extras" ); println!("{}", "-".repeat(72)); + + let mut names: HashMap)> = HashMap::new(); + let mut clock_ns = 0u64; let mut pos = 0; while pos < bytes.len() { match decode_trace_frame(&bytes[pos..]) { - Ok((event, consumed)) => { - match &event { - TraceEvent::SpanStart { - sequence, - name, - source_type, - timestamp_ns, - relative_deadline_ms, - .. - } => { - let ts_ms = *timestamp_ns as f64 / 1_000_000.0; - let source = match source_type { - SourceType::Isr => "ISR", - SourceType::Task => "Task", - }; - let mut extras = String::new(); - if let Some(dl) = relative_deadline_ms { - extras.push_str(&format!("rel_deadline={dl:.1}ms ")); - } + Ok((frame, consumed)) => { + pos += consumed; + + // A dictionary entry and the header carry an absolute timestamp + // and re-establish the origin; everything else is a delta. + clock_ns = match frame.kind { + FrameKind::NameRegistered | FrameKind::TraceStart => frame.timestamp_ticks, + _ => clock_ns + frame.timestamp_ticks, + }; + let ts_ms = clock_ns as f64 / 1_000_000.0; + + match frame.kind { + FrameKind::TraceStart => { println!( - "{:<6} {:<14} {:<12} {:<10} {:<8.3} {}", - sequence, - name.as_str(), - "SpanStart", - source, + "{:<6} {:<14} {:<12} {:<10} {:<8.3} mask=0x{:02X} timebase={:?}", + frame.sequence, + "-", + "TraceStart", + "-", ts_ms, - extras, + frame.source_mask, + frame.timebase, ); } - TraceEvent::SpanEnd { - sequence, - name, - timestamp_ns, - } => { - let ts_ms = *timestamp_ns as f64 / 1_000_000.0; + FrameKind::NameRegistered => { + let source = frame.source_type.unwrap_or(SourceType::Task); println!( - "{:<6} {:<14} {:<12} {:<10} {:<8.3}", - sequence, - name.as_str(), - "SpanEnd", - "-", + "{:<6} {:<14} {:<12} {:<10} {:<8.3} id={}", + frame.sequence, + frame.name.as_str(), + "NameReg", + match source { + SourceType::Isr => "ISR", + SourceType::Task => "Task", + }, ts_ms, + frame.name_id, + ); + names.insert( + frame.name_id, + ( + frame.name.as_str().to_string(), + source, + frame.relative_deadline_ms, + ), ); } - TraceEvent::Marker { - sequence, - name, - timestamp_ns, - marker_value, - } => { - let ts_ms = *timestamp_ns as f64 / 1_000_000.0; - let mut extras = String::new(); - if let Some(v) = marker_value { + kind => { + let (name, source, deadline) = match names.get(&frame.name_id) { + Some(entry) => entry.clone(), + // id 0: the registry was full when this was emitted. + None => ("".to_string(), SourceType::Task, None), + }; + let (label, source_label, mut extras) = match kind { + FrameKind::SpanStart => ( + "SpanStart", + match source { + SourceType::Isr => "ISR", + SourceType::Task => "Task", + }, + match deadline { + Some(dl) => format!("rel_deadline={dl:.1}ms "), + None => String::new(), + }, + ), + FrameKind::SpanEnd => ("SpanEnd", "-", String::new()), + _ => ("Marker", "-", String::new()), + }; + if let Some(v) = frame.marker_value { extras.push_str(&format!("value={v}")); } println!( "{:<6} {:<14} {:<12} {:<10} {:<8.3} {}", - sequence, - name.as_str(), - "Marker", - "-", - ts_ms, - extras, + frame.sequence, name, label, source_label, ts_ms, extras, ); } } - pos += consumed; } Err(e) => { eprintln!("decode error at byte {pos}: {e:?}"); diff --git a/proto/tracing.proto b/proto/tracing.proto index 32a31bd..02e0d9b 100644 --- a/proto/tracing.proto +++ b/proto/tracing.proto @@ -1,15 +1,54 @@ syntax = "proto3"; package tracing; -message TraceEvent { - uint64 timestamp_ns = 1; - string name = 2; - TraceEventSourceType source_type = 3; - TraceEventType event_type = 4; - uint32 sequence = 5; - uint32 priority = 6; - optional float relative_deadline_ms = 7; - optional uint32 marker_value = 8; +// Execution-trace wire format v2 (EXEC-TRACE-002 §6). +// +// Not wire-compatible with v1. A v2 stream is recognised by its leading +// TRACE_START frame; see §6.4. +// +// One flat message carries every frame class, discriminated by `event_type`, +// rather than a `oneof` envelope. proto3 omits unset fields, so a per-occurrence +// event pays nothing for the dictionary fields it does not use, where an +// envelope would tax every event with a tag and a length (§18.1). +// +// Field numbers stay at or below 15 so that every tag costs a single byte. +message TraceFrame { + // Ticks elapsed since the previous sequenced frame, in the timebase declared + // by the TRACE_START frame. Absolute, not a delta, on NAME_REGISTERED and + // TRACE_START frames, which re-establish the time origin (§5.6). + // + // Declared 64-bit so that an idle stream cannot overflow it; a varint costs + // the same for the small values this normally carries. + uint64 timestamp_ticks = 1; + + // Dictionary key assigned by NAME_REGISTERED. Id 0 is reserved for "unknown", + // emitted when the registry is full (§5.5, REQ-T11). + uint32 name_id = 2; + + TraceEventType event_type = 3; + + // Wraps at 16384 (see SEQUENCE_MODULUS). The counter exists only to detect + // gaps, which are read modulo the wrap, so a full 32-bit counter would spend + // up to five varint bytes to buy nothing (§18.1). + uint32 sequence = 4; + + // MARKER only. + optional uint32 marker_value = 5; + + // ── NAME_REGISTERED only: the dictionary entry (§6.2) ──────────────────── + string name = 6; + TraceEventSourceType source_type = 7; + uint32 priority = 8; + optional float relative_deadline_ms = 9; + + // ── TRACE_START only (§6.3) ────────────────────────────────────────────── + TimeBase timebase = 10; + // Core clock in Hz, for converting CYCLES ticks to wall time. Unset when the + // timebase is NANOSECONDS. + uint32 core_frequency_hz = 11; + // The source mask active in this build (§5.4), so the host can tell a masked + // group from a silent one without hard-coding the mask (REQ-T05). + uint32 source_mask = 12; } enum TraceEventSourceType { @@ -23,4 +62,17 @@ enum TraceEventType { SPAN_START = 1; SPAN_END = 2; MARKER = 3; + NAME_REGISTERED = 4; + TRACE_START = 5; +} + +// The unit of `timestamp_ticks`, declared once on the TRACE_START frame. +// +// Increment 6 emits NANOSECONDS; increment 7 (§5.8) switches the firmware to a +// raw DWT cycle counter and emits CYCLES with `core_frequency_hz` set. Declaring +// the unit on the wire keeps that a firmware change rather than a second +// incompatible schema. +enum TimeBase { + NANOSECONDS = 0; + CYCLES = 1; } diff --git a/python/src/execution_trace/__init__.py b/python/src/execution_trace/__init__.py index 55c551c..1328938 100644 --- a/python/src/execution_trace/__init__.py +++ b/python/src/execution_trace/__init__.py @@ -4,7 +4,7 @@ from execution_trace import ( SequenceTracker, iter_frames, encode_varint, decode_varint, - TraceEvent, MarkerRecord, TraceEventBuffer, + TraceEvent, MarkerRecord, NameEntry, TraceEventBuffer, TraceStreamState, decode_tracing_stream, write_tracing_csv, ) @@ -16,9 +16,12 @@ """ from execution_trace.decode import ( + SEQUENCE_MODULUS, MarkerRecord, + NameEntry, TraceEvent, TraceEventBuffer, + TraceStreamState, decode_tracing_stream, write_tracing_csv, ) @@ -36,7 +39,10 @@ "decode_varint", "TraceEvent", "MarkerRecord", + "NameEntry", "TraceEventBuffer", + "TraceStreamState", + "SEQUENCE_MODULUS", "decode_tracing_stream", "write_tracing_csv", ] diff --git a/python/src/execution_trace/_cli/decode.py b/python/src/execution_trace/_cli/decode.py index 7bb732f..1743a5c 100644 --- a/python/src/execution_trace/_cli/decode.py +++ b/python/src/execution_trace/_cli/decode.py @@ -17,10 +17,10 @@ from execution_trace.decode import ( TraceEventBuffer, + TraceStreamState, decode_tracing_stream, write_tracing_csv, ) -from execution_trace.stream import SequenceTracker def _die(msg: str) -> NoReturn: @@ -60,9 +60,9 @@ def main() -> None: output_dir = args.output if args.output is not None else str(binary_path.parent) buf = bytearray(binary_path.read_bytes()) - tracker = SequenceTracker("etrace-decode") + state = TraceStreamState("etrace-decode") event_buffer = TraceEventBuffer() - decode_tracing_stream(buf, tracker, event_buffer) + decode_tracing_stream(buf, state, event_buffer) event_buffer.flush_pending() records = event_buffer.records + event_buffer.markers diff --git a/python/src/execution_trace/_proto/tracing_pb2.py b/python/src/execution_trace/_proto/tracing_pb2.py index 90abce8..2997306 100644 --- a/python/src/execution_trace/_proto/tracing_pb2.py +++ b/python/src/execution_trace/_proto/tracing_pb2.py @@ -24,17 +24,19 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rtracing.proto\x12\x07tracing\"\x9d\x02\n\nTraceEvent\x12\x14\n\x0ctimestamp_ns\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x32\n\x0bsource_type\x18\x03 \x01(\x0e\x32\x1d.tracing.TraceEventSourceType\x12+\n\nevent_type\x18\x04 \x01(\x0e\x32\x17.tracing.TraceEventType\x12\x10\n\x08sequence\x18\x05 \x01(\r\x12\x10\n\x08priority\x18\x06 \x01(\r\x12!\n\x14relative_deadline_ms\x18\x07 \x01(\x02H\x00\x88\x01\x01\x12\x19\n\x0cmarker_value\x18\x08 \x01(\rH\x01\x88\x01\x01\x42\x17\n\x15_relative_deadline_msB\x0f\n\r_marker_value*R\n\x14TraceEventSourceType\x12\'\n#TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03ISR\x10\x01\x12\x08\n\x04TASK\x10\x02*\\\n\x0eTraceEventType\x12 \n\x1cTRACE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSPAN_START\x10\x01\x12\x0c\n\x08SPAN_END\x10\x02\x12\n\n\x06MARKER\x10\x03\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rtracing.proto\x12\x07tracing\"\x86\x03\n\nTraceFrame\x12\x17\n\x0ftimestamp_ticks\x18\x01 \x01(\x04\x12\x0f\n\x07name_id\x18\x02 \x01(\r\x12+\n\nevent_type\x18\x03 \x01(\x0e\x32\x17.tracing.TraceEventType\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x19\n\x0cmarker_value\x18\x05 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x32\n\x0bsource_type\x18\x07 \x01(\x0e\x32\x1d.tracing.TraceEventSourceType\x12\x10\n\x08priority\x18\x08 \x01(\r\x12!\n\x14relative_deadline_ms\x18\t \x01(\x02H\x01\x88\x01\x01\x12#\n\x08timebase\x18\n \x01(\x0e\x32\x11.tracing.TimeBase\x12\x19\n\x11\x63ore_frequency_hz\x18\x0b \x01(\r\x12\x13\n\x0bsource_mask\x18\x0c \x01(\rB\x0f\n\r_marker_valueB\x17\n\x15_relative_deadline_ms*R\n\x14TraceEventSourceType\x12\'\n#TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03ISR\x10\x01\x12\x08\n\x04TASK\x10\x02*\x82\x01\n\x0eTraceEventType\x12 \n\x1cTRACE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSPAN_START\x10\x01\x12\x0c\n\x08SPAN_END\x10\x02\x12\n\n\x06MARKER\x10\x03\x12\x13\n\x0fNAME_REGISTERED\x10\x04\x12\x0f\n\x0bTRACE_START\x10\x05*\'\n\x08TimeBase\x12\x0f\n\x0bNANOSECONDS\x10\x00\x12\n\n\x06\x43YCLES\x10\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tracing_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_TRACEEVENTSOURCETYPE']._serialized_start=314 - _globals['_TRACEEVENTSOURCETYPE']._serialized_end=396 - _globals['_TRACEEVENTTYPE']._serialized_start=398 - _globals['_TRACEEVENTTYPE']._serialized_end=490 - _globals['_TRACEEVENT']._serialized_start=27 - _globals['_TRACEEVENT']._serialized_end=312 + _globals['_TRACEEVENTSOURCETYPE']._serialized_start=419 + _globals['_TRACEEVENTSOURCETYPE']._serialized_end=501 + _globals['_TRACEEVENTTYPE']._serialized_start=504 + _globals['_TRACEEVENTTYPE']._serialized_end=634 + _globals['_TIMEBASE']._serialized_start=636 + _globals['_TIMEBASE']._serialized_end=675 + _globals['_TRACEFRAME']._serialized_start=27 + _globals['_TRACEFRAME']._serialized_end=417 # @@protoc_insertion_point(module_scope) diff --git a/python/src/execution_trace/_proto/tracing_pb2.pyi b/python/src/execution_trace/_proto/tracing_pb2.pyi index 7cd25dd..d9a1119 100644 --- a/python/src/execution_trace/_proto/tracing_pb2.pyi +++ b/python/src/execution_trace/_proto/tracing_pb2.pyi @@ -17,6 +17,13 @@ class TraceEventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): SPAN_START: _ClassVar[TraceEventType] SPAN_END: _ClassVar[TraceEventType] MARKER: _ClassVar[TraceEventType] + NAME_REGISTERED: _ClassVar[TraceEventType] + TRACE_START: _ClassVar[TraceEventType] + +class TimeBase(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + NANOSECONDS: _ClassVar[TimeBase] + CYCLES: _ClassVar[TimeBase] TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED: TraceEventSourceType ISR: TraceEventSourceType TASK: TraceEventSourceType @@ -24,23 +31,35 @@ TRACE_EVENT_TYPE_UNSPECIFIED: TraceEventType SPAN_START: TraceEventType SPAN_END: TraceEventType MARKER: TraceEventType +NAME_REGISTERED: TraceEventType +TRACE_START: TraceEventType +NANOSECONDS: TimeBase +CYCLES: TimeBase -class TraceEvent(_message.Message): - __slots__ = ("timestamp_ns", "name", "source_type", "event_type", "sequence", "priority", "relative_deadline_ms", "marker_value") - TIMESTAMP_NS_FIELD_NUMBER: _ClassVar[int] - NAME_FIELD_NUMBER: _ClassVar[int] - SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int] +class TraceFrame(_message.Message): + __slots__ = ("timestamp_ticks", "name_id", "event_type", "sequence", "marker_value", "name", "source_type", "priority", "relative_deadline_ms", "timebase", "core_frequency_hz", "source_mask") + TIMESTAMP_TICKS_FIELD_NUMBER: _ClassVar[int] + NAME_ID_FIELD_NUMBER: _ClassVar[int] EVENT_TYPE_FIELD_NUMBER: _ClassVar[int] SEQUENCE_FIELD_NUMBER: _ClassVar[int] + MARKER_VALUE_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int] PRIORITY_FIELD_NUMBER: _ClassVar[int] RELATIVE_DEADLINE_MS_FIELD_NUMBER: _ClassVar[int] - MARKER_VALUE_FIELD_NUMBER: _ClassVar[int] - timestamp_ns: int - name: str - source_type: TraceEventSourceType + TIMEBASE_FIELD_NUMBER: _ClassVar[int] + CORE_FREQUENCY_HZ_FIELD_NUMBER: _ClassVar[int] + SOURCE_MASK_FIELD_NUMBER: _ClassVar[int] + timestamp_ticks: int + name_id: int event_type: TraceEventType sequence: int + marker_value: int + name: str + source_type: TraceEventSourceType priority: int relative_deadline_ms: float - marker_value: int - def __init__(self, timestamp_ns: _Optional[int] = ..., name: _Optional[str] = ..., source_type: _Optional[_Union[TraceEventSourceType, str]] = ..., event_type: _Optional[_Union[TraceEventType, str]] = ..., sequence: _Optional[int] = ..., priority: _Optional[int] = ..., relative_deadline_ms: _Optional[float] = ..., marker_value: _Optional[int] = ...) -> None: ... + timebase: TimeBase + core_frequency_hz: int + source_mask: int + def __init__(self, timestamp_ticks: _Optional[int] = ..., name_id: _Optional[int] = ..., event_type: _Optional[_Union[TraceEventType, str]] = ..., sequence: _Optional[int] = ..., marker_value: _Optional[int] = ..., name: _Optional[str] = ..., source_type: _Optional[_Union[TraceEventSourceType, str]] = ..., priority: _Optional[int] = ..., relative_deadline_ms: _Optional[float] = ..., timebase: _Optional[_Union[TimeBase, str]] = ..., core_frequency_hz: _Optional[int] = ..., source_mask: _Optional[int] = ...) -> None: ... diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index 5146265..cadabda 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -1,18 +1,24 @@ -"""High-level decoder for execution-trace protobuf streams. +"""High-level decoder for execution-trace protobuf streams (wire format v2). Receives raw bytes from any transport (RTT, UART, TCP, …), parses -length-delimited :class:`TraceEvent` protobuf frames, matches -``SPAN_START`` / ``SPAN_END`` pairs into :class:`TraceEvent` records, -and records :class:`MarkerRecord` annotations directly. +length-delimited :class:`tracing_pb2.TraceFrame` frames, matches +``SPAN_START`` / ``SPAN_END`` pairs into :class:`TraceEvent` records, and +records :class:`MarkerRecord` annotations directly. + +A v2 frame is **not** self-contained: its name is a dictionary id assigned by an +earlier ``NAME_REGISTERED`` frame, and its timestamp is a delta against the +previous frame. Both are resolved here rather than on the device, which is why +:class:`TraceStreamState` carries the dictionary, the running clock and the +sequence tracker across every frame of one connection. Typical usage:: buf = bytearray() - tracker = SequenceTracker("tracing") + state = TraceStreamState("tracing") event_buffer = TraceEventBuffer() # ... fill buf from hardware ... - decode_tracing_stream(buf, tracker, event_buffer) + decode_tracing_stream(buf, state, event_buffer) csv_path = write_tracing_csv( event_buffer.records + event_buffer.markers, @@ -32,6 +38,17 @@ logger = logging.getLogger(__name__) +# The firmware's sequence counter wraps here rather than at 2**32: it exists only +# to detect gaps, a gap is read modulo the wrap, and a full 32-bit counter would +# spend up to five varint bytes per frame to buy nothing. +SEQUENCE_MODULUS: int = 16_384 + +# Reserved dictionary id, emitted when the device's name registry is full. The +# stream stays decodable; the name of that one event is lost. +UNKNOWN_NAME_ID: int = 0 + +UNKNOWN_NAME: str = "" + @dataclass class TraceEvent: @@ -77,6 +94,125 @@ class MarkerRecord: value: Optional[int] = None +@dataclass +class NameEntry: + """One dictionary entry: everything fixed by a span or marker name. + + These attributes are sent once, on the ``NAME_REGISTERED`` frame that assigns + the id, rather than repeated on every occurrence of the event. + + Attributes: + name: Task, ISR or marker label. + source_type: ``tracing_pb2.ISR`` or ``tracing_pb2.TASK``. + priority: Scheduler priority; 0 when the name was first seen on a frame + that carries no attributes (a ``SPAN_END`` or ``MARKER``). + relative_deadline_ms: Deadline relative to activation, or ``None``. + """ + + name: str + source_type: int = tracing_pb2.TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED + priority: int = 0 + relative_deadline_ms: Optional[float] = None + + +class TraceStreamState: + """Per-connection decoder state for one v2 trace stream. + + A v2 frame carries a dictionary id instead of a name and a delta instead of a + timestamp, so decoding is stateful: this holds the name dictionary, the + running clock the deltas accumulate into, the timebase declared by the + ``TRACE_START`` frame, and the sequence tracker that detects gaps and resets. + + Create one per connection — the device's dictionary and clock both restart + when it does. + + Args: + label: Human-readable stream name used in log messages. + + Attributes: + source_mask: The source-group mask the firmware was built with, or + ``None`` until a ``TRACE_START`` frame arrives. A group absent from + the mask is silent by design, which is what distinguishes it from a + group whose frames were lost. + """ + + def __init__(self, label: str = "Trace event") -> None: + self.label = label + self.tracker = SequenceTracker(label, modulus=SEQUENCE_MODULUS) + self.names: dict[int, NameEntry] = {} + self.clock_ticks: int = 0 + self.timebase: int = tracing_pb2.NANOSECONDS + self.core_frequency_hz: int = 0 + self.source_mask: Optional[int] = None + + def reset(self) -> None: + """Discard all per-stream state after a device reset. + + The dictionary must go with it: the device reassigns ids from one on + reboot, so a stale entry would resolve a new id to the wrong name. + """ + self.tracker = SequenceTracker(self.label, modulus=SEQUENCE_MODULUS) + self.names.clear() + self.clock_ticks = 0 + self.timebase = tracing_pb2.NANOSECONDS + self.core_frequency_hz = 0 + self.source_mask = None + + def ticks_to_us(self, ticks: int) -> float: + """Convert a tick count to microseconds using the declared timebase. + + Args: + ticks: A tick count in the unit the ``TRACE_START`` frame declared. + + Returns: + The equivalent duration in microseconds. + """ + if self.timebase == tracing_pb2.CYCLES and self.core_frequency_hz > 0: + return ticks * 1_000_000.0 / self.core_frequency_hz + return ticks / 1_000.0 + + def advance(self, frame: tracing_pb2.TraceFrame) -> float: + """Advance the stream clock by *frame* and return its absolute time in µs. + + ``NAME_REGISTERED`` and ``TRACE_START`` frames carry an absolute + timestamp and re-establish the origin; every other frame carries a delta + against the frame before it. + + Args: + frame: The decoded frame. + + Returns: + The frame's absolute timestamp in microseconds. + """ + if frame.event_type in (tracing_pb2.NAME_REGISTERED, tracing_pb2.TRACE_START): + self.clock_ticks = frame.timestamp_ticks + else: + self.clock_ticks += frame.timestamp_ticks + return self.ticks_to_us(self.clock_ticks) + + def resolve(self, name_id: int) -> NameEntry: + """Look up a dictionary id. + + Args: + name_id: The id carried by the frame. + + Returns: + The registered entry, or a placeholder entry when the id is the + reserved "unknown" value or has not been registered — which happens + when the device's registry was full, or when the host attached after + the entry was sent. + """ + entry = self.names.get(name_id) + if entry is not None: + return entry + if name_id != UNKNOWN_NAME_ID: + logger.warning( + "Tracing: name id %d was never registered — the host may have " + "attached mid-stream", name_id, + ) + return NameEntry(name=UNKNOWN_NAME) + + class TraceEventBuffer: """Accumulates :class:`TraceEvent` records by matching ``SPAN_START`` / ``SPAN_END`` pairs. @@ -87,49 +223,54 @@ class TraceEventBuffer: """ def __init__(self) -> None: - # name → (timestamp_ns, source_type, priority, relative_deadline_ms) - self._pending: dict[str, tuple[int, int, int, Optional[float]]] = {} + # name → (start_us, NameEntry) + self._pending: dict[str, tuple[float, NameEntry]] = {} self._records: list[TraceEvent] = [] self._markers: list[MarkerRecord] = [] - def push(self, msg: tracing_pb2.TraceEvent) -> None: - """Process one decoded :class:`tracing_pb2.TraceEvent`. + def push( + self, + frame: tracing_pb2.TraceFrame, + entry: NameEntry, + timestamp_us: float, + ) -> None: + """Process one decoded frame whose name and timestamp are already resolved. Args: - msg: Decoded protobuf message from the firmware. + frame: The decoded frame, for its event type and marker value. + entry: The dictionary entry its ``name_id`` resolves to. + timestamp_us: Its absolute timestamp in microseconds. """ - name = msg.name - if msg.event_type == tracing_pb2.SPAN_START: + name = entry.name + if frame.event_type == tracing_pb2.SPAN_START: if name in self._pending: logger.warning("Tracing: duplicate START for '%s' — discarding previous", name) - relative_deadline_ms = msg.relative_deadline_ms if msg.HasField("relative_deadline_ms") else None - self._pending[name] = (msg.timestamp_ns, msg.source_type, int(msg.priority), relative_deadline_ms) - elif msg.event_type == tracing_pb2.SPAN_END: + self._pending[name] = (timestamp_us, entry) + elif frame.event_type == tracing_pb2.SPAN_END: if name not in self._pending: logger.warning("Tracing: END for '%s' with no matching START — discarding", name) return - start_ns, source_type, priority, relative_deadline_ms = self._pending.pop(name) - type_str = "isr" if source_type == tracing_pb2.ISR else "task" - start_us = start_ns / 1_000.0 - deadline_us = start_us + relative_deadline_ms * 1_000.0 if relative_deadline_ms is not None else None + start_us, start_entry = self._pending.pop(name) + type_str = "isr" if start_entry.source_type == tracing_pb2.ISR else "task" + deadline_us = ( + start_us + start_entry.relative_deadline_ms * 1_000.0 + if start_entry.relative_deadline_ms is not None + else None + ) self._records.append( TraceEvent( name=name, type=type_str, start_us=start_us, - end_us=msg.timestamp_ns / 1_000.0, - priority=priority, + end_us=timestamp_us, + priority=start_entry.priority, deadline_us=deadline_us, ) ) - elif msg.event_type == tracing_pb2.MARKER: - value = int(msg.marker_value) if msg.HasField("marker_value") else None + elif frame.event_type == tracing_pb2.MARKER: + value = int(frame.marker_value) if frame.HasField("marker_value") else None self._markers.append( - MarkerRecord( - name=name, - timestamp_us=msg.timestamp_ns / 1_000.0, - value=value, - ) + MarkerRecord(name=name, timestamp_us=timestamp_us, value=value) ) def flush_pending(self) -> None: @@ -156,31 +297,75 @@ def markers(self) -> list[MarkerRecord]: def decode_tracing_stream( buf: bytearray, - tracker: SequenceTracker, + state: TraceStreamState, event_buffer: TraceEventBuffer, ) -> None: """Decode all complete frames from *buf* and push events into *event_buffer*. - Consumes *buf* in-place. Stops and clears *buf* if a device reset is detected - (sequence number jumped backward), also calling :meth:`TraceEventBuffer.flush_pending`. + Consumes *buf* in-place. Resolves each frame's dictionary id and delta + timestamp against *state*, which must be the same object across every call + for one connection. + + Stops and clears *buf* if a device reset is detected — either a backwards + sequence jump or a second ``TRACE_START`` frame — also resetting *state* and + calling :meth:`TraceEventBuffer.flush_pending`. Args: buf: Mutable byte buffer containing raw RTT / transport bytes. - tracker: Sequence number tracker shared across calls for the same stream. + state: Per-connection decoder state, shared across calls for one stream. event_buffer: Accumulator for decoded events. """ for raw in iter_frames(buf): - msg = tracing_pb2.TraceEvent() + frame = tracing_pb2.TraceFrame() try: - msg.ParseFromString(raw) + frame.ParseFromString(raw) except Exception as exc: - logger.warning("Failed to decode TraceEvent: %s", exc) + logger.warning("Failed to decode TraceFrame: %s", exc) continue - if tracker.observe(msg.sequence): + + # A TRACE_START after the stream is already running means the device + # rebooted: its dictionary and clock both restarted, so nothing carried + # over is still valid. + if frame.event_type == tracing_pb2.TRACE_START and state.source_mask is not None: + logger.warning("Tracing: TRACE_START mid-stream — device reset") buf.clear() event_buffer.flush_pending() + state.reset() break - event_buffer.push(msg) + + if state.tracker.observe(frame.sequence): + buf.clear() + event_buffer.flush_pending() + state.reset() + break + + timestamp_us = state.advance(frame) + + if frame.event_type == tracing_pb2.TRACE_START: + state.timebase = frame.timebase + state.core_frequency_hz = frame.core_frequency_hz + state.source_mask = frame.source_mask + # The clock was set from the raw ticks before the timebase was known; + # it is a tick count either way, so only the conversion changes. + logger.info( + "Tracing: stream start, timebase=%s core=%d Hz source_mask=0x%02X", + tracing_pb2.TimeBase.Name(frame.timebase), + frame.core_frequency_hz, + frame.source_mask, + ) + elif frame.event_type == tracing_pb2.NAME_REGISTERED: + state.names[frame.name_id] = NameEntry( + name=frame.name, + source_type=frame.source_type, + priority=int(frame.priority), + relative_deadline_ms=( + frame.relative_deadline_ms + if frame.HasField("relative_deadline_ms") + else None + ), + ) + else: + event_buffer.push(frame, state.resolve(frame.name_id), timestamp_us) def write_tracing_csv( diff --git a/python/src/execution_trace/stream.py b/python/src/execution_trace/stream.py index 58770e9..93ea641 100644 --- a/python/src/execution_trace/stream.py +++ b/python/src/execution_trace/stream.py @@ -24,9 +24,12 @@ logger = logging.getLogger(__name__) -# A "drop" count exceeding half the u32 range means the sequence went backward — +# The default counter width, for streams that carry a full 32-bit sequence. +DEFAULT_MODULUS: int = 1 << 32 + +# A "drop" count exceeding half the counter range means the sequence went backward — # a device reset rather than actual packet loss. -RESET_THRESHOLD: int = 1 << 31 +RESET_THRESHOLD: int = DEFAULT_MODULUS >> 1 class SequenceTracker: @@ -40,17 +43,31 @@ class SequenceTracker: Args: label: Human-readable stream name used in log messages. + modulus: The value the counter wraps at. Defaults to the full 32-bit + range; the execution-trace v2 format wraps far earlier, at + :data:`execution_trace.decode.SEQUENCE_MODULUS`, because the counter + exists only to detect gaps and a gap is read modulo the wrap. + + Raises: + ValueError: If *modulus* is not a positive power of two, which the + masking arithmetic below assumes. """ - def __init__(self, label: str = "Frame") -> None: + def __init__(self, label: str = "Frame", modulus: int = DEFAULT_MODULUS) -> None: + if modulus <= 0 or modulus & (modulus - 1): + raise ValueError(f"modulus must be a positive power of two, got {modulus}") self._last: int | None = None self._label = label + self._modulus = modulus + self._mask = modulus - 1 + # Half the range: a larger apparent forward jump is really a backward one. + self._reset_threshold = modulus >> 1 def observe(self, sequence: int) -> bool: """Record a sequence number and detect resets or drops. Args: - sequence: The 32-bit sequence number from the received frame. + sequence: The sequence number from the received frame. Returns: ``True`` if a device reset was detected (sequence jumped backward), @@ -61,14 +78,13 @@ def observe(self, sequence: int) -> bool: frame — whatever its sequence number — is accepted silently as the new baseline. """ - # Use & 0xFFFFFFFF to emulate unsigned 32-bit counter math in Python - # (mod 2^32), so increment/subtraction behave correctly across counter - # wraparound. + # Mask to emulate the firmware's unsigned counter math in Python, so + # increment and subtraction behave correctly across a wraparound. if self._last is not None: - expected = (self._last + 1) & 0xFFFFFFFF + expected = (self._last + 1) & self._mask if sequence != expected: - dropped = (sequence - expected) & 0xFFFFFFFF - if dropped > RESET_THRESHOLD: + dropped = (sequence - expected) & self._mask + if dropped > self._reset_threshold: logger.warning( "%s: device reset detected, resuming from #%d", self._label, sequence, diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 688556f..81f1c8e 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -3,27 +3,43 @@ import pytest from execution_trace._proto import tracing_pb2 +from execution_trace.decode import SEQUENCE_MODULUS from execution_trace.stream import encode_varint -def _make_frame( +def make_frame( event_type: int, - source_type: int, - name: str, - timestamp_ns: int, + *, + timestamp_ticks: int = 0, + name_id: int = 0, sequence: int = 0, + name: str = "", + source_type: int = tracing_pb2.TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED, priority: int = 0, relative_deadline_ms: float | None = None, marker_value: int | None = None, + timebase: int = tracing_pb2.NANOSECONDS, + core_frequency_hz: int = 0, + source_mask: int = 0, ) -> bytes: - """Encode a single TraceEvent as a length-delimited protobuf frame.""" - msg = tracing_pb2.TraceEvent() - msg.timestamp_ns = timestamp_ns - msg.name = name - msg.source_type = source_type + """Encode a single v2 TraceFrame as a length-delimited protobuf frame. + + Every field is optional so a test can build exactly the frame class it means: + a per-occurrence event carries only a delta, an id and a sequence; a + NAME_REGISTERED frame carries the dictionary entry; a TRACE_START frame + carries the timebase and mask. + """ + msg = tracing_pb2.TraceFrame() + msg.timestamp_ticks = timestamp_ticks + msg.name_id = name_id msg.event_type = event_type msg.sequence = sequence + msg.name = name + msg.source_type = source_type msg.priority = priority + msg.timebase = timebase + msg.core_frequency_hz = core_frequency_hz + msg.source_mask = source_mask if relative_deadline_ms is not None: msg.relative_deadline_ms = relative_deadline_ms if marker_value is not None: @@ -32,42 +48,139 @@ def _make_frame( return encode_varint(len(raw)) + raw -@pytest.fixture() -def sample_span_start_frame() -> bytes: - """A well-formed SPAN_START frame for a task named 'main_task'.""" - return _make_frame( - event_type=tracing_pb2.SPAN_START, - source_type=tracing_pb2.TASK, - name="main_task", - timestamp_ns=1_000_000, - sequence=0, - priority=4, - ) +class StreamBuilder: + """Builds a v2 byte stream the way the firmware encoder does. + + Mirrors `TraceEncoder`: it assigns dictionary ids on first sight of a name, + delta-encodes timestamps against the previous frame, and advances a sequence + counter that wraps at :data:`SEQUENCE_MODULUS`. Tests describe events in + absolute nanoseconds and by name; this turns them into wire bytes. + """ + + def __init__(self, timebase: int = tracing_pb2.NANOSECONDS, core_frequency_hz: int = 0): + self.buf = bytearray() + self._names: dict[str, int] = {} + self._attrs: dict[str, tuple[int, int, float | None]] = {} + self._last_ticks = 0 + self._sequence = 0 + self._timebase = timebase + self._core_frequency_hz = core_frequency_hz + + def _next_sequence(self) -> int: + seq = self._sequence + self._sequence = (self._sequence + 1) % SEQUENCE_MODULUS + return seq + + def start(self, timestamp_ticks: int = 0, source_mask: int = 0x1F) -> "StreamBuilder": + """Append the TRACE_START header.""" + self.buf += make_frame( + tracing_pb2.TRACE_START, + timestamp_ticks=timestamp_ticks, + sequence=self._next_sequence(), + timebase=self._timebase, + core_frequency_hz=self._core_frequency_hz, + source_mask=source_mask, + ) + self._last_ticks = timestamp_ticks + return self + + def _name_id( + self, + name: str, + ticks: int, + source_type: int, + priority: int, + relative_deadline_ms: float | None, + ) -> int: + if name in self._names: + return self._names[name] + name_id = len(self._names) + 1 + self._names[name] = name_id + self.buf += make_frame( + tracing_pb2.NAME_REGISTERED, + timestamp_ticks=ticks, + name_id=name_id, + sequence=self._next_sequence(), + name=name, + source_type=source_type, + priority=priority, + relative_deadline_ms=relative_deadline_ms, + ) + self._last_ticks = ticks + return name_id + + def event( + self, + event_type: int, + name: str, + ticks: int, + *, + source_type: int = tracing_pb2.TASK, + priority: int = 0, + relative_deadline_ms: float | None = None, + marker_value: int | None = None, + name_id: int | None = None, + ) -> "StreamBuilder": + """Append one per-occurrence event, registering its name if new. + + Pass *name_id* to emit a specific id without registering it — used to + exercise the unknown-id fallback. + """ + if name_id is None: + name_id = self._name_id(name, ticks, source_type, priority, relative_deadline_ms) + delta = ticks - self._last_ticks + self._last_ticks = ticks + self.buf += make_frame( + event_type, + timestamp_ticks=delta, + name_id=name_id, + sequence=self._next_sequence(), + marker_value=marker_value, + ) + return self + + def span( + self, + name: str, + start_ticks: int, + end_ticks: int, + *, + source_type: int = tracing_pb2.TASK, + priority: int = 0, + relative_deadline_ms: float | None = None, + ) -> "StreamBuilder": + """Append a matched SPAN_START / SPAN_END pair.""" + self.event( + tracing_pb2.SPAN_START, name, start_ticks, + source_type=source_type, priority=priority, + relative_deadline_ms=relative_deadline_ms, + ) + return self.event(tracing_pb2.SPAN_END, name, end_ticks) + + def marker(self, name: str, ticks: int, value: int | None = None) -> "StreamBuilder": + """Append a MARKER.""" + return self.event(tracing_pb2.MARKER, name, ticks, marker_value=value) + + def bytes(self) -> bytearray: + """The stream built so far.""" + return bytearray(self.buf) @pytest.fixture() -def sample_span_end_frame() -> bytes: - """A well-formed SPAN_END frame for a task named 'main_task'.""" - return _make_frame( - event_type=tracing_pb2.SPAN_END, - source_type=tracing_pb2.TASK, - name="main_task", - timestamp_ns=2_000_000, - sequence=1, - ) +def builder() -> StreamBuilder: + """A fresh v2 stream builder with the header already written.""" + return StreamBuilder().start() @pytest.fixture() -def sample_marker_frame() -> bytes: - """A well-formed MARKER frame.""" - return _make_frame( - event_type=tracing_pb2.MARKER, - source_type=tracing_pb2.TASK, - name="loop_tick", - timestamp_ns=1_500_000, - sequence=2, - marker_value=42, - ) +def sample_stream(builder: StreamBuilder) -> bytearray: + """A complete stream: one task span, one ISR span and one marker.""" + builder.span("main_task", 1_000_000, 2_000_000, + source_type=tracing_pb2.TASK, priority=4) + builder.span("gyro_isr", 2_100_000, 2_200_000, + source_type=tracing_pb2.ISR, priority=8) + builder.marker("loop_tick", 2_300_000, 42) + return builder.bytes() @pytest.fixture() diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 6d9a426..78c1e29 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -6,44 +6,62 @@ import pytest +from conftest import StreamBuilder, make_frame + from execution_trace._proto import tracing_pb2 from execution_trace.decode import ( + SEQUENCE_MODULUS, + UNKNOWN_NAME, MarkerRecord, + NameEntry, TraceEvent, TraceEventBuffer, + TraceStreamState, decode_tracing_stream, write_tracing_csv, ) -from execution_trace.stream import SequenceTracker, encode_varint # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def _make_frame( +def _push( + buf: TraceEventBuffer, event_type: int, - source_type: int, name: str, - timestamp_ns: int, - sequence: int = 0, + timestamp_us: float, + *, + source_type: int = tracing_pb2.TASK, priority: int = 0, relative_deadline_ms: float | None = None, marker_value: int | None = None, -) -> bytes: - msg = tracing_pb2.TraceEvent() - msg.timestamp_ns = timestamp_ns - msg.name = name - msg.source_type = source_type - msg.event_type = event_type - msg.sequence = sequence - msg.priority = priority - if relative_deadline_ms is not None: - msg.relative_deadline_ms = relative_deadline_ms +) -> None: + """Push one already-resolved event into *buf*. + + `TraceEventBuffer.push` takes a frame plus the dictionary entry and absolute + timestamp the stream decoder resolved for it, so these unit tests supply + those directly rather than going through a stream. + """ + frame = tracing_pb2.TraceFrame() + frame.event_type = event_type if marker_value is not None: - msg.marker_value = marker_value - raw = msg.SerializeToString() - return encode_varint(len(raw)) + raw + frame.marker_value = marker_value + entry = NameEntry( + name=name, + source_type=source_type, + priority=priority, + relative_deadline_ms=relative_deadline_ms, + ) + buf.push(frame, entry, timestamp_us) + + +def _decode(stream: bytearray) -> tuple[TraceStreamState, TraceEventBuffer]: + """Decode a whole stream and return the resulting state and buffer.""" + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + decode_tracing_stream(stream, state, event_buffer) + return state, event_buffer # --------------------------------------------------------------------------- @@ -53,19 +71,8 @@ def _make_frame( class TestTraceEventBuffer: def test_matches_start_end_pair(self): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "task_a" - msg.event_type = tracing_pb2.SPAN_START - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 1_000_000 - msg.priority = 4 - buf.push(msg) - - msg2 = tracing_pb2.TraceEvent() - msg2.name = "task_a" - msg2.event_type = tracing_pb2.SPAN_END - msg2.timestamp_ns = 2_000_000 - buf.push(msg2) + _push(buf, tracing_pb2.SPAN_START, "task_a", 1_000.0, priority=4) + _push(buf, tracing_pb2.SPAN_END, "task_a", 2_000.0) records = buf.records assert len(records) == 1 @@ -79,41 +86,30 @@ def test_matches_start_end_pair(self): def test_isr_source_type_sets_type_field(self): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "gyro_isr" - msg.event_type = tracing_pb2.SPAN_START - msg.source_type = tracing_pb2.ISR - msg.timestamp_ns = 100 - buf.push(msg) - - msg2 = tracing_pb2.TraceEvent() - msg2.name = "gyro_isr" - msg2.event_type = tracing_pb2.SPAN_END - msg2.timestamp_ns = 200 - buf.push(msg2) - + _push(buf, tracing_pb2.SPAN_START, "gyro_isr", 0.1, source_type=tracing_pb2.ISR) + _push(buf, tracing_pb2.SPAN_END, "gyro_isr", 0.2) assert buf.records[0].type == "isr" + def test_span_attributes_come_from_the_start_not_the_end(self): + # In v2 only the dictionary carries priority and deadline, and a SPAN_END + # resolves to the same entry — but the record must be built from the + # entry seen at the START, which is the one the span was opened with. + buf = TraceEventBuffer() + _push(buf, tracing_pb2.SPAN_START, "t", 0.0, priority=7, relative_deadline_ms=1.0) + _push(buf, tracing_pb2.SPAN_END, "t", 500.0) + assert buf.records[0].priority == 7 + assert buf.records[0].deadline_us == pytest.approx(1_000.0) + def test_unmatched_end_discarded_and_logs(self, caplog): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "ghost" - msg.event_type = tracing_pb2.SPAN_END - msg.timestamp_ns = 999 with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): - buf.push(msg) + _push(buf, tracing_pb2.SPAN_END, "ghost", 999.0) assert buf.records == [] assert "no matching START" in caplog.text def test_marker_recorded_directly(self): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "loop_tick" - msg.event_type = tracing_pb2.MARKER - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 5_000_000 - msg.marker_value = 7 - buf.push(msg) + _push(buf, tracing_pb2.MARKER, "loop_tick", 5_000.0, marker_value=7) assert buf.records == [] markers = buf.markers @@ -127,40 +123,23 @@ def test_marker_recorded_directly(self): def test_marker_without_value_is_none(self): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "tick" - msg.event_type = tracing_pb2.MARKER - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 1_000 - buf.push(msg) + _push(buf, tracing_pb2.MARKER, "tick", 1.0) assert buf.markers[0].value is None - def test_relative_deadline_ms_converted_to_us(self): + def test_marker_value_of_zero_is_kept(self): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "t" - msg.event_type = tracing_pb2.SPAN_START - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 0 - msg.relative_deadline_ms = 2.5 - buf.push(msg) - - msg2 = tracing_pb2.TraceEvent() - msg2.name = "t" - msg2.event_type = tracing_pb2.SPAN_END - msg2.timestamp_ns = 1_000 - buf.push(msg2) + _push(buf, tracing_pb2.MARKER, "tick", 1.0, marker_value=0) + assert buf.markers[0].value == 0 + def test_relative_deadline_ms_converted_to_us(self): + buf = TraceEventBuffer() + _push(buf, tracing_pb2.SPAN_START, "t", 0.0, relative_deadline_ms=2.5) + _push(buf, tracing_pb2.SPAN_END, "t", 1.0) assert buf.records[0].deadline_us == pytest.approx(2_500.0) def test_flush_pending_warns_and_clears(self, caplog): buf = TraceEventBuffer() - msg = tracing_pb2.TraceEvent() - msg.name = "hanging_task" - msg.event_type = tracing_pb2.SPAN_START - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 0 - buf.push(msg) + _push(buf, tracing_pb2.SPAN_START, "hanging_task", 0.0) with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): buf.flush_pending() @@ -175,53 +154,228 @@ def test_flush_pending_warns_and_clears(self, caplog): def test_duplicate_start_warns_and_replaces(self, caplog): buf = TraceEventBuffer() - for ts in (100, 200): - msg = tracing_pb2.TraceEvent() - msg.name = "dup" - msg.event_type = tracing_pb2.SPAN_START - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = ts - with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): - buf.push(msg) - - # The second START replaces the first; final END should use ts=200 - msg = tracing_pb2.TraceEvent() - msg.name = "dup" - msg.event_type = tracing_pb2.SPAN_END - msg.source_type = tracing_pb2.TASK - msg.timestamp_ns = 300 - buf.push(msg) - - assert buf.records[0].start_us == pytest.approx(200.0 / 1_000) + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + _push(buf, tracing_pb2.SPAN_START, "dup", 0.1) + _push(buf, tracing_pb2.SPAN_START, "dup", 0.2) + _push(buf, tracing_pb2.SPAN_END, "dup", 0.3) + + assert buf.records[0].start_us == pytest.approx(0.2) assert "duplicate START" in caplog.text +# --------------------------------------------------------------------------- +# TraceStreamState +# --------------------------------------------------------------------------- + +class TestTraceStreamState: + def test_nanosecond_ticks_convert_to_us(self): + state = TraceStreamState() + assert state.ticks_to_us(1_500) == pytest.approx(1.5) + + def test_cycle_ticks_convert_using_the_core_frequency(self): + state = TraceStreamState() + state.timebase = tracing_pb2.CYCLES + state.core_frequency_hz = 72_000_000 + # 72 000 cycles at 72 MHz is exactly 1 ms. + assert state.ticks_to_us(72_000) == pytest.approx(1_000.0) + + def test_cycles_without_a_frequency_fall_back_to_nanoseconds(self): + # Rather than dividing by zero: a header that declares CYCLES but no + # frequency is malformed, and a wrong-but-finite scale is still readable. + state = TraceStreamState() + state.timebase = tracing_pb2.CYCLES + state.core_frequency_hz = 0 + assert state.ticks_to_us(1_000) == pytest.approx(1.0) + + def test_unregistered_id_resolves_to_a_placeholder_and_warns(self, caplog): + state = TraceStreamState() + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + entry = state.resolve(7) + assert entry.name == UNKNOWN_NAME + assert "never registered" in caplog.text + + def test_the_reserved_unknown_id_resolves_silently(self, caplog): + # Id 0 means the device's registry was full. That is reported by a + # firmware fault counter, so the host need not warn per event. + state = TraceStreamState() + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + entry = state.resolve(0) + assert entry.name == UNKNOWN_NAME + assert caplog.text == "" + + def test_reset_clears_the_dictionary(self): + # Ids restart at one on reboot, so a stale entry would resolve a new id + # to the wrong name. + state = TraceStreamState() + state.names[1] = NameEntry(name="old_task") + state.clock_ticks = 12345 + state.source_mask = 0x1F + state.reset() + assert state.names == {} + assert state.clock_ticks == 0 + assert state.source_mask is None + + # --------------------------------------------------------------------------- # decode_tracing_stream # --------------------------------------------------------------------------- class TestDecodeTracingStream: - def test_happy_path(self, sample_span_start_frame, sample_span_end_frame): - buf = bytearray(sample_span_start_frame + sample_span_end_frame) - tracker = SequenceTracker("test") + def test_happy_path(self, sample_stream): + state, event_buffer = _decode(sample_stream) + assert len(event_buffer.records) == 2 + assert len(event_buffer.markers) == 1 + assert sample_stream == bytearray() + + def test_header_fields_reach_the_state(self): + stream = StreamBuilder().start(timestamp_ticks=1_000, source_mask=0x17).bytes() + state, _ = _decode(stream) + assert state.source_mask == 0x17 + assert state.timebase == tracing_pb2.NANOSECONDS + + def test_a_cycles_header_rescales_every_timestamp(self): + builder = StreamBuilder(timebase=tracing_pb2.CYCLES, core_frequency_hz=72_000_000) + builder.start(timestamp_ticks=0) + builder.span("t", 0, 72_000) # 72 000 cycles at 72 MHz = 1 ms + state, event_buffer = _decode(builder.bytes()) + assert state.core_frequency_hz == 72_000_000 + assert event_buffer.records[0].end_us == pytest.approx(1_000.0) + + def test_names_resolve_through_the_dictionary(self, sample_stream): + state, event_buffer = _decode(sample_stream) + assert {e.name for e in state.names.values()} == {"main_task", "gyro_isr", "loop_tick"} + assert [r.name for r in event_buffer.records] == ["main_task", "gyro_isr"] + assert event_buffer.markers[0].name == "loop_tick" + + def test_a_name_is_registered_once_and_reused(self, builder): + builder.span("t", 0, 1_000) + builder.span("t", 2_000, 3_000) + state, event_buffer = _decode(builder.bytes()) + assert len(state.names) == 1 + assert len(event_buffer.records) == 2 + + def test_deltas_accumulate_into_absolute_timestamps(self, builder): + builder.span("main_task", 1_000_000, 2_000_000, priority=4) + builder.span("main_task", 5_000_000, 5_500_000) + _, event_buffer = _decode(builder.bytes()) + spans = [(r.start_us, r.end_us) for r in event_buffer.records] + assert spans == [ + pytest.approx((1_000.0, 2_000.0)), + pytest.approx((5_000.0, 5_500.0)), + ] + + def test_attributes_are_carried_by_the_dictionary_not_the_event(self, builder): + builder.span( + "gyro_isr", 0, 400_000, + source_type=tracing_pb2.ISR, priority=8, relative_deadline_ms=1.0, + ) + _, event_buffer = _decode(builder.bytes()) + r = event_buffer.records[0] + assert r.type == "isr" + assert r.priority == 8 + assert r.deadline_us == pytest.approx(1_000.0) + + def test_an_unregistered_id_still_yields_a_decodable_marker(self, builder, caplog): + # What the firmware emits when its name registry is full: the event goes + # out with the reserved id rather than being dropped or mis-decoded. + builder.event(tracing_pb2.MARKER, "", 1_000, marker_value=5, name_id=0) + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + _, event_buffer = _decode(builder.bytes()) + assert event_buffer.markers[0].name == UNKNOWN_NAME + assert event_buffer.markers[0].value == 5 + + def test_partial_frame_is_left_in_the_buffer(self, builder): + builder.span("t", 0, 1_000) + stream = builder.bytes() + head, tail = stream[:-2], stream[-2:] + state = TraceStreamState("test") event_buffer = TraceEventBuffer() - decode_tracing_stream(buf, tracker, event_buffer) + decode_tracing_stream(head, state, event_buffer) + assert len(head) > 0, "the incomplete frame must be kept for the next read" + assert event_buffer.records == [] + # Feeding the rest completes the span. + head.extend(tail) + decode_tracing_stream(head, state, event_buffer) assert len(event_buffer.records) == 1 - assert buf == bytearray() - - def test_reset_detected_clears_buffer(self): - # Frame with sequence 1000 followed by sequence 0 (reset) - frame_high = _make_frame(tracing_pb2.SPAN_START, tracing_pb2.TASK, "t", 0, sequence=1000) - frame_reset = _make_frame(tracing_pb2.SPAN_START, tracing_pb2.TASK, "t", 0, sequence=0) - buf = bytearray(frame_high + frame_reset + b"\xaa\xbb") # trailing garbage - tracker = SequenceTracker("test") - tracker.observe(999) # pretend we've seen up to 999 + assert head == bytearray() + + def test_sequence_gap_is_reported(self, caplog): + stream = StreamBuilder().start().bytes() + # Skip a sequence number: the next frame claims 5, not 1. + stream += make_frame(tracing_pb2.MARKER, name_id=0, sequence=5) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + _decode(stream) + assert "drop detected" in caplog.text + + def test_sequence_wraps_at_the_modulus_without_a_false_reset(self, caplog): + state = TraceStreamState("test") event_buffer = TraceEventBuffer() + # Walk the tracker up to the last sequence before the wrap, then feed the + # wrapped frame. A u32 tracker would read 0 after 16383 as a huge jump. + state.tracker.observe(SEQUENCE_MODULUS - 1) + stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=0, sequence=0)) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + decode_tracing_stream(stream, state, event_buffer) + assert caplog.text == "" + assert len(event_buffer.markers) == 1 - decode_tracing_stream(buf, tracker, event_buffer) - - # buf is cleared on reset detection - assert buf == bytearray() + def test_backwards_sequence_is_a_reset_that_clears_the_buffer(self): + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + state.tracker.observe(1_000) + state.names[1] = NameEntry(name="stale") + # Restarting at 0 from 1000 is an apparent forward jump of 15 383, more + # than half the modulus, so it reads as a backwards step: a reset. + stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=1, sequence=0)) + stream += b"\xaa\xbb" # trailing garbage from the pre-reset stream + decode_tracing_stream(stream, state, event_buffer) + assert stream == bytearray() + assert state.names == {} + + def test_a_reset_from_high_in_the_range_is_not_visible_in_the_sequence(self, caplog): + # A consequence of wrapping at 16 384 rather than 2**32: restarting at 0 + # only looks backwards when the pre-reset counter was below half the + # modulus. From 9 000 it reads as an ordinary 7 383-frame gap, and the + # stale dictionary survives. This is why TRACE_START, not the sequence, + # is the reliable reset signal — see the test below. + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + state.tracker.observe(9_000) + state.names[1] = NameEntry(name="stale") + stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=1, sequence=0)) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + decode_tracing_stream(stream, state, event_buffer) + assert "drop detected" in caplog.text + assert "reset" not in caplog.text + assert state.names == {1: NameEntry(name="stale")} + + def test_a_second_header_is_treated_as_a_device_reset(self, caplog): + builder = StreamBuilder().start() + builder.span("t", 0, 1_000) + stream = builder.bytes() + state, event_buffer = _decode(stream) + assert len(state.names) == 1 + + # The device reboots: a fresh header arrives on the same connection, and + # its sequence restarts at 0 — which on its own is only a small backwards + # step, not the large one the tracker reads as a reset. + reboot = StreamBuilder().start(timestamp_ticks=0, source_mask=0x1F) + reboot.span("t", 0, 500) + buf = reboot.bytes() + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + decode_tracing_stream(buf, state, event_buffer) + assert "device reset" in caplog.text + assert state.names == {} + assert state.source_mask is None + + def test_malformed_payload_is_skipped_without_killing_the_stream(self, caplog): + stream = StreamBuilder().start().bytes() + # A frame whose length prefix is right but whose payload is not a + # TraceFrame: field 1 declared as a length-delimited string. + stream += bytes([3, 0x0A, 0x7F, 0x7F]) + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + _decode(stream) + assert stream == bytearray(), "the bad frame is consumed, not left to jam the buffer" # --------------------------------------------------------------------------- diff --git a/src/encode.rs b/src/encode.rs index 3e6f493..648ba86 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -1,38 +1,364 @@ -use crate::TraceEvent; +//! Wire encoding for the v2 trace format (EXEC-TRACE-002 §6). +//! +//! Every frame is `[varint: byte length][protobuf-encoded TraceFrame]`, and every +//! frame class — the three per-occurrence events, the dictionary entry and the +//! stream header — shares one flat message discriminated by `event_type` (§18.1). +//! +//! [`TraceEncoder`] owns the three pieces of per-stream state the format needs: +//! the name dictionary (§5.5), the timestamp base the deltas are taken against +//! (§5.6) and the sequence counter (§5.7). It runs in the task that owns the +//! transport, off the control path. + +use crate::types::{SourceType, TraceEvent}; use micropb::{MessageDecode, MessageEncode, PbDecoder, PbEncoder}; +/// Upper bound on one encoded frame, length prefix included. pub const MAX_TRACE_FRAME_SIZE: usize = 128; -/// Errors returned by [`encode_trace_frame`] and [`SequenceEncoder::encode`]. +/// Upper bound on one [`TraceEncoder::encode`] call, which emits a dictionary +/// frame ahead of the event when a name is seen for the first time. +pub const MAX_TRACE_BURST_SIZE: usize = 2 * MAX_TRACE_FRAME_SIZE; + +/// Distinct names the dictionary holds, against roughly 30 in the firmware today. +pub const NAME_REGISTRY_CAPACITY: usize = 64; + +/// The reserved "unknown" name id, emitted when the registry is full (REQ-T11). +pub const UNKNOWN_NAME_ID: u32 = 0; + +/// Sequence numbers wrap here rather than at 2³². +/// +/// The counter exists only to detect gaps, a gap is read modulo the wrap, and the +/// largest burst ever observed was 103 frames. A full 32-bit counter costs five +/// varint bytes once past 2²⁸; this costs at most two (§18.1). +pub const SEQUENCE_MODULUS: u32 = 16_384; + +/// The unit of the `timestamp_ticks` field, declared once on the stream header. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TimeBase { + /// Ticks are nanoseconds; `core_frequency_hz` is unused. + #[default] + Nanoseconds, + /// Ticks are core clock cycles, converted by `core_frequency_hz` (§5.8). + Cycles, +} + +/// Errors returned by the encoding entry points. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TracingEncodeError { /// The output buffer is too small or the encoded message exceeds [`MAX_TRACE_FRAME_SIZE`]. BufferFull, } -/// Encodes a [`TraceEvent`] as a length-delimited protobuf frame into `out`. +/// Result of one [`TraceEncoder::encode`] call. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Encoded { + /// Bytes written to the output buffer. + pub len: usize, + /// The name was unknown and the dictionary was full, so the event went out + /// with [`UNKNOWN_NAME_ID`]. The caller should count this as a fault: the + /// stream stays decodable, but the name is lost. + pub name_registry_full: bool, +} + +/// The frame classes the wire carries. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameKind { + SpanStart, + SpanEnd, + Marker, + /// A dictionary entry: `name_id` now resolves to `name` (§6.2). + NameRegistered, + /// The stream header, carrying the timebase and the source mask (§6.3). + TraceStart, +} + +/// One decoded frame, mirroring the wire message field for field. /// -/// Frame format: `[varint: byte length][protobuf-encoded TraceEvent]`. +/// Decoding deliberately stops here rather than returning a [`TraceEvent`]: in v2 +/// an event is not recoverable from a single frame, because its name is a +/// dictionary id and its timestamp is a delta. Resolving both needs per-stream +/// state, which belongs in the host decoder (§18.3). +#[derive(Debug, Clone, PartialEq)] +pub struct RawTraceFrame { + /// A delta against the previous sequenced frame, except on + /// [`FrameKind::NameRegistered`] and [`FrameKind::TraceStart`], where it is + /// absolute and re-establishes the time origin. + pub timestamp_ticks: u64, + pub name_id: u32, + pub kind: FrameKind, + pub sequence: u32, + /// [`FrameKind::Marker`] only. + pub marker_value: Option, + /// [`FrameKind::NameRegistered`] only. + pub name: heapless::String<32>, + /// [`FrameKind::NameRegistered`] only. + pub source_type: Option, + /// [`FrameKind::NameRegistered`] only. + pub priority: u32, + /// [`FrameKind::NameRegistered`] only. + pub relative_deadline_ms: Option, + /// [`FrameKind::TraceStart`] only. + pub timebase: TimeBase, + /// [`FrameKind::TraceStart`] only; zero when the timebase is nanoseconds. + pub core_frequency_hz: u32, + /// [`FrameKind::TraceStart`] only. + pub source_mask: u32, +} + +impl RawTraceFrame { + fn new(kind: FrameKind, timestamp_ticks: u64, sequence: u32, name_id: u32) -> Self { + Self { + timestamp_ticks, + name_id, + kind, + sequence, + marker_value: None, + name: heapless::String::new(), + source_type: None, + priority: 0, + relative_deadline_ms: None, + timebase: TimeBase::Nanoseconds, + core_frequency_hz: 0, + source_mask: 0, + } + } +} + +/// Encodes [`TraceEvent`]s into the v2 wire format, holding the per-stream state +/// the format needs. /// -/// The `sequence` parameter **overwrites** the `sequence` field already present in -/// `msg`. [`TraceSink`] always produces events with `sequence = 0`; use -/// [`SequenceEncoder`] or manage the counter manually here to enable drop detection -/// on the host. +/// # Name interning /// -/// Returns the number of bytes written on success. +/// The dictionary is keyed on the name's **content**, not on the `&'static str` +/// pointer §5.5 proposed. By the time an event reaches this layer the literal's +/// pointer is gone — [`TraceEvent`] carries an inline copy that has been moved +/// through a channel — so pointer keying is only available to a registry in the +/// recording layer, which would mean a process-global with atomics and a changed +/// [`TraceSink`] API (§18.2). The scan is linear over at most +/// [`NAME_REGISTRY_CAPACITY`] entries with a length check before any byte +/// comparison, and it runs off the control path. +/// +/// [`TraceSink`]: crate::TraceSink +pub struct TraceEncoder { + names: heapless::Vec, NAME_REGISTRY_CAPACITY>, + last_ticks: u64, + sequence: u32, +} + +impl TraceEncoder { + /// Creates an encoder with an empty dictionary and the counters at zero. + pub const fn new() -> Self { + Self { + names: heapless::Vec::new(), + last_ticks: 0, + sequence: 0, + } + } + + /// Returns the sequence number of the *next* frame. + pub fn sequence(&self) -> u32 { + self.sequence + } + + /// Returns the number of names currently held in the dictionary. + pub fn registered_names(&self) -> usize { + self.names.len() + } + + /// Encodes the stream header (§6.3) into `out`, returning the bytes written. + /// + /// The header gives the host the tick unit and the active source mask without + /// either being hard-coded in the decoder, and it is what marks the stream as + /// v2 (§6.4). Emit it once at init, before any event. + /// + /// Its timestamp is absolute and becomes the base the following deltas are + /// taken against. + /// + /// # Errors + /// Returns [`TracingEncodeError::BufferFull`] if `out` is too small. + pub fn encode_trace_start( + &mut self, + timestamp_ticks: u64, + timebase: TimeBase, + core_frequency_hz: u32, + source_mask: u32, + out: &mut [u8], + ) -> Result { + let mut frame = RawTraceFrame::new( + FrameKind::TraceStart, + timestamp_ticks, + self.next_sequence(), + UNKNOWN_NAME_ID, + ); + frame.timebase = timebase; + frame.core_frequency_hz = core_frequency_hz; + frame.source_mask = source_mask; + self.last_ticks = timestamp_ticks; + encode_trace_frame(&frame, out) + } + + /// Encodes `event` into `out`, interning its name, delta-encoding its + /// timestamp and assigning its sequence number. + /// + /// When the name is seen for the first time this writes **two** frames: the + /// dictionary entry, carrying the name and its fixed attributes, followed by + /// the event itself. `out` must therefore be at least + /// [`MAX_TRACE_BURST_SIZE`] bytes. + /// + /// # Errors + /// Returns [`TracingEncodeError::BufferFull`] if `out` is too small. + pub fn encode( + &mut self, + event: &TraceEvent, + out: &mut [u8], + ) -> Result { + let (name, timestamp_ns) = match event { + TraceEvent::SpanStart { + name, timestamp_ns, .. + } + | TraceEvent::SpanEnd { + name, timestamp_ns, .. + } + | TraceEvent::Marker { + name, timestamp_ns, .. + } => (name, *timestamp_ns), + }; + + let mut written = 0usize; + let mut name_registry_full = false; + + let name_id = match self.lookup(name) { + Some(id) => id, + None => match self.register(event, timestamp_ns, out) { + Ok((id, n)) => { + written += n; + id + } + Err(RegisterError::Full) => { + name_registry_full = true; + UNKNOWN_NAME_ID + } + Err(RegisterError::Encode(e)) => return Err(e), + }, + }; + + let delta = timestamp_ns.saturating_sub(self.last_ticks); + self.last_ticks = timestamp_ns; + let sequence = self.next_sequence(); + + let mut frame = match event { + TraceEvent::SpanStart { .. } => { + RawTraceFrame::new(FrameKind::SpanStart, delta, sequence, name_id) + } + TraceEvent::SpanEnd { .. } => { + RawTraceFrame::new(FrameKind::SpanEnd, delta, sequence, name_id) + } + TraceEvent::Marker { marker_value, .. } => { + let mut f = RawTraceFrame::new(FrameKind::Marker, delta, sequence, name_id); + f.marker_value = *marker_value; + f + } + }; + // proto3 omits a zero-valued scalar, so a delta of zero costs nothing. + frame.timestamp_ticks = delta; + + let out_tail = out + .get_mut(written..) + .ok_or(TracingEncodeError::BufferFull)?; + written += encode_trace_frame(&frame, out_tail)?; + + Ok(Encoded { + len: written, + name_registry_full, + }) + } + + /// Linear scan keyed on content, length checked before any byte comparison. + fn lookup(&self, name: &heapless::String<32>) -> Option { + let needle = name.as_bytes(); + self.names + .iter() + .position(|candidate| candidate.len() == needle.len() && candidate.as_bytes() == needle) + // Ids are one-based: zero is reserved for "unknown". + .and_then(|index| u32::try_from(index + 1).ok()) + } + + /// Assigns the next id and writes the dictionary frame, whose timestamp is + /// absolute so a host attaching mid-run can re-establish the time origin. + fn register( + &mut self, + event: &TraceEvent, + timestamp_ns: u64, + out: &mut [u8], + ) -> Result<(u32, usize), RegisterError> { + let (name, source_type, priority, relative_deadline_ms) = match event { + TraceEvent::SpanStart { + name, + source_type, + priority, + relative_deadline_ms, + .. + } => (name, Some(*source_type), *priority, *relative_deadline_ms), + // A SpanEnd or Marker reaching registration before its SpanStart + // carries no attributes to register; the name alone is the entry. + TraceEvent::SpanEnd { name, .. } | TraceEvent::Marker { name, .. } => { + (name, None, 0, None) + } + }; + + self.names + .push(name.clone()) + .map_err(|_| RegisterError::Full)?; + let id = u32::try_from(self.names.len()).map_err(|_| RegisterError::Full)?; + + let sequence = self.next_sequence(); + let mut frame = RawTraceFrame::new(FrameKind::NameRegistered, timestamp_ns, sequence, id); + frame.name = name.clone(); + frame.source_type = source_type; + frame.priority = priority; + frame.relative_deadline_ms = relative_deadline_ms; + self.last_ticks = timestamp_ns; + + let n = encode_trace_frame(&frame, out).map_err(RegisterError::Encode)?; + Ok((id, n)) + } + + fn next_sequence(&mut self) -> u32 { + let current = self.sequence; + self.sequence = (self.sequence + 1) % SEQUENCE_MODULUS; + current + } +} + +impl Default for TraceEncoder { + fn default() -> Self { + Self::new() + } +} + +enum RegisterError { + Full, + Encode(TracingEncodeError), +} + +/// Encodes one [`RawTraceFrame`] as a length-delimited protobuf frame into `out`. +/// +/// Frame format: `[varint: byte length][protobuf-encoded TraceFrame]`. Returns the +/// number of bytes written. +/// +/// This is the stateless half: it writes exactly the fields the frame carries and +/// applies no interning, delta or sequencing. Use [`TraceEncoder`] to produce +/// frames from [`TraceEvent`]s. /// /// # Errors /// Returns [`TracingEncodeError::BufferFull`] if `out` is too small or the message /// exceeds [`MAX_TRACE_FRAME_SIZE`]. -/// -/// [`TraceSink`]: crate::TraceSink #[allow(clippy::indexing_slicing)] // bounds-checked: n <= vec.len() <= MAX_TRACE_FRAME_SIZE <= out.len() pub fn encode_trace_frame( - msg: &TraceEvent, - sequence: u32, + frame: &RawTraceFrame, out: &mut [u8], ) -> Result { - let proto_msg = to_proto(msg, sequence); + let proto_msg = to_proto(frame); let mut vec: heapless::Vec = heapless::Vec::new(); let mut encoder = PbEncoder::new(vec); @@ -60,74 +386,33 @@ pub enum TracingDecodeError { DecodeError, } -/// Decodes one length-delimited protobuf frame produced by [`encode_trace_frame`]. +/// Decodes one length-delimited frame produced by [`encode_trace_frame`]. /// -/// `frame` must begin with a varint-encoded byte count followed by that many bytes -/// of protobuf-encoded [`TraceEvent`]. Returns the decoded event and the total number -/// of bytes consumed (varint header + payload). Useful for host-side tooling. +/// Returns the raw frame and the total number of bytes consumed (varint header + +/// payload). The frame's name is a dictionary id and its timestamp is usually a +/// delta; resolving either needs the per-stream state the host decoder holds +/// (§18.3). /// /// # Errors -/// Returns [`TracingDecodeError`] if the frame is truncated, the varint is malformed, -/// or the protobuf payload cannot be decoded. -pub fn decode_trace_frame(frame: &[u8]) -> Result<(TraceEvent, usize), TracingDecodeError> { +/// Returns [`TracingDecodeError`] if the frame is truncated, the varint is +/// malformed, or the protobuf payload cannot be decoded. +pub fn decode_trace_frame(frame: &[u8]) -> Result<(RawTraceFrame, usize), TracingDecodeError> { let (payload_len, header_len) = decode_varint(frame).ok_or(TracingDecodeError::MalformedVarint)?; let total = header_len + payload_len; if frame.len() < total { return Err(TracingDecodeError::Truncated); } - let payload = &frame[header_len..total]; + let payload = frame + .get(header_len..total) + .ok_or(TracingDecodeError::Truncated)?; let mut decoder = PbDecoder::new(payload); - let mut proto_event = crate::proto::tracing_::TraceEvent::default(); - proto_event + let mut proto_frame = crate::proto::tracing_::TraceFrame::default(); + proto_frame .decode(&mut decoder, payload_len) .map_err(|_| TracingDecodeError::DecodeError)?; - let event = from_proto(proto_event).ok_or(TracingDecodeError::DecodeError)?; - Ok((event, total)) -} - -/// Encodes [`TraceEvent`]s into a byte buffer while tracking the sequence counter. -/// -/// [`TraceSink`] does not manage sequence numbers — that is the responsibility of the -/// transport layer (the code that owns the wire). Use `SequenceEncoder` when encoding -/// events manually so that the host-side decoder can detect dropped frames. -/// -/// [`TraceSink`]: crate::TraceSink -pub struct SequenceEncoder { - sequence: u32, -} - -impl SequenceEncoder { - /// Creates a new encoder with the sequence counter initialised to zero. - pub const fn new() -> Self { - Self { sequence: 0 } - } - - /// Encodes `event` into `out`, injecting the current sequence number and - /// advancing the counter. Returns the number of bytes written. - /// - /// # Errors - /// Returns [`TracingEncodeError::BufferFull`] if `out` is too small. - pub fn encode( - &mut self, - event: &TraceEvent, - out: &mut [u8], - ) -> Result { - let n = encode_trace_frame(event, self.sequence, out)?; - self.sequence = self.sequence.wrapping_add(1); - Ok(n) - } - - /// Returns the current sequence counter value (the number of the *next* frame). - pub fn sequence(&self) -> u32 { - self.sequence - } -} - -impl Default for SequenceEncoder { - fn default() -> Self { - Self::new() - } + let decoded = from_proto(proto_frame).ok_or(TracingDecodeError::DecodeError)?; + Ok((decoded, total)) } /// Returns `(value, bytes_consumed)` for a varint at the start of `buf`, @@ -148,121 +433,107 @@ fn decode_varint(buf: &[u8]) -> Option<(usize, usize)> { None } -fn to_proto(event: &TraceEvent, sequence: u32) -> crate::proto::tracing_::TraceEvent { +fn to_proto(frame: &RawTraceFrame) -> crate::proto::tracing_::TraceFrame { use crate::proto::tracing_ as pb; - use crate::types::SourceType; - match event { - TraceEvent::SpanStart { - timestamp_ns, - name, - source_type, - priority, - relative_deadline_ms, - .. - } => { - let pb_source = match source_type { - SourceType::Isr => pb::TraceEventSourceType::Isr, - SourceType::Task => pb::TraceEventSourceType::Task, - }; - let mut msg = pb::TraceEvent { - timestamp_ns: *timestamp_ns, - name: name.clone(), - source_type: pb_source, - event_type: pb::TraceEventType::SpanStart, - sequence, - priority: *priority, - ..Default::default() - }; - if let Some(dl) = relative_deadline_ms { - msg.set_relative_deadline_ms(*dl); - } - msg - } - TraceEvent::SpanEnd { - timestamp_ns, name, .. - } => pb::TraceEvent { - timestamp_ns: *timestamp_ns, - name: name.clone(), - event_type: pb::TraceEventType::SpanEnd, - sequence, - ..Default::default() + let mut msg = pb::TraceFrame { + timestamp_ticks: frame.timestamp_ticks, + name_id: frame.name_id, + event_type: match frame.kind { + FrameKind::SpanStart => pb::TraceEventType::SpanStart, + FrameKind::SpanEnd => pb::TraceEventType::SpanEnd, + FrameKind::Marker => pb::TraceEventType::Marker, + FrameKind::NameRegistered => pb::TraceEventType::NameRegistered, + FrameKind::TraceStart => pb::TraceEventType::TraceStart, }, - TraceEvent::Marker { - timestamp_ns, - name, - marker_value, - .. - } => { - let mut msg = pb::TraceEvent { - timestamp_ns: *timestamp_ns, - name: name.clone(), - event_type: pb::TraceEventType::Marker, - sequence, - ..Default::default() - }; - if let Some(v) = marker_value { - msg.set_marker_value(*v); - } - msg - } + sequence: frame.sequence, + name: frame.name.clone(), + source_type: match frame.source_type { + Some(SourceType::Isr) => pb::TraceEventSourceType::Isr, + Some(SourceType::Task) => pb::TraceEventSourceType::Task, + None => pb::TraceEventSourceType::Unspecified, + }, + priority: frame.priority, + timebase: match frame.timebase { + TimeBase::Nanoseconds => pb::TimeBase::Nanoseconds, + TimeBase::Cycles => pb::TimeBase::Cycles, + }, + core_frequency_hz: frame.core_frequency_hz, + source_mask: frame.source_mask, + ..Default::default() + }; + if let Some(v) = frame.marker_value { + msg.set_marker_value(v); } + if let Some(dl) = frame.relative_deadline_ms { + msg.set_relative_deadline_ms(dl); + } + msg } -fn from_proto(p: crate::proto::tracing_::TraceEvent) -> Option { +fn from_proto(p: crate::proto::tracing_::TraceFrame) -> Option { use crate::proto::tracing_ as pb; - use crate::types::SourceType; - - let et = p.event_type; - if et == pb::TraceEventType::SpanStart { - let source_type = if p.source_type == pb::TraceEventSourceType::Isr { - SourceType::Isr - } else if p.source_type == pb::TraceEventSourceType::Task { - SourceType::Task - } else { - return None; - }; - let relative_deadline_ms = p.relative_deadline_ms().copied(); - Some(TraceEvent::SpanStart { - timestamp_ns: p.timestamp_ns, - source_type, - sequence: p.sequence, - priority: p.priority, - relative_deadline_ms, - name: p.name, - }) - } else if et == pb::TraceEventType::SpanEnd { - Some(TraceEvent::SpanEnd { - timestamp_ns: p.timestamp_ns, - sequence: p.sequence, - name: p.name, - }) - } else if et == pb::TraceEventType::Marker { - let marker_value = p.marker_value().copied(); - Some(TraceEvent::Marker { - timestamp_ns: p.timestamp_ns, - sequence: p.sequence, - marker_value, - name: p.name, - }) + + let kind = if p.event_type == pb::TraceEventType::SpanStart { + FrameKind::SpanStart + } else if p.event_type == pb::TraceEventType::SpanEnd { + FrameKind::SpanEnd + } else if p.event_type == pb::TraceEventType::Marker { + FrameKind::Marker + } else if p.event_type == pb::TraceEventType::NameRegistered { + FrameKind::NameRegistered + } else if p.event_type == pb::TraceEventType::TraceStart { + FrameKind::TraceStart + } else { + return None; + }; + + let source_type = if p.source_type == pb::TraceEventSourceType::Isr { + Some(SourceType::Isr) + } else if p.source_type == pb::TraceEventSourceType::Task { + Some(SourceType::Task) } else { None - } + }; + + let timebase = if p.timebase == pb::TimeBase::Cycles { + TimeBase::Cycles + } else { + TimeBase::Nanoseconds + }; + + Some(RawTraceFrame { + timestamp_ticks: p.timestamp_ticks, + name_id: p.name_id, + kind, + sequence: p.sequence, + marker_value: p.marker_value().copied(), + source_type, + priority: p.priority, + relative_deadline_ms: p.relative_deadline_ms().copied(), + timebase, + core_frequency_hz: p.core_frequency_hz, + source_mask: p.source_mask, + name: p.name, + }) } #[cfg(all(test, feature = "std", feature = "enabled"))] mod tests { use super::*; - use crate::SourceType; use heapless::String; use insta::assert_debug_snapshot; - fn make_span_start() -> TraceEvent { - let mut name: String<32> = String::new(); - name.push_str("led_task").unwrap(); + fn name(v: &str) -> String<32> { + let mut n: String<32> = String::new(); + n.push_str(v).unwrap(); + n + } + + fn span_start(n: &str, ts: u64) -> TraceEvent { TraceEvent::SpanStart { - timestamp_ns: 1_000_000, - name, + timestamp_ns: ts, + name: name(n), source_type: SourceType::Task, sequence: 0, priority: 2, @@ -270,326 +541,526 @@ mod tests { } } - fn make_span_start_with_deadline() -> TraceEvent { - let mut name: String<32> = String::new(); - name.push_str("led_task").unwrap(); + fn span_start_with_deadline(n: &str, ts: u64) -> TraceEvent { TraceEvent::SpanStart { - timestamp_ns: 1_000_000, - name, - source_type: SourceType::Task, + timestamp_ns: ts, + name: name(n), + source_type: SourceType::Isr, sequence: 0, - priority: 2, + priority: 8, relative_deadline_ms: Some(0.5), } } - fn make_span_end() -> TraceEvent { - let mut name: String<32> = String::new(); - name.push_str("gyro_isr").unwrap(); + fn span_end(n: &str, ts: u64) -> TraceEvent { TraceEvent::SpanEnd { - timestamp_ns: 2_000_000, - name, + timestamp_ns: ts, + name: name(n), sequence: 0, } } - fn make_marker() -> TraceEvent { - let mut name: String<32> = String::new(); - name.push_str("ukf_predict").unwrap(); + fn marker(n: &str, ts: u64, value: Option) -> TraceEvent { TraceEvent::Marker { - timestamp_ns: 1_500_000, - name, + timestamp_ns: ts, + name: name(n), sequence: 0, - marker_value: None, + marker_value: value, } } - fn make_marker_with_value() -> TraceEvent { - let mut name: String<32> = String::new(); - name.push_str("ukf_predict").unwrap(); - TraceEvent::Marker { - timestamp_ns: 1_500_000, - name, - sequence: 0, - marker_value: Some(42), + /// Encodes into a fresh buffer and returns the bytes actually written. + fn encode_one(enc: &mut TraceEncoder, event: &TraceEvent) -> (Vec, Encoded) { + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let outcome = enc.encode(event, &mut buf).unwrap(); + (buf[..outcome.len].to_vec(), outcome) + } + + /// Decodes every frame in `bytes`, asserting the buffer is consumed exactly. + fn decode_all(bytes: &[u8]) -> Vec { + let mut frames = Vec::new(); + let mut offset = 0; + while offset < bytes.len() { + let (frame, consumed) = decode_trace_frame(&bytes[offset..]).unwrap(); + frames.push(frame); + offset += consumed; } + assert_eq!(offset, bytes.len(), "frames did not tile the buffer"); + frames } - fn encode(msg: &TraceEvent, seq: u32) -> ([u8; MAX_TRACE_FRAME_SIZE], usize) { - let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - let n = encode_trace_frame(msg, seq, &mut buf).unwrap(); - (buf, n) - } - - fn check_frame_length(frame: &[u8]) { - let mut value: u64 = 0; - let mut shift = 0u32; - for (i, &byte) in frame.iter().enumerate() { - value |= ((byte & 0x7F) as u64) << shift; - if byte & 0x80 == 0 { - let header_bytes = i + 1; - assert_eq!( - header_bytes + value as usize, - frame.len(), - "varint length prefix does not match frame length" - ); - return; - } - shift += 7; + /// An encoder whose dictionary already holds `names`, so that following + /// encodes are steady-state rather than cold. + fn warmed(names: &[&str]) -> TraceEncoder { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for n in names { + enc.encode(&span_start(n, 0), &mut buf).unwrap(); } - panic!("incomplete varint in frame"); + enc } - fn contains_bytes(frame: &[u8], needle: &[u8]) -> bool { - frame.windows(needle.len()).any(|w| w == needle) + // ── Frame structure ─────────────────────────────────────────────────────── + + #[test] + fn first_sight_of_a_name_emits_a_dictionary_frame_then_the_event() { + let mut enc = TraceEncoder::new(); + let (bytes, outcome) = encode_one(&mut enc, &span_start_with_deadline("gyro_isr", 5_000)); + assert!(!outcome.name_registry_full); + + let frames = decode_all(&bytes); + assert_eq!(frames.len(), 2, "expected dictionary frame + event"); + + assert_eq!(frames[0].kind, FrameKind::NameRegistered); + assert_eq!(frames[0].name.as_str(), "gyro_isr"); + assert_eq!(frames[0].name_id, 1); + assert_eq!(frames[0].source_type, Some(SourceType::Isr)); + assert_eq!(frames[0].priority, 8); + assert_eq!(frames[0].relative_deadline_ms, Some(0.5)); + + assert_eq!(frames[1].kind, FrameKind::SpanStart); + assert_eq!(frames[1].name_id, 1); + assert!(frames[1].name.is_empty(), "event must not carry the name"); } #[test] - fn span_start_encodes_without_error() { - let (buf, n) = encode(&make_span_start(), 0); - assert!(n > 0 && n <= MAX_TRACE_FRAME_SIZE); - check_frame_length(&buf[..n]); + fn dictionary_frame_timestamp_is_absolute() { + let mut enc = TraceEncoder::new(); + let (bytes, _) = encode_one(&mut enc, &span_start("main_task", 7_000_000)); + let frames = decode_all(&bytes); + assert_eq!(frames[0].timestamp_ticks, 7_000_000); } #[test] - fn span_start_with_deadline_encodes_without_error() { - let (buf, n) = encode(&make_span_start_with_deadline(), 0); - assert!(n > 0 && n <= MAX_TRACE_FRAME_SIZE); - check_frame_length(&buf[..n]); + fn second_sight_of_a_name_emits_the_event_alone() { + let mut enc = warmed(&["main_task"]); + let (bytes, _) = encode_one(&mut enc, &span_end("main_task", 1_000)); + let frames = decode_all(&bytes); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].kind, FrameKind::SpanEnd); + assert_eq!(frames[0].name_id, 1); } #[test] - fn span_end_encodes_without_error() { - let (buf, n) = encode(&make_span_end(), 0); - assert!(n > 0 && n <= MAX_TRACE_FRAME_SIZE); - check_frame_length(&buf[..n]); + fn distinct_names_get_distinct_ids() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for (i, n) in ["a", "b", "c"].iter().enumerate() { + let outcome = enc.encode(&span_start(n, 0), &mut buf).unwrap(); + let frames = decode_all(&buf[..outcome.len]); + assert_eq!(frames[0].name_id, u32::try_from(i + 1).unwrap()); + assert_eq!(frames[1].name_id, u32::try_from(i + 1).unwrap()); + } + assert_eq!(enc.registered_names(), 3); } #[test] - fn marker_encodes_without_error() { - let (buf, n) = encode(&make_marker(), 0); - assert!(n > 0 && n <= MAX_TRACE_FRAME_SIZE); - check_frame_length(&buf[..n]); + fn name_ids_are_one_based_so_zero_stays_reserved() { + let mut enc = TraceEncoder::new(); + let (bytes, _) = encode_one(&mut enc, &span_start("first", 0)); + assert_eq!(decode_all(&bytes)[0].name_id, 1); + assert_ne!(decode_all(&bytes)[0].name_id, UNKNOWN_NAME_ID); } #[test] - fn marker_with_value_encodes_without_error() { - let (buf, n) = encode(&make_marker_with_value(), 0); - assert!(n > 0 && n <= MAX_TRACE_FRAME_SIZE); - check_frame_length(&buf[..n]); + fn interning_is_keyed_on_content_not_on_pointer() { + // Two separately constructed names with equal content must share an id — + // this is the property §18.2 chose content keying to get. + let mut enc = warmed(&["main_task"]); + let mut owned = std::string::String::from("main_"); + owned.push_str("task"); + let (bytes, _) = encode_one(&mut enc, &span_end(&owned, 10)); + let frames = decode_all(&bytes); + assert_eq!(frames.len(), 1, "equal content must not re-register"); + assert_eq!(frames[0].name_id, 1); } #[test] - fn span_start_fits_in_max_buffer() { - let (_, n) = encode(&make_span_start_with_deadline(), u32::MAX); - assert!(n <= MAX_TRACE_FRAME_SIZE); + fn names_sharing_a_prefix_are_distinct_entries() { + let mut enc = warmed(&["main", "main_task"]); + assert_eq!(enc.registered_names(), 2); + let (bytes, _) = encode_one(&mut enc, &span_end("main", 0)); + assert_eq!(decode_all(&bytes)[0].name_id, 1); + let (bytes, _) = encode_one(&mut enc, &span_end("main_task", 0)); + assert_eq!(decode_all(&bytes)[0].name_id, 2); } + // ── Delta timestamps (§5.6) ─────────────────────────────────────────────── + #[test] - fn span_end_fits_in_max_buffer() { - let (_, n) = encode(&make_span_end(), u32::MAX); - assert!(n <= MAX_TRACE_FRAME_SIZE); + fn event_timestamps_are_deltas_against_the_previous_frame() { + let mut enc = warmed(&["main_task"]); + let (bytes, _) = encode_one(&mut enc, &span_start("main_task", 1_000)); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, 1_000); + let (bytes, _) = encode_one(&mut enc, &span_end("main_task", 1_700)); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, 700); } #[test] - fn marker_fits_in_max_buffer() { - let (_, n) = encode(&make_marker_with_value(), u32::MAX); - assert!(n <= MAX_TRACE_FRAME_SIZE); + fn the_event_following_its_own_dictionary_frame_has_a_zero_delta() { + let mut enc = TraceEncoder::new(); + let (bytes, _) = encode_one(&mut enc, &span_start("main_task", 9_999)); + let frames = decode_all(&bytes); + assert_eq!( + frames[0].timestamp_ticks, 9_999, + "dictionary frame absolute" + ); + assert_eq!(frames[1].timestamp_ticks, 0, "event delta against it"); } #[test] - fn name_present_in_output() { - let (buf, n) = encode(&make_span_start(), 0); - assert!(contains_bytes(&buf[..n], b"led_task")); + fn accumulated_deltas_reconstruct_the_original_timestamps() { + let mut enc = warmed(&["a"]); + let stamps = [1_000u64, 1_001, 250_000, 250_003, 9_000_000]; + let mut bytes = Vec::new(); + for ts in stamps { + let (b, _) = encode_one(&mut enc, &span_end("a", ts)); + bytes.extend_from_slice(&b); + } + let mut clock = 0u64; + let reconstructed: Vec = decode_all(&bytes) + .iter() + .map(|f| { + clock += f.timestamp_ticks; + clock + }) + .collect(); + assert_eq!(reconstructed, stamps); } #[test] - fn marker_label_present_in_output() { - let (buf, n) = encode(&make_marker(), 0); - assert!(contains_bytes(&buf[..n], b"ukf_predict")); + fn a_backwards_timestamp_saturates_rather_than_wrapping() { + // Cannot happen with a monotonic clock, but a delta of ~2^64 would be + // five varint bytes and would desynchronise the host's clock for good. + let mut enc = warmed(&["a"]); + encode_one(&mut enc, &span_end("a", 10_000)); + let (bytes, _) = encode_one(&mut enc, &span_end("a", 9_000)); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, 0); } + // ── Sequencing (§18.1) ──────────────────────────────────────────────────── + #[test] - fn deadline_present_in_span_start_output() { - let (buf, n) = encode(&make_span_start_with_deadline(), 0); - assert!(contains_bytes(&buf[..n], &0.5_f32.to_le_bytes())); + fn sequence_starts_at_zero_and_advances_per_frame() { + let mut enc = TraceEncoder::new(); + assert_eq!(enc.sequence(), 0); + let (bytes, _) = encode_one(&mut enc, &span_start("a", 0)); + let frames = decode_all(&bytes); + assert_eq!(frames[0].sequence, 0, "dictionary frame is sequenced too"); + assert_eq!(frames[1].sequence, 1); + assert_eq!(enc.sequence(), 2); } #[test] - fn span_start_with_deadline_larger_than_span_end() { - let (_, n_start) = encode(&make_span_start_with_deadline(), 0); - let (_, n_end) = encode(&make_span_end(), 0); - assert!(n_start > n_end); + fn sequence_wraps_at_the_modulus_not_at_u32_max() { + let mut enc = warmed(&["a"]); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + while enc.sequence() != SEQUENCE_MODULUS - 1 { + enc.encode(&span_end("a", 0), &mut buf).unwrap(); + } + let (bytes, _) = encode_one(&mut enc, &span_end("a", 0)); + assert_eq!(decode_all(&bytes)[0].sequence, SEQUENCE_MODULUS - 1); + assert_eq!(enc.sequence(), 0, "wraps to zero, not to 16384"); } #[test] - fn sequence_included_in_output() { - let msg = make_span_start(); - let (buf0, n0) = encode(&msg, 0); - let (buf1, n1) = encode(&msg, 99); - assert_ne!(&buf0[..n0], &buf1[..n1]); + fn sequence_never_exceeds_the_modulus() { + let mut enc = warmed(&["a"]); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for _ in 0..(SEQUENCE_MODULUS * 2) { + let n = enc.encode(&span_end("a", 0), &mut buf).unwrap(); + let frame = decode_all(&buf[..n.len]).remove(0); + assert!(frame.sequence < SEQUENCE_MODULUS); + } } + // ── Registry exhaustion (REQ-T11) ───────────────────────────────────────── + #[test] - fn sequence_wraps_at_max() { - let (buf, n) = encode(&make_span_start(), u32::MAX); - check_frame_length(&buf[..n]); + fn registry_exhaustion_falls_back_to_the_unknown_id_and_reports_it() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for i in 0..NAME_REGISTRY_CAPACITY { + let outcome = enc + .encode(&span_start(&format!("n{i}"), 0), &mut buf) + .unwrap(); + assert!(!outcome.name_registry_full); + } + assert_eq!(enc.registered_names(), NAME_REGISTRY_CAPACITY); + + let (bytes, outcome) = encode_one(&mut enc, &span_start("one_too_many", 0)); + assert!(outcome.name_registry_full); + let frames = decode_all(&bytes); + assert_eq!(frames.len(), 1, "no dictionary frame is emitted"); + assert_eq!(frames[0].name_id, UNKNOWN_NAME_ID); + assert_eq!(frames[0].kind, FrameKind::SpanStart); } #[test] - fn timestamp_ns_present_in_output() { - // varint(1_000_000) = [0xC0, 0x84, 0x3D] - let (buf, n) = encode(&make_span_start(), 0); - assert!(contains_bytes(&buf[..n], &[0xC0u8, 0x84, 0x3D])); + fn a_registered_name_still_resolves_after_the_registry_fills() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for i in 0..NAME_REGISTRY_CAPACITY { + enc.encode(&span_start(&format!("n{i}"), 0), &mut buf) + .unwrap(); + } + enc.encode(&span_start("overflow", 0), &mut buf).unwrap(); + let (bytes, outcome) = encode_one(&mut enc, &span_end("n0", 0)); + assert!(!outcome.name_registry_full); + assert_eq!(decode_all(&bytes)[0].name_id, 1); } + // ── Stream header (§6.3) ────────────────────────────────────────────────── + #[test] - fn buffer_too_small_returns_error() { - let mut buf = [0u8; 4]; - assert_eq!( - encode_trace_frame(&make_span_start(), 0, &mut buf), - Err(TracingEncodeError::BufferFull) - ); + fn trace_start_carries_the_timebase_frequency_and_mask() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; + let n = enc + .encode_trace_start(1_234, TimeBase::Cycles, 72_000_000, 0b10111, &mut buf) + .unwrap(); + let frames = decode_all(&buf[..n]); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].kind, FrameKind::TraceStart); + assert_eq!(frames[0].timestamp_ticks, 1_234, "absolute, not a delta"); + assert_eq!(frames[0].timebase, TimeBase::Cycles); + assert_eq!(frames[0].core_frequency_hz, 72_000_000); + assert_eq!(frames[0].source_mask, 0b10111); + assert_eq!(frames[0].sequence, 0, "the header is the first frame"); } #[test] - fn marker_value_preserved_after_encode_decode() { - let (buf, n) = encode(&make_marker_with_value(), 0); - assert!(n > 0); - // The value 42 as varint is just 0x2A; verify it appears in the frame - assert!(contains_bytes(&buf[..n], &[0x2A])); + fn trace_start_sets_the_base_for_the_following_delta() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode_trace_start(1_000, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) + .unwrap(); + let (bytes, _) = encode_one(&mut enc, &span_start("a", 1_500)); + let frames = decode_all(&bytes); + assert_eq!(frames[0].timestamp_ticks, 1_500, "dictionary absolute"); + assert_eq!(frames[1].timestamp_ticks, 0); } #[test] - fn span_start_wire_format_snapshot() { - let (buf, n) = encode(&make_span_start_with_deadline(), 1); - assert_debug_snapshot!(&buf[..n]); + fn a_nanosecond_timebase_is_the_default_and_costs_no_bytes() { + // proto3 omits zero-valued scalars, so NANOSECONDS and an unset core + // frequency are free on the header. + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; + let ns = enc + .encode_trace_start(0, TimeBase::Nanoseconds, 0, 0, &mut buf) + .unwrap(); + let mut enc = TraceEncoder::new(); + let cycles = enc + .encode_trace_start(0, TimeBase::Cycles, 72_000_000, 0, &mut buf) + .unwrap(); + assert!(cycles > ns); } - // ── SequenceEncoder tests ───────────────────────────────────────────────── + // ── Marker payloads ─────────────────────────────────────────────────────── #[test] - fn sequence_encoder_starts_at_zero() { - assert_eq!(SequenceEncoder::new().sequence(), 0); + fn marker_value_round_trips() { + let mut enc = warmed(&["ukf_predict"]); + let (bytes, _) = encode_one(&mut enc, &marker("ukf_predict", 0, Some(42))); + assert_eq!(decode_all(&bytes)[0].marker_value, Some(42)); } #[test] - fn sequence_encoder_advances_on_each_encode() { - let mut enc = SequenceEncoder::new(); - let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - enc.encode(&make_span_start(), &mut buf).unwrap(); - assert_eq!(enc.sequence(), 1); - enc.encode(&make_span_end(), &mut buf).unwrap(); - assert_eq!(enc.sequence(), 2); + fn a_bare_marker_carries_no_value() { + let mut enc = warmed(&["ukf_predict"]); + let (bytes, _) = encode_one(&mut enc, &marker("ukf_predict", 0, None)); + assert_eq!(decode_all(&bytes)[0].marker_value, None); } #[test] - fn sequence_encoder_injects_sequence_into_frame() { - let mut enc = SequenceEncoder::new(); - let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - let n = enc.encode(&make_span_start(), &mut buf).unwrap(); - let (decoded, _) = decode_trace_frame(&buf[..n]).unwrap(); - let TraceEvent::SpanStart { sequence, .. } = decoded else { - panic!("expected SpanStart"); - }; - assert_eq!(sequence, 0); - let n = enc.encode(&make_span_start(), &mut buf).unwrap(); - let (decoded, _) = decode_trace_frame(&buf[..n]).unwrap(); - let TraceEvent::SpanStart { sequence, .. } = decoded else { - panic!("expected SpanStart"); - }; - assert_eq!(sequence, 1); + fn a_marker_value_of_zero_survives_as_zero_not_as_absent() { + // proto3 would drop a plain zero field; `marker_value` is optional so the + // presence bit distinguishes them. Regression guard for f3ccb25. + let mut enc = warmed(&["m"]); + let (bytes, _) = encode_one(&mut enc, &marker("m", 0, Some(0))); + assert_eq!(decode_all(&bytes)[0].marker_value, Some(0)); + } + + // ── Per-occurrence frames carry no fixed field (REQ-T06) ────────────────── + + #[test] + fn events_carry_no_name_bytes_once_registered() { + let mut enc = warmed(&["main_task"]); + let (bytes, _) = encode_one(&mut enc, &span_start("main_task", 0)); + assert!( + !bytes.windows(9).any(|w| w == b"main_task"), + "the name must appear only in the dictionary frame" + ); } #[test] - fn sequence_encoder_wraps_at_max() { - let mut enc = SequenceEncoder { sequence: u32::MAX }; - let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - enc.encode(&make_span_start(), &mut buf).unwrap(); - assert_eq!(enc.sequence(), 0); + fn events_carry_neither_priority_nor_deadline() { + let mut enc = warmed(&["gyro_isr"]); + let (bytes, _) = encode_one(&mut enc, &span_start_with_deadline("gyro_isr", 0)); + let frame = decode_all(&bytes).remove(0); + assert_eq!(frame.priority, 0); + assert_eq!(frame.relative_deadline_ms, None); + assert_eq!(frame.source_type, None); } - // ── decode_trace_frame round-trip tests ─────────────────────────────────── + // ── Size budget (§18.1, AC 5) ───────────────────────────────────────────── - fn round_trip(msg: &TraceEvent, seq: u32) -> TraceEvent { - let (buf, n) = encode(msg, seq); - let (decoded, consumed) = decode_trace_frame(&buf[..n]).unwrap(); - assert_eq!(consumed, n); - decoded + /// Steady-state frame size for each event class at a representative delta. + fn steady_state_sizes(delta: u64) -> [usize; 4] { + let mut enc = warmed(&["main_task", "ukf_predict"]); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let mut t = 1_000_000_000u64; + let cases = [ + span_start("main_task", 0), + span_end("main_task", 0), + marker("ukf_predict", 0, Some(42)), + marker("ukf_predict", 0, None), + ]; + // Push the sequence into its two-varint-byte range, the common case. + for _ in 0..200 { + t += delta; + enc.encode(&span_end("main_task", t), &mut buf).unwrap(); + } + let mut sizes = [0usize; 4]; + for (i, case) in cases.iter().enumerate() { + t += delta; + let mut ev = case.clone(); + match &mut ev { + TraceEvent::SpanStart { timestamp_ns, .. } + | TraceEvent::SpanEnd { timestamp_ns, .. } + | TraceEvent::Marker { timestamp_ns, .. } => *timestamp_ns = t, + } + sizes[i] = enc.encode(&ev, &mut buf).unwrap().len; + } + sizes } #[test] - fn round_trip_span_start_preserves_fields() { - let decoded = round_trip(&make_span_start(), 7); - let TraceEvent::SpanStart { - name, - source_type, - timestamp_ns, - priority, - sequence, - relative_deadline_ms, - } = decoded - else { - panic!("expected SpanStart"); - }; - assert_eq!(name.as_str(), "led_task"); - assert_eq!(source_type, SourceType::Task); - assert_eq!(timestamp_ns, 1_000_000); - assert_eq!(priority, 2); - assert_eq!(sequence, 7); - assert_eq!(relative_deadline_ms, None); - } - - #[test] - fn round_trip_span_start_with_deadline_preserves_deadline() { - let decoded = round_trip(&make_span_start_with_deadline(), 0); - let TraceEvent::SpanStart { - relative_deadline_ms, - .. - } = decoded - else { - panic!("expected SpanStart"); - }; - assert_eq!(relative_deadline_ms, Some(0.5_f32)); + fn steady_state_frames_stay_within_the_size_budget() { + // §18.1 predicted 13 / 12 / 15 / 12 for SpanStart / SpanEnd / Marker with + // a value / bare Marker. At 1300-3800 events/s the mean spacing is + // 260-770 us and bursts are far tighter, so deltas up to 1 ms are the + // operating range; the implementation is at or under budget across it. + for delta in [1_000u64, 10_000, 100_000, 1_000_000] { + let sizes = steady_state_sizes(delta); + for (size, budget) in sizes.iter().zip([13usize, 12, 15, 12]) { + assert!( + *size <= budget, + "delta {delta} ns: frame of {size} B exceeds the {budget} B budget" + ); + } + } } #[test] - fn round_trip_span_end_preserves_fields() { - let decoded = round_trip(&make_span_end(), 3); - let TraceEvent::SpanEnd { name, sequence, .. } = decoded else { - panic!("expected SpanEnd"); - }; - assert_eq!(name.as_str(), "gyro_isr"); - assert_eq!(sequence, 3); + fn a_delta_past_the_operating_range_costs_one_more_byte_and_no_more() { + // A 10 ms delta — an idle gap, not a traced workload — pushes the varint + // to three bytes. Recorded rather than hidden: it is where §18.1's + // per-class figures stop holding, and the tail is bounded at one byte. + let wide = steady_state_sizes(10_000_000); + let operating = steady_state_sizes(1_000_000); + for (w, o) in wide.iter().zip(operating.iter()) { + assert_eq!(*w, *o + 1); + } } #[test] - fn round_trip_marker_preserves_fields() { - let decoded = round_trip(&make_marker(), 0); - let TraceEvent::Marker { - name, marker_value, .. - } = decoded - else { - panic!("expected Marker"); - }; - assert_eq!(name.as_str(), "ukf_predict"); - assert_eq!(marker_value, None); + fn frame_sizes_across_the_delta_range_snapshot() { + let table: Vec<(u64, [usize; 4])> = [1_000u64, 10_000, 100_000, 1_000_000, 10_000_000] + .into_iter() + .map(|d| (d, steady_state_sizes(d))) + .collect(); + assert_debug_snapshot!(table); + } + + #[test] + fn v2_more_than_halves_the_mean_event_size_against_v1() { + // v1 measured 33 / 24 / 25 / 27 B for the same four classes (§18.1), a + // mean of 27.25. The saving is in the mean, which is what the bandwidth + // budget is spent from: per class it ranges from a 1.8x cut on a bare + // SpanEnd to a 2.75x cut on a SpanStart, which no longer repeats the + // name, the priority and the deadline on every occurrence. + let sizes = steady_state_sizes(100_000); + let v2_mean = sizes.iter().sum::() as f64 / 4.0; + let v1_mean = (33.0 + 24.0 + 25.0 + 27.0) / 4.0; + assert!( + v2_mean * 2.0 < v1_mean, + "v2 mean {v2_mean} B is not less than half of v1 mean {v1_mean} B" + ); } #[test] - fn round_trip_marker_with_value_preserves_value() { - let decoded = round_trip(&make_marker_with_value(), 0); - let TraceEvent::Marker { marker_value, .. } = decoded else { - panic!("expected Marker"); + fn span_start_wire_format_snapshot() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let n = enc + .encode(&span_start_with_deadline("gyro_isr", 1_000_000), &mut buf) + .unwrap() + .len; + assert_debug_snapshot!(&buf[..n]); + } + + // ── Framing ─────────────────────────────────────────────────────────────── + + #[test] + fn each_frame_declares_its_own_length() { + let mut enc = TraceEncoder::new(); + let (bytes, _) = encode_one(&mut enc, &span_start_with_deadline("gyro_isr", 1_000)); + // decode_all asserts the frames tile the buffer exactly. + assert_eq!(decode_all(&bytes).len(), 2); + } + + #[test] + fn a_buffer_too_small_for_the_event_returns_buffer_full() { + let mut enc = warmed(&["a"]); + let mut buf = [0u8; 2]; + assert_eq!( + enc.encode(&span_end("a", 0), &mut buf), + Err(TracingEncodeError::BufferFull) + ); + } + + #[test] + fn a_buffer_holding_only_the_dictionary_frame_returns_buffer_full() { + let mut enc = TraceEncoder::new(); + let mut probe = [0u8; MAX_TRACE_BURST_SIZE]; + let dictionary_len = { + let mut e = TraceEncoder::new(); + let n = e + .encode(&span_start("main_task", 0), &mut probe) + .unwrap() + .len; + n - 1 // everything but the trailing event frame }; - assert_eq!(marker_value, Some(42_u32)); + let mut buf = vec![0u8; dictionary_len]; + assert_eq!( + enc.encode(&span_start("main_task", 0), &mut buf), + Err(TracingEncodeError::BufferFull) + ); } + #[test] + fn a_maximum_length_name_still_fits_one_frame() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let longest = "x".repeat(32); + let outcome = enc + .encode(&span_start(&longest, u64::MAX / 2), &mut buf) + .unwrap(); + let frames = decode_all(&buf[..outcome.len]); + assert_eq!(frames[0].name.as_str(), longest); + assert!(outcome.len <= MAX_TRACE_BURST_SIZE); + } + + // ── Decoding failures ───────────────────────────────────────────────────── + #[test] fn decode_truncated_frame_returns_error() { - let (buf, n) = encode(&make_span_start(), 0); + let mut enc = warmed(&["a"]); + let (bytes, _) = encode_one(&mut enc, &span_end("a", 0)); assert_eq!( - decode_trace_frame(&buf[..n - 1]), + decode_trace_frame(&bytes[..bytes.len() - 1]), Err(TracingDecodeError::Truncated) ); } @@ -604,15 +1075,8 @@ mod tests { #[test] fn decode_varint_overflow_returns_malformed_varint() { - // 10 bytes each with the continuation bit set — shift reaches 70, exceeding - // the 64-bit limit and hitting the `shift >= 64` guard in decode_varint. let overlong: Vec = (0..10).map(|_| 0xFF).collect(); - assert_eq!( - decode_varint(&overlong), - None, - "varint with shift >= 64 must return None" - ); - // Confirm this surfaces as MalformedVarint when used through the public API. + assert_eq!(decode_varint(&overlong), None); let frame: Vec = overlong.into_iter().chain(std::iter::once(0x00)).collect(); assert_eq!( decode_trace_frame(&frame), @@ -621,9 +1085,82 @@ mod tests { } #[test] - fn decode_returns_correct_consumed_byte_count() { - let (buf, n) = encode(&make_marker_with_value(), 5); - let (_, consumed) = decode_trace_frame(&buf[..n]).unwrap(); - assert_eq!(consumed, n); + fn decode_rejects_an_unknown_event_type() { + // event_type is field 3, varint: tag 0x18, value 9 — past TRACE_START. + let payload = [0x18u8, 0x09]; + let frame = [&[payload.len() as u8][..], &payload[..]].concat(); + assert_eq!( + decode_trace_frame(&frame), + Err(TracingDecodeError::DecodeError) + ); + } + + #[test] + fn decode_returns_the_consumed_byte_count() { + let mut enc = warmed(&["a"]); + let (bytes, _) = encode_one(&mut enc, &marker("a", 0, Some(5))); + let (_, consumed) = decode_trace_frame(&bytes).unwrap(); + assert_eq!(consumed, bytes.len()); + } + + // ── A whole stream ──────────────────────────────────────────────────────── + + #[test] + fn a_full_stream_round_trips_through_a_host_style_decoder() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let mut wire = Vec::new(); + + let n = enc + .encode_trace_start(1_000, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) + .unwrap(); + wire.extend_from_slice(&buf[..n]); + + let events = [ + span_start_with_deadline("gyro_isr", 1_100), + marker("ukf_predict", 1_150, Some(7)), + span_end("gyro_isr", 1_400), + span_start_with_deadline("gyro_isr", 2_100), + span_end("gyro_isr", 2_380), + ]; + for ev in &events { + let outcome = enc.encode(ev, &mut buf).unwrap(); + wire.extend_from_slice(&buf[..outcome.len]); + } + + // Host side: resolve ids through the dictionary, accumulate the deltas. + let mut dictionary: std::collections::HashMap = + std::collections::HashMap::new(); + let mut clock = 0u64; + let mut expect_sequence = 0u32; + let mut resolved: Vec<(std::string::String, u64, FrameKind)> = Vec::new(); + + for frame in decode_all(&wire) { + assert_eq!(frame.sequence, expect_sequence, "no gaps in a clean stream"); + expect_sequence = (expect_sequence + 1) % SEQUENCE_MODULUS; + clock = match frame.kind { + FrameKind::NameRegistered | FrameKind::TraceStart => frame.timestamp_ticks, + _ => clock + frame.timestamp_ticks, + }; + match frame.kind { + FrameKind::NameRegistered => { + dictionary.insert(frame.name_id, frame.name.as_str().into()); + } + FrameKind::TraceStart => assert_eq!(frame.source_mask, 0x1F), + kind => resolved.push((dictionary[&frame.name_id].clone(), clock, kind)), + } + } + + assert_eq!( + resolved, + vec![ + ("gyro_isr".into(), 1_100, FrameKind::SpanStart), + ("ukf_predict".into(), 1_150, FrameKind::Marker), + ("gyro_isr".into(), 1_400, FrameKind::SpanEnd), + ("gyro_isr".into(), 2_100, FrameKind::SpanStart), + ("gyro_isr".into(), 2_380, FrameKind::SpanEnd), + ] + ); + assert_eq!(dictionary.len(), 2, "each name registered exactly once"); } } diff --git a/src/lib.rs b/src/lib.rs index 7395fd2..89e3a2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ mod proto { } #[cfg(feature = "enabled")] -pub use encode::SequenceEncoder; +pub use encode::{FrameKind, RawTraceFrame, TimeBase, TraceEncoder}; #[cfg(feature = "enabled")] pub use sink::TraceTransport; pub use sink::{NoopSink, TraceSink, TracingError}; diff --git a/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap b/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap new file mode 100644 index 0000000..7914045 --- /dev/null +++ b/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap @@ -0,0 +1,51 @@ +--- +source: src/encode.rs +expression: table +--- +[ + ( + 1000, + [ + 11, + 11, + 13, + 11, + ], + ), + ( + 10000, + [ + 11, + 11, + 13, + 11, + ], + ), + ( + 100000, + [ + 12, + 12, + 14, + 12, + ], + ), + ( + 1000000, + [ + 12, + 12, + 14, + 12, + ], + ), + ( + 10000000, + [ + 13, + 13, + 15, + 13, + ], + ), +] diff --git a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap index df5d8bc..d51a041 100644 --- a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap +++ b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap @@ -1,5 +1,5 @@ --- -source: execution-trace/src/encode.rs +source: src/encode.rs expression: "&buf[..n]" --- [ @@ -8,27 +8,34 @@ expression: "&buf[..n]" 192, 132, 61, - 18, + 16, + 1, + 24, + 4, + 50, 8, - 108, - 101, - 100, + 103, + 121, + 114, + 111, 95, - 116, - 97, + 105, 115, - 107, - 24, - 2, - 32, - 1, - 40, + 114, + 56, 1, - 48, - 2, - 61, + 64, + 8, + 77, 0, 0, 0, 63, + 6, + 16, + 1, + 24, + 1, + 32, + 1, ] From 3a71ee3559ed2764e664dcd2767fd8a17ea6ba4f Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 21:45:56 +0200 Subject: [PATCH 03/16] Re-emit the dictionary periodically so a mid-run attach can decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first hardware run of the v2 format decoded nothing: every event logged "name id N was never registered", all spans collapsed onto one lane, and the diagram failed on "row 1012: end_us must be > start_us". A NAME_REGISTERED frame is sent the first time the encoder sees a name, which for every name is within the first control cycles after boot. The device had been up for 55 s when the host attached, so the host received no dictionary at all and could resolve nothing. §5.5 anticipated this and gave the job to a "decoder-visible reset", but a host attach is not a device reset and the device cannot observe one — RTT is one-way. The re-emission has to be unconditional and periodic, so the encoder now exposes `encode_dictionary_entry` for a caller to walk. That means keeping each entry's attributes, not just its name, since the re-emitted frame has to reproduce the original. A name first seen on a SpanEnd — its SpanStart dropped upstream under load — is now backfilled when a SpanStart for it arrives, so a refresh cannot re-send a bare entry and lose that name's priority for the rest of the run. Three decoder changes follow, each tested: A repeated header is no longer a reset. Treating any TRACE_START on a running stream as a reboot would clear the dictionary on every refresh, undoing the thing the refresh exists to do. A reboot is identified by the device clock going backwards, which a refresh never does. An unresolvable event is dropped rather than renamed. Attributing distinct unknown ids to one placeholder is what produced the malformed pairs: two different spans interleaved on one lane, so a SpanEnd closed a SpanStart belonging to another name. `resolve` returns None and the event is discarded and counted in `unresolved_events`. The warning fires once per id. At 1354 events/s the per-event warning produced thousands of identical lines a second, which is how this presented in the first place. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- python/src/execution_trace/decode.py | 68 +++++++--- python/tests/test_decode.py | 87 +++++++++++-- src/encode.rs | 184 ++++++++++++++++++++++++++- 3 files changed, 304 insertions(+), 35 deletions(-) diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index cadabda..fe79511 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -144,6 +144,11 @@ def __init__(self, label: str = "Trace event") -> None: self.timebase: int = tracing_pb2.NANOSECONDS self.core_frequency_hz: int = 0 self.source_mask: Optional[int] = None + # Ids already reported as unresolvable. At 1300+ events/s an unregistered + # id would otherwise log thousands of identical lines per second. + self._warned_ids: set[int] = set() + #: Events dropped because their name id could not be resolved. + self.unresolved_events: int = 0 def reset(self) -> None: """Discard all per-stream state after a device reset. @@ -153,6 +158,8 @@ def reset(self) -> None: """ self.tracker = SequenceTracker(self.label, modulus=SEQUENCE_MODULUS) self.names.clear() + self._warned_ids.clear() + self.unresolved_events = 0 self.clock_ticks = 0 self.timebase = tracing_pb2.NANOSECONDS self.core_frequency_hz = 0 @@ -190,27 +197,33 @@ def advance(self, frame: tracing_pb2.TraceFrame) -> float: self.clock_ticks += frame.timestamp_ticks return self.ticks_to_us(self.clock_ticks) - def resolve(self, name_id: int) -> NameEntry: + def resolve(self, name_id: int) -> Optional[NameEntry]: """Look up a dictionary id. Args: name_id: The id carried by the frame. Returns: - The registered entry, or a placeholder entry when the id is the - reserved "unknown" value or has not been registered — which happens - when the device's registry was full, or when the host attached after - the entry was sent. + The registered entry, or ``None`` when the id cannot be resolved — + either the reserved "unknown" id, meaning the device's registry was + full, or an id whose ``NAME_REGISTERED`` frame the host never saw + because it attached after the entry was sent. + + Callers must **drop** an unresolvable event rather than attribute it + to a placeholder name: distinct ids would otherwise share one lane, + and their spans would interleave into pairs that never existed. """ entry = self.names.get(name_id) if entry is not None: return entry - if name_id != UNKNOWN_NAME_ID: + if name_id != UNKNOWN_NAME_ID and name_id not in self._warned_ids: + self._warned_ids.add(name_id) logger.warning( - "Tracing: name id %d was never registered — the host may have " - "attached mid-stream", name_id, + "Tracing: name id %d not resolvable yet — dropping its events " + "until the device's next dictionary refresh", name_id, ) - return NameEntry(name=UNKNOWN_NAME) + self.unresolved_events += 1 + return None class TraceEventBuffer: @@ -323,11 +336,17 @@ def decode_tracing_stream( logger.warning("Failed to decode TraceFrame: %s", exc) continue - # A TRACE_START after the stream is already running means the device - # rebooted: its dictionary and clock both restarted, so nothing carried - # over is still valid. - if frame.event_type == tracing_pb2.TRACE_START and state.source_mask is not None: - logger.warning("Tracing: TRACE_START mid-stream — device reset") + # The device re-emits the header periodically so that a host attaching + # mid-run learns the timebase and the mask. That is not a reset — only a + # header whose absolute timestamp has gone *backwards* is, because the + # device clock restarts at zero on reboot and nothing carried over from + # before it is still valid. + if ( + frame.event_type == tracing_pb2.TRACE_START + and state.source_mask is not None + and frame.timestamp_ticks < state.clock_ticks + ): + logger.warning("Tracing: device clock restarted — device reset") buf.clear() event_buffer.flush_pending() state.reset() @@ -344,16 +363,19 @@ def decode_tracing_stream( if frame.event_type == tracing_pb2.TRACE_START: state.timebase = frame.timebase state.core_frequency_hz = frame.core_frequency_hz + first_header = state.source_mask is None state.source_mask = frame.source_mask # The clock was set from the raw ticks before the timebase was known; # it is a tick count either way, so only the conversion changes. - logger.info( - "Tracing: stream start, timebase=%s core=%d Hz source_mask=0x%02X", - tracing_pb2.TimeBase.Name(frame.timebase), - frame.core_frequency_hz, - frame.source_mask, - ) + if first_header: + logger.info( + "Tracing: stream start, timebase=%s core=%d Hz source_mask=0x%02X", + tracing_pb2.TimeBase.Name(frame.timebase), + frame.core_frequency_hz, + frame.source_mask, + ) elif frame.event_type == tracing_pb2.NAME_REGISTERED: + state._warned_ids.discard(frame.name_id) state.names[frame.name_id] = NameEntry( name=frame.name, source_type=frame.source_type, @@ -365,7 +387,11 @@ def decode_tracing_stream( ), ) else: - event_buffer.push(frame, state.resolve(frame.name_id), timestamp_us) + entry = state.resolve(frame.name_id) + # An unresolvable id is dropped, not bucketed under a placeholder: + # see TraceStreamState.resolve. + if entry is not None: + event_buffer.push(frame, entry, timestamp_us) def write_tracing_csv( diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 78c1e29..51db094 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -191,8 +191,18 @@ def test_unregistered_id_resolves_to_a_placeholder_and_warns(self, caplog): state = TraceStreamState() with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): entry = state.resolve(7) - assert entry.name == UNKNOWN_NAME - assert "never registered" in caplog.text + assert entry is None + assert "not resolvable yet" in caplog.text + assert state.unresolved_events == 1 + + def test_an_unresolvable_id_warns_only_once(self, caplog): + # At 1300+ events/s a per-event warning buries the log; the run that + # found this produced thousands of identical lines a second. + state = TraceStreamState() + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + for _ in range(50): + state.resolve(7) + assert caplog.text.count("not resolvable yet") == 1 def test_the_reserved_unknown_id_resolves_silently(self, caplog): # Id 0 means the device's registry was full. That is reported by a @@ -200,7 +210,7 @@ def test_the_reserved_unknown_id_resolves_silently(self, caplog): state = TraceStreamState() with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): entry = state.resolve(0) - assert entry.name == UNKNOWN_NAME + assert entry is None assert caplog.text == "" def test_reset_clears_the_dictionary(self): @@ -275,14 +285,16 @@ def test_attributes_are_carried_by_the_dictionary_not_the_event(self, builder): assert r.priority == 8 assert r.deadline_us == pytest.approx(1_000.0) - def test_an_unregistered_id_still_yields_a_decodable_marker(self, builder, caplog): - # What the firmware emits when its name registry is full: the event goes - # out with the reserved id rather than being dropped or mis-decoded. + def test_an_event_with_the_reserved_unknown_id_is_dropped(self, builder, caplog): + # What the firmware emits when its name registry is full. The event is + # real, but nothing can say what it was, and attributing it to a shared + # placeholder would interleave unrelated spans into one lane. The + # firmware's TracingNameRegistryFull counter is what reports the loss. builder.event(tracing_pb2.MARKER, "", 1_000, marker_value=5, name_id=0) with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): - _, event_buffer = _decode(builder.bytes()) - assert event_buffer.markers[0].name == UNKNOWN_NAME - assert event_buffer.markers[0].value == 5 + state, event_buffer = _decode(builder.bytes()) + assert event_buffer.markers == [], "an unattributable event is dropped" + assert state.unresolved_events == 1 def test_partial_frame_is_left_in_the_buffer(self, builder): builder.span("t", 0, 1_000) @@ -312,8 +324,9 @@ def test_sequence_wraps_at_the_modulus_without_a_false_reset(self, caplog): event_buffer = TraceEventBuffer() # Walk the tracker up to the last sequence before the wrap, then feed the # wrapped frame. A u32 tracker would read 0 after 16383 as a huge jump. + state.names[1] = NameEntry(name="tick") state.tracker.observe(SEQUENCE_MODULUS - 1) - stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=0, sequence=0)) + stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=1, sequence=0)) with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): decode_tracing_stream(stream, state, event_buffer) assert caplog.text == "" @@ -349,6 +362,60 @@ def test_a_reset_from_high_in_the_range_is_not_visible_in_the_sequence(self, cap assert "reset" not in caplog.text assert state.names == {1: NameEntry(name="stale")} + def test_a_dictionary_refresh_resolves_events_seen_before_it(self, builder): + # What a host attaching mid-run sees: events for ids it never saw + # registered, then the device's periodic re-emission of the dictionary. + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + + # Events arrive with an id the host has no entry for. + orphan = bytearray() + orphan += make_frame(tracing_pb2.SPAN_START, name_id=1, sequence=0) + orphan += make_frame(tracing_pb2.SPAN_END, timestamp_ticks=500, name_id=1, sequence=1) + decode_tracing_stream(orphan, state, event_buffer) + assert event_buffer.records == [], "unresolvable events are dropped" + assert state.unresolved_events == 2 + + # The refresh arrives: the entry is now known, and later events resolve. + refresh = bytearray() + refresh += make_frame( + tracing_pb2.NAME_REGISTERED, + timestamp_ticks=10_000, + name_id=1, + sequence=2, + name="main_task", + source_type=tracing_pb2.TASK, + priority=4, + ) + refresh += make_frame(tracing_pb2.SPAN_START, name_id=1, sequence=3) + refresh += make_frame(tracing_pb2.SPAN_END, timestamp_ticks=500, name_id=1, sequence=4) + decode_tracing_stream(refresh, state, event_buffer) + + assert [r.name for r in event_buffer.records] == ["main_task"] + assert event_buffer.records[0].priority == 4 + + def test_a_repeated_header_is_not_a_reset(self, caplog): + # The device re-emits the header periodically so a late host learns the + # timebase and mask. Treating that as a reset would clear the dictionary + # every refresh and undo the very thing the refresh exists to fix. + builder = StreamBuilder().start(timestamp_ticks=1_000) + builder.span("t", 2_000, 3_000) + state, event_buffer = _decode(builder.bytes()) + assert len(state.names) == 1 + + later = bytearray( + make_frame( + tracing_pb2.TRACE_START, + timestamp_ticks=9_000, + sequence=state.tracker._last + 1, + source_mask=0x1F, + ) + ) + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + decode_tracing_stream(later, state, event_buffer) + assert "reset" not in caplog.text + assert len(state.names) == 1, "the dictionary must survive a refresh" + def test_a_second_header_is_treated_as_a_device_reset(self, caplog): builder = StreamBuilder().start() builder.span("t", 0, 1_000) diff --git a/src/encode.rs b/src/encode.rs index 648ba86..dfdd773 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -140,11 +140,25 @@ impl RawTraceFrame { /// /// [`TraceSink`]: crate::TraceSink pub struct TraceEncoder { - names: heapless::Vec, NAME_REGISTRY_CAPACITY>, + names: heapless::Vec, last_ticks: u64, sequence: u32, } +/// One dictionary entry: a name and everything fixed by it. +/// +/// The attributes are kept, not just the name, so that the dictionary can be +/// **re-emitted** in full ([`TraceEncoder::encode_dictionary_entry`]). A host +/// that attaches after startup never saw the original frames, and without a +/// re-emission every id it receives resolves to nothing. +#[derive(Debug, Clone, PartialEq)] +struct DictionaryEntry { + name: heapless::String<32>, + source_type: Option, + priority: u32, + relative_deadline_ms: Option, +} + impl TraceEncoder { /// Creates an encoder with an empty dictionary and the counters at zero. pub const fn new() -> Self { @@ -228,7 +242,10 @@ impl TraceEncoder { let mut name_registry_full = false; let name_id = match self.lookup(name) { - Some(id) => id, + Some(id) => { + self.backfill(id, event); + id + } None => match self.register(event, timestamp_ns, out) { Ok((id, n)) => { written += n; @@ -278,11 +295,83 @@ impl TraceEncoder { let needle = name.as_bytes(); self.names .iter() - .position(|candidate| candidate.len() == needle.len() && candidate.as_bytes() == needle) + .position(|candidate| { + candidate.name.len() == needle.len() && candidate.name.as_bytes() == needle + }) // Ids are one-based: zero is reserved for "unknown". .and_then(|index| u32::try_from(index + 1).ok()) } + /// Fills in the attributes of an entry that was first registered without them. + /// + /// A name is normally first seen on its `SpanStart`, which carries the + /// attributes. It is seen first on a `SpanEnd` only when the `SpanStart` was + /// dropped upstream of the encoder — at the producer queue, under load — and + /// without this the priority and deadline for that name would stay lost for + /// the rest of the run. + fn backfill(&mut self, name_id: u32, event: &TraceEvent) { + let TraceEvent::SpanStart { + source_type, + priority, + relative_deadline_ms, + .. + } = event + else { + return; + }; + let Some(index) = usize::try_from(name_id) + .ok() + .and_then(|id| id.checked_sub(1)) + else { + return; + }; + let Some(entry) = self.names.get_mut(index) else { + return; + }; + if entry.source_type.is_none() { + entry.source_type = Some(*source_type); + entry.priority = *priority; + entry.relative_deadline_ms = *relative_deadline_ms; + } + } + + /// Re-encodes the dictionary entry at `index` as a `NAME_REGISTERED` frame. + /// + /// Each entry is otherwise sent once, when its name is first seen, so a host + /// that attaches later resolves every id to nothing. Re-emitting the whole + /// dictionary periodically is what makes a mid-run attach decodable: the + /// frames are identical to the originals apart from their timestamp and + /// sequence, and a host that already holds an entry overwrites it with the + /// same value. + /// + /// Returns `None` once `index` is past the end, so a caller can walk from + /// zero until it stops. + /// + /// # Errors + /// Returns [`TracingEncodeError::BufferFull`] if `out` is too small. + pub fn encode_dictionary_entry( + &mut self, + index: usize, + timestamp_ticks: u64, + out: &mut [u8], + ) -> Option> { + let entry = self.names.get(index)?.clone(); + let name_id = u32::try_from(index + 1).ok()?; + let sequence = self.next_sequence(); + let mut frame = RawTraceFrame::new( + FrameKind::NameRegistered, + timestamp_ticks, + sequence, + name_id, + ); + frame.name = entry.name; + frame.source_type = entry.source_type; + frame.priority = entry.priority; + frame.relative_deadline_ms = entry.relative_deadline_ms; + self.last_ticks = timestamp_ticks; + Some(encode_trace_frame(&frame, out)) + } + /// Assigns the next id and writes the dictionary frame, whose timestamp is /// absolute so a host attaching mid-run can re-establish the time origin. fn register( @@ -307,7 +396,12 @@ impl TraceEncoder { }; self.names - .push(name.clone()) + .push(DictionaryEntry { + name: name.clone(), + source_type, + priority, + relative_deadline_ms, + }) .map_err(|_| RegisterError::Full)?; let id = u32::try_from(self.names.len()).map_err(|_| RegisterError::Full)?; @@ -685,6 +779,88 @@ mod tests { assert_eq!(decode_all(&bytes)[0].name_id, 2); } + // ── Dictionary refresh ──────────────────────────────────────────────────── + + #[test] + fn the_dictionary_can_be_re_emitted_in_full() { + // What makes a mid-run host attach decodable: the device re-sends every + // entry, so ids it never saw registered become resolvable. + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode(&span_start_with_deadline("gyro_isr", 0), &mut buf) + .unwrap(); + enc.encode(&marker("ukf_predict", 0, None), &mut buf) + .unwrap(); + + let mut wire = Vec::new(); + let mut index = 0; + while let Some(result) = enc.encode_dictionary_entry(index, 5_000, &mut buf) { + wire.extend_from_slice(&buf[..result.unwrap()]); + index += 1; + } + assert_eq!(index, 2, "one frame per entry, then it stops"); + + let frames = decode_all(&wire); + assert!(frames.iter().all(|f| f.kind == FrameKind::NameRegistered)); + assert_eq!(frames[0].name_id, 1); + assert_eq!(frames[0].name.as_str(), "gyro_isr"); + assert_eq!(frames[0].source_type, Some(SourceType::Isr)); + assert_eq!(frames[0].priority, 8); + assert_eq!(frames[0].relative_deadline_ms, Some(0.5)); + assert_eq!(frames[1].name_id, 2); + assert_eq!(frames[1].name.as_str(), "ukf_predict"); + } + + #[test] + fn a_refresh_keeps_the_ids_it_originally_assigned() { + let mut enc = warmed(&["a", "b", "c"]); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + for index in 0..3 { + let n = enc + .encode_dictionary_entry(index, 0, &mut buf) + .unwrap() + .unwrap(); + let frame = decode_all(&buf[..n]).remove(0); + assert_eq!(frame.name_id, u32::try_from(index + 1).unwrap()); + } + // Following events still resolve to the same ids. + let (bytes, _) = encode_one(&mut enc, &span_end("b", 0)); + assert_eq!(decode_all(&bytes)[0].name_id, 2); + } + + #[test] + fn a_refresh_is_sequenced_so_it_cannot_look_like_a_gap() { + let mut enc = warmed(&["a"]); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let before = enc.sequence(); + enc.encode_dictionary_entry(0, 0, &mut buf) + .unwrap() + .unwrap(); + assert_eq!(enc.sequence(), before + 1); + } + + #[test] + fn a_name_first_seen_on_a_span_end_gains_its_attributes_later() { + // The SpanStart was dropped upstream of the encoder, so the entry is + // registered bare. When a later SpanStart for that name arrives the + // attributes must be filled in, or the refresh re-sends a bare entry + // and the priority is lost for the rest of the run. + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode(&span_end("gyro_isr", 0), &mut buf).unwrap(); + enc.encode(&span_start_with_deadline("gyro_isr", 0), &mut buf) + .unwrap(); + + let n = enc + .encode_dictionary_entry(0, 0, &mut buf) + .unwrap() + .unwrap(); + let frame = decode_all(&buf[..n]).remove(0); + assert_eq!(frame.source_type, Some(SourceType::Isr)); + assert_eq!(frame.priority, 8); + assert_eq!(frame.relative_deadline_ms, Some(0.5)); + } + // ── Delta timestamps (§5.6) ─────────────────────────────────────────────── #[test] From a772b6026c8d9a7bfa89279244917dbd65de0729 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 22:07:53 +0200 Subject: [PATCH 04/16] Sign the timestamp delta; stop discarding the buffer on a reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refresh build decoded a readable diagram but the run still showed the host clock drifting ahead of the device, a false "device reset" every couple of seconds, and bursts of "Failed to decode TraceFrame". `record_span_start` reads the clock before the event reaches the producer queue, so a priority-8 ISR preempting a task between those points puts a later timestamp ahead of an earlier one in drain order. §5.6 assumed record order was monotonic and specified an unsigned difference, so an inversion was clamped to zero while the delta base still moved backwards — and every inversion added permanent forward drift to the reconstructed clock: device 100 -> host 100 device 90 -> host 100 (delta clamped to 0) device 110 -> host 120 (delta 110-90 = 20) At ~416 ISR events/s that is milliseconds per second, so within seconds the host clock passed the device's and the periodic header — carrying the true uptime — arrived looking like the clock had gone backwards. `timestamp_ticks` is now sint64. Zigzag costs nothing at these magnitudes: only the 10 µs row of the size table moved, by one byte. Measured on a stream with an inversion on every control cycle over 10 s: 0 ns drift and 0 false resets, against four before. `decode_tracing_stream` no longer clears the byte buffer on a reset. The stream is length-prefixed with no sync marker, so discarding bytes mid-frame misaligns everything after it unrecoverably — which is what produced the decode failures at the same millisecond as each false reset. The frames after a reset are intact; only the decoder state is stale. Two corrections follow from the same reading. A reset no longer discards the frame that revealed it: that frame is the first of the new run, usually the reboot's own header carrying the timebase and mask. And a reboot is now separated from ordering jitter by a margin rather than a strict comparison — a reboot drops the clock by the whole uptime, jitter is microseconds, and without the margin one inverted microsecond blinds the trace for a full refresh interval. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- examples/simulate.rs | 4 +- proto/tracing.proto | 12 ++- .../src/execution_trace/_proto/tracing_pb2.py | 2 +- python/src/execution_trace/decode.py | 61 ++++++++++----- python/tests/test_decode.py | 69 ++++++++++++++--- src/encode.rs | 76 +++++++++++++++---- ...sizes_across_the_delta_range_snapshot.snap | 8 +- ...ests__span_start_wire_format_snapshot.snap | 6 +- 8 files changed, 182 insertions(+), 56 deletions(-) diff --git a/examples/simulate.rs b/examples/simulate.rs index 760c19d..95eb726 100644 --- a/examples/simulate.rs +++ b/examples/simulate.rs @@ -171,7 +171,9 @@ fn main() -> std::io::Result<()> { println!("{}", "-".repeat(72)); let mut names: HashMap)> = HashMap::new(); - let mut clock_ns = 0u64; + // Signed: a frame's delta is negative when an event was recorded before the + // one encoded ahead of it. + let mut clock_ns = 0i64; let mut pos = 0; while pos < bytes.len() { match decode_trace_frame(&bytes[pos..]) { diff --git a/proto/tracing.proto b/proto/tracing.proto index 02e0d9b..10d2039 100644 --- a/proto/tracing.proto +++ b/proto/tracing.proto @@ -17,9 +17,15 @@ message TraceFrame { // by the TRACE_START frame. Absolute, not a delta, on NAME_REGISTERED and // TRACE_START frames, which re-establish the time origin (§5.6). // - // Declared 64-bit so that an idle stream cannot overflow it; a varint costs - // the same for the small values this normally carries. - uint64 timestamp_ticks = 1; + // **Signed**, because the delta can be negative: an event is timestamped when + // it is recorded, before it reaches the producer queue, so a high-priority ISR + // preempting a task between those two points puts a later timestamp ahead of + // an earlier one in drain order. Clamping such an inversion to zero would make + // the host's reconstructed clock drift permanently ahead of the device's + // (§19.10). Zigzag encoding costs no extra byte at the magnitudes seen here. + // + // Declared 64-bit so that an idle stream cannot overflow it. + sint64 timestamp_ticks = 1; // Dictionary key assigned by NAME_REGISTERED. Id 0 is reserved for "unknown", // emitted when the registry is full (§5.5, REQ-T11). diff --git a/python/src/execution_trace/_proto/tracing_pb2.py b/python/src/execution_trace/_proto/tracing_pb2.py index 2997306..9cbc2e2 100644 --- a/python/src/execution_trace/_proto/tracing_pb2.py +++ b/python/src/execution_trace/_proto/tracing_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rtracing.proto\x12\x07tracing\"\x86\x03\n\nTraceFrame\x12\x17\n\x0ftimestamp_ticks\x18\x01 \x01(\x04\x12\x0f\n\x07name_id\x18\x02 \x01(\r\x12+\n\nevent_type\x18\x03 \x01(\x0e\x32\x17.tracing.TraceEventType\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x19\n\x0cmarker_value\x18\x05 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x32\n\x0bsource_type\x18\x07 \x01(\x0e\x32\x1d.tracing.TraceEventSourceType\x12\x10\n\x08priority\x18\x08 \x01(\r\x12!\n\x14relative_deadline_ms\x18\t \x01(\x02H\x01\x88\x01\x01\x12#\n\x08timebase\x18\n \x01(\x0e\x32\x11.tracing.TimeBase\x12\x19\n\x11\x63ore_frequency_hz\x18\x0b \x01(\r\x12\x13\n\x0bsource_mask\x18\x0c \x01(\rB\x0f\n\r_marker_valueB\x17\n\x15_relative_deadline_ms*R\n\x14TraceEventSourceType\x12\'\n#TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03ISR\x10\x01\x12\x08\n\x04TASK\x10\x02*\x82\x01\n\x0eTraceEventType\x12 \n\x1cTRACE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSPAN_START\x10\x01\x12\x0c\n\x08SPAN_END\x10\x02\x12\n\n\x06MARKER\x10\x03\x12\x13\n\x0fNAME_REGISTERED\x10\x04\x12\x0f\n\x0bTRACE_START\x10\x05*\'\n\x08TimeBase\x12\x0f\n\x0bNANOSECONDS\x10\x00\x12\n\n\x06\x43YCLES\x10\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rtracing.proto\x12\x07tracing\"\x86\x03\n\nTraceFrame\x12\x17\n\x0ftimestamp_ticks\x18\x01 \x01(\x12\x12\x0f\n\x07name_id\x18\x02 \x01(\r\x12+\n\nevent_type\x18\x03 \x01(\x0e\x32\x17.tracing.TraceEventType\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x19\n\x0cmarker_value\x18\x05 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x32\n\x0bsource_type\x18\x07 \x01(\x0e\x32\x1d.tracing.TraceEventSourceType\x12\x10\n\x08priority\x18\x08 \x01(\r\x12!\n\x14relative_deadline_ms\x18\t \x01(\x02H\x01\x88\x01\x01\x12#\n\x08timebase\x18\n \x01(\x0e\x32\x11.tracing.TimeBase\x12\x19\n\x11\x63ore_frequency_hz\x18\x0b \x01(\r\x12\x13\n\x0bsource_mask\x18\x0c \x01(\rB\x0f\n\r_marker_valueB\x17\n\x15_relative_deadline_ms*R\n\x14TraceEventSourceType\x12\'\n#TRACE_EVENT_SOURCE_TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03ISR\x10\x01\x12\x08\n\x04TASK\x10\x02*\x82\x01\n\x0eTraceEventType\x12 \n\x1cTRACE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSPAN_START\x10\x01\x12\x0c\n\x08SPAN_END\x10\x02\x12\n\n\x06MARKER\x10\x03\x12\x13\n\x0fNAME_REGISTERED\x10\x04\x12\x0f\n\x0bTRACE_START\x10\x05*\'\n\x08TimeBase\x12\x0f\n\x0bNANOSECONDS\x10\x00\x12\n\n\x06\x43YCLES\x10\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index fe79511..196c3ff 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -49,6 +49,16 @@ UNKNOWN_NAME: str = "" +# How far the device clock must appear to jump *backwards* on a TRACE_START +# before it is read as a reboot rather than as ordering jitter. +# +# A reboot restarts the clock at zero, so it shows up as the whole uptime — many +# seconds. Jitter is microseconds: the header's timestamp is read after the +# producer queue is drained, but an event recorded just before that read can +# still be encoded after it. Treating a microsecond inversion as a reboot would +# throw the dictionary away and blind the trace for a whole refresh interval. +RESET_BACKWARD_MARGIN_US: float = 1_000_000.0 + @dataclass class TraceEvent: @@ -140,6 +150,8 @@ def __init__(self, label: str = "Trace event") -> None: self.label = label self.tracker = SequenceTracker(label, modulus=SEQUENCE_MODULUS) self.names: dict[int, NameEntry] = {} + #: Reconstructed device time, in ticks. Signed, because a frame's delta + #: can be negative — see the `timestamp_ticks` field comment in the proto. self.clock_ticks: int = 0 self.timebase: int = tracing_pb2.NANOSECONDS self.core_frequency_hz: int = 0 @@ -319,9 +331,11 @@ def decode_tracing_stream( timestamp against *state*, which must be the same object across every call for one connection. - Stops and clears *buf* if a device reset is detected — either a backwards - sequence jump or a second ``TRACE_START`` frame — also resetting *state* and - calling :meth:`TraceEventBuffer.flush_pending`. + On a detected device reset, resets *state* and calls + :meth:`TraceEventBuffer.flush_pending`, then carries on parsing. It does + **not** discard *buf*: the stream is length-prefixed with no sync marker, so + dropping bytes mid-frame leaves every following frame misaligned and + unrecoverable. Args: buf: Mutable byte buffer containing raw RTT / transport bytes. @@ -336,27 +350,36 @@ def decode_tracing_stream( logger.warning("Failed to decode TraceFrame: %s", exc) continue + # A device reset invalidates everything carried over: the dictionary is + # reassigned from id one and the clock restarts at zero, so a stale entry + # would resolve a new id to an old name. Two things reveal one. + # # The device re-emits the header periodically so that a host attaching - # mid-run learns the timebase and the mask. That is not a reset — only a - # header whose absolute timestamp has gone *backwards* is, because the - # device clock restarts at zero on reboot and nothing carried over from - # before it is still valid. - if ( + # mid-run learns the timebase and the mask, so a header alone is not a + # reset — only one whose absolute timestamp has gone *far* backwards, + # which separates a reboot from ordering jitter (RESET_BACKWARD_MARGIN_US). + clock_restarted = ( frame.event_type == tracing_pb2.TRACE_START and state.source_mask is not None - and frame.timestamp_ticks < state.clock_ticks - ): - logger.warning("Tracing: device clock restarted — device reset") - buf.clear() - event_buffer.flush_pending() - state.reset() - break - - if state.tracker.observe(frame.sequence): - buf.clear() + and state.ticks_to_us(state.clock_ticks - frame.timestamp_ticks) + > RESET_BACKWARD_MARGIN_US + ) + if clock_restarted or state.tracker.observe(frame.sequence): + if clock_restarted: + logger.warning("Tracing: device clock restarted — device reset") event_buffer.flush_pending() state.reset() - break + # Seed the fresh tracker from this frame so gap detection is exact + # from the reset onward rather than from the frame after it. + state.tracker.observe(frame.sequence) + # Then fall through and process this frame: it is the first frame of + # the new run — often the reboot's own header, carrying the timebase + # and mask — and skipping it would leave the stream unattributed + # until the next refresh. + # + # Deliberately no `buf.clear()`: the frames after this point are + # intact and correctly delimited, and discarding bytes mid-frame + # would misalign the rest of the stream permanently. timestamp_us = state.advance(frame) diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 51db094..7dae6d9 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -10,6 +10,7 @@ from execution_trace._proto import tracing_pb2 from execution_trace.decode import ( + RESET_BACKWARD_MARGIN_US, SEQUENCE_MODULUS, UNKNOWN_NAME, MarkerRecord, @@ -332,7 +333,7 @@ def test_sequence_wraps_at_the_modulus_without_a_false_reset(self, caplog): assert caplog.text == "" assert len(event_buffer.markers) == 1 - def test_backwards_sequence_is_a_reset_that_clears_the_buffer(self): + def test_backwards_sequence_is_a_reset_that_clears_the_dictionary(self): state = TraceStreamState("test") event_buffer = TraceEventBuffer() state.tracker.observe(1_000) @@ -340,10 +341,31 @@ def test_backwards_sequence_is_a_reset_that_clears_the_buffer(self): # Restarting at 0 from 1000 is an apparent forward jump of 15 383, more # than half the modulus, so it reads as a backwards step: a reset. stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=1, sequence=0)) - stream += b"\xaa\xbb" # trailing garbage from the pre-reset stream decode_tracing_stream(stream, state, event_buffer) - assert stream == bytearray() - assert state.names == {} + assert state.names == {}, "a stale entry would resolve new ids to old names" + + def test_a_reset_does_not_discard_the_byte_buffer(self): + # The stream is length-prefixed with no sync marker, so dropping bytes + # mid-frame misaligns everything after it permanently — which showed up + # on hardware as a burst of "Failed to decode TraceFrame" immediately + # after every reset (§19.10). + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + state.tracker.observe(1_000) + state.names[1] = NameEntry(name="stale") + + stream = bytearray(make_frame(tracing_pb2.MARKER, name_id=1, sequence=0)) + # A complete, well-formed frame arriving after the reset point. + stream += make_frame( + tracing_pb2.NAME_REGISTERED, name_id=1, sequence=1, name="fresh", + source_type=tracing_pb2.TASK, + ) + stream += make_frame(tracing_pb2.MARKER, name_id=1, sequence=2, marker_value=9) + decode_tracing_stream(stream, state, event_buffer) + + assert stream == bytearray(), "every complete frame is still consumed" + assert state.names[1].name == "fresh", "frames after the reset still parse" + assert [m.value for m in event_buffer.markers] == [9] def test_a_reset_from_high_in_the_range_is_not_visible_in_the_sequence(self, caplog): # A consequence of wrapping at 16 384 rather than 2**32: restarting at 0 @@ -416,24 +438,49 @@ def test_a_repeated_header_is_not_a_reset(self, caplog): assert "reset" not in caplog.text assert len(state.names) == 1, "the dictionary must survive a refresh" + def test_a_header_slightly_behind_the_clock_is_not_a_reset(self, caplog): + # The header's timestamp is read after the producer queue is drained, but + # an event recorded just before that read can still be encoded after it, + # putting the header microseconds behind the reconstructed clock. Reading + # that as a reboot would discard the dictionary every refresh (§19.10). + builder = StreamBuilder().start(timestamp_ticks=1_000) + builder.span("t", 2_000_000, 3_000_000) + state, event_buffer = _decode(builder.bytes()) + assert len(state.names) == 1 + + behind = bytearray( + make_frame( + tracing_pb2.TRACE_START, + timestamp_ticks=state.clock_ticks - 300_000, # 300 µs behind + sequence=state.tracker._last + 1, + source_mask=0x1F, + ) + ) + with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): + decode_tracing_stream(behind, state, event_buffer) + assert "reset" not in caplog.text + assert len(state.names) == 1 + def test_a_second_header_is_treated_as_a_device_reset(self, caplog): - builder = StreamBuilder().start() - builder.span("t", 0, 1_000) + # A real device has been up for seconds before it reboots, so the clock + # drops by its whole uptime — far past RESET_BACKWARD_MARGIN_US. + builder = StreamBuilder().start(timestamp_ticks=30_000_000_000) + builder.span("t", 30_001_000_000, 30_002_000_000) stream = builder.bytes() state, event_buffer = _decode(stream) assert len(state.names) == 1 - # The device reboots: a fresh header arrives on the same connection, and - # its sequence restarts at 0 — which on its own is only a small backwards - # step, not the large one the tracker reads as a reset. reboot = StreamBuilder().start(timestamp_ticks=0, source_mask=0x1F) reboot.span("t", 0, 500) buf = reboot.bytes() with caplog.at_level(logging.WARNING, logger="execution_trace.decode"): decode_tracing_stream(buf, state, event_buffer) assert "device reset" in caplog.text - assert state.names == {} - assert state.source_mask is None + # The pre-reboot dictionary is gone, and the reboot's own frames — which + # follow in the same buffer — are parsed rather than thrown away. + assert state.names[1].name == "t" + assert state.source_mask == 0x1F + assert buf == bytearray() def test_malformed_payload_is_skipped_without_killing_the_stream(self, caplog): stream = StreamBuilder().start().bytes() diff --git a/src/encode.rs b/src/encode.rs index dfdd773..1997b42 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -83,7 +83,11 @@ pub struct RawTraceFrame { /// A delta against the previous sequenced frame, except on /// [`FrameKind::NameRegistered`] and [`FrameKind::TraceStart`], where it is /// absolute and re-establishes the time origin. - pub timestamp_ticks: u64, + /// + /// Signed: the delta is negative whenever an event was recorded before the + /// one encoded ahead of it, which happens whenever an ISR preempts a task + /// between its timestamp being taken and its being queued (§19.10). + pub timestamp_ticks: i64, pub name_id: u32, pub kind: FrameKind, pub sequence: u32, @@ -106,7 +110,7 @@ pub struct RawTraceFrame { } impl RawTraceFrame { - fn new(kind: FrameKind, timestamp_ticks: u64, sequence: u32, name_id: u32) -> Self { + fn new(kind: FrameKind, timestamp_ticks: i64, sequence: u32, name_id: u32) -> Self { Self { timestamp_ticks, name_id, @@ -200,7 +204,7 @@ impl TraceEncoder { ) -> Result { let mut frame = RawTraceFrame::new( FrameKind::TraceStart, - timestamp_ticks, + Self::absolute(timestamp_ticks), self.next_sequence(), UNKNOWN_NAME_ID, ); @@ -259,7 +263,10 @@ impl TraceEncoder { }, }; - let delta = timestamp_ns.saturating_sub(self.last_ticks); + // Signed: a negative delta is an event that was recorded before the one + // encoded ahead of it. Clamping it to zero while still moving the base + // backwards would leave the host's clock permanently ahead (§19.10). + let delta = Self::absolute(timestamp_ns) - Self::absolute(self.last_ticks); self.last_ticks = timestamp_ns; let sequence = self.next_sequence(); @@ -360,7 +367,7 @@ impl TraceEncoder { let sequence = self.next_sequence(); let mut frame = RawTraceFrame::new( FrameKind::NameRegistered, - timestamp_ticks, + Self::absolute(timestamp_ticks), sequence, name_id, ); @@ -406,7 +413,12 @@ impl TraceEncoder { let id = u32::try_from(self.names.len()).map_err(|_| RegisterError::Full)?; let sequence = self.next_sequence(); - let mut frame = RawTraceFrame::new(FrameKind::NameRegistered, timestamp_ns, sequence, id); + let mut frame = RawTraceFrame::new( + FrameKind::NameRegistered, + Self::absolute(timestamp_ns), + sequence, + id, + ); frame.name = name.clone(); frame.source_type = source_type; frame.priority = priority; @@ -417,6 +429,16 @@ impl TraceEncoder { Ok((id, n)) } + /// Device timestamps as the wire carries them. + /// + /// Saturating rather than wrapping: at nanosecond ticks `i64::MAX` is 292 + /// years of uptime and at cycles it is further still, so this cannot be + /// reached in practice — but a wrap would desynchronise the host's clock for + /// good, where a saturation only stops it advancing. + fn absolute(ticks: u64) -> i64 { + i64::try_from(ticks).unwrap_or(i64::MAX) + } + fn next_sequence(&mut self) -> u32 { let current = self.sequence; self.sequence = (self.sequence + 1) % SEQUENCE_MODULUS; @@ -893,25 +915,51 @@ mod tests { let (b, _) = encode_one(&mut enc, &span_end("a", ts)); bytes.extend_from_slice(&b); } - let mut clock = 0u64; + let mut clock = 0i64; let reconstructed: Vec = decode_all(&bytes) .iter() .map(|f| { clock += f.timestamp_ticks; - clock + u64::try_from(clock).unwrap() }) .collect(); assert_eq!(reconstructed, stamps); } #[test] - fn a_backwards_timestamp_saturates_rather_than_wrapping() { - // Cannot happen with a monotonic clock, but a delta of ~2^64 would be - // five varint bytes and would desynchronise the host's clock for good. + fn a_backwards_timestamp_is_encoded_as_a_negative_delta() { + // An event recorded before the one encoded ahead of it: the timestamp is + // taken before the event reaches the producer queue, so an ISR preempting + // a task in between reorders them. The delta has to carry the inversion, + // not clamp it (§19.10). let mut enc = warmed(&["a"]); encode_one(&mut enc, &span_end("a", 10_000)); let (bytes, _) = encode_one(&mut enc, &span_end("a", 9_000)); - assert_eq!(decode_all(&bytes)[0].timestamp_ticks, 0); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, -1_000); + } + + #[test] + fn out_of_order_timestamps_do_not_drift_the_reconstructed_clock() { + // The recording layer timestamps an event before it reaches the queue, so + // a priority-8 ISR preempting a task between those two points puts a + // *later* timestamp ahead of an earlier one in drain order. The + // reconstructed clock must still track the device, or it runs ahead and a + // later absolute timestamp looks like the device clock went backwards. + let mut enc = warmed(&["a"]); + let mut wire = Vec::new(); + // 100, then an inverted 90, then 110 — one preemption. + for ts in [100_000u64, 90_000, 110_000] { + let (bytes, _) = encode_one(&mut enc, &span_end("a", ts)); + wire.extend_from_slice(&bytes); + } + let mut clock = 0i64; + for frame in decode_all(&wire) { + clock += frame.timestamp_ticks; + } + assert_eq!( + clock, 110_000, + "the reconstructed clock must land on the last timestamp, not past it" + ); } // ── Sequencing (§18.1) ──────────────────────────────────────────────────── @@ -1307,9 +1355,9 @@ mod tests { // Host side: resolve ids through the dictionary, accumulate the deltas. let mut dictionary: std::collections::HashMap = std::collections::HashMap::new(); - let mut clock = 0u64; + let mut clock = 0i64; let mut expect_sequence = 0u32; - let mut resolved: Vec<(std::string::String, u64, FrameKind)> = Vec::new(); + let mut resolved: Vec<(std::string::String, i64, FrameKind)> = Vec::new(); for frame in decode_all(&wire) { assert_eq!(frame.sequence, expect_sequence, "no gaps in a clean stream"); diff --git a/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap b/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap index 7914045..2488342 100644 --- a/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap +++ b/src/snapshots/execution_trace__encode__tests__frame_sizes_across_the_delta_range_snapshot.snap @@ -15,10 +15,10 @@ expression: table ( 10000, [ - 11, - 11, - 13, - 11, + 12, + 12, + 14, + 12, ], ), ( diff --git a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap index d51a041..ddddd64 100644 --- a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap +++ b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap @@ -5,9 +5,9 @@ expression: "&buf[..n]" [ 27, 8, - 192, - 132, - 61, + 128, + 137, + 122, 16, 1, 24, From bd897126b61d3cc070e98eefa6c4bcab6ae0945e Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 22:22:52 +0200 Subject: [PATCH 05/16] Number frames at the producer, and let the host tolerate reordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXEC-TRACE-002 increment 5 (§5.7). A frame is numbered where it is recorded, not where it is written: one process-global AtomicU32, drawn from by the recording layer immediately before it hands the event to the transport, and by the encoder for the two frame classes that were never recorded — the header and the dictionary entries. The encoder forwards whatever number an event already carries. That closes §2.6. Loss at the producer queue used to happen before any number existed, so the host saw a contiguous stream with events simply absent; it now leaves a hole. It costs nothing on the wire — the field was already there, only who fills it changed, and the size table is unchanged. §5.7 justified this with "the RTIC channel is FIFO, so the transmitted order still matches". That is the assumption §19.10 already disproved for timestamps, and it fails here too: the number is taken before the send, so anything preempting in between reorders it. Two sources, the second larger than the preemption window §19.10 dealt with — transport frames are numbered when written and go straight out while events numbered earlier are still queued, so the distance is bounded by queue depth. The common case is exact and unavoidable: a dictionary frame is written ahead of the event that triggered it but numbered after it, inverting a pair on every first sight of a name. SequenceTracker therefore takes a reorder_window. A number arriving ahead of a hole is held rather than treated as evidence of loss; the hole is declared once something lands more than the window past it, which bounds how late the report can be. Stragglers close their own hole, and one arriving after its hole was reported is ignored rather than read as a backwards jump. The trace stream uses 64, covering the 48-slot channel; single-producer streams keep zero and report immediately. The counter is thread-local under cfg(test) — it is global by necessity, so absolute assertions would otherwise depend on test order — while production keeps the single atomic. The wrap arithmetic is shared and tested directly, including across the u32 rollover, which is continuous because 2^32 is an exact multiple of the modulus. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- python/src/execution_trace/decode.py | 20 ++- python/src/execution_trace/stream.py | 153 ++++++++++++++---- python/tests/test_decode.py | 49 +++++- python/tests/test_stream.py | 80 +++++++++ src/encode.rs | 131 +++++++++++---- src/lib.rs | 85 ++++++++++ src/sink.rs | 22 ++- ...ests__span_start_wire_format_snapshot.snap | 8 +- src/types.rs | 32 ++++ 9 files changed, 495 insertions(+), 85 deletions(-) diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index 196c3ff..c204999 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -49,6 +49,18 @@ UNKNOWN_NAME: str = "" +# How far a frame may arrive ahead of a missing one before that one is called +# lost (EXEC-TRACE-002 §5.7, §19.12). +# +# Frames are numbered by their producer, before the queue the transport drains, +# so an ISR preempting a task between those two points takes a later number and +# reaches the wire first. The transport's own frames — the header and the +# dictionary — are numbered when written and can overtake events already queued, +# which bounds the reordering by the depth of that queue rather than by the +# preemption window. 64 covers the 48-slot channel with margin; the cost of the +# margin is only that a genuine loss is reported this many frames later. +REORDER_WINDOW: int = 64 + # How far the device clock must appear to jump *backwards* on a TRACE_START # before it is read as a reboot rather than as ordering jitter. # @@ -148,7 +160,9 @@ class TraceStreamState: def __init__(self, label: str = "Trace event") -> None: self.label = label - self.tracker = SequenceTracker(label, modulus=SEQUENCE_MODULUS) + self.tracker = SequenceTracker( + label, modulus=SEQUENCE_MODULUS, reorder_window=REORDER_WINDOW + ) self.names: dict[int, NameEntry] = {} #: Reconstructed device time, in ticks. Signed, because a frame's delta #: can be negative — see the `timestamp_ticks` field comment in the proto. @@ -168,7 +182,9 @@ def reset(self) -> None: The dictionary must go with it: the device reassigns ids from one on reboot, so a stale entry would resolve a new id to the wrong name. """ - self.tracker = SequenceTracker(self.label, modulus=SEQUENCE_MODULUS) + self.tracker = SequenceTracker( + self.label, modulus=SEQUENCE_MODULUS, reorder_window=REORDER_WINDOW + ) self.names.clear() self._warned_ids.clear() self.unresolved_events = 0 diff --git a/python/src/execution_trace/stream.py b/python/src/execution_trace/stream.py index 93ea641..767b0f1 100644 --- a/python/src/execution_trace/stream.py +++ b/python/src/execution_trace/stream.py @@ -33,13 +33,26 @@ class SequenceTracker: - """Detects device resets by watching for backwards sequence number jumps. - - Embedded firmware typically increments a 32-bit sequence counter with every - frame. When the device resets (panic, watchdog, or reflash), the counter - starts over at zero on the same connection. This class distinguishes a reset - (counter jumped backward by more than half the u32 range) from ordinary - packet loss (small forward gap). + """Tracks a frame sequence, reporting loss and device resets. + + Embedded firmware increments a counter with every frame. A hole in the + numbers is loss; a large backwards jump is the counter starting over, which + means the device reset. + + Reordering: + With a *reorder_window* above zero the tracker tolerates frames arriving + slightly out of order before calling a hole loss. The execution-trace v2 + format needs this: a frame is numbered by its producer, before it reaches + the queue the transport drains (EXEC-TRACE-002 §5.7), so an ISR that + preempts a task between those two points takes a later number and reaches + the wire first. The transport's own frames — the stream header and the + dictionary — are numbered when written and can likewise overtake events + already queued. + + A hole is therefore only reported once a number arrives more than + *reorder_window* ahead of it, which bounds how long the report is + delayed. Single-producer streams should leave the window at zero and get + the report immediately. Args: label: Human-readable stream name used in log messages. @@ -47,21 +60,44 @@ class SequenceTracker: range; the execution-trace v2 format wraps far earlier, at :data:`execution_trace.decode.SEQUENCE_MODULUS`, because the counter exists only to detect gaps and a gap is read modulo the wrap. + reorder_window: How far ahead a number may arrive before the numbers it + skipped are declared lost. Zero means strictly ordered. + + Attributes: + dropped: Running total of frames reported lost. Raises: ValueError: If *modulus* is not a positive power of two, which the masking arithmetic below assumes. """ - def __init__(self, label: str = "Frame", modulus: int = DEFAULT_MODULUS) -> None: + def __init__( + self, + label: str = "Frame", + modulus: int = DEFAULT_MODULUS, + reorder_window: int = 0, + ) -> None: if modulus <= 0 or modulus & (modulus - 1): raise ValueError(f"modulus must be a positive power of two, got {modulus}") - self._last: int | None = None + if not 0 <= reorder_window < modulus // 2: + raise ValueError( + f"reorder_window must be in [0, {modulus // 2}), got {reorder_window}" + ) self._label = label self._modulus = modulus self._mask = modulus - 1 # Half the range: a larger apparent forward jump is really a backward one. self._reset_threshold = modulus >> 1 + self._window = reorder_window + self._expected: int | None = None + # Numbers seen ahead of _expected, still within the window. + self._pending: set[int] = set() + self.dropped = 0 + + @property + def _last(self) -> int | None: + """The last number consumed in order, or ``None`` before the first.""" + return None if self._expected is None else (self._expected - 1) & self._mask def observe(self, sequence: int) -> bool: """Record a sequence number and detect resets or drops. @@ -70,34 +106,83 @@ def observe(self, sequence: int) -> bool: sequence: The sequence number from the received frame. Returns: - ``True`` if a device reset was detected (sequence jumped backward), - ``False`` for normal sequential frames or ordinary packet drops. - - Note: - After a reset is detected, ``_last`` is cleared so the very next - frame — whatever its sequence number — is accepted silently as the - new baseline. + ``True`` if a device reset was detected (the counter jumped far + backwards), ``False`` otherwise — including for ordinary loss, which + is logged and counted rather than signalled. """ - # Mask to emulate the firmware's unsigned counter math in Python, so - # increment and subtraction behave correctly across a wraparound. - if self._last is not None: - expected = (self._last + 1) & self._mask - if sequence != expected: - dropped = (sequence - expected) & self._mask - if dropped > self._reset_threshold: - logger.warning( - "%s: device reset detected, resuming from #%d", - self._label, sequence, - ) - self._last = None - return True - logger.warning( - "%s drop detected: expected #%d, got #%d (%d dropped)", - self._label, expected, sequence, dropped, - ) - self._last = sequence + if self._expected is None: + self._expected = (sequence + 1) & self._mask + return False + expected = self._expected + + # Distance forward from what we expect next, in the counter's own + # arithmetic. Anything past the halfway point is really a step backwards. + ahead = (sequence - expected) & self._mask + + if ahead > self._reset_threshold: + behind = self._modulus - ahead + if behind <= self._window: + # A frame that overtook us earlier and is only now arriving, or a + # duplicate. Either way it fills nothing we have not moved past. + self._pending.discard(sequence) + return False + logger.warning( + "%s: device reset detected, resuming from #%d", self._label, sequence + ) + self._reset_to(sequence) + return True + + if ahead == 0: + self._expected = self._absorb_pending((sequence + 1) & self._mask) + return False + + if ahead <= self._window: + # Early: hold it and wait for the numbers it skipped. + self._pending.add(sequence) + return False + + # Past the window, so whatever is still missing is genuinely lost. + missing = ahead - sum( + 1 for p in self._pending if (p - expected) & self._mask < ahead + ) + if missing > 0: + self.dropped += missing + logger.warning( + "%s drop detected: expected #%d, got #%d (%d dropped)", + self._label, expected, sequence, missing, + ) + resumed = (sequence + 1) & self._mask + self._discard_passed(resumed) + self._expected = self._absorb_pending(resumed) return False + def _reset_to(self, sequence: int) -> None: + """Drop all state so the next frame, whatever its number, is the baseline. + + The resetting frame is not itself taken as the baseline: the counter has + restarted and the first numbers of the new run are as likely to be + reordered as any others, so anchoring on one of them would manufacture a + gap. Callers that want an exact anchor re-observe on a fresh tracker. + """ + del sequence + self._expected = None + self._pending.clear() + + def _absorb_pending(self, expected: int) -> int: + """Advance *expected* past any numbers already held that continue the run.""" + while expected in self._pending: + self._pending.discard(expected) + expected = (expected + 1) & self._mask + return expected + + def _discard_passed(self, expected: int) -> None: + """Forget held numbers that now sit behind *expected*.""" + self._pending = { + p + for p in self._pending + if (p - expected) & self._mask <= self._reset_threshold + } + def iter_frames(buf: bytearray) -> Generator[bytes, None, None]: """Yield raw protobuf bytes for each complete length-delimited frame in *buf*. diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 7dae6d9..78c59f8 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -10,6 +10,7 @@ from execution_trace._proto import tracing_pb2 from execution_trace.decode import ( + REORDER_WINDOW, RESET_BACKWARD_MARGIN_US, SEQUENCE_MODULUS, UNKNOWN_NAME, @@ -312,13 +313,51 @@ def test_partial_frame_is_left_in_the_buffer(self, builder): assert len(event_buffer.records) == 1 assert head == bytearray() - def test_sequence_gap_is_reported(self, caplog): - stream = StreamBuilder().start().bytes() - # Skip a sequence number: the next frame claims 5, not 1. - stream += make_frame(tracing_pb2.MARKER, name_id=0, sequence=5) + def test_a_frame_lost_at_the_producer_queue_is_reported(self, caplog): + """AC 7: a frame dropped before the transport still shows as a gap. + + The number is taken when the event is recorded, upstream of the queue the + transport drains, so a drop there burns a number and leaves a hole + (§5.7). Before increment 5 the transport numbered frames itself and this + loss was invisible — the host saw a contiguous stream with events simply + absent (§2.6). + """ + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + state.names[1] = NameEntry(name="main_task") + + stream = bytearray() + seq = 0 + for i in range(REORDER_WINDOW + 4): + if i == 1: + seq += 1 # this one never reached the transport + continue + stream += make_frame(tracing_pb2.MARKER, name_id=1, sequence=seq) + seq += 1 + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): - _decode(stream) + decode_tracing_stream(stream, state, event_buffer) + assert "drop detected" in caplog.text + assert "1 dropped" in caplog.text + assert state.tracker.dropped == 1 + + def test_reordered_frames_are_not_reported_as_loss(self, caplog): + # Producer-side numbering means an ISR can take a later number and reach + # the wire first. That is not loss and must not be reported as any. + state = TraceStreamState("test") + event_buffer = TraceEventBuffer() + state.names[1] = NameEntry(name="main_task") + + stream = bytearray() + for seq in (0, 2, 1, 3, 5, 4, 6): + stream += make_frame(tracing_pb2.MARKER, name_id=1, sequence=seq) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + decode_tracing_stream(stream, state, event_buffer) + + assert caplog.text == "" + assert state.tracker.dropped == 0 + assert len(event_buffer.markers) == 7, "every frame is still delivered" def test_sequence_wraps_at_the_modulus_without_a_false_reset(self, caplog): state = TraceStreamState("test") diff --git a/python/tests/test_stream.py b/python/tests/test_stream.py index 94c3ad4..da5fb87 100644 --- a/python/tests/test_stream.py +++ b/python/tests/test_stream.py @@ -210,3 +210,83 @@ def test_frame_with_empty_payload(self): frames = list(iter_frames(buf)) assert frames == [b""] assert buf == bytearray() + + +class TestSequenceTrackerReordering: + """The v2 trace stream numbers frames at their producer, so they can arrive + slightly out of order; see SequenceTracker's Reordering note.""" + + def _tracker(self, window: int = 4) -> SequenceTracker: + return SequenceTracker("Test", modulus=64, reorder_window=window) + + def test_a_swapped_pair_is_not_reported_as_loss(self, caplog): + t = self._tracker() + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + for s in (10, 12, 11, 13): # 12 overtook 11 + assert t.observe(s) is False + assert caplog.text == "" + assert t.dropped == 0 + + def test_a_frame_arriving_late_within_the_window_closes_its_hole(self, caplog): + t = self._tracker() + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + for s in (10, 14, 13, 12, 11, 15): + t.observe(s) + assert caplog.text == "" + assert t.dropped == 0 + + def test_a_real_hole_is_reported_once_the_window_is_passed(self, caplog): + # 12 never arrives. Nothing is said until a frame lands more than the + # window past it, which bounds how late the report can be. + t = self._tracker(window=4) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + for s in (10, 11, 13, 14, 15, 16): + t.observe(s) + assert t.dropped == 0, "still inside the window" + t.observe(17) + assert t.dropped == 1 + assert "1 dropped" in caplog.text + + def test_the_count_is_right_when_several_are_missing(self, caplog): + t = self._tracker(window=2) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + t.observe(10) + t.observe(20) # 11..19 missing, far past the window + assert t.dropped == 9 + + def test_a_window_does_not_hide_a_device_reset(self, caplog): + # Detectable only when the pre-reset counter was low in the range: from + # high in it a restart at zero is indistinguishable from a forward gap, + # which is why the trace decoder leans on TRACE_START instead + # (EXEC-TRACE-002 §19.6). + t = self._tracker() + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + t.observe(10) + assert t.observe(0) is True + assert "device reset" in caplog.text + + def test_a_late_arrival_after_its_hole_was_reported_is_ignored(self, caplog): + # The window has already passed the hole and called it lost; the straggler + # must not then be read as a backwards jump. + t = self._tracker(window=2) + t.observe(10) + t.observe(15) # 11..14 declared lost + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + assert t.observe(14) is False + assert "reset" not in caplog.text + + def test_zero_window_reports_immediately(self, caplog): + # Single-producer streams — attitude, status — keep strict ordering and + # should not have their gap reports delayed. + t = SequenceTracker("Strict", modulus=64, reorder_window=0) + with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): + t.observe(10) + t.observe(12) + assert t.dropped == 1 + assert "1 dropped" in caplog.text + + def test_window_must_be_sane(self): + with pytest.raises(ValueError): + SequenceTracker("Test", modulus=64, reorder_window=32) + with pytest.raises(ValueError): + SequenceTracker("Test", modulus=64, reorder_window=-1) diff --git a/src/encode.rs b/src/encode.rs index 1997b42..7f2d2eb 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -173,7 +173,11 @@ impl TraceEncoder { } } - /// Returns the sequence number of the *next* frame. + /// Returns the sequence number this encoder last took for a frame of its own + /// (the stream header or a dictionary entry). + /// + /// Recorded events are numbered by the producer, not here, so this does not + /// track them; see [`next_sequence`](crate::next_sequence). pub fn sequence(&self) -> u32 { self.sequence } @@ -268,7 +272,10 @@ impl TraceEncoder { // backwards would leave the host's clock permanently ahead (§19.10). let delta = Self::absolute(timestamp_ns) - Self::absolute(self.last_ticks); self.last_ticks = timestamp_ns; - let sequence = self.next_sequence(); + // Forwarded, not assigned: the number was taken when the event was + // recorded, upstream of the producer queue, so a frame lost there still + // leaves a gap on the wire (§5.7). + let sequence = event.sequence(); let mut frame = match event { TraceEvent::SpanStart { .. } => { @@ -439,10 +446,16 @@ impl TraceEncoder { i64::try_from(ticks).unwrap_or(i64::MAX) } + /// Takes a number for a frame the encoder generates itself — the stream + /// header and the dictionary entries, which were never "recorded" and so + /// have no producer-side number of their own. + /// + /// Drawn from the same global counter as recorded events: one sequence space + /// is what makes a gap mean anything. fn next_sequence(&mut self) -> u32 { - let current = self.sequence; - self.sequence = (self.sequence + 1) % SEQUENCE_MODULUS; - current + let taken = crate::next_sequence(); + self.sequence = taken; + taken } } @@ -646,12 +659,14 @@ mod tests { n } + /// Builds an event the way the recording layer does, including taking its + /// producer-side sequence number (§5.7). fn span_start(n: &str, ts: u64) -> TraceEvent { TraceEvent::SpanStart { timestamp_ns: ts, name: name(n), source_type: SourceType::Task, - sequence: 0, + sequence: crate::next_sequence(), priority: 2, relative_deadline_ms: None, } @@ -662,7 +677,7 @@ mod tests { timestamp_ns: ts, name: name(n), source_type: SourceType::Isr, - sequence: 0, + sequence: crate::next_sequence(), priority: 8, relative_deadline_ms: Some(0.5), } @@ -672,7 +687,7 @@ mod tests { TraceEvent::SpanEnd { timestamp_ns: ts, name: name(n), - sequence: 0, + sequence: crate::next_sequence(), } } @@ -680,7 +695,7 @@ mod tests { TraceEvent::Marker { timestamp_ns: ts, name: name(n), - sequence: 0, + sequence: crate::next_sequence(), marker_value: value, } } @@ -964,37 +979,66 @@ mod tests { // ── Sequencing (§18.1) ──────────────────────────────────────────────────── + /// Serialises the tests that assert on absolute sequence numbers. The + /// counter is process-global by design (§5.7), so the test harness running + /// them on separate threads would otherwise make them depend on each other. + static SEQUENCE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + fn with_fresh_sequence(body: impl FnOnce() -> T) -> T { + let guard = SEQUENCE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + crate::reset_sequence(); + let out = body(); + drop(guard); + out + } + #[test] - fn sequence_starts_at_zero_and_advances_per_frame() { - let mut enc = TraceEncoder::new(); - assert_eq!(enc.sequence(), 0); - let (bytes, _) = encode_one(&mut enc, &span_start("a", 0)); - let frames = decode_all(&bytes); - assert_eq!(frames[0].sequence, 0, "dictionary frame is sequenced too"); - assert_eq!(frames[1].sequence, 1); - assert_eq!(enc.sequence(), 2); + fn the_encoder_forwards_the_number_the_event_was_recorded_with() { + // The whole point of §5.7: the transport does not number events, so an + // event lost before it reaches the transport still burned a number. + let mut enc = warmed(&["a"]); + let mut event = span_end("a", 0); + event.set_sequence(4_321); + let (bytes, _) = encode_one(&mut enc, &event); + assert_eq!(decode_all(&bytes)[0].sequence, 4_321); } #[test] - fn sequence_wraps_at_the_modulus_not_at_u32_max() { + fn a_gap_in_recorded_numbers_survives_to_the_wire() { + // A frame dropped at the producer queue: the encoder never sees it, and + // the numbers either side of it must still show the hole (AC 7). let mut enc = warmed(&["a"]); - let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; - while enc.sequence() != SEQUENCE_MODULUS - 1 { - enc.encode(&span_end("a", 0), &mut buf).unwrap(); + let mut wire = Vec::new(); + for seq in [10u32, 11, /* 12 dropped at the queue */ 13] { + let mut event = span_end("a", 0); + event.set_sequence(seq); + let (bytes, _) = encode_one(&mut enc, &event); + wire.extend_from_slice(&bytes); } - let (bytes, _) = encode_one(&mut enc, &span_end("a", 0)); - assert_eq!(decode_all(&bytes)[0].sequence, SEQUENCE_MODULUS - 1); - assert_eq!(enc.sequence(), 0, "wraps to zero, not to 16384"); + let seen: Vec = decode_all(&wire).iter().map(|f| f.sequence).collect(); + assert_eq!(seen, vec![10, 11, 13]); } #[test] - fn sequence_never_exceeds_the_modulus() { - let mut enc = warmed(&["a"]); - let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + fn recorded_numbers_are_consecutive_and_wrap_at_the_modulus() { + with_fresh_sequence(|| { + let drawn: Vec = (0..SEQUENCE_MODULUS + 3) + .map(|_| crate::next_sequence()) + .collect(); + assert_eq!(drawn[0], 0); + assert_eq!(drawn[SEQUENCE_MODULUS as usize - 1], SEQUENCE_MODULUS - 1); + assert_eq!( + drawn[SEQUENCE_MODULUS as usize], 0, + "wraps to zero, not to 16384" + ); + assert_eq!(drawn[SEQUENCE_MODULUS as usize + 1], 1); + }); + } + + #[test] + fn recorded_numbers_never_exceed_the_modulus() { for _ in 0..(SEQUENCE_MODULUS * 2) { - let n = enc.encode(&span_end("a", 0), &mut buf).unwrap(); - let frame = decode_all(&buf[..n.len]).remove(0); - assert!(frame.sequence < SEQUENCE_MODULUS); + assert!(crate::next_sequence() < SEQUENCE_MODULUS); } } @@ -1132,6 +1176,9 @@ mod tests { /// Steady-state frame size for each event class at a representative delta. fn steady_state_sizes(delta: u64) -> [usize; 4] { + // From a known counter: the sequence is a varint, so its magnitude is + // part of the frame size being measured. + crate::reset_sequence(); let mut enc = warmed(&["main_task", "ukf_predict"]); let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; let mut t = 1_000_000_000u64; @@ -1141,7 +1188,6 @@ mod tests { marker("ukf_predict", 0, Some(42)), marker("ukf_predict", 0, None), ]; - // Push the sequence into its two-varint-byte range, the common case. for _ in 0..200 { t += delta; enc.encode(&span_end("main_task", t), &mut buf).unwrap(); @@ -1155,6 +1201,10 @@ mod tests { | TraceEvent::SpanEnd { timestamp_ns, .. } | TraceEvent::Marker { timestamp_ns, .. } => *timestamp_ns = t, } + // A mid-range sequence, which is what a running device carries: the + // number is a varint, and only the 128 values below the first + // boundary — 0.8 % of the 16 384-wide space — cost a single byte. + ev.set_sequence(8_000); sizes[i] = enc.encode(&ev, &mut buf).unwrap().len; } sizes @@ -1216,6 +1266,7 @@ mod tests { #[test] fn span_start_wire_format_snapshot() { + crate::reset_sequence(); let mut enc = TraceEncoder::new(); let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; let n = enc @@ -1356,12 +1407,11 @@ mod tests { let mut dictionary: std::collections::HashMap = std::collections::HashMap::new(); let mut clock = 0i64; - let mut expect_sequence = 0u32; + let mut seen_sequences: Vec = Vec::new(); let mut resolved: Vec<(std::string::String, i64, FrameKind)> = Vec::new(); for frame in decode_all(&wire) { - assert_eq!(frame.sequence, expect_sequence, "no gaps in a clean stream"); - expect_sequence = (expect_sequence + 1) % SEQUENCE_MODULUS; + seen_sequences.push(frame.sequence); clock = match frame.kind { FrameKind::NameRegistered | FrameKind::TraceStart => frame.timestamp_ticks, _ => clock + frame.timestamp_ticks, @@ -1386,5 +1436,18 @@ mod tests { ] ); assert_eq!(dictionary.len(), 2, "each name registered exactly once"); + + // Every number in the range is present exactly once — a clean stream has + // no gaps. They are *not* in order on the wire: a dictionary frame is + // written ahead of the event that triggered it but numbered after it, + // because the event was numbered by its producer (§5.7, §19.12). + let mut sorted = seen_sequences.clone(); + sorted.sort_unstable(); + let expected: Vec = (0..u32::try_from(seen_sequences.len()).unwrap()).collect(); + assert_eq!(sorted, expected, "no gaps in a clean stream"); + assert_ne!( + seen_sequences, expected, + "and the wire is not in sequence order" + ); } } diff --git a/src/lib.rs b/src/lib.rs index 89e3a2a..5d52451 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,91 @@ #[cfg(feature = "enabled")] pub mod encode; + +/// The producer-side sequence counter (EXEC-TRACE-002 §5.7). +/// +/// Numbering at record time rather than in the transport is what makes loss at +/// the producer queue visible: a frame dropped there has already burned a +/// number, so the host sees a gap instead of a contiguous stream with events +/// simply missing (§2.6). +#[cfg(feature = "enabled")] +mod sequence { + /// Folds a freely running counter into the wire's sequence range. + /// + /// Applied on read rather than by keeping the stored counter in range: + /// `fetch_add` cannot wrap at a non-power-of-two bound atomically, and 2³² is + /// an exact multiple of 16 384, so taking the modulus of a wrapping `u32` + /// yields the same sequence either way. + fn wrap(raw: u32) -> u32 { + raw % crate::encode::SEQUENCE_MODULUS + } + + /// Takes the next sequence number. + /// + /// One `fetch_add` — on Cortex-M4 an `LDREX`/`STREX` pair, so no critical + /// section and nothing for a preempting ISR to block on. The counter is + /// process-global because every producer and the transport itself must draw + /// from one space for a gap to mean anything. + #[cfg(not(test))] + pub fn next_sequence() -> u32 { + use core::sync::atomic::{AtomicU32, Ordering}; + static SEQUENCE: AtomicU32 = AtomicU32::new(0); + wrap(SEQUENCE.fetch_add(1, Ordering::Relaxed)) + } + + // Under test the counter is per-thread. The harness runs each test on its own + // thread, so a shared global would make every test that asserts an absolute + // number depend on whichever others happened to run first. The production + // path above is the one §5.7 specifies; `wrap`, which is where the only + // arithmetic lives, is shared by both and tested directly. + #[cfg(test)] + std::thread_local! { + static SEQUENCE: core::cell::Cell = const { core::cell::Cell::new(0) }; + } + + #[cfg(test)] + pub fn next_sequence() -> u32 { + SEQUENCE.with(|c| { + let taken = c.get(); + c.set(taken.wrapping_add(1)); + wrap(taken) + }) + } + + /// Restarts this thread's counter. Tests only. + #[cfg(test)] + pub fn reset_sequence() { + SEQUENCE.with(|c| c.set(0)); + } + + #[cfg(test)] + mod tests { + use super::wrap; + use crate::encode::SEQUENCE_MODULUS; + + #[test] + fn wrap_folds_into_the_sequence_range() { + assert_eq!(wrap(0), 0); + assert_eq!(wrap(SEQUENCE_MODULUS - 1), SEQUENCE_MODULUS - 1); + assert_eq!(wrap(SEQUENCE_MODULUS), 0, "wraps to zero, not to 16384"); + assert_eq!(wrap(SEQUENCE_MODULUS + 1), 1); + } + + #[test] + fn wrap_is_continuous_across_the_u32_boundary() { + // 2^32 is an exact multiple of the modulus, so a counter rolling over + // u32::MAX stays continuous in sequence space — which is what lets the + // stored counter run free instead of being masked on every increment. + assert_eq!(wrap(u32::MAX), SEQUENCE_MODULUS - 1); + assert_eq!(wrap(u32::MAX.wrapping_add(1)), 0); + } + } +} + +#[cfg(feature = "enabled")] +pub use sequence::next_sequence; +#[cfg(all(test, feature = "enabled"))] +pub(crate) use sequence::reset_sequence; mod sink; mod types; diff --git a/src/sink.rs b/src/sink.rs index d93d541..267d805 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -93,14 +93,20 @@ pub trait TraceSink: TraceTransport { let mut name: String<32> = String::new(); name.push_str(source_name) .map_err(|_| TracingError::MessageDropped)?; - self.write_event(TraceEvent::SpanStart { + let mut event = TraceEvent::SpanStart { timestamp_ns: self.get_elapsed_nanoseconds(), name, source_type, sequence: 0, priority: u32::from(priority), relative_deadline_ms, - }) + }; + // Numbered here, not in the transport, so that an event lost at the + // producer queue still leaves a host-visible gap (§5.7). Taken as late + // as possible: everything between this and the enqueue is a window in + // which a preempting ISR can take a later number and arrive first. + event.set_sequence(crate::next_sequence()); + self.write_event(event) } /// Records the end of a named execution span previously started with [`record_span_start`]. @@ -117,11 +123,13 @@ pub trait TraceSink: TraceTransport { let mut name: String<32> = String::new(); name.push_str(source_name) .map_err(|_| TracingError::MessageDropped)?; - self.write_event(TraceEvent::SpanEnd { + let mut event = TraceEvent::SpanEnd { timestamp_ns: self.get_elapsed_nanoseconds(), name, sequence: 0, - }) + }; + event.set_sequence(crate::next_sequence()); + self.write_event(event) } /// Records a point-in-time annotation. No matching `record_span_end` is needed. @@ -143,12 +151,14 @@ pub trait TraceSink: TraceTransport { let mut name: String<32> = String::new(); name.push_str(label) .map_err(|_| TracingError::MessageDropped)?; - self.write_event(TraceEvent::Marker { + let mut event = TraceEvent::Marker { timestamp_ns: self.get_elapsed_nanoseconds(), name, sequence: 0, marker_value: value, - }) + }; + event.set_sequence(crate::next_sequence()); + self.write_event(event) } } diff --git a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap index ddddd64..e66a962 100644 --- a/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap +++ b/src/snapshots/execution_trace__encode__tests__span_start_wire_format_snapshot.snap @@ -3,7 +3,7 @@ source: src/encode.rs expression: "&buf[..n]" --- [ - 27, + 29, 8, 128, 137, @@ -12,6 +12,8 @@ expression: "&buf[..n]" 1, 24, 4, + 32, + 1, 50, 8, 103, @@ -31,11 +33,9 @@ expression: "&buf[..n]" 0, 0, 63, - 6, + 4, 16, 1, 24, 1, - 32, - 1, ] diff --git a/src/types.rs b/src/types.rs index b9f96dc..6020ead 100644 --- a/src/types.rs +++ b/src/types.rs @@ -42,3 +42,35 @@ pub enum TraceEvent { marker_value: Option, }, } + +#[cfg(feature = "enabled")] +impl TraceEvent { + /// The sequence number assigned when this event was recorded. + /// + /// Zero until [`set_sequence`] is called; the transport forwards whatever is + /// here rather than numbering the event itself, so that a frame lost between + /// the recording layer and the wire still leaves a gap (EXEC-TRACE-002 §5.7). + /// + /// [`set_sequence`]: TraceEvent::set_sequence + #[must_use] + pub fn sequence(&self) -> u32 { + match self { + TraceEvent::SpanStart { sequence, .. } + | TraceEvent::SpanEnd { sequence, .. } + | TraceEvent::Marker { sequence, .. } => *sequence, + } + } + + /// Stamps this event with its record-time sequence number. + /// + /// Called by the recording layer immediately before handing the event to the + /// transport, so that the window in which a preempting ISR can take a later + /// number and reach the queue first is as narrow as possible. + pub fn set_sequence(&mut self, value: u32) { + match self { + TraceEvent::SpanStart { sequence, .. } + | TraceEvent::SpanEnd { sequence, .. } + | TraceEvent::Marker { sequence, .. } => *sequence = value, + } + } +} From 0d8ddcaa1b6f8fa35dc4b823a9736e8394f44930 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 22:35:19 +0200 Subject: [PATCH 06/16] Time the trace from the raw cycle counter, extending the wrap in the encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXEC-TRACE-002 increment 7 (§5.8). `TraceSink::now_ticks` — renamed from `get_elapsed_nanoseconds`, which on this firmware would now be a lie — can be a single volatile load of the core cycle counter instead of a globally interrupt-disabled read with a 64-bit divide in it. The encoder is built for its clock (`new` for nanoseconds, `with_cycle_counter(hz)`), and `encode_trace_start` no longer takes a timebase: a header disagreeing with the deltas around it was a state worth making unrepresentable. §5.8 put the 64-bit extension on the host, reconstructing it "from the monotonic sequence". Two things since make that the wrong place. The host no longer sees a monotonic sequence (§20.1), and the absolute timestamps on the header and dictionary frames — re-emitted every 2 s since §19.9 — would each be a raw wrapped counter, so the host's clock would jump backwards at every rollover. TraceEncoder therefore holds the extension. It runs off the control path, so §5.8's actual objective is still met; only the bookkeeping moved to the nearest place that is still cheap. The step is taken in the counter's own 32-bit arithmetic and read as signed, which covers both the rollover and the small backwards steps of §19.10 without telling them apart. The one assumption — consecutive readings less than half a wrap apart — is discharged by the 2 s refresh, which reads the clock. Duration markers had to learn the timebase too: §5.3's collapsed markers carry an interval, and dividing nanoseconds by 1 000 is wrong twice over against cycles, since the divisor is 72 and an interval spanning the rollover needs 32-bit subtraction. The sink now reports `ticks_per_us` and `tick_mask`, both defaulted to the nanosecond case, and `ticks_since` does the subtraction; every call site is unchanged. Verified against a stream spanning the rollover: 2 000 spans of 500 µs at 1 ms spacing decode to 500.000 and 1000.000 µs with zero deviation. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- README.md | 10 +- examples/simulate.rs | 15 ++- src/encode.rs | 228 +++++++++++++++++++++++++++++++++---------- src/sink.rs | 50 ++++++++-- 4 files changed, 232 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 169a7d2..a9b8610 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ impl TraceTransport for MyRttSink { } impl TraceSink for MyRttSink { - fn get_elapsed_nanoseconds(&self) -> u64 { + fn now_ticks(&self) -> u64 { 0 // replace with your hardware timer } } @@ -69,13 +69,17 @@ lets the host detect dropped frames. Emit the stream header once, then encode ev ```rust use execution_trace::{SourceType, TraceEncoder, TraceEvent}; -use execution_trace::encode::{MAX_TRACE_BURST_SIZE, TimeBase}; +use execution_trace::encode::MAX_TRACE_BURST_SIZE; +// `new()` reads timestamps as 64-bit nanoseconds. For a sink returning a raw +// core cycle counter use `TraceEncoder::with_cycle_counter(72_000_000)`: the +// encoder then extends the 32-bit counter itself, so reading it costs the +// caller one volatile load. let mut enc = TraceEncoder::new(); let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; // Once, at startup: declares the tick unit and the active source mask. -if let Ok(n) = enc.encode_trace_start(0, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) { +if let Ok(n) = enc.encode_trace_start(0, 0x1F, &mut buf) { let _ = &buf[..n]; } diff --git a/examples/simulate.rs b/examples/simulate.rs index 95eb726..2ca0d0d 100644 --- a/examples/simulate.rs +++ b/examples/simulate.rs @@ -7,7 +7,7 @@ // diagram from it, run the companion Python script: // python examples/visualize.py -use execution_trace::encode::{MAX_TRACE_BURST_SIZE, TimeBase, decode_trace_frame}; +use execution_trace::encode::{MAX_TRACE_BURST_SIZE, decode_trace_frame}; use execution_trace::{ FrameKind, SourceType, TraceEncoder, TraceEvent, TraceSink, TraceTransport, TracingError, }; @@ -36,13 +36,10 @@ impl FileSink { // Emits the stream header. On real hardware this runs once, from `init`. fn write_header(&mut self, source_mask: u32) { let mut frame = [0u8; MAX_TRACE_BURST_SIZE]; - if let Ok(n) = self.encoder.encode_trace_start( - self.tick_ns, - TimeBase::Nanoseconds, - 0, - source_mask, - &mut frame, - ) { + if let Ok(n) = self + .encoder + .encode_trace_start(self.tick_ns, source_mask, &mut frame) + { self.buf.extend_from_slice(&frame[..n]); } } @@ -72,7 +69,7 @@ impl TraceTransport for FileSink { } impl TraceSink for FileSink { - fn get_elapsed_nanoseconds(&self) -> u64 { + fn now_ticks(&self) -> u64 { self.tick_ns } } diff --git a/src/encode.rs b/src/encode.rs index 7f2d2eb..e3f99f0 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -145,10 +145,64 @@ impl RawTraceFrame { /// [`TraceSink`]: crate::TraceSink pub struct TraceEncoder { names: heapless::Vec, - last_ticks: u64, + clock: TickClock, + /// Extended tick count of the frame emitted most recently, which the next + /// frame's delta is taken against. + last_emitted: i64, sequence: u32, } +/// Turns the readings a sink produces into the monotonic tick count the wire +/// carries. +/// +/// The distinction exists because a raw hardware cycle counter is 32 bits and +/// free-running. Extending it to 64 is bookkeeping, and §5.8's whole point is +/// that the ISR recording an event must not pay for it — so it happens here, in +/// the task that owns the transport, off the control path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TickWidth { + /// Readings are already a 64-bit monotonic count and are used as given. + Monotonic64, + /// Readings are a free-running 32-bit counter that wraps. + Wrapping32, +} + +#[derive(Debug, Clone, Copy)] +struct TickClock { + width: TickWidth, + timebase: TimeBase, + core_frequency_hz: u32, + last_raw: u64, + absolute: i64, + started: bool, +} + +impl TickClock { + /// Folds a raw reading into the extended, absolute tick count. + /// + /// For a wrapping source the step is taken in the counter's own 32-bit + /// arithmetic and read as signed, which handles both the wrap — every + /// 59.65 s at 72 MHz — and the small backwards steps that ISR preemption + /// produces (§19.10), without telling them apart or needing to. The only + /// assumption is that consecutive readings are less than half a wrap apart; + /// the dictionary refresh reads the clock every 2 s, so nothing else has to + /// guarantee it. + fn extend(&mut self, raw: u64) -> i64 { + match self.width { + TickWidth::Monotonic64 => self.absolute = i64::try_from(raw).unwrap_or(i64::MAX), + TickWidth::Wrapping32 if self.started => { + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + let step = i64::from((raw as u32).wrapping_sub(self.last_raw as u32) as i32); + self.absolute = self.absolute.wrapping_add(step); + } + TickWidth::Wrapping32 => self.absolute = i64::try_from(raw).unwrap_or(i64::MAX), + } + self.last_raw = raw; + self.started = true; + self.absolute + } +} + /// One dictionary entry: a name and everything fixed by it. /// /// The attributes are kept, not just the name, so that the dictionary can be @@ -164,11 +218,36 @@ struct DictionaryEntry { } impl TraceEncoder { - /// Creates an encoder with an empty dictionary and the counters at zero. + /// Creates an encoder for a sink whose readings are 64-bit monotonic + /// nanoseconds. pub const fn new() -> Self { + Self::with_clock(TickWidth::Monotonic64, TimeBase::Nanoseconds, 0) + } + + /// Creates an encoder for a sink whose readings are a free-running 32-bit + /// core cycle counter (§5.8). + /// + /// The counter wraps every `2³² / core_frequency_hz` seconds — 59.65 s at + /// 72 MHz — and this encoder extends it, so a reading costs the sink one + /// volatile load with no critical section and no division. The frequency is + /// put on the stream header so the host can convert to wall time without + /// hard-coding it. + pub const fn with_cycle_counter(core_frequency_hz: u32) -> Self { + Self::with_clock(TickWidth::Wrapping32, TimeBase::Cycles, core_frequency_hz) + } + + const fn with_clock(width: TickWidth, timebase: TimeBase, core_frequency_hz: u32) -> Self { Self { names: heapless::Vec::new(), - last_ticks: 0, + clock: TickClock { + width, + timebase, + core_frequency_hz, + last_raw: 0, + absolute: 0, + started: false, + }, + last_emitted: 0, sequence: 0, } } @@ -201,21 +280,20 @@ impl TraceEncoder { pub fn encode_trace_start( &mut self, timestamp_ticks: u64, - timebase: TimeBase, - core_frequency_hz: u32, source_mask: u32, out: &mut [u8], ) -> Result { + let now = self.clock.extend(timestamp_ticks); let mut frame = RawTraceFrame::new( FrameKind::TraceStart, - Self::absolute(timestamp_ticks), + now, self.next_sequence(), UNKNOWN_NAME_ID, ); - frame.timebase = timebase; - frame.core_frequency_hz = core_frequency_hz; + frame.timebase = self.clock.timebase; + frame.core_frequency_hz = self.clock.core_frequency_hz; frame.source_mask = source_mask; - self.last_ticks = timestamp_ticks; + self.last_emitted = now; encode_trace_frame(&frame, out) } @@ -270,8 +348,9 @@ impl TraceEncoder { // Signed: a negative delta is an event that was recorded before the one // encoded ahead of it. Clamping it to zero while still moving the base // backwards would leave the host's clock permanently ahead (§19.10). - let delta = Self::absolute(timestamp_ns) - Self::absolute(self.last_ticks); - self.last_ticks = timestamp_ns; + let now = self.clock.extend(timestamp_ns); + let delta = now - self.last_emitted; + self.last_emitted = now; // Forwarded, not assigned: the number was taken when the event was // recorded, upstream of the producer queue, so a frame lost there still // leaves a gap on the wire (§5.7). @@ -371,18 +450,14 @@ impl TraceEncoder { ) -> Option> { let entry = self.names.get(index)?.clone(); let name_id = u32::try_from(index + 1).ok()?; + let now = self.clock.extend(timestamp_ticks); let sequence = self.next_sequence(); - let mut frame = RawTraceFrame::new( - FrameKind::NameRegistered, - Self::absolute(timestamp_ticks), - sequence, - name_id, - ); + let mut frame = RawTraceFrame::new(FrameKind::NameRegistered, now, sequence, name_id); frame.name = entry.name; frame.source_type = entry.source_type; frame.priority = entry.priority; frame.relative_deadline_ms = entry.relative_deadline_ms; - self.last_ticks = timestamp_ticks; + self.last_emitted = now; Some(encode_trace_frame(&frame, out)) } @@ -419,33 +494,19 @@ impl TraceEncoder { .map_err(|_| RegisterError::Full)?; let id = u32::try_from(self.names.len()).map_err(|_| RegisterError::Full)?; + let now = self.clock.extend(timestamp_ns); let sequence = self.next_sequence(); - let mut frame = RawTraceFrame::new( - FrameKind::NameRegistered, - Self::absolute(timestamp_ns), - sequence, - id, - ); + let mut frame = RawTraceFrame::new(FrameKind::NameRegistered, now, sequence, id); frame.name = name.clone(); frame.source_type = source_type; frame.priority = priority; frame.relative_deadline_ms = relative_deadline_ms; - self.last_ticks = timestamp_ns; + self.last_emitted = now; let n = encode_trace_frame(&frame, out).map_err(RegisterError::Encode)?; Ok((id, n)) } - /// Device timestamps as the wire carries them. - /// - /// Saturating rather than wrapping: at nanosecond ticks `i64::MAX` is 292 - /// years of uptime and at cycles it is further still, so this cannot be - /// reached in practice — but a wrap would desynchronise the host's clock for - /// good, where a saturation only stops it advancing. - fn absolute(ticks: u64) -> i64 { - i64::try_from(ticks).unwrap_or(i64::MAX) - } - /// Takes a number for a frame the encoder generates itself — the stream /// header and the dictionary entries, which were never "recorded" and so /// have no producer-side number of their own. @@ -1082,11 +1143,10 @@ mod tests { #[test] fn trace_start_carries_the_timebase_frequency_and_mask() { - let mut enc = TraceEncoder::new(); + crate::reset_sequence(); + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - let n = enc - .encode_trace_start(1_234, TimeBase::Cycles, 72_000_000, 0b10111, &mut buf) - .unwrap(); + let n = enc.encode_trace_start(1_234, 0b10111, &mut buf).unwrap(); let frames = decode_all(&buf[..n]); assert_eq!(frames.len(), 1); assert_eq!(frames[0].kind, FrameKind::TraceStart); @@ -1098,11 +1158,84 @@ mod tests { } #[test] - fn trace_start_sets_the_base_for_the_following_delta() { + fn a_nanosecond_encoder_declares_nanoseconds_and_no_frequency() { let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; + let n = enc.encode_trace_start(1_234, 0x1F, &mut buf).unwrap(); + let frame = decode_all(&buf[..n]).remove(0); + assert_eq!(frame.timebase, TimeBase::Nanoseconds); + assert_eq!(frame.core_frequency_hz, 0); + } + + // ── A wrapping 32-bit cycle source (§5.8) ──────────────────────────────── + + #[test] + fn a_cycle_counter_wrap_is_an_ordinary_small_delta() { + // The counter rolls over every 59.65 s at 72 MHz. The encoder extends + // it, so the wire never sees the discontinuity. + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode(&span_end("a", u64::from(u32::MAX - 1_000)), &mut buf) + .unwrap(); + // 2 001 cycles later — 1 001 to roll past u32::MAX, then 1 000 more. + let (bytes, _) = encode_one(&mut enc, &span_end("a", 1_000)); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, 2_001); + } + + #[test] + fn a_wrapped_absolute_timestamp_keeps_climbing() { + // A dictionary refresh after a wrap must not send the host's clock + // backwards: the extension, not the raw counter, goes on the wire. + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; - enc.encode_trace_start(1_000, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) + enc.encode(&span_end("a", u64::from(u32::MAX - 1_000)), &mut buf) .unwrap(); + let n = enc.encode_trace_start(1_000, 0x1F, &mut buf).unwrap(); + let header = decode_all(&buf[..n]).remove(0); + assert!( + header.timestamp_ticks > i64::from(u32::MAX - 1_000), + "extended past the wrap, got {}", + header.timestamp_ticks + ); + } + + #[test] + fn a_cycle_source_still_carries_backwards_steps_exactly() { + // ISR preemption inverts a pair (§19.10). Against a wrapping source that + // is a small negative step, and must stay one rather than being read as + // a nearly-complete wrap forward. + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode(&span_end("a", 500_000), &mut buf).unwrap(); + let (bytes, _) = encode_one(&mut enc, &span_end("a", 499_000)); + assert_eq!(decode_all(&bytes)[0].timestamp_ticks, -1_000); + } + + #[test] + fn cycle_deltas_reconstruct_the_elapsed_time_across_a_wrap() { + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + let mut wire = Vec::new(); + // 100 steps of 1 000 000 cycles, starting just before the rollover. + let mut raw = u32::MAX - 50_000_000; + for _ in 0..100 { + raw = raw.wrapping_add(1_000_000); + let (bytes, _) = encode_one(&mut enc, &span_end("a", u64::from(raw))); + wire.extend_from_slice(&bytes); + } + let elapsed: i64 = decode_all(&wire).iter().map(|f| f.timestamp_ticks).sum(); + assert_eq!( + elapsed - i64::from(u32::MAX - 50_000_000), + 100_000_000, + "100 steps of a million cycles, wrap included" + ); + } + + #[test] + fn trace_start_sets_the_base_for_the_following_delta() { + let mut enc = TraceEncoder::new(); + let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; + enc.encode_trace_start(1_000, 0x1F, &mut buf).unwrap(); let (bytes, _) = encode_one(&mut enc, &span_start("a", 1_500)); let frames = decode_all(&bytes); assert_eq!(frames[0].timestamp_ticks, 1_500, "dictionary absolute"); @@ -1115,13 +1248,10 @@ mod tests { // frequency are free on the header. let mut enc = TraceEncoder::new(); let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; - let ns = enc - .encode_trace_start(0, TimeBase::Nanoseconds, 0, 0, &mut buf) - .unwrap(); + let ns = enc.encode_trace_start(0, 0, &mut buf).unwrap(); let mut enc = TraceEncoder::new(); - let cycles = enc - .encode_trace_start(0, TimeBase::Cycles, 72_000_000, 0, &mut buf) - .unwrap(); + let mut enc = TraceEncoder::with_cycle_counter(72_000_000); + let cycles = enc.encode_trace_start(0, 0, &mut buf).unwrap(); assert!(cycles > ns); } @@ -1386,9 +1516,7 @@ mod tests { let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; let mut wire = Vec::new(); - let n = enc - .encode_trace_start(1_000, TimeBase::Nanoseconds, 0, 0x1F, &mut buf) - .unwrap(); + let n = enc.encode_trace_start(1_000, 0x1F, &mut buf).unwrap(); wire.extend_from_slice(&buf[..n]); let events = [ diff --git a/src/sink.rs b/src/sink.rs index 267d805..6e69a52 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -52,7 +52,7 @@ pub trait TraceTransport { /// `TraceSink` operates in two layers: /// /// 1. **Recording layer** — `record_span_start`, `record_span_end`, and `record_marker` -/// read the hardware clock via `get_elapsed_nanoseconds`, construct [`TraceEvent`]s +/// read the hardware clock via `now_ticks`, construct [`TraceEvent`]s /// (sequence left at zero), and hand them to [`TraceTransport::write_event`]. /// 2. **Transport layer** — code that owns the wire (e.g. [`crate::SequenceEncoder`]) injects a /// monotonic sequence counter before writing bytes to RTT, UART, etc. The sequence @@ -61,12 +61,44 @@ pub trait TraceTransport { /// For tests or placeholders, use [`NoopSink`], which discards all events at zero cost. #[cfg(feature = "enabled")] pub trait TraceSink: TraceTransport { - /// Returns the current monotonic time in nanoseconds. + /// Returns the current time as a tick count. + /// + /// The unit is whatever the encoder for this stream declares on its header + /// — nanoseconds by default, or raw core cycles for a sink using + /// [`TraceEncoder::with_cycle_counter`]. A cycle source may be a + /// free-running 32-bit counter returned widened: the encoder extends it, so + /// that this can be a single volatile load with no critical section and no + /// division on the path an ISR takes (EXEC-TRACE-002 §5.8). /// /// This method has no default — every `TraceSink` implementor must wire up a real clock /// source. Returning a constant `0` is valid for stubs, but must be done explicitly to /// avoid silent zero timestamps in production code. - fn get_elapsed_nanoseconds(&self) -> u64; + /// + /// [`TraceEncoder::with_cycle_counter`]: crate::TraceEncoder::with_cycle_counter + fn now_ticks(&self) -> u64; + + /// How many ticks make a microsecond in this sink's timebase. + /// + /// Defaults to a nanosecond tick. A sink returning core cycles reports its + /// core clock in MHz — 72 on this target. Used to turn a measured interval + /// into the microseconds a marker payload carries. + fn ticks_per_us(&self) -> u32 { + 1_000 + } + + /// The significant bits of [`now_ticks`](TraceSink::now_ticks). + /// + /// Defaults to the full 64 bits. A sink returning a free-running 32-bit + /// counter reports `u32::MAX`, so that an interval measured across the + /// counter's wrap still comes out right. + fn tick_mask(&self) -> u64 { + u64::MAX + } + + /// Ticks elapsed from `started` to now, correct across a counter wrap. + fn ticks_since(&self, started: u64) -> u64 { + self.now_ticks().wrapping_sub(started) & self.tick_mask() + } /// Records the start of a named execution span. /// @@ -94,7 +126,7 @@ pub trait TraceSink: TraceTransport { name.push_str(source_name) .map_err(|_| TracingError::MessageDropped)?; let mut event = TraceEvent::SpanStart { - timestamp_ns: self.get_elapsed_nanoseconds(), + timestamp_ns: self.now_ticks(), name, source_type, sequence: 0, @@ -124,7 +156,7 @@ pub trait TraceSink: TraceTransport { name.push_str(source_name) .map_err(|_| TracingError::MessageDropped)?; let mut event = TraceEvent::SpanEnd { - timestamp_ns: self.get_elapsed_nanoseconds(), + timestamp_ns: self.now_ticks(), name, sequence: 0, }; @@ -152,7 +184,7 @@ pub trait TraceSink: TraceTransport { name.push_str(label) .map_err(|_| TracingError::MessageDropped)?; let mut event = TraceEvent::Marker { - timestamp_ns: self.get_elapsed_nanoseconds(), + timestamp_ns: self.now_ticks(), name, sequence: 0, marker_value: value, @@ -213,7 +245,7 @@ impl TraceTransport for NoopSink { #[cfg(feature = "enabled")] impl TraceSink for NoopSink { - fn get_elapsed_nanoseconds(&self) -> u64 { + fn now_ticks(&self) -> u64 { 0 } } @@ -254,7 +286,7 @@ mod tests { } impl TraceSink for CaptureSink { - fn get_elapsed_nanoseconds(&self) -> u64 { + fn now_ticks(&self) -> u64 { self.timestamp_ns } } @@ -268,7 +300,7 @@ mod tests { } impl TraceSink for ErrorSink { - fn get_elapsed_nanoseconds(&self) -> u64 { + fn now_ticks(&self) -> u64 { 0 } } From 7ca50c71a3f5d538f0ab6e4c0879ee7bf2ef85eb Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sun, 13 Sep 2026 22:44:12 +0200 Subject: [PATCH 07/16] =?UTF-8?q?Render=20trace=20gaps=20and=20cut=20the?= =?UTF-8?q?=20spans=20they=20interrupt=20(=C2=A75.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lost frame used to be invisible except as damage: the SPAN_END that went with it never arrived, so the span stayed open until the next unrelated end for that name and drew one enormous bar across the gap and past it. The decoder now closes every span that was open at the hole, marks it `interrupted`, and discards the first SPAN_END that arrives for such a name afterwards rather than pairing it with whatever opens next — which would invent a span that never ran. The hole itself becomes a GapRecord carrying the frame count, written as a `gap` row and drawn as a hatched band across every lane with the count in the label and the tooltip. Two details that are load-bearing: - The spans to cut are those open *at the hole*, not those open when the hole is detected. The reorder window of 64 means those differ, and a span that started after the hole cannot have been affected by it. A test caught this interrupting a healthy gyro_isr span. - The gap is placed from the timestamps either side of the hole, which the stream state now keeps a short history of, not from whichever frame happened to trigger the report. On a real trace that is tens of milliseconds of difference. An interrupted span's end is a lower bound, not a measurement, so it is exempt from the deadline check and may equal its start; the CSV validator accepts a zero-length span only when it is marked interrupted, and such bars get a minimum render width so they stay visible. etrace-decode now writes gap rows too — without them the CSV shows an unexplained hole and cut spans with no reason beside them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D41mVCmySyLh1Mfn8a2tmK --- python/src/execution_trace/__init__.py | 4 +- python/src/execution_trace/_cli/decode.py | 13 +- python/src/execution_trace/decode.py | 148 +++++++++++++++++++++- python/src/execution_trace/diagram.py | 98 ++++++++++++-- python/src/execution_trace/stream.py | 9 ++ python/tests/test_decode.py | 98 ++++++++++++++ python/tests/test_diagram.py | 88 +++++++++++++ 7 files changed, 440 insertions(+), 18 deletions(-) diff --git a/python/src/execution_trace/__init__.py b/python/src/execution_trace/__init__.py index 1328938..803ac98 100644 --- a/python/src/execution_trace/__init__.py +++ b/python/src/execution_trace/__init__.py @@ -4,7 +4,7 @@ from execution_trace import ( SequenceTracker, iter_frames, encode_varint, decode_varint, - TraceEvent, MarkerRecord, NameEntry, TraceEventBuffer, TraceStreamState, + TraceEvent, MarkerRecord, GapRecord, NameEntry, TraceEventBuffer, TraceStreamState, decode_tracing_stream, write_tracing_csv, ) @@ -17,6 +17,7 @@ from execution_trace.decode import ( SEQUENCE_MODULUS, + GapRecord, MarkerRecord, NameEntry, TraceEvent, @@ -39,6 +40,7 @@ "decode_varint", "TraceEvent", "MarkerRecord", + "GapRecord", "NameEntry", "TraceEventBuffer", "TraceStreamState", diff --git a/python/src/execution_trace/_cli/decode.py b/python/src/execution_trace/_cli/decode.py index 1743a5c..1246bfd 100644 --- a/python/src/execution_trace/_cli/decode.py +++ b/python/src/execution_trace/_cli/decode.py @@ -13,9 +13,12 @@ import argparse import sys from pathlib import Path -from typing import NoReturn +from typing import NoReturn, Union from execution_trace.decode import ( + GapRecord, + MarkerRecord, + TraceEvent, TraceEventBuffer, TraceStreamState, decode_tracing_stream, @@ -65,7 +68,13 @@ def main() -> None: decode_tracing_stream(buf, state, event_buffer) event_buffer.flush_pending() - records = event_buffer.records + event_buffer.markers + # Gap rows carry the frames the link lost; without them the CSV shows an + # unexplained hole and interrupted spans with no reason beside them. + records: list[Union[TraceEvent, MarkerRecord, GapRecord]] = [ + *event_buffer.records, + *event_buffer.markers, + *event_buffer.gaps, + ] csv_path = write_tracing_csv(records, output_dir=output_dir) if csv_path is None: diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index c204999..687b70c 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -26,9 +26,11 @@ ) """ +import collections import csv import logging import os +from collections.abc import Sequence from dataclasses import dataclass, field from datetime import datetime from typing import Optional, Union @@ -49,6 +51,10 @@ UNKNOWN_NAME: str = "" +# How many recent frames to remember the arrival time of, for placing a hole the +# reorder window only reveals later. Comfortably more than REORDER_WINDOW. +SEEN_HISTORY: int = 512 + # How far a frame may arrive ahead of a missing one before that one is called # lost (EXEC-TRACE-002 §5.7, §19.12). # @@ -95,6 +101,32 @@ class TraceEvent: priority: int = 0 deadline_us: Optional[float] = None value: Optional[int] = None + #: The span was cut short by a gap rather than closed by its own ``SPAN_END`` + #: — its real end is unknown and at least ``end_us`` (§5.9). + interrupted: bool = False + + +@dataclass +class GapRecord: + """A stretch of the trace where frames were lost (EXEC-TRACE-002 §5.9). + + Emitted as a row of its own so that missing data *looks* missing. Silent + truncation is what made a 2 % frame-loss rate read as a broken instrument + (§2.5). + + Attributes: + name: Fixed label, so the row reads for itself in the CSV. + type: Always ``"gap"``. + start_us: Timestamp of the last frame received before the hole. + end_us: Timestamp of the first frame received after it. + frames_lost: How many frames the sequence says are missing. + """ + + start_us: float + end_us: float + frames_lost: int + name: str = "trace gap" + type: str = field(default="gap", init=False) @dataclass @@ -175,6 +207,11 @@ def __init__(self, label: str = "Trace event") -> None: self._warned_ids: set[int] = set() #: Events dropped because their name id could not be resolved. self.unresolved_events: int = 0 + # sequence -> absolute timestamp, for the most recent frames. A hole is + # reported up to REORDER_WINDOW frames after it happened, so locating it + # in time means looking up the numbers either side of it rather than + # using whatever frame happened to trigger the report. + self._seen_at: collections.OrderedDict[int, float] = collections.OrderedDict() def reset(self) -> None: """Discard all per-stream state after a device reset. @@ -225,6 +262,27 @@ def advance(self, frame: tracing_pb2.TraceFrame) -> float: self.clock_ticks += frame.timestamp_ticks return self.ticks_to_us(self.clock_ticks) + def note_seen(self, sequence: int, timestamp_us: float) -> None: + """Remember when a numbered frame arrived, for locating a later-found hole.""" + self._seen_at[sequence] = timestamp_us + while len(self._seen_at) > SEEN_HISTORY: + self._seen_at.popitem(last=False) + + def gap_bounds(self, first_missing: int, count: int) -> Optional[tuple[float, float]]: + """Timestamps bracketing a hole, from the frames either side of it. + + Returns ``None`` when neither neighbour is still in history, which leaves + the gap unplaceable — it is then reported in the log only. + """ + before = self._seen_at.get((first_missing - 1) % SEQUENCE_MODULUS) + after = self._seen_at.get((first_missing + count) % SEQUENCE_MODULUS) + if before is None and after is None: + return None + start = before if before is not None else after + end = after if after is not None else before + assert start is not None and end is not None # noqa: S101 - narrowing for mypy + return (start, max(end, start)) + def resolve(self, name_id: int) -> Optional[NameEntry]: """Look up a dictionary id. @@ -268,6 +326,11 @@ def __init__(self) -> None: self._pending: dict[str, tuple[float, NameEntry]] = {} self._records: list[TraceEvent] = [] self._markers: list[MarkerRecord] = [] + self._gaps: list[GapRecord] = [] + # Names whose span a gap cut short. The next SPAN_END for each is the + # other half of a pair whose START is already closed, so it is dropped + # rather than paired with a later, unrelated START (§5.9). + self._orphaned_ends: set[str] = set() def push( self, @@ -288,6 +351,11 @@ def push( logger.warning("Tracing: duplicate START for '%s' — discarding previous", name) self._pending[name] = (timestamp_us, entry) elif frame.event_type == tracing_pb2.SPAN_END: + if name in self._orphaned_ends: + # Its START was closed by interrupt_open_spans; pairing this with + # whatever opens next would invent a span that never ran. + self._orphaned_ends.discard(name) + return if name not in self._pending: logger.warning("Tracing: END for '%s' with no matching START — discarding", name) return @@ -314,6 +382,55 @@ def push( MarkerRecord(name=name, timestamp_us=timestamp_us, value=value) ) + def interrupt_open_spans(self, at_us: float, frames_lost: int, resumed_us: float) -> None: + """Close every span open at a gap, and record the gap itself. + + A span whose ``SPAN_END`` was lost would otherwise stay open until the + next unrelated end for that name, producing one enormous bar across the + hole and well past it — which is the visual damage §2.5 describes. Each + open span is instead cut at the last frame before the gap and marked + interrupted, so the diagram shows a span of unknown length rather than a + wrong one. + + Args: + at_us: Timestamp of the last frame received before the hole. + frames_lost: How many frames the sequence says are missing. + resumed_us: Timestamp of the first frame received after it. + """ + self._gaps.append( + GapRecord(start_us=at_us, end_us=max(resumed_us, at_us), frames_lost=frames_lost) + ) + # Only spans that were already running when the hole happened. The + # reorder window means this is discovered up to 64 frames later, by which + # time other spans have opened — and one that started after the hole + # cannot have been affected by it. + caught = [ + (name, value) for name, value in self._pending.items() if value[0] <= at_us + ] + for name, (start_us, entry) in caught: + self._records.append( + TraceEvent( + name=name, + type="isr" if entry.source_type == tracing_pb2.ISR else "task", + start_us=start_us, + end_us=at_us, + priority=entry.priority, + deadline_us=( + start_us + entry.relative_deadline_ms * 1_000.0 + if entry.relative_deadline_ms is not None + else None + ), + interrupted=True, + ) + ) + self._orphaned_ends.add(name) + del self._pending[name] + + @property + def gaps(self) -> list[GapRecord]: + """Gap records in arrival order.""" + return list(self._gaps) + def flush_pending(self) -> None: """Warn about any unmatched ``SPAN_START`` events and clear pending state. @@ -398,6 +515,16 @@ def decode_tracing_stream( # would misalign the rest of the stream permanently. timestamp_us = state.advance(frame) + state.note_seen(frame.sequence, timestamp_us) + + # A hole the tracker has just become certain of. Close whatever was open + # across it, so a span whose end was lost does not run on for ever, and + # record the gap as a row of its own (§5.9). + if state.tracker.last_gap is not None: + first_missing, lost = state.tracker.last_gap + bounds = state.gap_bounds(first_missing, lost) + if bounds is not None: + event_buffer.interrupt_open_spans(bounds[0], lost, bounds[1]) if frame.event_type == tracing_pb2.TRACE_START: state.timebase = frame.timebase @@ -434,7 +561,7 @@ def decode_tracing_stream( def write_tracing_csv( - records: list[Union[TraceEvent, MarkerRecord]], + records: Sequence[Union[TraceEvent, MarkerRecord, GapRecord]], output_dir: str = "data", ) -> Optional[str]: """Write span and marker records to a timestamped CSV file. @@ -459,7 +586,10 @@ def write_tracing_csv( +---------------+------------------------------------------+ | ``deadline_us``| absolute deadline in µs, or empty | +---------------+------------------------------------------+ - | ``value`` | optional u32 payload, or empty | + | ``value`` | optional u32 payload; frames lost on a | + | | ``gap`` row, or empty | + +---------------+------------------------------------------+ + | ``interrupted``| ``1`` when a gap cut the span short | +---------------+------------------------------------------+ Args: @@ -488,19 +618,29 @@ def _optional(field: float | int | None) -> float | int | str: with open(path, "w", newline="") as f: writer = csv.writer(f) - writer.writerow(["name", "type", "start_us", "end_us", "priority", "deadline_us", "value"]) + writer.writerow([ + "name", "type", "start_us", "end_us", "priority", "deadline_us", "value", + "interrupted", + ]) for r in records: if isinstance(r, MarkerRecord): writer.writerow([ r.name, "marker", r.timestamp_us, r.timestamp_us, - 0, "", _optional(r.value), + 0, "", _optional(r.value), 0, + ]) + elif isinstance(r, GapRecord): + writer.writerow([ + r.name, "gap", + r.start_us, r.end_us, + 0, "", r.frames_lost, 0, ]) else: writer.writerow([ r.name, r.type, r.start_us, r.end_us, r.priority, _optional(r.deadline_us), _optional(r.value), + int(r.interrupted), ]) # Atomically replace the symlink so trace_latest.csv always points to newest. diff --git a/python/src/execution_trace/diagram.py b/python/src/execution_trace/diagram.py index 6174cac..707f5a6 100644 --- a/python/src/execution_trace/diagram.py +++ b/python/src/execution_trace/diagram.py @@ -194,9 +194,23 @@ def _coerce_numeric(df: "pd.DataFrame", col: str, *, required: bool = False) -> if len(df) == 0: die("tracing CSV contains no data rows") - # Markers have start_us == end_us == timestamp_us; only validate spans. + if "interrupted" in df.columns: + df["interrupted"] = ( + pd.to_numeric(df["interrupted"], errors="coerce").fillna(0).astype(bool) + ) + else: + df["interrupted"] = False + + # Markers have start_us == end_us == timestamp_us, and a gap row is a band + # rather than an interval that ran; only validate spans. spans = df[df["type"].isin(["task", "isr"])] - bad = spans[spans["end_us"] <= spans["start_us"]] + # An interrupted span is cut at the last frame before a gap, which can be the + # very frame that opened it — so it may be zero-length. Its end is a lower + # bound on when it finished, not a measurement, and zero is a true one. + bad = spans[ + (spans["end_us"] < spans["start_us"]) + | ((spans["end_us"] == spans["start_us"]) & ~spans["interrupted"]) + ] if not bad.empty: csv_row = bad.index[0] + 2 die(f"row {csv_row}: end_us must be > start_us") @@ -210,6 +224,9 @@ def _coerce_numeric(df: "pd.DataFrame", col: str, *, required: bool = False) -> df["missed"] = ( df["effective_deadline_us"].notna() & (df["end_us"] > df["effective_deadline_us"]) + # An interrupted span's end is unknown and only a lower bound, so it + # cannot be said to have overrun (§5.9). + & ~df["interrupted"] ) return df.reset_index(drop=True) @@ -225,7 +242,8 @@ def assign_lanes(df: "pd.DataFrame") -> "tuple[pd.DataFrame, list[str]]": Marker rows are assigned to the lane of the highest-priority span that contains the marker's timestamp. Uncontained markers receive lane ``-1`` - (rendered below all spans). + (rendered below all spans). Gap rows span every lane and are carried through + with lane ``-1`` as well. Args: df: Validated DataFrame from :func:`load_traces` (spans and markers). @@ -237,6 +255,7 @@ def assign_lanes(df: "pd.DataFrame") -> "tuple[pd.DataFrame, list[str]]": """ spans = df[df["type"].isin(["task", "isr"])].copy() markers = df[df["type"] == "marker"].copy() + gaps = df[df["type"] == "gap"].copy() name_type = spans.groupby("name", sort=False)["type"].first() name_priority = spans.groupby("name", sort=False)["priority"].first() @@ -267,7 +286,10 @@ def _infer_lane(ts: float) -> int: markers = markers.copy() markers["lane"] = markers["start_us"].apply(_infer_lane) - df = pd.concat([spans, markers], ignore_index=True) if not markers.empty else spans + if not gaps.empty: + gaps["lane"] = -1 + parts = [part for part in (spans, markers, gaps) if not part.empty] + df = pd.concat(parts, ignore_index=True) if len(parts) > 1 else parts[0] return df, top_to_bottom @@ -309,10 +331,19 @@ def _fmt_ms(v: float) -> str: return "—" if pd.isna(v) else f"{v / 1_000:,.3f} ms" -def _bar_source(sub: "pd.DataFrame", colors: dict[str, str]) -> ColumnDataSource: +def _bar_source( + sub: "pd.DataFrame", colors: dict[str, str], min_width: float = 0.0 +) -> ColumnDataSource: + # An interrupted span can be zero-length; without a floor it would vanish, + # which is the opposite of what §5.9 is for. + right = sub["end_us"] if min_width <= 0 else ( + sub[["end_us", "start_us"]].max(axis=1).combine( + sub["start_us"] + min_width, max + ) + ) return ColumnDataSource({ "left": sub["start_us"].tolist(), - "right": sub["end_us"].tolist(), + "right": right.tolist(), "top": (sub["lane"] + _HALF_H).tolist(), "bottom": (sub["lane"] - _HALF_H).tolist(), "name": sub["name"].tolist(), @@ -346,6 +377,7 @@ def build_figure( """ spans = df[df["type"].isin(["task", "isr"])] markers = df[df["type"] == "marker"] + gaps = df[df["type"] == "gap"] n = len(lane_names) x_lo = float(spans["start_us"].min()) @@ -373,6 +405,42 @@ def build_figure( fill_alpha=0.55, line_color=None, ) + # Gap bands go down before the bars so the spans either side stay legible on + # top of them. A trace that is missing data has to look like one (§5.9). + if not gaps.empty: + # A gap of zero width would be invisible; give it a hairline so the + # annotation still has something to sit on. + min_w = max((x_hi - x_lo) * 0.0008, 1.0) + widths = (gaps["end_us"] - gaps["start_us"]).clip(lower=min_w) + gap_src = ColumnDataSource({ + "left": gaps["start_us"].tolist(), + "right": (gaps["start_us"] + widths).tolist(), + "bottom": [-0.6] * len(gaps), + "top": [n - 0.4] * len(gaps), + "frames_lost": [ + int(v) if pd.notna(v) else 0 for v in gaps["value"] + ], + "start_us": gaps["start_us"].tolist(), + }) + gap_renderer = p.quad( + left="left", right="right", bottom="bottom", top="top", + source=gap_src, + fill_color="#b71c1c", fill_alpha=0.13, + hatch_pattern="/", hatch_color="#b71c1c", hatch_alpha=0.45, hatch_scale=12, + line_color="#b71c1c", line_width=1.0, line_alpha=0.55, + legend_label="Trace gap", + ) + p.add_tools(HoverTool( + renderers=[gap_renderer], + tooltips=[("trace gap", "@frames_lost frames lost"), ("at", "@start_us{0,0.0} µs")], + )) + labels = LabelSet( + x="start_us", y=n - 0.45, text="frames_lost", + source=gap_src, text_color="#b71c1c", text_font_size="9pt", + x_offset=3, y_offset=-12, + ) + p.add_layout(labels) + span_renderers = [] marker_renderers = [] @@ -382,11 +450,12 @@ def _add_bars( line_color: str, line_width: float, legend_label: str, + min_width: float = 0.0, ) -> None: sub = spans[mask] if sub.empty: return - src = _bar_source(sub, colors) + src = _bar_source(sub, colors, min_width=min_width) hatch_kw = ( dict(hatch_pattern=hatch, hatch_color="white", hatch_alpha=0.40, hatch_scale=9) if hatch is not None else {} @@ -400,14 +469,21 @@ def _add_bars( ) span_renderers.append(r) - _add_bars((spans["type"] == "task") & ~spans["missed"], + cut = spans["interrupted"] + _add_bars((spans["type"] == "task") & ~spans["missed"] & ~cut, None, "#555555", _NORMAL_LW, "Task") - _add_bars((spans["type"] == "isr") & ~spans["missed"], + _add_bars((spans["type"] == "isr") & ~spans["missed"] & ~cut, "/", "#555555", _NORMAL_LW, "ISR") - _add_bars((spans["type"] == "task") & spans["missed"], + _add_bars((spans["type"] == "task") & spans["missed"] & ~cut, None, "crimson", _MISSED_LW, "Missed deadline") - _add_bars((spans["type"] == "isr") & spans["missed"], + _add_bars((spans["type"] == "isr") & spans["missed"] & ~cut, "/", "crimson", _MISSED_LW, "ISR — missed deadline") + # Cut short by a gap: the end is a lower bound, not a measurement, so it is + # drawn open-ended rather than as a span of that length. + _add_bars( + cut, "x", "#b71c1c", _MISSED_LW, "Interrupted by gap", + min_width=max((x_hi - x_lo) * 0.0008, 1.0), + ) dl = spans[spans["effective_deadline_us"].notna()].copy() if not dl.empty: diff --git a/python/src/execution_trace/stream.py b/python/src/execution_trace/stream.py index 767b0f1..5dfaa37 100644 --- a/python/src/execution_trace/stream.py +++ b/python/src/execution_trace/stream.py @@ -65,6 +65,10 @@ class SequenceTracker: Attributes: dropped: Running total of frames reported lost. + last_gap: ``(first_missing, count)`` for the hole the most recent + :meth:`observe` declared, or ``None`` if it declared none. Lets a + caller locate the hole in its own terms — the numbers either side of + it were received, so their timestamps bound it. Raises: ValueError: If *modulus* is not a positive power of two, which the @@ -93,6 +97,7 @@ def __init__( # Numbers seen ahead of _expected, still within the window. self._pending: set[int] = set() self.dropped = 0 + self.last_gap: tuple[int, int] | None = None @property def _last(self) -> int | None: @@ -102,6 +107,8 @@ def _last(self) -> int | None: def observe(self, sequence: int) -> bool: """Record a sequence number and detect resets or drops. + Sets :attr:`last_gap` to the hole this call declared, if any. + Args: sequence: The sequence number from the received frame. @@ -110,6 +117,7 @@ def observe(self, sequence: int) -> bool: backwards), ``False`` otherwise — including for ordinary loss, which is logged and counted rather than signalled. """ + self.last_gap = None if self._expected is None: self._expected = (sequence + 1) & self._mask return False @@ -147,6 +155,7 @@ def observe(self, sequence: int) -> bool: ) if missing > 0: self.dropped += missing + self.last_gap = (expected, missing) logger.warning( "%s drop detected: expected #%d, got #%d (%d dropped)", self._label, expected, sequence, missing, diff --git a/python/tests/test_decode.py b/python/tests/test_decode.py index 78c59f8..6522e5b 100644 --- a/python/tests/test_decode.py +++ b/python/tests/test_decode.py @@ -10,6 +10,7 @@ from execution_trace._proto import tracing_pb2 from execution_trace.decode import ( + GapRecord, REORDER_WINDOW, RESET_BACKWARD_MARGIN_US, SEQUENCE_MODULUS, @@ -642,3 +643,100 @@ def test_span_row_values(self, tmp_path): assert float(row["end_us"]) == pytest.approx(2000.0) assert row["priority"] == "4" assert float(row["deadline_us"]) == pytest.approx(5000.0) + + +# --------------------------------------------------------------------------- +# Gap handling (§5.9) +# --------------------------------------------------------------------------- + +class TestGapHandling: + """AC 8: a stream with an injected gap renders every span open at the gap as + interrupted, and the gap as an annotated band.""" + + def _stream_with_lost_span_end(self) -> tuple[TraceStreamState, TraceEventBuffer]: + b = StreamBuilder().start(timestamp_ticks=0) + t = 1_000_000 + for _ in range(5): + b.span("main_task", t, t + 400_000, priority=4) + t += 1_000_000 + # A cycle whose SPAN_END never reached the transport: its number is + # burned, which is what makes the loss visible at all (§5.7). + b.event(tracing_pb2.SPAN_START, "main_task", t, priority=4) + b._next_sequence() + t += 1_000_000 + # Enough traffic afterwards to carry past the reorder window. + for _ in range(REORDER_WINDOW + 4): + b.span("gyro_isr", t, t + 40_000, source_type=tracing_pb2.ISR, priority=8) + t += 1_000_000 + state, event_buffer = TraceStreamState("gap"), TraceEventBuffer() + decode_tracing_stream(b.bytes(), state, event_buffer) + return state, event_buffer + + def test_a_gap_is_recorded_with_its_frame_count(self): + state, eb = self._stream_with_lost_span_end() + assert state.tracker.dropped == 1 + assert len(eb.gaps) == 1 + gap = eb.gaps[0] + assert isinstance(gap, GapRecord) + assert gap.frames_lost == 1 + assert gap.type == "gap" + assert gap.end_us >= gap.start_us + + def test_the_gap_is_placed_between_the_frames_either_side_of_it(self): + # Not where the reorder window happened to notice: that is up to 64 + # frames later, which on this trace would be tens of milliseconds off. + state, eb = self._stream_with_lost_span_end() + gap = eb.gaps[0] + assert gap.start_us == pytest.approx(6_000.0), "the lost END's own SPAN_START" + assert gap.end_us == pytest.approx(7_000.0), "the next frame that did arrive" + + def test_the_span_open_at_the_gap_is_interrupted(self): + state, eb = self._stream_with_lost_span_end() + cut = [r for r in eb.records if r.interrupted] + assert len(cut) == 1 + assert cut[0].name == "main_task" + assert cut[0].start_us == pytest.approx(6_000.0) + assert cut[0].end_us == pytest.approx(6_000.0), "cut at the last good frame" + + def test_an_uninterrupted_span_is_untouched(self): + state, eb = self._stream_with_lost_span_end() + whole = [r for r in eb.records if not r.interrupted and r.name == "main_task"] + assert len(whole) == 5 + assert all(r.end_us - r.start_us == pytest.approx(400.0) for r in whole) + + def test_the_first_end_after_the_gap_is_discarded_not_paired(self): + # Otherwise it closes whatever START opens next and invents a span that + # never ran — the damage §5.9 exists to prevent. + b = StreamBuilder().start(timestamp_ticks=0) + b.event(tracing_pb2.SPAN_START, "main_task", 1_000_000, priority=4) + b._next_sequence() # a frame lost at the queue + for i in range(REORDER_WINDOW + 4): + b.span("gyro_isr", 2_000_000 + i * 1_000_000, + 2_040_000 + i * 1_000_000, + source_type=tracing_pb2.ISR, priority=8) + # main_task's END finally arrives, long after its START was closed. + b.event(tracing_pb2.SPAN_END, "main_task", 90_000_000) + state, eb = TraceStreamState("gap"), TraceEventBuffer() + decode_tracing_stream(b.bytes(), state, eb) + + main = [r for r in eb.records if r.name == "main_task"] + assert len(main) == 1, "the orphaned END must not produce a second span" + assert main[0].interrupted + assert main[0].end_us == pytest.approx(1_000.0) + + def test_a_clean_stream_records_no_gap(self, builder): + builder.span("t", 0, 1_000) + builder.span("t", 2_000, 3_000) + _, eb = _decode(builder.bytes()) + assert eb.gaps == [] + assert not any(r.interrupted for r in eb.records) + + def test_gap_rows_reach_the_csv(self, tmp_path): + _, eb = self._stream_with_lost_span_end() + path = write_tracing_csv(eb.records + eb.markers + eb.gaps, output_dir=str(tmp_path)) + assert path is not None + rows = list(csv.DictReader(open(path))) + gaps = [r for r in rows if r["type"] == "gap"] + assert len(gaps) == 1 + assert gaps[0]["value"] == "1", "frames lost travels in the value column" + assert any(r["interrupted"] == "1" for r in rows) diff --git a/python/tests/test_diagram.py b/python/tests/test_diagram.py index 7715930..b995472 100644 --- a/python/tests/test_diagram.py +++ b/python/tests/test_diagram.py @@ -179,3 +179,91 @@ def test_raises_diagram_error_on_missing_csv(self, tmp_path): out = tmp_path / "diagram.html" with pytest.raises(DiagramError): generate_diagram(csv_path=tmp_path / "nosuch.csv", output_path=out) + + +# --------------------------------------------------------------------------- +# Gap rendering (§5.9, AC 8) +# --------------------------------------------------------------------------- + +HEADER = "name,type,start_us,end_us,priority,deadline_us,value,interrupted\n" + + +def _gap_csv(tmp_path: Path) -> Path: + p = tmp_path / "gap.csv" + p.write_text( + HEADER + + "main_task,task,1000.0,1400.0,4,,,0\n" + + "main_task,task,2000.0,2000.0,4,,,1\n" # interrupted by the gap + + "trace gap,gap,2000.0,3000.0,0,,7,0\n" + + "main_task,task,3000.0,3400.0,4,,,0\n" + + "gyro_isr,isr,3100.0,3140.0,8,,,0\n" + ) + return p + + +class TestGapRendering: + def test_a_gap_row_loads_without_being_treated_as_a_span(self, tmp_path): + pytest.importorskip("pandas") + from execution_trace.diagram import load_traces + + df = load_traces(_gap_csv(tmp_path)) + gaps = df[df["type"] == "gap"] + assert len(gaps) == 1 + assert gaps.iloc[0]["value"] == 7 + + def test_a_zero_length_interrupted_span_is_accepted(self, tmp_path): + # Its end is a lower bound on when it finished, not a measurement, and a + # span cut at the frame that opened it is genuinely zero-length. + pytest.importorskip("pandas") + from execution_trace.diagram import load_traces + + df = load_traces(_gap_csv(tmp_path)) + cut = df[df["interrupted"]] + assert len(cut) == 1 + assert cut.iloc[0]["start_us"] == cut.iloc[0]["end_us"] + + def test_a_zero_length_span_that_is_not_interrupted_is_still_rejected(self, tmp_path): + pytest.importorskip("pandas") + from execution_trace.diagram import DiagramError, load_traces + + p = tmp_path / "bad.csv" + p.write_text(HEADER + "main_task,task,1000.0,1000.0,4,,,0\n") + with pytest.raises(DiagramError, match="end_us must be"): + load_traces(p) + + def test_an_interrupted_span_cannot_miss_its_deadline(self, tmp_path): + # Its end is unknown, so calling it an overrun would be an invention. + pytest.importorskip("pandas") + from execution_trace.diagram import load_traces + + p = tmp_path / "dl.csv" + p.write_text( + HEADER + + "main_task,task,1000.0,1200.0,4,1100.0,,1\n" + + "main_task,task,2000.0,2200.0,4,2100.0,,0\n" + ) + df = load_traces(p) + assert list(df["missed"]) == [False, True] + + def test_gaps_keep_their_own_lane_slot(self, tmp_path): + pytest.importorskip("pandas") + from execution_trace.diagram import assign_lanes, load_traces + + df, lanes = assign_lanes(load_traces(_gap_csv(tmp_path))) + assert set(lanes) == {"main_task", "gyro_isr"} + gaps = df[df["type"] == "gap"] + assert len(gaps) == 1, "the gap row survives lane assignment" + assert gaps.iloc[0]["lane"] == -1, "it spans every lane, so it owns none" + + def test_the_diagram_renders_the_band_and_the_interrupted_span(self, tmp_path): + pytest.importorskip("bokeh") + pytest.importorskip("pandas") + from execution_trace.diagram import generate_diagram + + out = tmp_path / "gap.html" + result = generate_diagram(_gap_csv(tmp_path), output_path=out) + assert result.output_path == out + html = out.read_text() + assert "Trace gap" in html, "the band needs a legend entry" + assert "Interrupted by gap" in html + assert "frames lost" in html, "the band is annotated with the count" From c8d7fdf7c8c1b82dfbec32c71c0cbadb1a5819c5 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Mon, 14 Sep 2026 18:07:18 +0200 Subject: [PATCH 08/16] Size the name registry to the names that exist, not to 64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under flip-link every byte of .bss is a byte the stack does not get, and the firmware had just hard-faulted on exception entry for want of them (EXEC-TRACE-002 §23). This array is the largest thing the trace format puts in RAM: 64 entries of ~52 B, against 27 distinct names the firmware actually registers. 40 keeps half again as many entries as anything uses and gives 1 248 B back to the stack. TracingNameRegistryFull and UNKNOWN_NAME_ID (REQ-T11) still guard the other side, so overshooting the capacity degrades the trace rather than breaking it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wox8fFjuR68NPpd6sc4dLR --- src/encode.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/encode.rs b/src/encode.rs index e3f99f0..e2f7cd8 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -19,8 +19,14 @@ pub const MAX_TRACE_FRAME_SIZE: usize = 128; /// frame ahead of the event when a name is seen for the first time. pub const MAX_TRACE_BURST_SIZE: usize = 2 * MAX_TRACE_FRAME_SIZE; -/// Distinct names the dictionary holds, against roughly 30 in the firmware today. -pub const NAME_REGISTRY_CAPACITY: usize = 64; +/// Distinct names the dictionary holds, against 27 in the firmware today. +/// +/// Sized close to that count on purpose. Each entry is ~52 B, so this array is +/// the single largest thing the trace format puts in RAM — and under flip-link +/// every byte of it comes out of the stack, which EXEC-TRACE-002 §23 found had +/// no room left to give. Raising it is not free; `TracingNameRegistryFull` and +/// `UNKNOWN_NAME_ID` (REQ-T11) are what guard the other side. +pub const NAME_REGISTRY_CAPACITY: usize = 40; /// The reserved "unknown" name id, emitted when the registry is full (REQ-T11). pub const UNKNOWN_NAME_ID: u32 = 0; From 134c8ffccb1657173c58547739e2052c5c897e47 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 15:12:25 +0200 Subject: [PATCH 09/16] Document the v2 format for the people it breaks 0.2.0 changes the wire format, and nothing outside the commit messages said so. Both packages now carry a changelog whose first line is the incompatibility: a v2 stream needs a v2 decoder either way round, and a recording must be read by the version that made it. The Python README had drifted past being merely stale into being wrong. Its quick start passed a SequenceTracker where decode_tracing_stream now takes a TraceStreamState, so the first thing a new user copies could not run; the CSV table was missing `interrupted` and the `gap` row; and the wire-format section still described each frame as a self-contained TraceEvent, which is the one thing v2 stopped being. That section is the reason the decoder holds per-connection state, so leaving it wrong makes TraceStreamState look like ceremony. Two rustdoc links pointed at names the rewrite removed and rendered as plain text on docs.rs. The one in sink.rs was stale twice over: it credited a `SequenceEncoder` with injecting sequence numbers, when increment 5 moved numbering to the recording layer precisely so that a loss at the producer queue leaves a visible hole. The crate's `include` gains CHANGELOG.md, which is otherwise the one artifact of this commit that would not ship. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 128 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- README.md | 2 +- python/CHANGELOG.md | 103 ++++++++++++++++++++++++++++++++--- python/README.md | 59 +++++++++++++++----- src/sink.rs | 13 +++-- 6 files changed, 279 insertions(+), 28 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f42d368 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,128 @@ +# Changelog + +All notable changes to the `execution-trace` crate are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +The Python decoder ships from this repository as +[`embedded-etrace`](python/CHANGELOG.md) and is released from the same tag, so the two +version numbers move together. + +## [Unreleased] — next release is 0.2.0 + +**A breaking release.** The wire format is v2 and is not compatible with the v1 format +0.1.x produced: a firmware built on 0.2.0 needs `embedded-etrace` 0.2.0 on the host, and +a recording made with either version must be decoded by its own. A v2 stream is +recognised by the `TRACE_START` frame at its head. + +### Wire format + +- Every frame class now travels in one flat `TraceFrame` message discriminated by + `event_type`, rather than a `oneof` envelope. proto3 omits unset fields, so an event + pays nothing for the dictionary fields it does not use, where an envelope taxes each + one with a tag and a length. Field numbers stay at or below 15 so every tag is a + single byte. +- Names are interned. A `NAME_REGISTERED` frame carries the name once and assigns a + dictionary id, along with everything else the name fixes — source type, priority and + relative deadline — and per-occurrence events carry only the id. +- Timestamps are deltas against the previous sequenced frame. `TRACE_START` and + `NAME_REGISTERED` carry an absolute timestamp and re-establish the origin. +- `timestamp_ticks` is `sint64`, not unsigned. An event is stamped when recorded rather + than when queued, so an ISR preempting a task between those points legitimately + produces a negative delta; clamping it to zero drifted the host's reconstructed clock + permanently ahead of the device's, at milliseconds per second on a real trace. Zigzag + costs nothing at these magnitudes. +- Sequence numbers wrap at 16 384 (`SEQUENCE_MODULUS`). The counter exists only to make + gaps visible, and a gap is read modulo the wrap, so a full 32-bit counter would spend + up to five varint bytes to buy nothing. Because a wrapped restart only looks backwards + from the low half of the range, `TRACE_START` — not the sequence — is now the reliable + reset signal. +- A new `TRACE_START` header declares the timebase (nanoseconds, or core cycles with + `core_frequency_hz`) and the source-group mask the build was compiled with, so a host + can tell a masked group from one whose frames were lost without hard-coding the mask. + +Measured steady state is 11–12 bytes per event over the delta range the reference +firmware produces, against 27 for v1. + +### Added + +- `TraceEncoder`, holding the three pieces of per-stream state the format needs: the + name dictionary, the delta base, and the sequence counter. Built for its clock with + `TraceEncoder::new()` (nanoseconds) or `TraceEncoder::with_cycle_counter(hz)`. +- `TraceEncoder::encode_dictionary_entry`, for walking the dictionary and re-emitting it + periodically. Without this, a host attaching mid-run receives no dictionary at all and + can resolve nothing — names are registered in the first control cycles after boot, and + the device cannot observe an attach over a one-way transport. +- `TraceEncoder::encode_trace_start`, `registered_names`, and the `Encoded` result, whose + `name_registry_full` flag reports that an event went out under `UNKNOWN_NAME_ID` + because the dictionary was full. +- `RawTraceFrame`, `FrameKind` and `TimeBase`, the decoded form of a single frame. +- `next_sequence()`, the process-global producer-side counter. +- `MAX_TRACE_BURST_SIZE`, `NAME_REGISTRY_CAPACITY`, `SEQUENCE_MODULUS`, `UNKNOWN_NAME_ID`. +- `TraceSink::ticks_per_us`, `tick_mask` and `ticks_since`, so that code measuring an + interval works in whichever timebase the sink reports. All three default to the + nanosecond case, so existing implementations need no change. + +### Changed + +- **`TraceSink::get_elapsed_nanoseconds` is now `now_ticks`.** The unit is whatever the + encoder for the stream declares, so the old name would be a lie on a cycle-counter + sink. A sink may return a free-running 32-bit counter widened to `u64`: the encoder + extends it, so reading the clock can be a single volatile load with no critical + section. +- **`decode_trace_frame` returns `RawTraceFrame`, not `TraceEvent`.** In v2 an event is + not recoverable from one frame — its name is a dictionary id and its timestamp is a + delta — so name resolution and delta accumulation belong to the host decoder, which + holds the per-stream state. +- **Frames are numbered at the producer**, by the recording layer immediately before it + hands the event to the transport, and by the encoder for the header and dictionary + frames it originates itself. Loss at the producer queue used to happen before any + number existed, so the host saw a contiguous stream with events simply absent; it now + leaves a hole. Nothing on the wire changed — only who fills the field. + - The cost is that arrival order is no longer numeric order: a dictionary frame is + written ahead of the event that triggered it but numbered after it, inverting a pair + on every first sight of a name. Hosts must tolerate a reorder window. +- `NAME_REGISTRY_CAPACITY` is 40, down from 64. This array is the largest thing the + format puts in RAM, and under flip-link every byte of `.bss` is a byte the stack does + not get. 40 is half again as many entries as the reference firmware registers, and + overshooting degrades the trace rather than breaking it. +- Interning is keyed on name content rather than on the `&'static str` pointer. By the + time an event reaches the encoder the literal's pointer is gone — `TraceEvent` carries + an inline copy. The scan is linear over at most `NAME_REGISTRY_CAPACITY` entries, with + a length check before any byte comparison, in the task that owns the wire. + +### Removed + +- `SequenceEncoder`, replaced by `TraceEncoder`. Sequencing moved to the producer and the + encoder now owns the dictionary and the delta base as well, so the old wrapper no + longer describes anything. + +### Fixed + +- A name first seen on a `SPAN_END` — its `SPAN_START` dropped upstream under load — is + backfilled when a `SPAN_START` for it arrives, so a dictionary refresh cannot re-send a + bare entry and lose that name's priority for the rest of the run. +- A marker value of `0` is written to the wire instead of being dropped as falsy. + +## [0.1.6] - 2026-05-28 + +### Added + +- The `enabled` feature flag. With it off, the recording API compiles to no-ops with + default implementations, needing neither a clock nor a transport, and the protobuf + codegen is skipped entirely. + +### Changed + +- README: dropped a Cargo.toml example that showed a less clean way to disable tracing. + +## [0.1.5] - 2026-05-25 + +### Changed + +- Improved the crate and Python READMEs. + +## [0.1.4] - 2026-05-24 + +Releases up to and including this one predate this file; see the git history for +0.1.0–0.1.4. diff --git a/Cargo.toml b/Cargo.toml index f27553b..88a64ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/erl987/execution-trace" keywords = ["embedded", "tracing", "no_std", "profiling", "rtos"] categories = ["development-tools::profiling", "embedded", "no-std"] -include = ["/src/**", "/proto/**", "/build.rs", "/examples/simulate.rs", "/README.md", "/LICENSE-MIT", "/LICENSE-APACHE"] +include = ["/src/**", "/proto/**", "/build.rs", "/examples/simulate.rs", "/README.md", "/CHANGELOG.md", "/LICENSE-MIT", "/LICENSE-APACHE"] [lints.clippy] unwrap_used = "deny" diff --git a/README.md b/README.md index a9b8610..300aa77 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ is why the buffer is [`encode::MAX_TRACE_BURST_SIZE`] rather than one frame. ### 4. Decode on the host A frame is not self-contained: its name is a dictionary id and its timestamp is a delta against -the previous frame. [`decode_trace_frame`] therefore returns a [`RawTraceFrame`], and the reader +the previous frame. [`encode::decode_trace_frame`] therefore returns a [`RawTraceFrame`], and the reader resolves both from state it carries across the stream: ```rust,ignore diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 26b5dd7..e22a768 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -1,19 +1,106 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to `embedded-etrace` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +The Rust crate this package decodes for ships from the same repository as +[`execution-trace`](../CHANGELOG.md) and is released from the same tag, so the two +version numbers move together. + +## [Unreleased] — next release is 0.2.0 + +**A breaking release.** This decodes the v2 wire format and **cannot decode a v1 stream**, +which is what `execution-trace` 0.1.x firmware emits; 0.1.x of this package cannot decode +a v2 one. A v2 stream is recognised by the `TRACE_START` frame at its head. Recordings +made with either version must be decoded by their own. + +### Added + +- `TraceStreamState` — the per-connection decoder state v2 requires. A frame carries a + dictionary id instead of a name and a delta instead of a timestamp, so it is not + self-contained: this holds the name dictionary, the running clock, the timebase the + `TRACE_START` frame declares, and the sequence tracker. One per connection; the device + restarts its dictionary and clock when it restarts. + - `source_mask` exposes the source-group mask the firmware was built with, so a group + that is silent by design can be told from one whose frames were lost. + - `unresolved_events` counts events dropped because their name id never arrived. +- `NameEntry` — one dictionary entry: the name plus everything it fixes (source type, + priority, relative deadline), sent once rather than on every occurrence. +- `GapRecord` and `TraceEventBuffer.gaps` — a stretch the sequence numbers say was lost, + emitted as a row of its own so that missing data looks missing. Written as a `gap` row + by `write_tracing_csv` and drawn as a hatched band across every lane, labelled with the + frame count. +- `TraceEvent.interrupted` and an `interrupted` CSV column — the span was cut short by a + gap rather than closed by its own `SPAN_END`, so its real end is unknown and at least + `end_us`. Such a span is exempt from the deadline check, may be zero-length, and is + given a minimum render width so it stays visible. +- `SequenceTracker` takes `modulus` and `reorder_window`. +- `SEQUENCE_MODULUS` (16 384), the value the v2 counter wraps at. + +### Changed + +- **`decode_tracing_stream(buf, state, event_buffer)` takes a `TraceStreamState` where it + took a `SequenceTracker`.** The tracker is now held inside the state, along with the + rest of what a stateful decode needs. +- **`SequenceTracker` tolerates reordering** when given a `reorder_window` (the trace + stream uses 64, covering the firmware's 48-slot channel). A frame is numbered by its + producer rather than at the wire, so a dictionary frame is written ahead of the event + that triggered it but numbered after it — inverting a pair on every first sight of a + name. A hole is now declared only once something lands more than the window past it; + stragglers close their own hole, and one arriving after its hole was reported is + ignored rather than read as a backwards jump. Single-producer streams keep the window + at zero and get the report immediately. +- A hole is placed in time from the frames either side of it, not from whichever frame + triggered the report — tens of milliseconds apart on a real trace. +- `etrace-decode` writes gap rows. Without them the CSV shows an unexplained hole and + interrupted spans with nothing beside them to say why. +- An unresolvable name id drops the event rather than renaming it to a placeholder. + Attributing distinct unknown ids to one `` lane interleaved unrelated spans, + so a `SPAN_END` could close a `SPAN_START` belonging to another name and produce a + malformed pair. +- The "never registered" warning fires once per id. At 1 300+ events/s the per-event + warning produced thousands of identical lines a second. ### Fixed -- `write_tracing_csv` wrote a marker value of `0` as an empty CSV cell, and likewise a span - `deadline_us` or `value` of `0`. The column is meant to be empty only when the field is - absent, but the writer tested the value for truth, and zero is a legitimate payload — a - reason code, a count of nothing, a deadline at the activation instant. Downstream the empty - cell reads as "no value": `diagram.py` renders it as `—`, hiding exactly the case the marker - was emitted to report. The decoder was always correct; only the writer dropped it. +- `write_tracing_csv` wrote a marker value of `0` as an empty CSV cell, and likewise a + span `deadline_us` or `value` of `0`. The column is meant to be empty only when the + field is absent, but the writer tested the value for truth, and zero is a legitimate + payload — a reason code, a count of nothing, a deadline at the activation instant. + Downstream the empty cell reads as "no value": `diagram.py` renders it as `—`, hiding + exactly the case the marker was emitted to report. The decoder was always correct; + only the writer dropped it. +- A lost frame no longer leaves its span open until the next unrelated `SPAN_END` for + that name, which drew one enormous bar across the gap and past it. Spans open *at the + hole* are closed and marked interrupted, and the first `SPAN_END` arriving for such a + name afterwards is discarded rather than paired with whatever opens next, which would + invent a span that never ran. +- `decode_tracing_stream` no longer clears the byte buffer on a device reset. The stream + is length-prefixed with no sync marker, so discarding bytes mid-frame misaligned + everything after it unrecoverably — the source of the decode-failure bursts that + arrived at the same millisecond as each false reset. The frames after a reset are + intact; only the decoder state is stale. +- The frame that reveals a reset is no longer discarded with it. That frame is the first + of the new run, usually the reboot's own header carrying the timebase and mask. +- A reboot is separated from ordering jitter by a margin rather than a strict backwards + comparison. A reboot drops the clock by the whole uptime and jitter is microseconds; + without the margin, one inverted microsecond blinded the trace for a full refresh + interval. +- A repeated `TRACE_START` on a running stream is a dictionary refresh, not a reset. + Treating it as a reboot cleared the dictionary on every refresh, undoing the thing the + refresh exists to do. A reboot is identified by the device clock going backwards, which + a refresh never does. + +## [0.1.6] - 2026-05-28 + +No changes to this package; released from the same tag as the crate. + +## [0.1.5] - 2026-05-25 + +### Changed + +- Improved the package README. ## [0.1.4] - 2026-05-24 diff --git a/python/README.md b/python/README.md index c834aa5..9e4b585 100644 --- a/python/README.md +++ b/python/README.md @@ -32,21 +32,21 @@ Implement a loop that appends raw bytes from your transport (RTT, UART, TCP…) ```python from execution_trace import ( - SequenceTracker, TraceEventBuffer, decode_tracing_stream, write_tracing_csv, + TraceEventBuffer, TraceStreamState, decode_tracing_stream, write_tracing_csv, ) buf = bytearray() -tracker = SequenceTracker("my-device") +state = TraceStreamState("my-device") # one per connection event_buffer = TraceEventBuffer() while True: buf += transport.read() # append whatever arrived - decode_tracing_stream(buf, tracker, event_buffer) + decode_tracing_stream(buf, state, event_buffer) # When the session ends: event_buffer.flush_pending() # warn about open spans csv_path = write_tracing_csv( - event_buffer.records + event_buffer.markers, # type: ignore[operator] + [*event_buffer.records, *event_buffer.markers, *event_buffer.gaps], output_dir="data", ) print(f"Trace written → {csv_path}") @@ -55,6 +55,15 @@ print(f"Trace written → {csv_path}") `decode_tracing_stream` consumes complete frames from `buf` in-place and handles device resets transparently (the sequence number jumps backward on firmware reboot). +`TraceStreamState` is what makes the decode stateful, and it must be the **same object +across every call for one connection**: a frame carries a dictionary id rather than a +name and a delta rather than a timestamp, so the name table and the running clock live +there. Create a fresh one per connection — the device restarts both when it does. + +Pass `event_buffer.gaps` to the CSV writer as above. A gap is a stretch where the +sequence numbers say frames were lost; writing those rows is what keeps missing data +looking missing rather than silently closing up. + ### 2. Decode a recorded binary file Use the `etrace-decode` console script: @@ -92,24 +101,48 @@ print(f"{result.n_events} events, {result.n_lanes} lanes, {result.n_missed} dead | Column | Type | Description | |----------------|---------|--------------------------------------------------| -| `name` | string | Span or marker name | -| `type` | string | `task`, `isr`, or `marker` | -| `start_us` | float | Activation timestamp in µs | -| `end_us` | float | Completion timestamp in µs (= `start_us` for markers) | +| `name` | string | Span or marker name; `trace gap` on a gap row | +| `type` | string | `task`, `isr`, `marker`, or `gap` | +| `start_us` | float | Activation timestamp in µs; for a gap, the last frame before it | +| `end_us` | float | Completion timestamp in µs (= `start_us` for markers); for a gap, the first frame after it | | `priority` | int | Scheduler priority (0 if unknown) | -| `deadline_us` | float | Absolute deadline in µs (optional) | -| `value` | int | Optional u32 marker payload | +| `deadline_us` | float | Absolute deadline in µs (empty when absent) | +| `value` | int | Optional u32 marker payload; frames lost on a gap row | +| `interrupted` | int | `1` when a gap cut the span short, so its real end is unknown and at least `end_us` | + +An empty `deadline_us` or `value` cell means the field was **absent**. A `0` is a real +payload and is written as `0`. ## Wire format Each frame is a standard protobuf length-delimited record: ``` -[ varint: payload_length ][ proto bytes: TraceEvent ] +[ varint: payload_length ][ proto bytes: TraceFrame ] ``` -The protobuf schema lives in :file:`execution-trace/proto/tracing.proto` (sibling -Rust crate in the same repository). +A frame is **not self-contained**, which is why the decoder is stateful. `TraceFrame` is +one flat message discriminated by `event_type`, and the stream carries three kinds: + +- `TRACE_START`, once at the head of the stream, declaring the timebase (nanoseconds or + raw core cycles plus the core frequency) and the source-group mask the firmware was + built with. A group absent from the mask is silent by design — that is what tells it + apart from a group whose frames were lost. +- `NAME_REGISTERED`, once per distinct name, assigning the dictionary id and everything + fixed about that span or marker: its source type, priority and relative deadline. +- `SPAN_START`, `SPAN_END` and `MARKER`, which carry only a dictionary id, a signed + timestamp delta against the previous frame, and a sequence number wrapping at 16384. + +`TRACE_START` and `NAME_REGISTERED` carry an **absolute** timestamp and re-establish the +time origin; every other frame is a delta against it. The delta is signed because an +event is stamped when recorded rather than when queued, so a preempting ISR can put a +later timestamp ahead of an earlier one. + +This is **v2, and it is not compatible with the v1 format** that `embedded-etrace` 0.1.x +decoded. A v2 stream is recognised by its leading `TRACE_START` frame. + +The protobuf schema lives in `proto/tracing.proto` in the +[repository](https://github.com/erl987/execution-trace), alongside the Rust crate. ## Development diff --git a/src/sink.rs b/src/sink.rs index 6e69a52..7df328e 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -52,11 +52,14 @@ pub trait TraceTransport { /// `TraceSink` operates in two layers: /// /// 1. **Recording layer** — `record_span_start`, `record_span_end`, and `record_marker` -/// read the hardware clock via `now_ticks`, construct [`TraceEvent`]s -/// (sequence left at zero), and hand them to [`TraceTransport::write_event`]. -/// 2. **Transport layer** — code that owns the wire (e.g. [`crate::SequenceEncoder`]) injects a -/// monotonic sequence counter before writing bytes to RTT, UART, etc. The sequence -/// allows the host decoder to detect dropped frames. +/// read the hardware clock via `now_ticks`, construct [`TraceEvent`]s, stamp each +/// with [`next_sequence`](crate::next_sequence), and hand them to +/// [`TraceTransport::write_event`]. Numbering happens here rather than at the wire +/// so that an event dropped on the way to the transport still leaves a gap the host +/// can see. +/// 2. **Transport layer** — code that owns the wire (e.g. [`TraceEncoder`](crate::TraceEncoder)) +/// turns each event into frames and writes the bytes to RTT, UART, etc., carrying the +/// sequence through unchanged. /// /// For tests or placeholders, use [`NoopSink`], which discards all events at zero cost. #[cfg(feature = "enabled")] From 8466de7ea5d5f7511b4355a129f001bdcc107546 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 15:12:25 +0200 Subject: [PATCH 10/16] Test the Python package before publishing it, not after The release job ran cargo test before cargo publish and ran nothing at all before the PyPI upload, so a tag was the first and only place the shipped Python package met its own test suite. crates.io allows a yank; PyPI has no unpublish. This is the same pytest and mypy gate the Python workflow already applies on every push, repeated against the version the tag builds. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 789c502..5acba4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,6 +37,12 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.12" + # The same gate the Python workflow applies on every push, repeated here: + # PyPI has no unpublish, so the upload below must not be the first time + # the tests are run against what is being shipped. + - run: pip install -e "python/.[diagram,dev]" + - run: pytest python/tests/ -v + - run: mypy python/src/execution_trace - run: pip install build - run: python -m build python/ - uses: pypa/gh-action-pypi-publish@release/v1 From 5e8550d2890e75f0a774a1e08db4c50c2d172850 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 15:15:39 +0200 Subject: [PATCH 11/16] Lint the tests and examples, instead of only the library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo clippy --all-targets` reported 61 errors, all of them in test modules and all of them `unwrap` or `panic!` — the crate denies both because a panic on the firmware is a hard fault, which is not a risk a test harness runs. CI passed because it linted only the library, so the examples and tests were never checked at all. Allowing the two lints where they are assertions lets CI lint every target, which is what finds the rest of this commit: two leftover bindings in encode.rs tests, one of them an encoder shadowed on the very next line, and an empty format literal in the example — the file a new user reads first and the one target nothing was checking. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/rust.yml | 2 +- examples/simulate.rs | 4 ++-- src/encode.rs | 5 +++-- src/sink.rs | 3 +++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d935ab6..6004cc7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,5 +14,5 @@ jobs: - uses: actions/checkout@v6 - run: sudo apt-get install -y protobuf-compiler - run: cargo test --features std - - run: cargo clippy --features std -- -D warnings + - run: cargo clippy --all-targets --features std -- -D warnings - run: cargo fmt --check diff --git a/examples/simulate.rs b/examples/simulate.rs index 2ca0d0d..f993e3e 100644 --- a/examples/simulate.rs +++ b/examples/simulate.rs @@ -162,8 +162,8 @@ fn main() -> std::io::Result<()> { // resolve them. This is the same job the Python decoder does. println!("\nDecoded events:"); println!( - "{:<6} {:<14} {:<12} {:<10} {:<8} {}", - "seq", "name", "type", "source", "ts_ms", "extras" + "{:<6} {:<14} {:<12} {:<10} {:<8} extras", + "seq", "name", "type", "source", "ts_ms" ); println!("{}", "-".repeat(72)); diff --git a/src/encode.rs b/src/encode.rs index e2f7cd8..aeb686a 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -714,7 +714,10 @@ fn from_proto(p: crate::proto::tracing_::TraceFrame) -> Option { }) } +// `unwrap` and `panic!` are how a test asserts. The crate denies both because a +// firmware panic is a hard fault, which is not a risk a test harness runs. #[cfg(all(test, feature = "std", feature = "enabled"))] +#[allow(clippy::unwrap_used, clippy::panic)] mod tests { use super::*; use heapless::String; @@ -1220,7 +1223,6 @@ mod tests { #[test] fn cycle_deltas_reconstruct_the_elapsed_time_across_a_wrap() { let mut enc = TraceEncoder::with_cycle_counter(72_000_000); - let mut buf = [0u8; MAX_TRACE_BURST_SIZE]; let mut wire = Vec::new(); // 100 steps of 1 000 000 cycles, starting just before the rollover. let mut raw = u32::MAX - 50_000_000; @@ -1255,7 +1257,6 @@ mod tests { let mut enc = TraceEncoder::new(); let mut buf = [0u8; MAX_TRACE_FRAME_SIZE]; let ns = enc.encode_trace_start(0, 0, &mut buf).unwrap(); - let mut enc = TraceEncoder::new(); let mut enc = TraceEncoder::with_cycle_counter(72_000_000); let cycles = enc.encode_trace_start(0, 0, &mut buf).unwrap(); assert!(cycles > ns); diff --git a/src/sink.rs b/src/sink.rs index 7df328e..f559820 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -256,7 +256,10 @@ impl TraceSink for NoopSink { #[cfg(not(feature = "enabled"))] impl TraceSink for NoopSink {} +// `unwrap` and `panic!` are how a test asserts. The crate denies both because a +// firmware panic is a hard fault, which is not a risk a test harness runs. #[cfg(all(test, feature = "std", feature = "enabled"))] +#[allow(clippy::unwrap_used, clippy::panic)] mod tests { use super::*; From de555950083ac9184f1b0e65d16baf1350cb6770 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 15:18:13 +0200 Subject: [PATCH 12/16] Date the 0.2.0 changelog entries The release workflow takes the version from the tag for Cargo.toml and pyproject.toml, but not for these, so they are the one place the number has to be written by hand before tagging. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- python/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f42d368..e646602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The Python decoder ships from this repository as [`embedded-etrace`](python/CHANGELOG.md) and is released from the same tag, so the two version numbers move together. -## [Unreleased] — next release is 0.2.0 +## [0.2.0] - 2026-09-19 **A breaking release.** The wire format is v2 and is not compatible with the v1 format 0.1.x produced: a firmware built on 0.2.0 needs `embedded-etrace` 0.2.0 on the host, and diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index e22a768..af607b9 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -8,7 +8,7 @@ The Rust crate this package decodes for ships from the same repository as [`execution-trace`](../CHANGELOG.md) and is released from the same tag, so the two version numbers move together. -## [Unreleased] — next release is 0.2.0 +## [0.2.0] - 2026-09-19 **A breaking release.** This decodes the v2 wire format and **cannot decode a v1 stream**, which is what `execution-trace` 0.1.x firmware emits; 0.1.x of this package cannot decode From a5eb42790e7720b155691a9308052dc88ffc206c Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 15:23:43 +0200 Subject: [PATCH 13/16] Show a v2 trace in the README screenshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old one was taken in May 2026 (395c142), so it shows a v1 stream decoded by the tooling as it stood before increments 5-8 — every change to the format and the diagram that 0.2.0 releases postdates it. Same path, so both READMEs and the raw.githubusercontent links in them are unchanged. The image only reaches crates.io and PyPI once this is on main, since that is the branch those links name. Co-Authored-By: Claude Opus 5 (1M context) --- docs/screenshot_python_app_1.png | Bin 172495 -> 216959 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshot_python_app_1.png b/docs/screenshot_python_app_1.png index ad375feb00128a52742fbedc4d4e728b695c2ba8..a93625498bb1fb21e6ae1df0dfaeb9994d1b271c 100644 GIT binary patch literal 216959 zcmeFYWmMbC*C-6dixrn3ZE2ynYmidBSPRA7U5W&EiaW&#v}lXFd+RBt8BYyh8u$R_wL_oml{OgUFz=#8Q>U{1bq3&dDXJ%n!V6Be8j=%0C^cRl5^=csS zaj|e;iPH-{4PgB>;ArGvVq%M6BL0b)ok&(Q3jyIJf{eK67gyc=1s5kG6L`uN?LA&m1`k%=$@Frd#Q4gyCUZ*FJJupKc0ue1imiZTI4oc^var8>^4(bQd5b5nfH^vX-HUOfOvvKj7UGCgGLssFKM;nb)0 znV)J6iA9`ISeaNIGe3tY?u*wwBpnYEkV3ANB`a;7$0u>P zz&@y2FLo`Sd-hYn19`CXb!xq_TJ#CGx{wVhbOe(YWPJ=K5}7;g6pfGC15W)XDCZd>Vr{UGlT$dE}MyOl~THW+$7F#$5&eX$BRQUGpL}LvQTu26D;MPtGfiz zAK&#d>ix}p=-=vJkI8v0TMI6bSj2F|`J{GNc=5>JY4`UkTvUYTgLBj3`M-RMQnTk9 z5el~7saAV2)vxw&{)TUDX(jK3li%8JWg_0k^BzAtbyCn@^^_0}zfzQOK#bzK-vGPR zmmhqc{g&U>VB&c`RWVpYuz7E1z59hNq-Eo?Io{*teJrLc4V3|X zN#Q%9tE*x+G=mMg3a7=_Jp0UT*)mvWxYPt9=&Xw^n{i;hihPOQ6%YMJ-$VS=_H%(TC7~{H18}&R z&?AZebJFu!69%qbSf6khC+EAepRso*B{0})A!ul105P7yI9UK^6FP>SjBF_aIoQ7B zS42)`i+=yda0TxzjtXVpyJrpp$;I{8D@paF2x$@gxDJp`d0R~I57g=a5q2P$S{FQA zR#1dZ&Z@J2>+tN#yE;0RyA4cedPHY%;f+7Zv_6MYdtBPXLf1Xiyf(L98+MjiYeO9vzdbqNntnE1R|b~DB&y~iJ0>rZvXJd4x8yt;$wx6 zUY?$hzV3`sWEt?svx&&Zd+Sjj&u?_Xnz9NiHv*8hm5t!i&mFN!Bp{?EV;dl+y3h`{ zsgqB*m8M#5mCbY2rlGwhD$F6WJQ8m>)pttzy!ncD2E(fewVSdnwE3x_qwOuf_yxV@ z0W|K8?ulsy4%Pc$HB;x!2}VtZz>7jTP4OUu3S34`upA~i<Bx_G zrK*>&?dLAzgy9ReYvD!eePJC1YJk%&?0Tbe#~KK3$F*yPoHiv!crGK_hGF)kRQbB-d1oH!KE||A zN98}3+Lih1*-7GH8?~Qf52Pjy90O)27F+&^V$$)PH_@V@W6?jB_5Js5Fbb&N$B5)* zReN5J^!JyQ`zemwA9`PKnht`y2I`kM_Mlg8J8&y9!VSI3*Wi&595G492N9~3Z?%hC z@vM35UbkE=$@g|tq+=<|{gcK?Z_90sOijK4eGPfwPSzeR>}tpR-%|!s*~3#Z1-rIE zi$rtf73y$VLGI`8s5J)9`TM@l5bfQHM{l0U=pEkxaoH;p)Vkb37(mkjePxRaqFh^4 zxPd`nvc5@fyvottC}V<2T!vM^%=eJ8TPn^Mu??hDqT)_ey%JLkeGr$(m#cwFj;#hw zo$=jZDf>0C$IWazQ}#6|z1)o@s$S~@E-DW28iv01& z^y6(5PoHCmXN?FBL0k9YF@cnl&q2pL8w`AQ^J*k?s-(7rAIhMU)19KLm#I0cwNb^X>O6*0V-8yl58)^(CLns*Y61HFJAY3?Wub*y{Uf?!2{LF~0ov&&Ft-^w-p=IWKxH-k2n= z&io=0f;A>;J#vJE>=w?t4fyPn>*}l}TK7KsNwHr4?0zFPu6I4;!8=-_`o32NbT60( zQ}3KT@Q@y2g`DI5{5po7)p%vp<^~}f6~0E+V|?%^r&kw>)?p`jpJ)H=}VO0mn?gfIz}S)|cv zno%fMd%pr5)WnFdBgFjc{tfb0Ua0otFa<~9R@T5L7I(Pg;f~H#+3Gw`sMg_K5R$D% z!9l#O@paCwauezKHdY4d>SAr7`uf3jhN1`y*N-~pX zcD9prrqO(cfC55Tpk2Xk(-pD$X z-y`*v?UPd<9BM5xhFltt;^fDtXg%|e42?L1wZ$em&cxJQsOB*NtHSJrZ&InJ=)TtV zzR6MH4T`9bEiax=667Wy#UU4zR8w8w-yq136JUv8(BM<9vo+ zQ2ZF{GjGpj_3GM)3iJB<)~Dl3t;Fv5^2un8nCSW3G#IzrKG z+*``ow1ZCqRmAi$-YJk{Y005SCH@UfinWxoH__I*^xA5{*Ei$X`ni`HyC9-qG>gkP zlj<2VZ+MJ>kt-9<<8%P@bqvmw-J-oXKSI5fZ+w^VR?n+{Ad@#fPQzbq4RkL2$Y9&l zR6;2zR-~Pd#vld>qV|4lLTT*{TQsdAm)@pq?rXI|sj)Gj9B8ZfS#f7XEvQZQ1vPu^ z-3L4K=p6*~j=N0&#=SXreIK#91|P20@a1|YnxHTphHRoko5^V1&`jsJ<1Kkn(RX}k zo5-8GS?9qvIzD8*kMs;6L^^MuO>=;3>!$LIhdIB9U30>rkRsDd=0E=3TT7KG`G2)r z3&qq;X2ySSO4zV+s5|x_o;7G2bxkaVb#aJ=P>vUyPS<&gh_u|2ma-~j5Ad)Fd7WJG zyo1S^J~X}IVbOt2Ji14vHK0twwGpC0Ysp9~PqPU3mULqEQ~VTnj(Afxi%MZIcv4&~8ZL&Jxz?hV6u(lzP9cmO zguM1yx9w++{J!G+K*2uSDphEqh=V~s=NTN}Mz@F&HL1<{6`!}ZG{F)c**{Y*oM8xP zd*d{mi}g@FK6*0E^3ma{+@zhCskCU`+*NXo=rbCwbhcI_vvKs0AIi11>zckg zo$X*5 z+E@&)iniPgN69&Kc!GAmWw!FdI>h$1`O|a?lPXSYwp@2l%{>cB)moyis67)i!)$&y z44j?)Y~uZgs9bocgN?%V#w|XnDy~#-rX%XC z;fygqU-Zc#Sn{Q?H^ea-y*(0!zjllZE5jzhr55p|%r(xpH66)dX3}d&kbz8HV9cZjQ%|}pM*=Wr*2!GE%{D>l~vl?Q<6)gc( zjdwoj;wo3xXkj7(-hV``aXwDCdye2bnq$M;P|qNcz)1Iv^6vS3;%})DRX7li*D|XU zVUz57i$bt!r<~p#4Gvb4QEvrA!-VQt`fXm8J8E2PHVZyHc=y{Njm>4oys*=j*vYX4 zzVDyp-pAdAgp*9QWLlXoRbkeio&-wWaI8$bo0>k874}GF4&54=Jb0BLkZ|AG%Hoq@ zVxrH}5VO@#MqSe&?MIIuH@2Nw9Nc^{h z&JxWDtd5awzNy3@hukudmq6l*SpFgV#ZvHOUsh2aVUyQMf!S2VGN{T9mD4+;Cr|b$ z$E6Gr2D!gKaHtq~XE8gLtwOz(7SC)*!I_or4+oJy zadda(QOS+8rg`S3#BlNB ztS(&Dx-UvQ&*huwO{=e*3hvn4z`S_{tyO)$RaH4EBO~-d!n;Rw?lN*(;6SA%wKYfw z4YTZ?e5^p(ho$aTlVzV!NzrwR)(93b&-PWh_(k>#W&1LaYPG+hKLtVZrO>TQm&<{B z*gpQGH+v|x)~}<8KMBuZULY zCV7h>Ovx27y#~0)FnKWxYU}vs1Fc2RLm^dD>#ejM0I9Q2Ybq`aSB%#&P&c#gyAw1% z5d!a{sm)Y$is_4OZarQi(KR`Y!?)fg7q@XpZ0?FbDt-m!OmAO1TusFEz&|uIOv)h; zIr7p?yT=3wyZX^nF0D)|Ok7RA`EfB5LFO(Ta0{ZN<*3VYFXqe?Ds0&-p3292T+?bA zD+DNJOl?oIJJOzBFvicsn5|90ISw=J2?8{44K|KU+kr!zCp|>fx4XZGV?du4`XWc( zyS-M<^v>XyF+p~2Ne9Bci2`PWS55fnr8U|~>{{9*Y?jWBc!8&jtwCewf#eg^asfTi zHTt)&yh%0^UP*D*2_2+S1L$>vuN;594#f(>U5TBbCjI(il7j-5hAasiv?Dso!5lIy z?V;q^fIr0=coUvf!Law5g-IP&@g?n;5p@){=6g3v6rqng564GBDC6Y);@4Gd@VZ**LM;MzMMZMp(0Loy#wS>@od z&O}3c>28uQTWCD=Su_Jol0Kc^&JlLrNBTy2t800`JS)Z~5;d&E^G-T1`pRnPL>8h4 z{k?urUgk3NcCR>ttGO7Bszi*i^+6crj=8OEn~U^uTOjJl&e+V(Oe+4{#AvN%xLFyq|Ejx^ zyEw2c{BWbAx8UfDWcQ@RKq?RTXpQEk$0u{WDf`voZj(6)!MA#Q%$>QKSX<}U_v6%m zX3P0y=pMH2D)cfoRt+Ges#`-6g3UQqv!k@dYpdo52Fm2E>D6wB>?s^I%vxemCiwWk zkX<(;t4D3&^hW9=H8l9?fbu+B*=uX8WwKen*egL8Hn^#rGl|VgMiq=m(I}Yw^Gc{= z!mHz$v+dkUX1)$cI@8e!Iy7CI^cR@2Lf_MW20UZJDqYleQ`R<_CT(j2Rxv-Y+yJ7N z#v7yTS1n9U;!|`kbnT{r2N+aof|>FjmGlg(`EQP1G^)bZi%+hOXDb?_t-x4{wcxJvr$SlhmqcJGLI?1y;B>8DFS7;VDG`#i2{{5 zl=aQ}sKWp}LVurG!HoJH4i>-_he$5k3x}%v-|{lHRB!2;Adk^0@}cNg9c9p7xuLFZ zT1Ng*3jZCEDiYyIQT%w!^77t~L!A}!_mB>Z-;4xK4KpUB?7XL)IuLW}eMDFM*&8Le zn~q`=R`~q2{F}@;qhz*}!)xyjIK-6H^!aLplVRn^EimJEIjGPwRju z{$+k}UZe(z3WT_qWV``OD6OPSp4C*%T&eq1cLFKH}IEtkIBq zGahPIKJAm^pcR=kOx7%q>ari;BL)v{}n)_DXmaaZ}57k<}=lkawmd{JRQ#9 zYS9NN>+-dA>L2=_GI1&b9*L|p6F6pVZJ<_&mnc~%5TtAiH0R!DjVO2yKypQXv1syc zYSD+6q}r(!rK)c(rA?HCPe8XL62buKMhK#7FLvixEyZ=b<9}s)P_p7R2Gi+|_!ewW zhS<4yiOzd&5mk%m%9FRZf&2C9n3j~gA2CygmTGEIHeQy74GZA0<&u*L3|lWdo8ep~ zdVa;%j@5a0DP`dht5y9q(>AchO-MLWuFT<`_loXi4cSMBb8ZSxBg{k-{VHzPK_PwrNcmO-VHxsE;_Y7+RX*#yAH%ozPG>MK2F%P{I7I zx&L5-5&EJVr^y2_bGtg5uZF8*dIW5?Lp^mC8~#@im^BzHG_;zdG&@S<}j z?eDR1$zdDjvb8JV5C^`Q_~bCm@Q8CJCb^E+TA!rY2gZhubD(8E11Cu9vB;hG<#I4U zo3C)DEIqOHbIT1NqS|~RV$IU+*b@nnviQhb!#W$b+PAy0Cu)VxteAN)Su8B$ohM7p`%n>&ml7ejK z1TNPGfWO&iVWu0ak%Tiiu!du$hMNgtCCThcvgtk~NXp0int6B5Nl9rf_a|)vp7ujB ztxuEDSj$Pi>qO{?)`OGNw>R^~26cTRGigHx98^(tCx^#3yHo zWpgWi57utZkDh!6x zFO%*GC}n-bmY5c5qOJ`AkXqg?T&!Y#Vp7C&Dk`Z;f_i1{v=?KwS&BWEma@(KlS}=a zac#AFc{{nWq=$2CO~+09M_e9dtTB`nZCzwR| zd2WKo=quW+$G;EjQ66L6zrMBatCMnb9;K55Ur?U4fMNLrnahbkKVKWhwjN++czDc| zqY)t-zcw6Z7JOLzvN@2}vp+<4+DpVNa|frSYQABeho4>PWUQx%JgQK;Q?J(`UT;#CyDg6+U|%E&_vrPXRd`R^2FKsB~i=S{g; zN=xMnOg1@I&j-PGtPFYc3^X<;d-Dl#okpw+GSr*L#5`imhmvP|OQ0Rt~p#!_5Iu>OXuq+>M|KrEkP)jjYcf`aM33b=XrN^B>0)xO& zj?rcz{}pQAOvDl)V7uKCQ#3g#Vzl$$O-=EiPylSD#gZSHm! zfbbhmiSZ)#3GKoD2J7>vUu`rou^8pgCPJF&HI{nO>Ocv%U2%W687C# z)f9*LXY~-}vN>;9a3cB>ojVEDbqM^_h`5<50bV%UZUpreFBx(dzyV?l-5mPGI&_(Z27YJggI z1UHzmHg2_2d0Bl8{ zW61Wrh2|Vg%b(dDwwHFxTacM#3J5lIORi0WT@VDhUi$?b5Kjv8t= z-bOnIOjFRm>qNy85%Ki7fKWa?rwPwaC2vYhYXsD}f54qZTNpHQyo^1AP7ORl-*-a`b&D7vYj|^0c*J#LJ z3MKEYXQ|Qk^>_-fsNkcS)**f2Qd=eFl!z-5{g7vOo{;R#>0qv>1*z*bZ@x`_1{MW> zknCF4uk7Qc9TV4;1Dp3I8-|r5FKnBBp)>3Ej+pn&LQ8kN3+r*c)r zz9RGFJL_+-+8Aw5GNNWScAv=?5HvdRR+`yuhIx3XA&(tsdwbt?g&n6GjMg3TQ8hnu zGz7VSN-U2p)pLuY2b{f=Nr74&Ih&nA0vJB*1o19T#2tV%$W-@a-QduXX;RW0&cgd? zey%3ZWB!`DacCQzVEQKrEhi_q=rvua1#!y*aCo$kncYSxrI9A~`zTe8*^E99jo?5t zO8ToHUSVNpr`KaeSmIh=hQ@fm>nOb!t;Y@$5%qKamX7r-z!EL9UBXs@g;<}6@{Ivh z^a{nY0$i7F@ARH|W{_7=xPLv8Up@J$PVmDySl?4f9;;0_PV$2TF-!iLI?>n0nEMX^ zHVkx}B2H3UWK;rlpSre_zK9W!3=Y2p@pMcjT}FDgSQe8jnT#1mw&tlgS3tA*Tj^7}57 z=0-)>DSA(8xP!Wv`!y+BnOCvZ-fx3pr%s*u<~5l`?Xuu&J34~L17obQV98>`?OC&T z*`=g1Q{C0IwvsxqYar-k{Em^Y4kU0?1jY$M^Oo0iRJ~G%weCB>!Z8ZiVO&jinuMEb zfxDEVe#qk^QP@W>{hO{SKg?k3Z+*Hxt|T%rf^>deJg9O#d+wR)W|&`0ca^Ca`yk6Q zd8v^jvKXFtl;K!4T2A>$wiEp&DDq*JK=4H71FLPM1<9w&!8wW+A1aev4ML%dXZ1+5 zBfpe&rocgt^$b>BKnmwCdcqizFPlrK#A@B_%6|%`$ZMU|7nn$IR1fF9vtd5V_kj$2 zOY3_&V1Q}s=|>lQ@uNOP^ggT9)kR9c1ditf;D~EyZ|{kIkrvf|LtH{oT=U=Z!iBRI z@k@R)3{n3xFC1#J7tLlYjS$4?XTmsEKJda2&AE`SZNFV(jrMGFx>rEViEg!aok1Z= z^nKEv>&}|vOw&Gzs@;5dRN%q>qEE7p)n>WX0e+(H1=C|oE&!fymv71GkB-CjvT#+6 zvx4&T1jLA{I3(({V_G>PQeMrD8hLpaeNc477;*g7#xk^Bma47mZiH5$%nqtd#_|RfTWI9^$oTmCZEd00(>L|& zb1#g^iMA#Z6m7jH7Rg+Lk(JemRV*E=LS7C8o)vb zr_k30rdr#pqdlb-%Vp@c1Km%^zIFuX?=mLiY2VzU%}?%BX}2geqDCi8@Z*+mFGZhZ zPRywDB)1FI-PF_73pN}|aYQV?@W<{&Ej#NirBI9P_x_ZTsf8zr$`*i*VY^MF;pwhB zAfI#-vpN!sN9oUt7wZ$}PMxnGxVQn(l)I8b6)BSlJRfVY;)RylT1Rqv=@a z0olz)xrgDRTAOtZF;2h8eE*?CL5VYd`q&5`-~&&&?$KRaMEu!vduX?l)@o8Dw7`-0 z`B==htJUic|F(PLnyYZyhbD_RQnxiD3(-zl$#Nx?H#2L#9hrv%fi72XFu!OVRZh^Z zF1R~eQ;n+ieN*?M?AnACKi^xVj|B9|<>4_3l!~6#_bRBuDvB826O}ad>)Rt@YO1U( z%U%sin7R`azvA%&SSO)$04&^CIEZ*xQ{NquD|3Kk2(J3IgI3(O^a7(7oTD`?D>s7$XV!gl0 z4zZ~;Q-I}nUepME@^5@>@92P>fbbx+C`DcfZv+*jd(?>3SAN9rVgM`{tC&dr(qglR zsqR{Z#p)|+0D&oIN2-VZ{#WoPsJ%Hlt;O`rj`2*o>+|J@Q_zcjzrAROZ0(bZhwdq#`b% zE3B4kW#VnH{j5IfQy$msu}g(ADst$MVvxpS#UN(2qMhZHjSni%N&>WzukHu|)(k2}LyhH+En)Sc>VHEk+8xXME{gW>uEEvsUuR%JU@NPQ|dBD=|YpX)G+x123 z=tfbuD;0S6%}Ti1jdd`ZkgaEwPt@Uu_nQL3J_p>Ut&ilK9!>4js?J9d2}xotD109* z=6|(}$o^57SIBR8?Dy$UsR|3vt`3$Txizt7a;UJn`G})I`>H&^eh6T6jYmlkiWFWX z514VJ)y=g|C3;1=r(U+YPImq6%3vSj;^0j~&?xv##{+ik(T**WW%N2K!_~@%!=oVB zP5VxIQCXENg56@ah~eB^`MZwPba@!KFAtmk3z&iPWr%NoBq>g`Q7O@o%vh+H0@Z;S8v3M0* zxlXWpB`sN!8PIU{OHNPItn-gz4aw2|>Kv_QbG*LatoGK1s?``!;h)EM78@aN7)B96 z^d51eQ8jzn`+{+C9J|wY0j3Ap7RLToN0K}yTsmcK0+0fvuMcJ7!pz$m<4t$t&*1gSDeGPZ&+hHt^4`r zRPoo!+FSjt@|6cEHv_nEzYQBwqXVQ;(eXm#A~+K9IQ*y4UV*~l>#qx_9EChql!gzt z%?SxI*n+dpIbL3GWj3M*v+>xsmf8cYfUABIx(=_&=-62FUB{~S-p+eGn!l!87Kjl) z9f_N|4{EU(EmC|JrUTUUqx$BP>VVF#hf zYjN?rFPR|M=9@*9u`ka;b|=u-1fn)F_HOa;wx#UT+V2kzWi2-52SqcI83ELR^7HowEXPA))=G!Lj6(GVA@wvzP!IYFvfMHgbesM^Xh?$ScCU{aph2v

CUBR@|g!pkJfoJN;gt!?O~>8pX7M`R0VBPOMz3 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

JJ`mHx&59 z4-Q)PX5ghF7B1#FVx@{hsa6nG%9Xm=-OzdtEqRmlB9rY?f}M`CsRV|%*XNwJ0o_KT zXhXBPn-%+-da#PUjSFqn3+JzMY~S&JT>Tnt+Bm`=a@iDVd5B?lwvpe~Z&G+U7{fo(n`L_|b;ScEl4g5Ve*q4=>d)$c6j_{>2FmvcmZgrqN#dGEQcAszM zRWo=i1Z690w057+*29$@l#yjTX*Iv?^j zs1AMBsx|&FdQGlrfhSIpmwfxbyXg1FaRmPVnHID~lXAf2N_Q%a74vqkN|6Jw9LpzwbwErwWxQ@gDGZC%(ulPuJeFZNGioR>*Q7CTD_)(G8NH%ui_@9LIUl`e z_dgkYL)Sgh&( z0xMApjs9z;E<^v(kpB|lKM3+=aXFIy8Swr-L*4uTh5Of$_`j&=X?X9S z?__9;!|^l1y1GQ<r$0jU zgMVOP;H}AzNBPEc^5r|Me-SW#ros6Wz8&^QVOKc+($Z9AmC=>M1*dWT^UnQ6tJ70U z#<+hKQAecc@9P!5hY7JMYb1>w!JpZvzn=a3l*DG|*MF%gKfTmgeu8;3?b$+tqBPxn`04$13nC@LvUdVx;`Lj+xO7i7qQORa9YT zZKEr}chdX$FGLn^4QhXH)v5$tTQ%=?fAlEBkE6X6?0+HovE;|S_;>yGF0ZxfoVFP%}+w(veGZrB`hW|xua zf=up97e}n^Qy0QI|2n;XOm*5+_4KQ#u$GjjrRtnv1(J|tr*cwsd>^%S>Wh|6XNi7K z5hnSg`|Fl>)#ERfSok%m8El`?DLUYg(vq@uafe`Yo>SPY-18*pt-dv$iY4c2EFE9iaV|2m4DN zJ-6CCf67TNeDm?Ka#dOV-n91DQ5-*2S!nhe-ac>|&$i2tn5UF0xZ3QOX=mOgZg0@P zCcY4`T)OwmJ>v;_xMShhYJM4%;v|%LgB-YAosZ9->*^b?;*TT^CFsUPE~iXb?JPpG zN_E{wzQ62)BVae=3WE7;@CTLBVb6pUESRuXP%}M_OVT^nM6}~I4;*GD4P(9kNs8n2 zi0S>*h^;XhzxveK;km9OW#=0i=6mhJKqMHm2}|6QEfTwvGF`Qi<$Blfm)*8CTiw;b z?R}+wSf)sIm1vR38cp>%-yEB#yfQp3@UB!-O>1rem=(@_8;|7P5B_TEroBqB(nBlQ z3S?Xt$O@Gfl2u^zyra(bkor8l-~PLnTPgLqpPtWU02>Emw3BNvzRhmKYTYZbBBTSC z>jVN&r`3pnpYjlSd_QI5WKqhOp>_5~o+U_CWW!zOKqJ_EdHn3FOKO1fr70g6KWCYw z%s;w?tK@#qxV{;)Y-en}I2LgzeJbUB-ln%*d6zBG)lHz@x!vhiV8PJUdTHtVXiATp zi*bD-k_Avb;;1rsm5OCLPo$Q9X_9ciJ|-PJwKw>FI?U-bxr$uN1=&)tivg|m9xcWs zQfG?(G5*Z&wti<5V{>;xyz8?(Te^Z6tD%64)0U*6p^}@a_1&c9Yu3jb?*bcT zwjVnpEtTnM=xDjE|8)vxaWA^t?L=>`up*I6T~j3Jm6LlN*h;ckSwwx_ir1n9&-E;hq$z*`$*i>^ZfVw_vc!! zh2wP%*WUY#<2cT9UwdHyedJD~m+HUIiCNpA6hcoFBsITbEEjhpUJ>a>c6wcaruglv zbOqCLjdKjTZM`?kOEiJk*7MKo;ws@$@}}^*zCU`7YQcyv1sS$zavCeSv`^4)KFTJ! z(sv~gn7osi`ynv^Wt30#hkwtQ0MWkiG)qRAVVTWUSTF-Kzi*AM0qrt0f#YpB4 zuxiH@N=dIk)b1cAW}zM~#DzH{^m0~7PG>?B6RKnkF4yPtS}ytKFM8aPBMBaBImUJH z89(?3zO%ln&>6@hHmVpQ53fgY^M8-PDTHF((M*q zryAHKeu9}X%dbmNwbda=3>*^mZO8IE0w*^K>K#M&G$>fIu|oY&fhI?-?@ z_J&p33Duxu_bX@2bgJQBxm8@hpwizxAsgS6ll|XEgOn8- z;x;97`NbVhyv9Njdt@j;;1@CpN z2}?zxv(#9Yp4MD=MP&Wn1&iR-GPi}Dl~+Kh!}HRLkKco%mcD-H`ZE(6nP^A)@S%D| z-!ofzL=*oQ)w?_ObIyEfmmL8I>y&M)!BJ@)v=TnwA{ClVQiFo-%U3&~R>Z{M^BnF( zd5uM01qy9gvRFHZ+@~)`i7?KHfJ5Um6;my->RrU`*M~kd-6p1yCZ6aLcDw#o?8g~Z zBdR%_&%WKYeS6aswoOTsst>I~nkm2O?PGK1=XV7qdw2`h} zmGamj*<=5l)%YAkIYvs;nbEGG zOMV@~uxF=11;SSwM_zjck}Hj)0Rz+Gi)}p2xrNN{A7{8Yq;$|SJ#uU5rh5ueEcO%br1aUoYJ_HtYOHHMwz93bTU^4dJ5rC)n}Q z@)eTT1JlOkwA3T_PTm|x_~!GVUu9j}{+ZAN9p~u^-^Yosjp0cpgac2imz@017afNh z;4I85(bC?;_@kwe$^_SDgol;M?rizdb`5z;{Ry4ekb{}F_)nw3@n{y2Kw9*ggunC3 z&r}f$Y_#_({7O3M$=rA%LF}&#-VD`Xnu3I9N3c6QG3r4C2({xC-^Cvm+U$Dm(6i!@ z;QJhVgG&X5zY?#Ik1#@8Bp6gR?VbI!9Tss zSMiRS%oiK%b$!5;=5x=yxrZWl`Ufh)`U}msimW_QHEf5k(&HaB-A_7LD7xxf1!G!c z9`@~{pR^^Vcm_P_Tq2f`-j@%RjhgEzaT5);)#%$>Ly!i<3>u%{LTqQe&@0gkHR%ys z(elVFyj}QFlnG*lyrtG$$5^y~hH2koq;uyaT4nGuJ$O;&!Il?~B~@%+7qT^JyX{Yu zW%|JRXlqXnC68W-7oL~{`>mY&b#+5kh}j*F|70<{sfS}UEJAyBswLP~o?8LPg%vh= zQtCnBg3~GC=W`I($H4@lQa0RbbG#7|_M?uYV*_5%0hr5(x!R}rW>z*f4jxOv4~AB1 z0@fD?*qN-1zU>Mw_BZcud<|2@@JvRr(8HIdTK)Su#NfFYbVwVE&7%L8lrngH@jA92 z6QoP_?xnrIO!h6%bfWs)d$)Q{)Qf*MXXtYTyZ2?6p*ylXX_DeE4nF?<)VY4U*ePdx(%m6b9Bg$pg(KI3+}wHZEGlv zD|u^kTE!8AsaM2IBrlmMkKFvxaGd6hvC9WCD8ueWW2d{0a|fIOsF|Xc^XeyfoJ#6& zJ1hj{?{C$u?WB|F^t%h`9b9ECsXqmMGYq&8)w|16H#)-yo;gnJpC4KWJT5OyZOQK+ zs}H*WW7B7X@cd*kir(Xb^rB%w&|SzFb2fN5c0yR`d)RMyyraglKO3BFMm^4dLO*gJ z;Ugj#u&>VQ;gNw3|4(LX6y-F2m-+X(i)x#HEa)ZN1oETnfwaiJh(Z=(Rr)J?B-p4+$Y?{kXsADTG|o z&R;-jFpk?bn+*I8xwPrkxLlu2e79Tysi;kkI1oe8I#EpeUeTmqjd}>RcHM0q3MP$v zH_{*Vr#q_b_t*otJj|$Fw69Lx_Xn2mMSZN-USz=1;?*u2m1Jgg0{?zRMU#0~ZOpC4 z)j9<+w4|}+a>FNPCBY#gM9;TjODA-IIDi%xPo-y|owL4T`;_aGR@3dh(!6TxVE+zv0wa^*@!*A|N;D zX!Yk7+gP~ksSB>RKK|QHBN$u2;Q#Ii3ecvgYo_;q#R%wFenCCpkH}u&nIPe^>W7Sy^pxu{aItetiSFVO~*DhjN`b|G#?w_vxhn z|DxWL^l|>(pXn8hra!_;yv){YmCKEKo7VMNk9^*_FXjl9y(Q~VyclMyr@tWD{rDgH zsGGG385Bo)x#*D%(FuE*abHsc#~33;NCEK}!%C^$jLd$gx}wXyFIcbd<#?^iWKY+t zKl+cQ+#caEk>`nhtI`srBiP!J!mspIqwOPqe=R+a!nQ$*Rex@wQYX&hmE#)|$KI!b z|Mfd&x&l9&1mWSRZ{6WB;Z0J8M&sQa8cKfa*Am}Yi|}$=@m-i{3gw3x6spzV4A4yv~ln7LLJ24l`HEKaWOZ&;Ms>YIn5#nY27~ z0t}Qm%vzz0A>R5io&gTF-hA}(crC}TV*58zF4Txo6+Suj&!2DPcHnUoX>0S_KZVTy zO7^5q@y->hDNI^UeR0p_kfpmXPYyy8k3>Ga`}*mJ52nxl=_I*+`tqzAYA((s2NMiq zf^8};&iq@-x00So-PYBit$5=Shh^}F`U(Dn$B#aA*EPJO$NnPw*GLi`ze+#5HH@wx zFPR^UJM<`&aDX85usJynrkJ)z#G+7(YLMwMoYv z>`cojn&ZvkPnnc=_fMI6%%I!b5#WMXuZP2|_>$+J`t`ne!9%Z(nAVjHY+7ecj%Bk} z87B%-`1qKf;x=Dh!hJQG=SulsmHGaX`c_x@W0sL}L+0smH%BHj^Ao{wVtt=Ksx9%z z?=RmXBYqqnep&y)+|!?@=1(Jc%Dl33i{Oo?0(aoBy+&j++dpirhLWp|C!&B!<<9SH z+`)}pVzut=sWT44ny+_hy+5z7uh-3ddC<{#np|?j%G!FVtNt?FRRsUrw-0v@360wx zOg&P^9B^F8+S=OA)`Cisq2HSM@!othY=XpYTD@zoe$k=_>M`A5O?Ti)4EK&7Z8Mg9 zNh9zvhK_J{zoV{5GqirA-*OoXYRAI#)$Omdx4z9I98ta11+9upcVdd2(wbm3-?t2oS3DX%A> zKu+}mNuzi-PM?)x{Hw&?0+9#z3rKiw(i=P>dLsEKhn*=n!8KV*HZX4O$ENYC=c@E0 z2j80>{BwqI9sB)K6k-c%(j<{%^r2y4$WaP>-})+PR%%R$Zd^uzeqh^%TcI& zP4nB}XLv=h`4{%Pj~4VQqPu8(Qca_%yQVGEDi=RTt{e!dN$}&m$K?`oZ1XBd>oLY= z^23v4Y{WT=PHOT6VokczABL>PP$Fba25vswIL&`nAFVruy?++By2@Kx!-YO#WyvEv z;lOH%Rj3k<&El8*D~8!F<1-#TFL@nfY^j6I+r0#lUT0_Kke|%ff;g?sTdj_qe+$XY zpz5VegB~@liOLT$>ApoMCR0@#ZyHHH>tMckGPR;sLru$%k_Sb~FF&)x$+%l!5~yyv zFS0qM25ld#_}Yh_NY6~%yLYc)Z^l*^?#-X1-0jnsQ0a@j2lBSpX(Q?RoF$h%VxAOc zSeq=l;3cS{qC&kL7>E^j#%_rY!nc<~0FR`(=p$wa_FPid5Uxv#OFA(=&V7=Xc$1lx zb@^M{>>~k4l^F}6Bx-{|BRprA`7W*N{k%MexcK-OMFwjtD>KA|cS5-YChLYY<2-{{}Ft&fTG<>?XD3KPT0Hjm>S7_r}65WoCr=$x~*+Qr)4$3iFgh%VVW zi+0G^1~C|F{?p%kbO7f}A75>`xX*NPO3MnYe)&bPoR1{FvcvtU!~ic5kHh9T`dDP82st;6IK32Gy{V}m+Al_ghwTHB=xt|^?nTdXx#P2+#M9S? zrSG->quZjE*Jr?h`eSED5We}rGJ`0!q`37xP`1A~u9-xkt|J6wZEaUuqUG|^_g>h& zkggOF<=%Nxad9t2gCWmv?=ik>O|~rT>^FX&F3x{X=C^IwY{w-NJ+-<+AQnwWK>L$+ zzX`a0($#<{yM9Y9huJ*IywBpT7I8;|LXzKKD@_|bmjkbp4`N2o+M2~ORpP^_nrQr% z7O&?{Wf#r|!G%;+K8iIE?FU7i;h&f=^L<(P4U&Pr5Fs0!?{hwX{BptT%7umkO5b>yrXFqhU=MC{K&G@Zm_(7ZsGMPCu@>Hf0@Y#JpnMIM_K;1yg5&9dvy3e zkkp8KUt+nHs06x3gN23D8kdY#z*p!l!R8=tK-$biGgokglA_{FWM6OZ49u4gv?=^p zQK3bMOFE2?Q|i@Nao@ngLK?b^$J9<$=Z$NWtHntlf-NJ;4h7IuY?C3!6VHc`#TeA z+KjSa!(;72h#ggj8|)o%V-)rb|^sdx}qhg*$^6}(Cd z%pJA95h?0U1+M2VG2N&3KWAoTyZCdQ%aW_ka-Gx;3Van!R-R#P-5yS$IIJtn%rv>x z7=q1V@}Nf(H%2~L_x8ufFX`1^D3lTgC!Ej`Z>d}@JEI=Pjj+{!o|7jCQ4Y#ZHS{K3 zeV|rgC7)lYP*cU3{>D$^V}uEtSCO`}JQ0pTkC}c*?Va3Y7c1+?q2$k+jPw*+>EBKN z0RqNU6%=~z2Ay8dWTk1HO2_G3uoiEW7 z;Y`2`)EIvaO_4bo*g80@!_E6{fT&hryx1Bpw(|_)wn z?{}$>V?K7yrg^p>XzGCSr(i*>GNadFk%FPqG-=iBF^+z{gVF2(P8aQ@!&(kG_gl|d z*L?svEMfj1M{wHLZQixXDquNo_=`WNijEMHzS_sA4=_vbM~m0Xl)Qbuvw6X=ujwX;L$kua8f z2Kox-Ja+cs6)ayyUfm2?#`6+>@HQqbAZ>U5AN+KC;(BnS5T7(&HOaI}Q%S@3p6kD+ z#m?GsukFfreKoc!@r0#twu$8Z4Q8(W9yNayFGxeTtg9Xl~As5#lM)Ma5fZG?||CZky#gJ&2@P?9J5+Z|v}HX+hHG z^VQ@{edVlLYh1hHwwm`^6R~hfcb!Mv>K z+7pm6HWslu^bVkYW-FGy2Df_8!x&cr6IK-s9y3$Z&mIW%<8HN)HooAIIp$%9 z1VI72QqC>R#r{TF^Ik7!fSKTeCtaeUvx2)<#`wrcI~J*7w9$IN)mZ>oUm16#>5+mj z=4`=->uk$U#7$g&bt!3Yz*waYQ8+|Yg|vZ6&d?q38P39rz4svBmGB>b@{D7lSLYm zu2bhp8|jva37FW)v02^NxYH7H!%f8ebhN_rfC#XhADeVhnYm_ikDb(htTvvVrq_bb$#6;(S2lQrgxb^Nm6sFK`x4h z%55y>SE?}O`|0Y9cb!`W=5AY~njrCi#KN6U$(VVD=e3dM0o0}}z}9?_8V@>kq&s}f zCg9I?c~dg2sT-3X-mS^+8AK?sZ``k2ofw6Bg=v13X4ikIS`#Nv&vc>P97l>@$R1iz zC;WnrTp(9^#6+HzD7rT!CQ=5wBYNBH8-kL!=FS7v$UCbqZ-u_SIK`Yf@dDPVP8!`~ zR#;wM(At^~e2m+2fC@}CO{CbNd|4aDJBWZf*)QaOXVF@){n_fUq$ChBt8aXHnPbRv zkX>3Peuf-`^wMHHNt2X=tVbbCG4g+=mAIzE-Xj)YnwrviEckH&q%^}}(6IUOsMgbIVKe~o897lDY4DmsW;V8*CU3eY6-D_$<%{Q97X9hctnvAI zd4W0w@H@|C%OO-J7@xSrv(?RTK zw^Wve^>m5pB`jct8s55461cfxbzV8B4(>zt~@ ze-lsj{4tAdW*RE^H*O9Us5xR#%bMWtSY*WhY*k*_?s=XL#C)&C{>j?F@z!VxK0@&R zUyg0!Q74GWqd`HmhWo69E#T8)sDiGU0n76fJOo9Hw9uWJESV^1R;2%;&|Sp z@eG`WF%w~o9xD*Cq;xs9O849I0#D}HBAk}32;8X#~XrvP~jlwsZk$laNKpvxZTmY4Mm;p4_0=$Q>`sR z#Aqxrm&j@JI|Nkni>-duef9)Gzu)gbMGipfNb`%3`Xq5K=rgZinep0(w(CqPBN80> zqoF-%W$$v4Zm;iZ&DBZ@A4Lj z>eg5}84Qs<{#^HaCw;2Q9M_=E4&c2JkObKM&ftW2A{dyawzl(pVwnZ7f*eqc_i6kD zj9!{ha>m{N7#u0G-{K;o%t6C{_-sJ*jB0(rF9l>EWzdogK+*EgBCcJRSk17oFyWPB z9Od*wb`vt^N%6CTypnRWq0A3)2?^?;W!c4r*Kcu9@v}*HZqHzi3jaWVzj=F6!JiJ= z#gRwP?YVC2>+5?S4@+w3=yZ`6?0=2Cv@R}N=x&z{6S6tB0u_PkL{Z&p3%McB=F_DW zqn`eLdr?(-{r7%scD}!+xJ!+iBPSsNpUilAX!jaoOF#iJCOD$o0qb`f7H;Fl!o@5P zIaB&O-$eZATP9S0m1;5s3?p+7|wE2MOz8GiYDs-pnbcE>usrL2R z2OMHXv&8WMrUSpBf|VKD^0mU8$X?D{uRwAoRulUU6q)C&<9i2321RSmV-3Z`6O2y@rh@bV*sP#(@KOd;q`Iu=h}2CFCPfqkR}k1 z+dxyg3~^>wjd>{>*G!sr=c}*ulk|>k5GML4je9s)33F?x{zw*I!?Y zM=KDHD1!$*i+vKWdM(c+LHIxQr`XPxp_`Ss*1reS?%wB%{P`9{=e1KZ?VO$ zE{%nk&R2ktV}n5R0UIdyM^h5PVAsI;DC44eCxPG&9@)2EtrF3(J2akef{zAPf4IvK z9v$J%z;;9#A+9%p@dIMqZN@X_vX3^z{2a8bKRBH|YPk9yEP6YF3F0cHtut9EuDdug z0)^MaUS+^VqoRsi&m=Jt)n+O5W^|NcxBt2=bGjRPTS_WUR`w^>fqpG#EhqEoX&t0d zd)9jvQw)>U-wqwIDS06yYxpWI?Y-S!Pi-H6zE4!e6x;I)iGC206pM44NFKXMtLOuIS^iD@+n79hE9^3_KEiWw9kyR4j9-tcRKP)|VJ`#PAQXTqMN_Zc2~DYJRaGx1BZ9nt*@=0t-tesEFHkVIP<1!DpNDNdr;*Ig4r*u@-aO z2_|Q>+hp(3JFB0)T8BtqZ&QpUztzyUnG>|09twqs;2(8%+aryLauJ~t@fz2yn%;%g zb-Y1%#`^|}nrp`^X*m?3qLm#(@KWeg=*fBJE0y`C@V$|P-n>iXzPaP*6 zdi%19s29&82(~weiLg$r%Fk7I$0Zn5*UIjb#OoCGuEfq)uty;LAIeD{G-`D^CauVjdV^t$P4OWURPzzJPm z%YiWx9Yd1{gE2&TTbb9D1k@>I-FjV1gg+@-BsjdDVVl6H&=!Wehf|E zGA_%`emZSeH`8T1*Wje6rgr8dI1kM1_bQ%K4caW((UQ#QB+l(*c{H`x0|ZaK2vRdp6<0y?sqzE z=|3H>11XZD@sfeoI4(*6)zVnheJ6U}VX~yWb-VtoVZ21g7J0ZP;=KCB*~KM3F_Ecx zo-?s851fL;!oi`EBNwGTdB@)h_Xpy{=4U90ezQBQ&T+BKv@5cxrN!{Ug9nO=it26t zXg;q)vs%Z+1ym9KOruKy*!7ZM-iFZ!_?eZBO~co>1sWSWUTvwSlFV16ml3PN?R)-C zC0(p>!KJC9;#FTF*W@PHX@??py@VZRYOI^kr?cp)rsCq_<6$}4trW-B*A5P4 z;5prPrnmz?e`bFA^o3EA>+35F+HET@kySuo0=0ELdy_u<@B*z8?X96KY3D}6?dj@* zYqoN-Yv9t8UTNB;FQJ55ja4gsT_W<*U;@ z-z_cl`2cN^?c_tm=}WNq<;KIF-O*A_OHC}WY}!A=P&L={KPD0I%c?tl-}0?(_S50e z+|QrSU#e~Hf(QV_S_XxgY7fUxXNyon&cwaO3snx|O7$>F)Rd!or55n&WN)+^SVL&; zOxI|19CSo>wr)R~J_!nD7rX-?k52B8UP12A2GhncZ71L+uR79-)+=^xuC+a*IoV9N znb+=HbKc6jyRWk_oKrw7#+n43C|`Wl zT(^XXsHM#alU->ts#UBv;7n8sGTF3@63>+nc^zE`N||8lT)f*lfnU2{*6Gu4w1Ivd&%d$Y(tZ4Q%6z^w@<<{>4OlB^j~Jb)wdMEx^F|zVz5w)MqtdW?CfJ;LTcZ9|LZzHXcVZ5FNLT{Nqw*wOowr` zU5+&6?(Eo^TUuV<>5YKZyCT;T5eq*1TDrPl-N1`R!JOUkg;-ixOt4XA6niXq$vMJL z62PgE7DnoNdP%*#z1Kz!gTVE#gO)=p(qbNb4HNo_*TM2n@1;P}OV05n_vTugaaNdq z?RGBKrp7R|%gTu3#QXNY{OR`(ypAFKNtM^{-c^9OR|Kko@ai{NG(N6DZnils$ntvI zup*%QR5Gfiz8hK0O2=o&%R||1Z9sDi3k!ia1-rP|;^uyNO%budFPnV zKpvD8lNkPLF$}edh)AhfnN6F4H?MpA1naXsUy|sZcQPhRdSXoyWuTD@eg^&m&9$Gc z%c!vPHN0pLvK`ChFlZ}6H@Ix5x@qf|+m@x%7mQ|cCHQHHg41HSbGXLiSGx38w7G;? z6lUw}6Y1^en^bZ}xMDOl?(Zc#dpo8^=a$~OYa_Wgl_i}PU)^1yl!a5p5D~Y95A`a2 zW$`-CtjSzxmuJ_6hw2G*^vN`xnf7Wgv*_bMM#a#o1%v1LlT02-23l)0>dS_)a3WaZ`67?ijuh4}39_ooOBmQPu(xi|9z z*~bs~!9<>Zc`K?IWS68^Od}Ks;(1Kzo-e{4aa3`p5nm@Z%M8EM&x_UIRzwI>I=1ez zU3Zmv-| zI3O*$Q9xw+MzlBmh?KOTriRb96!^qaF2?n8h4yO5_zE@oG9ZE`O73XAe{>OZDu{q) z0C1)e0R9^t+NpiCF*qrCg;cZk?bSadgO%h#qW)!;H~1n=$eO%IWK@zpB_i+~5C1I~(5RBp*{Xaco{}oLyOhtfC zA@r>Z!Om?5?O>2~je!|CvJ|G6G4`=XLCb{*Vs;abqv{!ow9ya zJuRv-w9v}a+>L30^zbocuxM%Zv6n?BxhRj{JzA5{XZ0vDkSJGcwJ9p@&e@j!>Fk@B z)9n`=XK0v9Lr{*Aof&6hlC;f)g*+` z)8ELAjLGkx?xL$YyFnPewovh_%cH7k)!jgw!E|wN)K&q4WU@1ox`g)XyvtwDVCe|y zdc67CmTCwXN$D^S`XDM`ti|hSJ585UuSy0i;aQ*R`g!HDs|dgUr6VdMd06$gdCRgqKz|qlnNBY<=`1;k`CIY*bk`BVFCBUZO$tLHb1bRnvEjj5mrQG$f?1 z(=y5=c1Ja7z+GKl2dVkkxCc^HtR5t)UTnu?;3-PpYDFHJU@~RKdz5=m!{Uq5OZl(L za?yB}6|30Lmi%8(#iU@|Jip*I4yO_xA?5hI_~NqD-im26LV|>?lU^~+LhO{H++6qV zW$^G7N+DV9fir`H>37UcAK%CrEC>%z_OKiqFVbK#avRqIxJetV;Nj8H$+Tt0cnYT; z0RA8V*?xOZyD;vw+8y&gR^u)nStfwI`T#2*dW!Y-^Z-EVr&+$)0^&c+xD5j4GBcPQi1?3qAM$9{$==S#Z_gGU9xcAoStfP*iVkiiCb@sFQiN^kVjg5`| z7wZFQB@gIc0wCMYt z=vTb?(7W9drVedAUk>w@RgLP(k-S_Y^q+PDrH zF)NaMpl5Str*;yep{?C-E**(`@7_p_^(Y@eQ9pnDU=|fM2qWd100ooHdM@w}5MJ9{ z0c=36SsH**!(wuiR}Ni!J3+@!oHfe$89l(>#7wH1Ha5io(gH=i)*tN+qV|+yhNi~X z+JuO{zCKFd9n=Y;0E50@0~33lc>oD3HBi-4qRwG*uKRaa;fsPoiTo~mEEN^DvOnBH z82}}fvUQ;l?sH~6H`S+qvVSQpEghGdnkyab?8+x;49p=1gp}^3iV_|DWGK}CJk4}N zHSKtJU|;j~>!Nrz9mH~2@ML2Fs<)4=W~Z;*#?fPEC~LrGcGWqssH#dmXsn9iS8Rjx zrO@t7tpPxPVEC^ML0PJ)&|2dQ@1*0egjRn4mTz$ z8V>}AV39^wWM_=cPQpYoe+taUH5QpamU<{ zz7jBldI(Yu_c&+PQA1w7g1)F~1O|E#3dDWR^d(mA-a?&?u9*T}Qr-8AzJqi6qxsBJ z^W>Prc+p&EjYCf}%?9%Xoo?pJKITIOWPDZQq50>aK5RFI81AZ{jJ@xEc2_i-J@Q_i zPh>Kuj;V59JxDHAR!nIA#a3Rh&N?a3EG1R%t3HEyJ901d?0-}gmDZH4FN(37^R15nXLlumq!jGW*d6k8F5U01IF zgPr8^KM3;iS`x><)&VEC6+fJy6K<8w#==DWC~=-yXJ@Fboewh3qN5QMjN91i$Cn4S^`j#rK$Fe?{P{Ngu$RrvUGW;j zU&{uYn+X?7S$o_DI!;cN{30wFgQS0Iw{_Z3@u=g2s}H7Qg|Dlo?NH#&5toP0qvk=; z?=Os>|6y##CJ-!PaHH3+vkKC^6#*YDsYZ;|j&aXKS0n!oWoLT%0?*NhTfnZf0f-nZ zeDpO7D5ea5ZJ!IkQ43tjyZK|8wgTG*;_1!Jsb7Hzu)54)&^jzlxM1Auc$}AW?Kkua zxQhn4f4;msvCquZhd{C>*Wp?1?N1fdZEWg~-c~uGx|QPmHlvr{nj+2|^@r!$BB@2J zuBD>Y6*Y53R9DK?h>?lOHh4?`?tQE6n=vQ*9yX4~pr>TpS!zQ>R9|~O%Zw&!mRRQ^e<>{f6>@>)x1vKqoH73bhEpfIv%?Az$h>LugZFw<3^NRM?F3v@^-0unV82=^Jkyb1frk^~`_h+1{(W9Lek zBjHgxWN4NSntvjk3Z3T^PUWw+7nmjz?eiagH*MOiwC8@f%Mx?ew|ltT7t_z4$i%2l zLXgNRE_JAV^=y9nnH^}ri(#YJjFD0e6t#1%IryH=Bt1=rotc^W{L|fwHrzbN#nzM1 zPW68JUrm7{E2UUfBO~fB2$*eiB1z{fQIe<4910$DPIBK?p7QXh*DrXNqz7QU+AaaN6tnE@gz`DB%A+qg zBpv_!V5g_gU2w75nX2@!k8%;(zS1!B!^rc;n!z}mHnY8DO^enC+g2h)`M`dZHs8^F zm0H?z-OB8Rv3-!nkR%a^rmg%GKrXVtyF+Gv?Yi6cC-Vc6?VFZnsUtnO@qDo(R@nQv z(EH?2@--eIVYv>UL|J9jJjk|Y5qvgd%x&3~^npP^+dWJfWA^p)`9NVp!P1HogkDlQ z3mE@9M30J#j6#p{nwi?&MgJ0N(N-8d4LXu{x-!AakQe6fdSUN<1Pi!8 zm$hgxU6pLPBPO&=x!RO7d{IftqhkoiOX*v8k0-wlC$c_y0)YZaWF(S$(Z-}RdqyJM zgiMjvOChDLw@!1EWP9wJ)|a(_Crgh!roRr0r25FD2-s@_2?!%_CW}ulqDx#xRSpD1 z#OeD`CB+)6;kz@smNt99=&YBubS9<)Cw{jtz}>R)g57s?vn&RYC843AZqGyw1~2!{ zpt+%;$sg=@XJofA7kgT+uA1{|wtjdiX4R9IgU3C`4GdAu?$i54mh$=YEObK3Re`-i z@03NnWh@}AzTou1X7_quFFA9^jaWH|77d$5o^|h;-}DrayYYl~RIx-qt=+@X)_oUE zb&4{oI8s2L;@!Zf6i^0oVs)oKVDmKq@B&0&1?13)HSf}&CPfyZo6>~hxj^c(9?F#3 zo-EI}&emVPV4v(S8lx}nw&Hd5w17x9)8Le2Y(W;34op;M0X6`Nv}Uaur#TWYmBkB&V0yZ*V@|J7AQe(PYv~0u@OCbln*2g zKHcTGTw}~(;zW1U;D!cJ^#tsuG65`_ZZeFTM2rhA8bh0;I&?+^&V{kx_onst&b(1TpA$5yS1eTpPRMA-yA zCrl5b;J)Dik=8CDlJ|Oo$^~sYCMf}=D=u_@4Zvs7WRW;TxfNY!FrFjl(YL*2yLkq) zjO|z|Omi>*{j%jg8fV*E2fMcU9RgY|e)B+=OGwG@Jx11e^=Sk7uB?A7fn#yAn5}#W zbedAIfAN7s{E*|$eurD?ZCkAX%#Hxj#rn^YzM{Om{BGAz28M$16ztrF^WeqN;0Yve zvb2l%022|A7BBXjrC} zCzs}n7cby%i|MI_-aQBFJ=7kEYfcsfDs` zBE^F1a!O0t*H%~0)4rrDgVfK;79cb&TxL-3Fj1J$u{5+>zvvKQi{-tuKA2ICJlZf= zA@0RsphO&bdxRWrX}zXn(I6CGtNq}XYmiV2wmtx(@8N?<3JBQkFTf|{fk%M!RMT0j zs)=$42fgzN=iEdk~Y_&Ch?sutKi#NRDFf6?0ozE!+Y z_2b8FFq5zHbJZk_wmsYoNBOjjj5ri{=s~Z+pLb0OZ$Z@vJz;RXrqg!<>Wn=bM@8A$ zFZNrSU+r_Co&&ixG$KUX(6CgwdcC+Cl#*D5+)GbwV{L+hja*K0wS9hh?2Y|DkqnMp zZ;MZu&^TpGkS-}HK`$5%<@I5?X%A8l4-ccZ)cl#Ll=x*=GBB$&vT||;?)B;eZ^&OC zsJmvb+Av|QDU8$fY@l1;u=iN@#;F8xX!7KkdlKcEzCI*rFsETp@UV>Zl?$`k%?~FyHQkwFBX07&0rDpbVSH5#78#)q zYiJNs;J+G52X67@8o6y=qcGs8fH#9&QD)R!V>`(S2X%*gI5@+gqr-L9zMfxbx2p1b z@%uQNOMA{nC-~OVw7afuIuP`G8CwE(h4SAktf|qw)=4EW=cWMr>m@Rbrw)PreAg1h z1cM~LiiGr3E(Z%gbW`u%`{5XW#wYs`|o9obwSteH-WeB+F~RZp@TXa zGS$;|$?iuH+q3m2BuXh7L9%J)o7qf9*IU?v*N`dNOnKaV3?UE|)q)pz9P_glp_ zFEcLBkvno@W|9kbb}ltYrU!{RF}z`lB7--QG;;BBau%AKk`D>kdl1!)7DZ66X7;^c>G8eoWTtAC>7AgDhN^)-KeIl4Vpyh|NF}x5F@JiA+5SNMRn(&l zgWeA?Fa-sO>!hftqM`*DE)g*?KhWx96CxOj0GRUz#ikmw9wI*=3(eHo8|;QyNrN(1 zE%*pX;fihOT}Ket2@we|U7F{U!FSRGTsk3$R5dcKn+r;^UfA2ZWJ5QdfDw#<1gz=! zmnWcEpp~u|Kr9x1el4SBcP)4Kx;^+Aq|Hwo=%y43#YxdVP05H#+pVtM=Ka>{m)k>G zpTPNyK$T)QZZ&MfoRp&%(wG&1zCj`4TIhYenKd@1>9#kgM@~+j%xf9BC{gm90Rr9_ z8F48@MbOqalzNT1{x<^E82Md5a&wSK{mbDlXf9i)(ZYIIC)hxD+xAc92MV#xA3B9_ zOp~p7{cNrCxJ^=iQ9vt_D=;{?MD9!zF`-QdfM?Ns)Bnx%qZ=;}&ftx9uRvnFGH}6& z=rqbjp1b+sDSUtOq{;j6N34I8{wENX=8slHL8YXAropS4facdk%p6@+u0d4CiA@X;fS>0y*mnkss=)raqj*3sr{52e%W5b7}BWHZAd$X`@2LLnn3hiLWj zw~H9Wk9^GX^krS{Lt*uc9t0lr^*db)y-H0Zj_P9lR>i1a&IOu}=&IRYScI^U#P_kY zC+KZqFpO@6~tCZk3{Ht-0o$V~jtHNe!5s*tF%0_2$W3w&de4 zE;-9KF`1l_lLM86XEfvs(yc3RIb~7k#pS=p^convfx%is9^mQ#5`pNZSSM;=F9lGp z$^Ycr{;(Z}d3kws)V{jOCRYu!t?Kq1x%d0Nc?*|)E3$K*%Obvg`*z0?p<3;xyP4mf zUsBdDcj5?`qDMA~8S@Vxi#o@PB1Mtl=AA5_aH2)?Bs_l3 ze^+TbY44l8+c>!R`Fm2Y=Xu)gxOw!SfJHg^9tm|!>+rnK;tq!&#qDP#JT*(ETC~Je zGs0cRhtfJ=Qm?lk4yxe{EieC7a!=DfOP5l4h0f^IRR0xTwVfI?S~SMSWZflPG|Q@0 zZwY@U$HL0WlN0(e{J<9FJnIym9l~;V=47rEEn0TqlEC3dLmQhn2x<-` zxSD?1%t&O)n92INn6m6C`q0-Ed)vk}qxgcs=m%5Ap*^wLJ)!F^naD@Bdq>kptr*)S z4tI$^y1R;Xr{5*36#;?wm1OfS^CalS8S!7yG}%Amgsg{i{k%E5bW`)$+rjxQ(-!oh z?=T6ziKI9pDvC!^^4JXNQCzG@h_Uq|*1*DYY|MfF;mH!3+t3>Ka{olIlN|PKAc9Ap z?ypCl%UTk&SL<|EJ^$-y%f{@dLJTnQQ2oXCIvFD$f0yYPYq%D_H{O#Y{c(5gy;fyK z#T#EwpL@FaHPIu`=^r;MLYLCuHs0l276z$-0TK$NzgpNO8s7mP6teq#Fegsv900ox zS67B0P)W-w&LW>(v08B(rCiSDK1iy1{F-m;)@SSl`$A5o5~UOrr@&bAP^Lbx^yTg) z|2)~yt}qjqQ?&*HI#;hQ7IzWVYTwSE#23br{J!hjHF^}2dAcUTUem;+*)QP`Bmxi`TZM~+_LBTa^5`S^Yi9kP7BgS&GinU(*A=K zEr*ea)Erhl!o?>o-N-%;c|!H)SJ&f{5}pp}=JbvM$gqUF6~*0xqN?%e9jwx-qCV zhp%3}dP+Q~W+ZTR+vsX+qz_+m(F|P&y@g_S0uYAbYYchkMBj}Y%Z{C{GX(%k2wlKR zZNAW$bVLSNGpMLiv@lNw^ctZ%SX@M0`bf2hiuNBkfLY$^8?>Bk3;FpE^H3{RaQFr` zthD-bQ`RMeam*-!saonQL&2B(U}SI4lQ6)uZ~5=iBUhA#U<^cJWeiWK-TK$6l<}Ni zCsoWoN2dSO!Xoop1G%$a#U1p5&bzRi)nzL_%HN*sD9Y^c-rG`df6(lrUZ0=P#ZSLR z-FUpE?%XS$vnFRUoX==FjL-K=@Q;1JUiiSJlG_4r?}t9zQg@-Nwb`pj;LqgUzk0my zdL&2CZm7JQe7(KA%#lR9>1@HA1e=nUM~cBSA+Lo5dsB8tKN2|Lq*i-t2x`{Zj=)yDXwP1 zb;l9^HMZBBz#c99B! zM1bQm+KrqW?sz)fQ28e8(B#m9-%y2al5{r5a&Q}+9(SKsZgd-uzqEXPWLuj#h(w~0 zK>6bgPr<~HysEr>;o_*4g+}S`9cz!BX$LYhP{hN{J#)j;gND;BzD-s27@fhwJnSfy z!0;U^kBE>&<@oXAq;QiO`8aQvPV!5eMCxSQA!)j8Oh-l};QpG!fYonF4&94X3W_NW z+Dljf@AVOP>o=tV=z6(_#g1miv=s*I962vPYKrc&+!T0#%`Gq@q zL|%dRiMhCxb`Fhaj|9vCzo6SBw{+kwq!OKX4sCiPRI49`VcIBh zRwcxJDg$(lS^c|EF11KZH$`_C38HwtEjezs^_H;?7i1?6PM4n1ExLRysa&)g$?Ou=5WFfl zK#)-ruw=YFDun+0uf-jeQMa(ORmN)ZAG0qJ8Z5m3KHa7kElwtqWgut1KQQ2caNJQ5 zaedpiZR)D3pM6_fTP;fi#oTmACJUX}4&Ne;fDC;;$&rs2#=30W^-i4dgtLo?#cJM` z1)XV##ta)VT`BR+gFyH>b5mY7z2QlG@<7YLDWbG=3>Eyqw-+lz?kI2s%HJ6l4Je5W znf!XfZS@sp4eaLecc?_9GgjZ@cEWRBa`q4nx!ybi5MmY-XNE((WlZ@Keo9`9Q&$)7#Zr(hWl$9Eej zhLpUWLijXjbE?^SAR$CNQZ+Yc;*cPW_`rDMbawTc!3*cl55yWy%47LH;tOJ6W|D_j zBf13&JT$4)G485MIRtd1Mth~?fJ&I5R^pRBdCRJ}(&m2!YzoE0$24`R_v8bmCE3Pa z`qR!bt9VXuO-%YNnXDd69`4~?P{+j{vaK*rhN2Xu27YsdcAZd#Z_v#-3W6V9Uk5_H z+&$WpGwnt=@uNqNx{Y3?a&TAxXU@c2i&KaSN8K~0sPNp-*Eh&gY&`9*0X)MAbrI7O zWz*`YrX4eaU%osjIZ%GUUktWOzZ>r>#}qAcJ72WyB?2PBe8IdR?Fc6t5b@Jr z?+Mi)4H^W79gC!~Cc+ujW7OqM&W8yL>lj^wIRWNnlJ?YjFqyB~7&wJl@jCKcKscuG zrC(`spGF3*idSV)QOhBbMZ57X@A)fem(=9>)r6D58EhY<30bKIU1;TiY{#B@4b00iwmDQ4GJ zexFEdS~i8BKR?$pJT@Uy-uKvPzdZs1X{Wn%yXv#F#Kgp6U|mA4tFIsUxv9w;eAJwI z3)_;5_$(jo=^$$eG-YAu;8+Wu>oubGI^Fedv_(`1ye|Rq33miBjv`@Bf$l)L#fgal zX)33fm=40DhO@JCiey53H38DGqzP{EF6#^c%j7i(QAERujJ4_Yt;8VC4!K@^@53aq4hLEcoOEX`vj8~|<@RjT*Bd(3RtaZ=@! z7&xz3zPx&Rs%M&x1wJ-VsSMqy6oN7nWpBruu*=9Q!f-PXc%g_{uxL?H*Xi;4L!q@& zG+a|vrLuH9NJfhS2FJT3jSQj0lfzxbWZa{8((9yqiK1HAZ0d;ngwntj;=PbIkVHf*Nf`D!@leh&tz6 z{!q8!cR%umS}mhcO4nh3!y{9Ga>Q*^mi1UTVdO#Txsj~Hz@=9}olFL?|<380!@kI#@{bh}Ab zvZq&>5xCg3JFIsnYrX=ksQdVx?gG)^dNgOIvB!OjG0jV+aDB zo;`b3!z{t${rM-Q)b#f!Jt-e=h=p8xcH(>q`_Df{X7KVq##3hL)hkyX%2CR#0KUYn zQkn;h{6Bta{z0;?e|pKt{140SfAO|dF67xU2?>wLn442eqwpX9OZ{B9n8;5qcuw$#!K|_%*H1Zkbd&L@Zc54XsyJCJ+ZOsN%mpX_RildprH8sM*Ke#J3+0ATK)dX ze#ci8UrGYF*3S&D?aeT&WuZsxM9=EXqg0*nR}D!G_V52Pty02bKpto;!PI)}?NZ#!s9Fdu+i&;{$j^g3*_0!RsUqpL; zEGyB*jUjaXY~vu)1r+wnvtzvbh??1f)fpa@vy-kh;&zqW9;?%qv%9xPrTUo|yO!=< z-(!sJ&6$1Oqrak;YwzsEm~T=f%NHYi^83$9@w3R_@2 z7zC0RQbaSF)J{K%xOnPdp0?(n{pSNPD~U4r`A*HksSj~A|(d8>^5@1Q}Vu1z=KNqA_^BnwFK;(K)*>m6&O zl|!U3!li68kH@y*7lrjy8h2x`#rMDDuX)o_LCNl z?~jRMe)^KoZED@m3&UJ3CN>t_tyw*nJJr8x_k_%2exD^|dvW_?{zdKW6j83( zjVDj=NP^4-Gk}QrvXdcOM+>j{eZK`;pP4azX*#Pe)6)m+Ax`UI&Tz{DbSThK^F%^OjusR)I=*uPn`VNnNB{on`;z~x zKAD;IaBPpOLFaGyGz{vZ-NbbGd1jt5%N{PIu)q8fb5*t9p((45j+xFJkGoB{H4k;u54ur$8t$ZJ9gWAV-O&w1uM02nISjEt7G=h{5n>HojW9wb|OTaoT zogBghOBJk4-63R^aQJ%$?6KpQnH@(V``R8So|)Z*62&7Sp^x0UMjJ?3+U9hdMguTX zj+%2m79)4y*s-IqaG$V0)AhOC;PwUGOnbY%iTc(GDC4n+uX8b#vla&pRm z^)d|PTic(DHkA`3Xc+rjs^-u)&VMdLRI2*1%WLuPDTS!kQDG?}E6koQYjC}M=$P5$ z=ILUS9e%&g{Y#}H{IGe}{3~96%vmE`;i6v^@=^DV>*?CL3EjHoi}bx&WNoWuWsk`& zkmY)=%ani3dGO%=@cH|9%53@d@Oo7m;@n( zCjttB=WA;Epi@?)@X2>=U@p{$yX#9h7~##$CWLy(I^6{kUktmj6%+a;RL1JH2Iu1A zYLbISgOs9z-Nr2NY`9ThkV4d7#KziZ4)a-uYf4gOyH{eOji2ENcNo+SX3PqF>yyae zv{+5{H3i#q;cSySdh|2 zX*Yd%BHukcegTz9r#Jr*NQj8(5w{V836D|VLTkWUg-C21nE|P+cRQrP+V6%u66_<~EsRF*L2S7gz z(XA%z)TvVvmc)5L(Cy%J-EUxm7;Fo>MF(aA=)S*7y^2g`d?>D0nU|kmdFfi-a~tGx@F`@=X-tO~r1!i$(k0lQmTwaH;gdgeCA;ATr%}@#yCFL~imo$QE z0$dG^R*T}^u|tkAK*pxnPaxQlNNq3;sWbw#VgbeCepPW5<+h1N?O|Q>PBLq6ocGvN)bb~Zv z)8JiuJ!LcZU|FO9=i=g`*Cqs^f=q=4@998=0%5;14l)J` zc@o=Di>kua0pX8vWNa0h^l;EX_I-PqklDdatB4A5i-G-4*#(AD-88y4@C)bR?yjrY zajFDm)lM(j6FcNG8>LkY>jEyy8^w^|rgHadV~!y-$akq^Sgzf?srknrp>yZXg{a~X z+7uW-x`|82Mi}fUJw1^EOaz}EK@12d-=*Gp@ks^n85}CoL{G&VE_|JAzZS zutmi|z<7{J*E+XAZPOh`RV($)^?ZN`cs20+|1 zSx3qTPGYjneBA5TosgIkd$EGG1{YQxfy9V(gm!mFK_i5EUS3`^s3)Y;mB_`P)=;qs z@k7EdRK;lUBn#rA_`j!PsZ_$wgEhZhqUk(-98~xYr9kFpw~>8NOjbcO ztKaRmha#lVK5dh8P$@(zo>-ap?c3+vcK@=3XD&XK*g{y~vcO766asRov48)0gtOLa zt0y>nkX8ZqP%>#70%0bUhEI&_zZ4VaGHEHI9F{~~Fn@kqbX0H!BwPvPqhY>`!Xi~Q zGJVBzWO+AK1W~Yri957~0!-$FasZ@!FWslKu&@G)BR2KT2o_`oOnwrzfZvEl1^iX` zb6VboMJtDzU0qzr#EaKX+7G!Kemd9;ve9YL629mM7Vlfdi&4#o8h;94Ca3QPh~5P1D8;qcTjIVn5tB1Gw{YNeG;S;Hg#0b?>VfaOeAJWXgyv>cF)N)Q zY=-`iW&L`dtom+m9)F_=;CfE4KuX0D4T)=?en%T|H( zhf2eqEGM^Q*)nxRTg^m+c=8x@QtYs^L&(0CR|S?+K>O@MI6sf!0JEbPlh-UsTYhq0 z=pHB`if_BPx>hF~+k4D?a*$oh<*`9|A`}q-!mJUqP8O=ck zpvU))-J039Ac6Oq!F)e6q^^ZUtLlzuPG36JVp5n9@OQnT@vIA{SNj%WH1QI;4eJQ} z0*OG_4G@bf?jN|!_x21i^}u(j2$gl^pYs+8o_r@rQw57voo1+rdY&wxkpBsXL=Ie+vFUBhtrQ3 zK%Wl1vn0DWIJZd4gp9~4xpsJ|HAnY~PQ&$w;E}h?7fl($up%~Yp;Gvp5Y?dk8uy{E>){`7Nh_M;JsXoxSPlNzy%XB_w9s((2A1Fm;^& z`}a~l^qLOu+LjPeZXp4InNcAoaC0EROMdyK99wY$Xtut%DbfDSa1Rg4@(rTpm6y*D zuQq&aWT!!*NA@%59`Y?cAz&r319SjHqm~3UN0Qdy)Vgu7yIyb3oH+)A6guh9$$tll zAsXu%zrLYJq+yz$S^UGC>owICDQTUjC$*hlFn6%G;WvsUf~eP zY*%yI3VGM)mw-Xgm-J`n7dYbqkxBxJMnVts=7e9Hvh5un7TH6F(j-aSBu!Ux`G?$B zCMk6>E@L8;Q0s>x{1TSf&PAn52?pu?!fA<&>&u+QbC~k8dlLBL+tm8g?@wl;QIGsd z-qM1C-3Q>COBA&KqKNFz`4xO(Lg+3r377C~^|xwgCPwTM+1=gUg)B8ElwN`%FS1VW zL|%fD&FreYI_vdUsvS=ka)-$Ytp{wRsTr$*Nrd3=tuRQ+RTn|YAZZ&OpE@?DG{8!( zfaCE~(N^^)d)WShsO?xXLVHxP1-p%WTBH-NnPyR$r>OOIz0H&RNGFxCwgz9uX~t%r z?utR`r@+AOBaz`B+UA7lEZVt=#A`(=$X1PdG-_!ZmqGL z#X4QtW(3yEl~u3GfdtP}@jlE&a-asyv`H#U=DSUUga8K(g_SpTbYz6{_utF&otLg$ zS&JSgRN6FDjoyKQR_YYya=Lno9ZFU-3;7)~MWK=C>W0<;D_VkI(Apgf%#gKd4i_2Xx&kz74)cH5Y6 zH^nvV%y;OkI`4Dm?+3_O@I62vqA`eR5>|d1_k;-p8B;qvjg=s8CDInH+%zE34GM|M z#mA=v+gCLh)n*5A$FTv6_+wd`cXu3NP)S}{R#ukMt#6&xC7fner;6MR?`^3(>Y>Zb za~;Zu?AHra-SpIO4(WG7Qb~9xFs4kXJc#ba#kI_h)d4K_Ai@L@H3W$}lC)CilmW8I z^2y=GL=wf2&Q}rw1SE0ShYU5)fploUx-&cZ>~$iRA;E`lz5>zB5m|93_6^@^Is#G_ zan&Q(DX(@say&_U;N@!NHXHZxfVI1H!%+t_ap%w+U`WTCPi2s})`lEIz!!5zUwmzW zx1(;UmtpZNNJEMFb9-_?oZb^@B{nj7UO7T6NOnvWU0wev7(5F`*6(I%#|%)zQ(>_WPGNRlp1whTNQ9 zmu$)|XntE~Vd%nGyOKOlcA*N$GiJsKpMl;pD(Tksg3bfxglA?n_w?Arac@X(AR<~& z-N-QnD@{*9lM#bxP7-&Jim1={h|OM0-|phnx69J`X0h+0cz*v~g*;%AvLio6)=z`{ z2UAB4#m z4NsWrCz(Q|?&gRbMAJ#+m;i1#IXMsG_lV5~&2p((_0}%0>^q^;NC2l#il8wtt6_vj z{`1d2&DcEH3qL396DUCCfZA;<=tX2y_beoPU#=#$1%mqkvOd=WM{#%dxOD`i=1^fp zBpj;$BT1XelJf%j@yGq$(?ucGWzu@{&OdFcTuIBmQ$P6D_?FX7a%U)9%)&Ois66e8 za&%33Zh6Q=MCfaSGQXRk))9WoSfqCO=SU5?r5(EqW62_6%_t=G7DdtXg+|?%msH8# zk>Xc7>X}7oeL*z}-+9R3Xct#Miq&=es-vdS#cbgI23{}+!a>rx1UukPx4vc?F-qLI zbLWH*yMijRW->Zhr4{C6%JN^d(ZEE;EB947fC3uhy`L#5ae*A1R1fp_jMt1M9;H{; zSt02kLnocqO$!z-q{Cz3FYZW;$krJxYZHaAFG9EHjM+}5k({_t>+;>5%KrD4(yN$v zTwGj=f5#{Cows-OxhxcVhi&#NWjI8)WfgS%a+_zT!U8eDSu#L{wTfwGOe6hf9Gml3 zk(Msdpbfsng^+~|NGJ4Q+Kf^PX=-WPiQJB}+;uWlSbU}DbGy6v7 zjVDc9Gw~Z=xN2r*g&F{)DeU;=j~N63&xa*No3~Kfu4;CWJ;cwO8RM6$mJWS6{ts@h zQjOLAeYQIOQZjQ(UJ zTz^SQo)?aZ2-DTJmLZ_N9#`& z?ILp0nSh zKyIBHB>ZCbtC*n-HP4^|J*rL#LJ{TT1N#hpx@l3R>ngLzZ_D04JSUxTD%rv4lY_u6 z8vU)Y$J}#MN_;1LZn_PeS1*(Z>+cen{NvLwg_6cLeQ_m5!a3UfJ3mB;SvUg&s^BQs zW$S-+^rmuIDg@r)()4kqgbHK*;p+y`jDO>qeSi9W-vk%DJ%8ZF%xaM-IX$$~J~00H ztvShKLbr?jB8o#a{Viyw?-x<%djm*DLFr&OG|XBXrIRY!RV$-WLbpDaTN)Eeh{7Kx483$Vp8(gd6cw`v-7Bb z6MO$dAciFmsTR;%eP1|~mT$QS!n|**jO1CX0mPbF2YeaHircN<8i&TYEm!|otX>mY zZKkVQH`HrUQM_!9O#A^1yEq3jG=@n^FZNu{8XzO2DqJ>?bHP7zKU9iD^LObsSRSGK z$;QifY2n7i+f-AVy~Bc^7alXB(sUL#viTXA*1g{O@G2X(FM(}Hg2+K54Kbd2DX+B4 z&?g8f(C@twkVydzM&N@yNtLLm{>Y^u?+hrS(sn8bR{0U|h$prAVBe>J2j z!sytTzbSJo7J2yd@rCLvO08s!|9N>h&rIp>P(#*$#mh$@RYpbG4bH~cu?;1KYuQ|H z0N6*o0K`DX)9oPc4Wl-WYqD&wh{%KtawbrbqR-J3t-A<-gG!GCWTO9qg~&Y`*tr>( zhj@&$VZlG&JM#zTR(6LDvQZ9&-QoJedO9O~siF{Hr&POz+2cYJ^cW$%S0D|pLJ=^t zwgA*XA9o*8dm`*1!r?-1o;2%vam2t*Ajcx zR2GK1-zx3@be`P{e_W4DP%DC2?Dmh(E3ZsW@Jm!bzA^sz*r-`dAu^RiL_>toFo>Fv z8{U$;O92r9UE z@?nr|0w`FK>_59b1y~6;93uPKQBsjoxG(<`ttjeIdz80pB8dB_+m40bi53TtfUciV zrWttVMH1fzOdGybWYlbReQd&3_gZ_A`^cN9r~&HSp%hkVw;vVIzg|@qH%}h}G{D@l zw@IS%$^nz!e&0^TN6G144PJT;u1l06X!lPRGq|Bf+)7iGk@2`A_8y98B8dYWSFGr* zfIDxslarIXy38Z<#?1cnE5u3)a_=vjH++fI%7;iVpOXn0x?GyZ8|H1>a2o1d~a`4bN87R|RZGhx1Qy#X@nTc*h`Z?_-=kJd zx=9o6k4H031r{NKcms_iCE;EeXFp`vw{&*K0-fnZLqQeH#EgrtQ8Sg4>%dzWiv)UF z4ZWs{*LaBk_w)1d{uad6XO8@g%5`pBZo3-q77!WUjZxzB7cJ@}`gkD4F+iyaOBf(x zqlt_gOdBNfu>?N=(Bc34@$+!U7+D`R1He)FK=%58Z_b45+Uu_r=P|>~|3gMsbu8h0 zzw-9e^Ygv$a-2AI-p^;CQ{6acA!{(*g<17D%aRkLN@Ebrc)`-O<`9*m`#=Ri zs6n)~D6mdn&l7>Z&9O+6&9qT^lflNf5-O9lCq?6?+Fj`%jC|%Y!{W?9Zl!>c!Ilwl z9HZ{F>YqIu>rafVzv`{im;lX3B|JqN^4IV*0+s1e^Co2@oOF{=pqmvcpb!^=vYaTj z8R39Cp~o&@ic7~<$CQ_=%=8@7&|;Bdg+5X+Mn&y$y*02cfo1ATX2Q{rhwULA`%&GW zjbWm+y}>AwL&GG-u69_R{TGY_oy;$0w;IdYQ5()v7mr56@BJ2hqKU01DJ2etTqLMH%GdeAe#J0rK%S z6@62gu<6Lj)&W&NOrUx^A!Cy{RT{!T8`Jzs@`@lMlQBg-LUOEsU^t3x628U`X;JZC zj&hOOIz!Pb)?Syzoz@V>4jT}$3Xtv~Rp|2y-L3+oqOub1pm3Nk;8s1*H#k^5(%WR3 zrs*OV9TF1K+FNjUC+Zf}ZP)#Cjgszd4XqH32&*Y9Ef|&<>J^)|X*qxXv+h;r)q_>O z2PFqNjw-v-s?-dB`+xS=wAkz`MO{8r9fzrfwDcvN*SyjiCOfA%ym2otHqA2~v<#hl zA~IPxUbvD`_~@4MU9(eJ*WF9EnNCkBy!_Bmn=^TmRZ{x+`AEg!I6&`~-5)YQcw5`; zTTE!Vd%KO-u+?SThNuqlcV!+v>S}8%plk)2gk7vpxXh8p^3+h4IV|Ve(plELR+_J; zvpSpGH_;fh!s&kL!>cNKE9UV^- zk5rvA3}6mt3;+N4hQauT$|c%W+s~f12^Hxvc-~j*TOQiP!U+F*vNc%lL#V7|xrK+Eg-*V!%F+}+m(n-&r0QJQ)I_0*Ve*~q zCy81u@rl=YyDJ(p=T!Y6Yur$&Eav$g-I`qi9`AA=2r$AgEuZJ9SWT$zz`ep9*;oF- zGZF$7gS1EfJky(K_3S=la;hYX`|>8(YH$@{&d84xkyZzXgSLpPAVuYC(L= z<^$-!THT|gR{dg%{$ruxPcp73L{`#NhaPSARNS5Ou!!T>A2D`fLvuXd-TbfUI-8{+ zASjBRvDsb8SQ52LZ(>5@pFDMR$S$j|WDTWC{5#t4&sTLk=~LXE5EGrZAWE5?jly*O zuZ{SBObmuHqS$@yL(Z#Nn6Xpn8?ob5aRH$#ENgAriO=S~_9Io9UD_TU75}RYU{1_+ zl(+A@gkQ)+yexz&&;(C6Im&}y>KYftOP9SyKVq$d+YCuSi)xKRfme|x>@{IWM$?UCy z(~!mX_Dkz8`B_ky8vk|wv}2hEqZNEPfoE_onz>Qo_57Cjbnj=g1?((-vSK!1Bx&=S z{lXNCohe#=x`eC$@zn6cRupSLy$3~=>YM*kO^2%g=ToClIK4}Lnw(Wa@LdTVVD-d) zhiHBR6q^N#NSD6DC3Fh`=|1SqO$6zoKwBl ziF;SqWAXVR{A?r)FoCm&mDgaWE2bf&QUdjzW#hOXRagg?2k z6bmLKvYvyMajtcylv+fw?Pb{@mheJEGH&9+A)rIvqjg3XcLnYUxou{)ghF5N^YdbM z&Roz*mj;WM7u7u1A3Gb>uhr<>M4~1IdQ8z^JM#0ZmMqgGxR?m zS_no?NAz6iwPS&+#s9uD@`#onU%GM2bsOW1K#SHuIjiMX!=36)YK8BIGyHtihg|U% zk9R+(Z7PXxw_l<$`d01|-t)@EUrvFsL}|jx)qyt z=%LGJ{EkzY-ajvZ!Amw-SuLwwF7~b9lWG+-(rc5cd$EYC=Ue(IQz|fsmiLFo5>e9Y z&)s|Az2zI010}X;>&7a#k%hvhIm0UceHSWPb$X4w1>4S?zUJ5W@X)e5ylXS;Z~AlH z>ifDq1Dg{PEz9zsCV$6*#?!<3pWh$s{@~EkJ@r-*kmmgyo;Wf4zQEPji?VLLQY2tP z+AKo#_xAwtO3%wsfvZF%S}9l}`meuA%+^u3i_I1SGXZntORckd@&xNxeg03$Xth&t@`#YPdY;A0 zMQhppj9UEZF2T4p&O}Jzh5rBp#k+@4e#O9bJhUhF%d)Gg~Kmz53_fN!DpxO60&MvOr6vJ>Lpc zKW2>Mc^iNL^rfQ1!Mpsk3d+h_MxWQ>Gq`s|-tt?>U5rliD6`Gh4bK0mB$KIcE!DE_ zQxxEiwyM1HG?HD*2p7n`Ovy!@kFA2rgVLR6F2ZtvkkB4QXXN5q3l#k#0v-1^Z@F!s7>*A;p+d530y^YeF5|VYJdSU&)g!tj%UF3a}#ha z5(U$W4Q~^Ph5~gYy(Z3&XigxoR0NCjwmJ+?5WDg$X#7GmDu7`&QL(f@#VCsPxpyj{8a*7Ju&q1KosnB&t z$ug(!>G_AJ_MWqVnvCpQD`B+At;S_ug^o5JEd+{q?~o@e$EZp)Gz8~XyVq$BXt{E9 zM&?5k1eUV=d=;4aNH1@w94lj;9&fXxez&1rl~>@I>KDVg5Ty0NXzLL)Jn@Ay<4jE^ zxV4T5UxO-`>m-v%o=gT7lhQgn<6Bv0pDoxJ)JM?N4X?jQ4v+v2(is&Z42Hz1mZ6Iv~zg!nMeQuilo|ps&(#Yk8-TW$-OOL!C>)!&{C8$cx`Wx5>a~4Xf(pEp@VoM zVRf{QI7%#P6@Y}sb+m<*LB0F}Vwv{Q(a%jHZG*MbyPz{li+nK5Dx@_SRPk@Hg8qw^ zQVbhI{gK7X>^os@L39^Yrz~k?YlV-1rh30gG?WJGb1+uRx4|kfI5^?u%Uy(Hsoxu^ zUy_IDiQQ>Fb{WU^3Zl3sQ9c&c{j1kD<(O`cP_m*yq5^5I!t+Fq+e&<+NcS`-Y0|v7 z-oA{Jw19?36RvMOpOk^7TfIayA!z+Uv@8>(VH<$5yOr(g@Wf4F7mP*1V`^qb?|LUh zG&djtTcY`6iO{3c++}j`0_ppKvjHqX2#uliO*Jc_8_Oo)nOOFImIWM1)P4?w)wN6z zc|^~;XtN>&yjg-c*l-YvR8lhz#{tv1AN6L0IBPuS%p=k(v^yd91D!)WMB%Q0$1-uN zlASS~Vxqo)7kR*{)@L2+ghZQYcA>E&J-S`T+Icy4?j$pTUHI$5GXHB0hE-NSjIRR#iP0`Ep~uh`~&m3{eb3U0X243?XxX%sfY(?n!kW`E(hiV8@4y z6d;{i5U_F32t5rOZUm98VYI6N3KE|u@%Z1N6164Hij-$7Rt^gqwNEX3+M$qtpaoFMqH}1<8g%AY`HJb;Tvr2#bzsSvIL_c z-`~n#{@3E^ftTFLJ0MQPfk=uBfJPsBEDr99cAvH+sydn~kS;1&-Zh@1_wN*h{CChp(uhgr>iM{TyIOIS#TgpwJ-YrS#A)UdOfy71os@Sx!T(z4<6rY? zms()fML7*7y`D-SCwM^8qhVkWOa+?71CbiE+srC|P?Loa{Xr26L}=jUk&WVOzCa(o zl}3E-CmO-*zf;pAz_TtPC9=S;$%OgAtw^;Yb-@VAhDF69Cnmk#9DFRbrHl`eS>KM_m?ypR=E%u z=f_K>wr>BT!q4wVo>X308pSS>`AxlP>DQ*y0jm(4BXJ{04X&=(B*BCbr|hq{fO%oj zK$Ek3_QEB>w@|hOyAFvR37$wE>S;{GQMM8Ie-0G*=o=6Nx;b;`UFQ>jW1i$M=tsr~ zN7r%0y0@#PeQ(@Ay&Iq%@!aeku{7}ZLTX?KrM%rna`s|#Ajtq@L?0xG0|teGI~RFK~NeJyG+-%_wCyB z7}DJ7x7gSovB$UjU3aO;_VClCeu4~To*MmcY7M45rtoxH-C1vJ-cqDt)o#1%*d?6@ zJ5KueUdQ^zpCT{@v*;{Dnc)PX#T^l?4+~2*8oInX@}Na$_1d+3G!j*z0(C6g#Hu{g1F1bLpE{J2bY6tr72&SQA?zNVy9j%BX<*}98yEt5p*VQHi;}FDsSR| z!ItGv?>O7b0bW)EAq5}ap-i8c9xR=R+nmQZl!#`ca1DlHR zV2^veftyn@WUDy-Ar!GfY}@fY37(kHzt+Xa&^x*_do`SQj8_{GbtQ@ndmQ&Hm*Z2N0nNPB{{a-@J0b7_2xw_a&@uWLxI+)Hrm}=So>4u6HH9-UO=C zDmsT`gTyZYg3KF`j4xUMDFehM_#^2hVu0q)myw7?5$^!+tPNyl3kc2ocJfTR%lSl= zXVKyIO4}b@T;V0@a{I%m&fj$+89c}^!G%fFDiBrE%t~}=syR~^ldg%nh{}=!xrh@4 z6oSC6^!Hs?7cD@IlOzPCC+P}91{vMDv#LbcU~01HYc3#7wjl1L(tr(LSh(DWisqJC zRdO{@UA47_6xEvV{UcWfX)b1`B?L=mDR)Hs(dQ$JWfFljbl}*m% zBXR~GEO#AO^?29PE)Kt;OJ-dZ*tzo%C`Ex6)k7Q9BS>Q5a$;IZ_UUSLK0*%-CpTy? zDf9A z+3v4v1X_-xVAbJ0)w)4QZBt9#A@Othth34CapPVL0D2ewhM88CrDYQq^A%+R@ngWL zH{-0LQa7>nW&f=O;KT_y=n^5Xs91%mhFqzdd2~ArFVW8}sW=gP^KBA>baAwam)k_| zA#`l!M1F|p{s!9=2Zme9H9?*1L7g*NxKFliNHft=D^)XI7rpfaam>$QlJ&q36AR?^ zY)8=pofI>)9&aRO!Lfrx<4Igg5Ni>BQ(2<-F*ZDf`gF|IYggOD$RMGIslW8JJ?UYB zF(i8RzZS1v!#W%Ua|j``$}*vIl4m5Nw=%p#oOl+hp9GsoximFC{Q#BTVFns)e64<{ zx3O%WMl8uj%mn5!rAV_N{8Dpgr)4BxG?chJkdX6y!68yGO%RDT_$b1mw+5{v8d9ud z;wu1IM1m}XXfi=M(jk9C8g&$pU%`6uk3g5p(el1~la5P3eo#Qdhx*MxK^VIR&Ri5a zp%iRyWal)@6=?ViMW%Wb(>4~#T$#iBd-tTg{eOF(>1avP8ps5-_CGhn-J7KxMDB$n zYEs}w@x^RsR6)NbIrP?eroYtfhdPTFpGfRPCW)5VT#4Gq&f%HajBaJD=tC7r5;LA2 zTT!6O#)G%qC7M`rZetDUp=ct$A81M9I26{NYr&&nhoVyvIs@N(<;z1rgbn4Qy>OL7A1_i8W=<)V`E9Nk48y#^obd+$TRQXkgJko zNYvMp);`phuAmbp*Xp)kI?U)>;UvQDtv= zGj^1$w*E5d+CS%wlY$6e`yGti`)AhkG98)s?lP#zC*phl2qz+NW9$)?2P{yk%OGcp9 z(OA20A&cGdRSP=ie&+n8 z=}bA>h7Ga{4lQuMd5y=K_d)5EYoRMbtseZsS-9*xZ^T;8ZNEgAs&PAn(e5^GzLT0^ zai8O)Md7&PG5s%MtrBMrjQ!dZxpU`YA_v9MgqKv;U*$M6kE9~;a7=cf_OcxA4j}?- zNKVn-?#rAC`qBWx+-*}*BR5v8;dCwg7LX_gP_vxYH>zOhW^ z;k63AdcHBAcQdU{jAIp=LZl+pks?kS3z3SQ$XcPC_F^y|K)Xgm5mzRrvbld zhhjg1jWP!Pi!gWpBJH3+xS}2|l8E>~y4!*WI)ai)_3)MPMT?PC5?v?S5vicXALOFM zG>G;?gYA{CJ82KV7vp4!0OBhGFmZsm7{_22%IZ0NYhxml2naxxzPYO`f2o~6`z~`V@ILb z#`p;sswvgTNlMuT>R+rYJSO&*_fB)L+=eQbd*tti(;j|4&B62yRehyP;_b^AbL6u} zPaku2G)!4TDb-sWbejq|huyExZXhGwdvYS@sgc6o;o6GQc~6WqOmp)h*dM+b_vCrA z@`ifx$Wepc6b+u8uO)ZCi%1EXIOf9?_s%gpqLF5^SV93r`RW~t6DsfXq5tD`^OI;!PnX%FJ@S?s+L{`2F z(q;v|jF)OaTmF9KW|%Eb;E4{byfVCN_nueWg4H8}o#cm85AuZ9v#wnWpiPJV_Thb; z4FPPAI4y`v7hS?CVC-O7w~iA%5dpE0LaiF{UFZ*w@t~ctT8Wy7$n4^mGR;3&7GKcX!KPb-#PUxEE$ck;AU_H zc)-TLWM1&^rETiMw-I@nMEluU5ftYdG)^ z8x|3gr#m{&N9K$t|Lh2b%L%VnAgD&W1(*=hr4XW>+o0><^IwCJp`j}k!BL6zo}3A& zw)xjzLkR!^tf5yBqHHLJNZ7^7X@`w5*bE_PN*kyWu`)_{<}pZTCdgTGD;@Owl$4ft zze|g)t!hU4VSz(Mh+z+t4+oh@(D#anXrWrRjSyy64iG+)a-wW#11k#)!NSGeK1fPE zf%=~8e>=$)5a<O z3l)mia|H4w9m`0?fkcR$3iq`x1*St$wEj!mzx)l9G|PgHI;h*Nb{eu-!0}xYJo9kZ z>91hvcCNi4&)q0!lo*{S6*rF}dPdi*tIESup~TkP+%K(i*&L6?n52?CVZHV<@y%YJ z1q$H3fs&4h_mIP(?*J?Z1Cdq0=x`Awm5hM01R zIGj|0*k<2K$YMS3>Q55W!|Dl#`_KB4!VF>w6&)n{&Nx6A(|8vR%th_P<1f|#3Ivwe z2@`%5;Ns(fq+Q1MS8m29@!sofeVk>2pYN)9E**9;0i|_fBPBPZa!==>wBj*HE-9s< zr?NCHZ^KR1hW$kYUU%&Aj&EdH+hNXDhg)zEla2To3Y|a|GVk{0;VTw-e_uN|y1r9iHt#vWC=~G&WxmR9jAGyHJsCAzk{aF@50;cN z5~lm!Y68do5p0S8g$N(*-bMNO-^6{wp_3-2z;_6WO{R;PwOkQNNE1;x+-Xkhnexa!RqHC>0KcCLZZ(E5LjtQmM!7k@wteK>bq$A) zSNz0x&i!n};?(JAwvOqrbCG*R*5yKV$L;6JaCFaH;F5+YencZ+G)78Zj3X>?+C500G@M6<#Y=Fzo9=;b9)zZ3fhpRzn;C)ppS@fZSJDk?Ud#(kKa+2X@@EM`U@2xrVEu@ji& zq-ub=Op^G7kYC)T5>ac2+gNl|!XBh6-Vsli*%*&n}(xa)8DkAt)sSxb) zhyb-%#e<|z0QxzQBWtPTTt9NWBS*B{5#xnF?k-gD&#UvOw4r5ZHt(~T+i{xVa49;k zMSBrNWzy>E@@n%po#Ik9ett2UcMgnKnlH;utS*F$gnf_66|!%TY6JU_1|oBaYU6V}8L1#2v_LejjJ=zEb9kyb6(k-qr$7T{=|G@KU* z^OX`gp%7^?plz~_xUv1B6iMRCnK;#-YF}uS)CLBss&RVi46(5w&ZPn9jRa&jIniGR zXq;Wt_K)b*5&uGsnOw86#_^rSOGFvRQiln{o(B3bt6<(c!i7ovsmRfeVo^}r$2t?W01+dxi~?Lr;BvH1 zxJ3dFhU#wqfcg(YNXCi*S(==YJ`Nu{k?bq59nZe^uXI~qgbnOxqPqmJC}CH{rf&n2xG9p|M13pIPDd z-;;7olJdRX%N3o}O*R&oMv^Pa*_UFsm=rUlQ3Wx-LW8ObW}ph#@iB)tvgjKyP_@f2 z`^ih4*&CSo-`vgfIXrK3aOx&Jw_=$Z2=?=#)_qPqT^lH>`{m{5YYE1?376op`F`Eo zU%|k@6Q75u_TweZ`c_Z}n{$8C9j>+a-okazm{Sai!-)7+Quk7FZ)unOcsJxDCyke% z-$A1!=t2cd*rZQ z%A!*h1BiBMDGuh|C12pAM56bR}XKQ$^11ax=Ku~8>Fws<^M@!E1qTDaTg zVh^wZ`hmSy6Z%QKf}h`egy-x#u_@{|gFOZ9;m6+iqJD?h91+3>(h;}^RDVLb8pJ=X zCu<*i?ku% zl)XcMO(k6gi%bRBOUy;fGHNz`SHbfxl!l`l4S4~w03(Q+?4pFs0B8Vb~`-gMNim5r0QQie&wfmLll3=@8+Wo+%?qi~b|ktMZdM;(?Z zl=2(xM7!@uf;4ZtKm1~$Lvq2=WOQPJ0hqi%qR~le26bFfzp|JO1X8`@SiwKmje~kQNW)1x32c(djeF z45R$TwG!e^-ctXTcDehzwynG z228pFWFMdd3})r&ZN!Sp%uDr1E(Wx}A_1Qf2cUb<2e<=@)_Kr*G6;-p2jveW=uXGU zsQ@k|l_xZyd8bns9DVMF-yVS-vGBPDQtoPMM*pq&jI^%14Y4%_*?~$KoqsnMx94vp z!R#;&bg`h{B!-lxKrUH3FiN2s+md6dQM9!vw^yT~5w}zXb32mq2ExoFRM&_`+@6Y! zLL+zhd1w)eqVxhjh<89AG7v-@4`@-LSPwi3*hA2aU^#Z$49P(iPJy}uIJ)j2i8Q^b z)*w_tDQES6gJcC6&riN&uy<$+E8bWo-(ISwMIZ*yZ|}38HcQ{?QkC70uTHzyp$bB( z|7gm=X|kmbQdZtb8aXfq@6xsg*YCHQ>0~E0Ob^luCD_J@;4H{g8m$r1i{q!yd^H+* z`5-32PPIhZg{h`i@hZ$i1&ZDkI^y1Z))5?U2gERd{!by17r-S!K3H@SO9^d#Z~%DI z+}tb{PV=`H4N7o#HX7sK1P6Z)5V9EcfEGCRiKlp$9p zVNs6-9TMBPbK)`}4FyF5@->=lzvpfU?Tm8TH!Tp#Ky+n4uk%*!PvvhwnWk|V7AqJ` zDFAj)8H&u^N1#&zjBU_GuUfKdX?8|>V@1U-FlJ~J+AS>JvJifeejXC8@8HdX1}K8q z88%6$4dOOBEfsU9Hz0!_+$abgU}=H!Zh2W&8iMB(m}?8YZ(27>PxN=*UQq-df-UG! ze3poQ{*(9l>$ZfGJn<8$I)0Mr?_LT5E-ok*7)ntv!Gu7#gU5*+P_3#j^K}yz1088} zogbh4>#X?g*UB>Vir1nUac~anDM{a5lQX(*D*w{cCx{vv)TsZAT13<^ni2JM2w&R8_ zwC2cIsgknH18D1hkVi2^kQYp0cMV`q=m~%f{HslzVv_X1JFPgtOWeE1*XAyu7 z{DwNffcPWyq@Y(v0UJmWZpG7y!bLy?S%Hr-KtE0_y9+JDN!{c};|W7ocF=*5bHVl$ zf$~}VXtgwIo6ytKH-l1MI_d-hJAV^yQES0kK;#P4Zo~TQ#Q;QVjc=4kgB&TTS70tL z1v+!6?>oLbq2*0s7e7(x4+?wi4{O*^lMQO@RJasDyjJV6Xp*?QT|x_45txHs2L4Wt z!pD2lMFVPH+?WA#4n-3**4I&o4P$fKqaC5HSNsoo<7B8adi%RwpB2537JETG3q3{@?d2i!o@j|qO!%^mEqpMD5a zEfe%`y`^Ecil&1QX4*2eXhor#D);vRnMObeFo^=$KVrGpvJpu(p{oa^Ei}DMfY;$) z2^i9jSwNf=5Y`e+0+<78^nbgv);dMy(mR>R0ym|{G;eN1lB+KTaj?K;1MVooY^^|O zHyr#tO(Tj}tuPWFWs=dSy=NYEIwkEzm?_0R0UR>Y5sGaYfRLqB8cI4KlQ+=3sCEy{9(N zdW(l}9R8`v77ABD3i!=f7l0+}MJSPL7#Ai(dBLd0IFxtg;db-{l^%QA(4T;h>`0D} z(TtDpjs=U%q?v1{vbXp-gLv>E$i)m^Ro&a9`ygql{M5)|JR&c;HAL~O;cLmq#-R+0 zfzCRc;RuY?+rUz_)~gQvKWcrrDInVvN$V^Fehr&zRA)t1K&E5hb>DeEAJ*b=-&{3+FtzrcNQUC~Wb4vud zJ;1;h@=H5HF${W2cVMr>P=Dw}5b_*BdQYRH2#BGx`4Jsk;2y{}6T;aSD|*6eMyc19 zF^?)}yuNQ^%W=*SxSr8_Zw6o%PohyRVNEzugj48iX%TUupGPshO-86 zPgh)a*Iaa7#AcPmiJ=<-V-&;ZclXQp_2B$g>thT&92H}mo7ZZJOF+z9vCp0p$L+`@|dA)2Bx zckX;3ZSF6c)%otpY=AID7p6}^P=y})-tgSXow+!7rn2!Q6_qKFdzL_pvhReK;NW#^m_g$ zd8HEn%fZ~gumH7h3hukTv{*d(*H~n`A$?7?KL`6w&247c+Z~@^f$}|Ce?4IhRVy{( zAuq6dLkXZHUCamz3KXc2Y9wtS^p9YUET*q7gn$JQf*8yz5?NVS#r*&|sjXcI-J%c8 zq25|rTJ_)bBMN=sy2eAw$B`Kiu(VuKUZ!M@&qwVty6F3h?ck&U@GRJR_`QSEWD02N zsESlhOi`F7rHX-cT-C4OF>F*xm5kvd$k+_R!F*D(m!X*7REO!Hl~-u;up+~+YXiT# zQjuOBw1)(&2+H@*;>+6?-jr8c^NB>crz&&L}>!j1}!E{wg^Ro=% zeg(c@3=A5@0Hc*Ih7fi5Ah*+r6lw(J|ACayzU*6T?S+{1Q$|-j;PtoLBF`9BZL~KcW^zMz)@wY?<+}%a^6u6PQ7VR0X9SU_AUV zL+Q~MT$1mLwdr?Y!tJL*JvkqJBlL>abGy)6(CkQQXT4}0WCo&fbUYg(fZXltE^^3; zlXOr^FA=Pt&RioEWl%vZ(i_tL`ol^;yqrVV?j#%#mL zFJ~n}TYE}l06R+d8DRBGjeY+5$P^M6FF)-4$cShBw9sOeP)(bXLv#0T+6p_F4|EOz zXNY=}iuH?QwMDPq55K5@0fk7#z4p2Kris#OZ0CD%MbOaeuWDyR_#*&r14U4F6=`Qe){|@T zNi-Mp=d|D%px(MLtso~C3kmHVgmQsFok3i3eIQd9q{+mllukW~hTQ;-;OEguqg$u_qf&)W_FDT8flS>x-$U<%{%l+t%<;1DUqi2> z@`ciqY&%lQPoAPiBfR-(fVsaJjqG$73veo2blOy<5k++g0>=gTqcsZ}uvvgB%qa>r zNL8@$9IB?PjzjDR*f%;#5XcnZjnjw%Wik%Vy1VC)&rqcg6<1l?xSx1Fg3tkBw~aQ6 z5zew$NcnG~??mpyK&)m{GiEH2?);>Z8aXE_3e3L9#P$FL2tcPe$e2PsHWa|uV|aLH zvNf3ivlU!61Tcp^GT9~CpWPSD|FmLUL(irRJUl$ve=YF!*}TQWqcse#X{c|X>*Kow zf3IVg{h{XwDk9Q_5CZ5WvBr0(FWa6SB4hxqR%kxk4?bQve0b5s12ya5OhFD#ll=~r zpqea?#(2o!TeA!gSl8PKRtd6TZxII8QXSF_YSHQTZD|GtM*!*5iNffl9n`gAQE>*d z2~nh_H^C2O5Ewd%SxApLeUHXRUg3}P{>2 zFdRZMTnH+QJQHCGE9Qsb6NpX`uLu)56>$r$d0;)u!WH!EZSm@G%o5S6*y>_y<7Hq- zZXKV^B7*5H8aep^gubI2z=O0lpE^!%`Txo8gNBcwXNYZ(l;iHxm#?(N{q{YGA!b`( zj!D`?PD!u$Ot{L_(Ol7;D)KF;iiXlb7iemsl6EpgH&OwLC?EpY zk5Rh}&Wk>MT3ij=AvdVAp9^l8MK(8o(qEg60^$UM<^o*c=8fmnAf3`WO##V+Ze++i zpkZhsjcAC4NZ(D{UN`R$qHRNXe5i|k-ACv~e zQL%p8NMjms3LVJsudl!b6~5?xLN5fK!K4{=E+Va^yeI`! zV5a#Eh)d|;MCc~*pfm&^Fv-NC52OUzgax$8?3}0^HM3Pf4Z8?P6d6cb)19iJ z#W-jCZgUrkxu=3^$5@|Um9$ra&k@-^x(0d&p!@^cXQ-+osE)qh|n zp=BXXs_t_xQhsu~ZVQveYa@frJZHVwvM{6BXgnv7)F}vf0 z&|BN0s;>wrGx($DiN5RVKW``e73n|EI~btASpAP@g&%zbulD;3p+CR>m#0hoKc23Y z9%iDT1dZD5Z^Qqn;C_BlF=m0})-*rU0{(sdNKo=Rex0jpi$;yGaKDh3%v>sG+dW2R zkVI3KUQ#%$(H;bh``-iwigsJ^4=BO<>N1VO`(0rms$>NR=alH8z0=gAmG{w+n|=xL z#ne-YEHCV*#FHdfwufewx%}{bSRK@w(-^4Vol0I>$?yKYg(Z(^Kgejl(%oQ2!{;?YFjdqRXCEYm# zD&YPLBJRHthy!$jI6wx7189LbKqUg`3jdzjoa{tG-w@gl*Ikm>Z zS*TT>Lo?9MatwYPv;}Q`=hrad zzsDI&7NV<9%YXU*>$3f0uCljPE)oARz1jOaG2uVvH0bYKWxDEMj>#709nS$@YJ7-- zA?ExsEFF9?@be4rf19eHpZ!-3J8_f{;_v?Vk&un-17H%$rkV;uwa}i^evb2*rDR)6BAz{ zeXGCo-jbUFWO@!C{!l&kp|+1{rv5>e=wlqeS{lV`nu#2T556ALmc(tRv&fsfVYQ`( z^OpY4sT#cTZ1373_8aB&|~a+glar#)U=GFO@E_ zT-tgjdxLE{m`+sY<{ZJxnZ9endAPjGy_FzXbh0@u@w|O@09(%0TLat8Fne-{P! zix+3uMnL$x$1pojtN)%$xR+@nrvDwO|AuJ*Z=f&a%gxy%6G6bpK?Kw_R>`9Te z6qV^;;{(#>V#sHYww@5)mP1ko$9cJi za7W_U9~>}9&LP08GyL4mAEYM~CO8ldbBM-Mm8C<++r$4o*U6>8fNQU}Y z-cXP2m=?e%^tGbsdMJGh599JDogMaa`w{&Cx%SIzUx%NxM(s_KiQI9c?2^o3ukeaN z+}ITXJqeX^NP>pUf;Hh&y*cso_5np1N-HD{>S{5(0yj|}c>kY!-Wq@^4rJzY1ewoqlP~35%^TRdA4d~l2}&;QI2f0k|A?_~ z6)Y+0uiT*2>{Ai-vyf)<=KgcfVsTg%X9Xp|=o{6Htb|_s;{twY&BqUN5{x z<}`(gk+_u!jNtWQCd=fC%Pn8RG;CU4)Q<`2oUuhNM{| zUIs|YK6p-pdL*Dqey7BF`w_4SY9168Rhr}&4wbrX0$>LLcmdCxfCwcpMrs4q=5&?c zHwPv`zlO~_II#i)ixbJ*0a&=#QWXkzXj;&Nz#mK)k)$@vsb;EsepmwrT?z_1=)cGU z_z%cY@z4|ouB8h6gl+;%@sz%tl&I*Q9Th+xlc6Lx==`?* z>daeSnE8nM)mOBT1C*faZ(u`E?RH+uczij;{I`y_PKPmf)SiUpIfNZvqhMz(FXhd7 zO+9AX{SXD*2v=TBkfLFoiN8afqJxo-2cjd;=;Kq`YaBW)EiK{r`=G>#{JR0VbRJCy z<^=Nmb8dN)jd2j=0YVvJcgM`Q0kc1hjA{P+xGG5mXVi5*&YVTAaEq}b?gUm zE2GqEokmT#foi{1isly(v)HQP(O1)e`+zjXQPqxyZ2+Ebg&T^zp`h`}R*Mw9fV*`M z8HYpL1mXLvz=J)f*P@oryMQ>63D9UT!$QUj;L0G4iZ19Aup4!LkePE&{{!8B+XFFW zfrDO-jLpHehz^Q2FpGwA^tRLH@L)=MG{K!oOouAC~ou{P-0{76hRf`T`Q zdXGxY*EOix%}4d*Y2U6^9Z3KG4yT23eAZ1ZoLZTYzJ}T1o0jcbJERfxyeigIa&{d} zk=f{@i;a1AGR4CORs&dbF0_DSgM&{apc>Q{$ovd3C!ud_+hz)kYGjfPT{a{i2=r^T zX^=$@8mhpm%1ROdCM1$kp901A4j9?CWs@CdcP5Oy5FMR!@&U$Hi0|DJ zmI7>n^H^)sNBQpn?N-S(i$ux)q|1&SFdO$g zQ6Ml-=h04sFp4rcmq{zCc)`m=Awj_panxah?KxY4f7^_soM3cb`@e<|Sz0 zSR2Hm2>b`csDil)AX_ehk}9kpBq|2s~Z`6>9AzAbTaIgtK|!uCoyl6 z&@vIN=_#y*^kL39nwoCS1w0ipUZ#vxHQY7p;}toTK!$c`l2xWT#1Q6!8P-1Y$}^E7 zTGiXb(*Q?huI_v72u(2tSj~wHX}P5>{EVW2v;Zca7d2uN6Q^xY#z!GOdm_LY_H2N5 z%z?^s3)$tM))-LUqzmc8!^wI2 zHasU9VYr9nLueyyuLD9F+KS8e5@5Ij!xTf9Kp~mEO%QVU2+cBxQ)vDsC|LY;YD_Cb zsb+g3_Vw$(8bOq0?{T%>a=?i&;bF38Yi679W{J>%aa^OgkAPs;Qt;7&6(5~$T^I5- z8*j>O7oGKRyHVdt^nF15i4Ng)Yng1d+K)e(H=+Jhz4;E^^=_^Jjyi#i+_NjXATzb5 z|5fvgE+lb}tx$5L4;fiocWET30fOO8-O*{_OtygkTHOZ#GyoA^%?cPtfIUe=P*1?t z_|YpAI2B;e&;kHOIZ^f^^3DKUC@4S}>my6=Ygee$YDEwKfM<<1fg{$4I!=tt|hu|Xz~V%eW;0xD71i+ zS1`Eow|F=dmA0#0B+DadR5U=>cg_RP2!)u^-Bk7M>_8^0#F25A8RFfw5?w^Lx^Sz1 zXhTg@66my615Pe2of$4IpQqDh!;bWw_>8Xk+f3UyBGX5^j!L12CHL41^CWHEMvS6x zD}kJXAT}St3mq{1lW=|5YN?62z65g6adC3CTNa$p1~+;?1i{b)zCmOz9c{e9z+KS5 zp+#PChztxwP~_c!u*?u&0EJNW2qxlT@d<#C4-g#R0-gRnTR^i!#P>(%9V@F0_{*n2 z5TMDUy-i5YDFLNA!nOcPK@wRFRCGNAHENv%30m^6M-Kl6a(&;EtM6aD>l}luHX5on zOG-lVt=W%m$=YH*KGljl15(oj3S$tD&EPy*jkaGmwlscq>g&u?aOQlB|r#2?yZ_`s! zKTgWfYdVhZR*3G7e%w~Whv=N|zApT6!1YWQXP(rFL(zFKrSm~4Q6OoA7>l0*<71Tu9oIcbSYV4{@JUbcNYd3;*EGB7qw0H;lt7# z%vw+naJ6mH5BLb>57e83w}(Blet=GD2gE$Rrt5xyH31$gGJHhGKOBjdCT9#vkk!aT zO0aN<1W$)R)}??kgfK(iEP;XJFW?;`|58XR1F1m~$KHbDnv5Sdf(Nv)A`dmuY`d(^ z@}*7XZ;z}W;g-N9{>$9df+4rL*+Rad?)mtqW>$`tO^(8XdkxK*sc||AHV-+~c-YE| z6s61v@A#e*=QeY8q1Y(hIDD&>J$h|vVQ|$keR|WeVetF*kJ@r8V3@nr>%_p(W59k2 zfaX6R8~C6=^LXl9b^@i0C#BDt&hF#=kLLRQ$vCF(BwGvcTHC*dbw~Qa#bEFaUCZ~j z0;*_DZ~c~4XLY+3p>P3*hL6d80M8@kx4pF1H{i=_bt5lJ`3U9A=HWN-DVFRgKyM%;F^ ziE?HjN?;kZuEzKm^$4A@#_F$X3|*WtLCk6o+EcogZ40?<`uhCrSIC=-Z|FWJaEPoA z%L%THfaH^kLOm#DuBC0JzbLE-yV??c!@(VIPP0)!dgZlBMf{ESv=&$R$lNZ16$v=& zV?hJ927_!LO)ROhr`g}iPg@_gx4)dM=+4*rFwgdiuaD5in>0d!>q=!8+4b#72!)y> zfL$87{3tn(UlHETJLv%&Il@HYsN(&(k0$+mmsVEBA_JL}G9Cy=&I#{bW?w_itghWU zpc#E#!AgV}b+EQ|b_t$8k6X_aVWgTQM+dR33eW@x2?#icAt8MLnj5a$!!m(DS--5? zKmlSTB6@n4U=D|OmV+Yz28jn^1Gy|)B_{Ld-Lm8YCzR}0j&InQ`0_2Cr`3G^O1{}r)$%_v_5FG_`GHhzwXMD_H_bF#g>-rOzLpbEZ7#!9Cm}g_^yr|B0U6vjosP7=lz|VAeS|hYCp1dh z+w(q+I#UCe;LwpHV@vu`q&{@wF(Sv5KRMz;*p3H_AOfDB>tou_*B3*Y^Qx<4apx08|?h=Y?OMUg5d{_(0{4dPI;r$3pCfr@~cKR0MSDZ85R) z?Cft~b%q9Q$Ed2R+QN|YG>-)}ROt_3Wyz(fhCsyx$XO8`or|zg-T^RT{QdcyXKe;i zp_(AN3r})>BW8&Xu7WSQRslc#r1i#VPKthEe}6y7zySx%yZ4#RMY0_6H5!1$+I8^mlSU68SeY0e>pFuHaTL2DxddlRT;3auC396 zSnH6vRQR$Ws2YB$t*!M?^XxCOZ!S=jw&(8;gKdR_?yDU(mCGx1+R1s>?#Hp1rp^cE zuv^H0YX1BNKB4*O_q&+!t;<1>9^_kPxxCXff$4e82hB#U>~5jEA3zi9DCO42M0!)h zj#T`&K*}UoaCUOoKaZUgzb!7VRr)N$*4Fm)21HZ0`T6Vv2M)}4#ZwWTI(4|y{hdSz zt2dpj&;+mpE%i8e2FrYS%!jE#t68)8>!Av>kokFzqt4c+0SoqA^MrTp-|$Ue<4> zeojaQo}y0ktE-%GoBP1@@eQP$pnPwrs;1VJV@d_-@ABM06o_NbR#3>F8d-jky9|iSV{e)X^gP`>_i;WO!LxSRGDN zm0MB$MHCkA#e;Mv{oIVYa|3agTtMBIa4@~7OX##(@i5_tH59l?MFunjj@vY~K}_TC zcggif!G8P(6usu*8n9Y5hn@^{=4u}6$uWNCur@Pv4y^Sk1RbewtqjaT41#41SsI%~ z%&n4Y7NN$m7HL6k$n_pgb$+QzUDtYV-otqA++#3e^nd*tbG5+Q*luK{LqguJwysVa zfVOWKNw40yBWl^5c!tmBK}PNsy+MDGm@A!OEimwY9u!2!%W zEd)^yy55!dEl>9O;~kr_ob9!{5GgAng2z(0ew<-PNi;Q-W8I?t=*v^}PV?eFR5)-t ztv7ZcGQz_|PRB8m=YT6s?g10w!f>ezt4XqlI^OkbUyOU5-?1Z}sI$>^>t^iR{IS$& z@ceq_Hn!6<1N{bW)N|mG0FyL30PEKesj7(^`<@p*&EF{)@P!m20qlPr0*x#tfGa*MUYS3 zTw3AUWm}XMDXT0Ul$jjbHeaO!;AgO5w#|)}EA*?G_B- znV0u}G9Zn^< zzYG|F9B5a48_LbglYzw=%2nWBFl!*?f6;r%h_XN1FsCddtbF)eVHlON>@$c)nl+;u z_W1$8JvUqKyaJ@xa%j38A|#|0=mXu!mJ`y;xcB zRi7gb^|VnEL#>G|7_eel+z!v<7wX-(90-D%=9|l;%<2+fJ39JD8TYofg__a0YXmG^ z=Ke^jcp`_k#AnGq9rB6?;^SmwVv~dS$G2V!xUJglE})tPSDMu{m$?F)HV`>eba(RcXT^KhvEZme>*}o2IHP7`#OZgcY{b;^y)CdrG{OV-sI&2|FYj@cfWHm_5{@RB84|!9B0>W4uA$xl;<*zXtnt z0=^aK@*F?zw`RKhpR5kfi8RnU(Pba*H@{`o?_zrV+_^R!r7Rue$ogE|@)g07w{QI< zBKcoHPZ0>mjY(}oDJsNL(8RcFxj0G(ecy_jfLe1Ko5qapG?nb6_4W1Bvae_*f^Ubl zr_L`fvdWJ$e-^^HK3hK^J3b}yYD*}PN#k0g%(bJG{2mqFWOq|l9Fj>b)(|croE<8`v>LY&!-8hYpY5$I3$@1 zu9`DV856wlIC;r{vzP#2nOEF@Z8Z4(Filu}MdlcT*GRF`=4tO-4B)ibO-OEbV7D5g ze69!{YO1VshYAZ!p6_8$3LGCLsoC_d9CI^Dpc3?-5*6wNk1>c}y^6^y4sMui(f!^Z zLxKUXbBPGfhzWN>%Y=IIYQM*>PP|Lm+)kTEYXj3iKQPytB^>N6GU_eDYE{Gza}BJsL=-%t z;df*LqV`BJwwtF_=Y^+tJQOF8jRed7JT|%NvND~>%)6WPWG{wED{)|CKHUo4i#lOZ z+a+)v4kv9n38KC9>#@ZExGBp?nN=Wl^yHA|_?+RZNKw`xtHc!+6U#Km+D8q%BzE~0 ztwqB7Xy$qleu4RpqnGcILy1z5Qg~?r$y+Rlfb&(BT#}qGobS+w(eH`NCV~v(A{a+E z+zsI}H8llP9nT435Sx9@6A>SL8auMG#^P%j1MWjEzGe&WZp<{lXXmw^D&7BskLZNu zmvi`LP}+rDyKG-)3zA$C#Ad5AJk6;JkTvT`z1aH{y_eH@1Lo5- zaC_dfr!=i;}1T$TeBlmBl-lMdn@9<1v8N^zn!h^o%dqIBkSPf z87*Af&cCW8&HKpl_4lj^3%N2#I4EA3lI+lkoz8*k@Jqto-G}v|*&W+k&?8^`<|~bH&J&qr#3NJrRs@&|Ne@}7GK=U zLL>qe8y9CW|LvS*&9#Q--+^E}JU(92l{Mjgs7`sDMXBoZwZ_wx%Ci1WPM$KUp-xF1 zP+$#1OIk!yPVVFgZ6}o_QIqfhn)w8D+|zG3u%FHcEnZ3hLhvr&n0gXF0ChT zW`6Dhb9FfDNqu{KW@cvXuNOmQpKr}xI4|=pu}kT&qvtgvG1Xw|NZexAwxW2+T~crs+2hu*kv;4&9<0 z8M>8!f}GqlCx=T~NZ_FBP@_+*aRI(c(@S(pv}hr&*sin%;RJISa{BtZ zpIPk5nZvvVdJuv6&$^9#Og`s$ns8y2ghVyW$(tOex()qgM2`+=N$v0WL`WI}+0O09 z?{u{GzWwJf=1G#%3XVL6c*n?}4^7{D1O51K*@n8#*p*w6&h~%J&d!SbfVUH+HTDC1 zYAS7K^Ul3tP&`FQNC;g4$vf3&2REZMDhYllXE@S<;@$x$wBE}lp6%^pIuJxJtfaSJ zq-c{>{1glI+0O$?BXyRI$G)_%VPmW+y-3~L4C?Rt7Jz0ciH*$>XmNzeVhUHr$>oxi zd_lVA-8*RZ2?;r#K2hQG9(<&}e*KD)E~wipZc#~%1HyA0aKy1tZlHge4RcDWP?oSO7gW4UMl!{aa|=7S5L*JjE1#3xZmO6L(9-4Tn&!68}fZ0XhAYBd=jN z!nCuq1BJ}gUnF~1CGW0av^>u3ej@!EZHDHLco+ZQnzng$%@=OoWJWZ`S29;R)m$IqHLk9xn=%U z9^3i9&hQCwHcCQ-pYY)k~TnIVYrKgD|V9_5Yl3%gcd@iSQe{1sOf2F|&2#nyG37S@k<9z)Xf zV(aqSt}}A|{Jc{PPm+B|LPQf|cdY8d@!-R#Iq_FZh|yeuv*MVmVuYAW zEow2mD5v=Aw?&%QClR_V*diFENuVKL{*qX!*eHWX^%A&VyXXxRpMa!^O~9UCM~dWN zi*8*kOab|Yws+sSD6t+MC)pwM@x2c0J|RPNfw-YvvKR_bN}f54+kesZtJK_40Lqs61;l?Vh4$I>L6*O>y%d!Md%%dzg9Z#n1UmU|SE;Jx z`TzT2sa%Ki>nd%m@mh=q^H866dwI-t8%r2GylcW?@ZH_{?+fJ?a*W*Fm?R{$n5yHv zdM?ur}Lyu3Qh)p-TXOyeR zVBFBHRwg*cAU6mA4lZ3`lfx0x)%9;}0)8zc)o2m#zbtRxjQ!U1O!w!f@^x{CLCy6W zH_%)-3Rb??e6@(T(_n>EqWs}xgN__w^R6*LgOyP&C{6{q|w5U z|GJ7q<~0upe!V03{iFyFxtC?A^V!Fy=UEn2^bU0gEtywEl)U*eN9%WJ*`naQ4O?#t z_Fkba9jf=F1$SH_<+SVu^{CP}iBVblEjl2_@9OSO3w7LBH7y>{yH8kd$g9mcqk_U^ zz`3%&2L>KRF zmdG*m47JWM{HS{3=1K9uPQPo|M5ke^_uMDT3FbH_!KbL<1z9P*V$y9Gy_2$PU&g`N zA;|uFEu24}6MFHtaCtjgcfYEHhVm@SgyhS%is^gIA80w5Zs9dh>t43A;kP89<^5_J z7-sGQMM7QVb^wo~n-g}e1u}5Z`m1P9?J=`iJ1Z#o3i{Yh@un_1jXT|iUlXVMOncXD zzPIEEmFyNjgUR8@*qE^PsHWidqr++if~7FNt`iUDHRc8CtajHTcoSJ1&=@$6TFKxh z(2_k}=D52}I8GzA-G?0;)iJl^j^tgjq;yKbhq`|PX1lfy4p}w>&c!?9E+36;gupw} zBH`zWkJe?|AFIMmMg4?#mX|5T)dGut?ask60rT-kkBLciO=o@x4brX7_LxfX-IkWt zg_Le&v|9F~kvW%3nrcIww_#U?(~%f;CsNPy(%gj;+>b7m7P2dL6buyFErmetCMYOu zt6R!#J=<9mSU;_tJIDoENR_@Zx9l92mE1Yp354fCdsZ{+i^oKi)>52R%02RiS3mCA zE>`bo>vW$M(4l}HyQNZ4&~(=v;L)_V^XK_X+EdjeA=`o^*4F;pRjoc~gN1B;anH@o zov$EI&oG>@<}^_%+q4?`XzC*L;)N(bm*WA*S}MqXEo9$&l5nWYBq*gSfN&_!-Z`-m zyVne-ORrVra}h6+0)h?mj399Knq3wIsc1A(jyn^(WrvcwEuyW zhGqkyCB(;Ai}zv@A&ZAVdJE@sZLKHH{>_D6%d)Dvk5{nCz41h0YK_jEVczB=1Gs}L zyF2cXs}0Z25?qDUNyq6*#OKeLQ!MJGKtJ-bvK+4~H>iKBm|kfIli5q^h1?iuF&8c_ zinpfTa|aE`9aUnJ6!UTW)uk0CvHSP0Mrr^cK&NN*4F8q4LXh2(PQ!DF>BM=P^ijUE zNsr7exQBsh$^GW9HU|$L99*7c>k#PcG{^>g(E~VyJ$l<8IiVC{wBkE-r15?aw^b$T zsl&5g#xspQ-dokVy$rqfP5PVLcBfp`uINA8w(v8ljj>31^1bzF#jMBsxgi$4tVf)!k$yG>~ILrg^EBFx>2hgmgaq6+B^8cUykMbeXws~W!l*_x2Ld^O^ZGMSkizmByKB*?IS zSi}>c{ZMV0=A}b^bKx5j8i779M_W1i7|zPq+Sx|3)Bt-99C!q1kTuax`B(!gOOpSENcP7_OuCaF7+5#_B^x7 zzs|zLF}>;SKl=}R*Zt&iWyy+9wHg~M2HVH^)mj9JamoYW_mWWSa+c`5`(NmXZPeX4dCzBD^j>SHXv0N=*1^rdog z8|P$p?xKXGP-Nm{<(cxGe&zG;#e2e@fYFF?O(&2I&1biGJ_|lxH%QYh8Ek%!;dUB7 z8WN%?<@S808%C<9Gul&Up_S2a;J`Zh6;}}LEZ*FN31jawXgl>$PEEaTtR2W3SR{6t z2NGm+o-D1_c?+qhc!I(&j?=7fuhBpW?+^|hODNqlV5%JHtaA{U@a%@xv^E0~*f(Q^ zVAJv#WUN2fweXJDW8DH=7-L`;piXd2@@}-;9Z>~Fjhc6EOG+npXbAFu3W80)g}fIj<-R3B~bqFiyiwx7E+6o^rt*=J;&y@uc6YT zs^mpOJp6m?hZ>%?&fZ5`HRGohgx~v_AB(7JqEldw)wxqw77#QQ@V07fvK-nTt&FqP zWdScSe}%k`v{+6c0c&E8c z_7Q%ZyQ!yi0y?p`Kv*_pdrYDxC`X#8SfQgp1BM+C^?eG;OTo^oP9H-rcd4;AZvb^p z;~Imji&DE~;!Q5~QO-6%gU4>!*F9us;|)Kpz{Gon)X>6(O~VT6F^KP*D?j$T!WhDz zw&WHx{V`@XIhQTInqi=Q@788uN?8n;2dG`qxdx4$_4Q6#7ceo1w6i~3A2uKg7&J(Q zu0xQXi=CDYk&7Mvg#|!~3)fzrt=iLuCc1w=eD~FqF9$rFtUf7{BGh<+;T?CV?L#di zlP5(?Mzbof0?yP923ls-9&euU{%FT>dEj0}7)F}Qa-FKBz=lOE;PCMG46UQK56(jF zaGr+74SFK1`pwuvp^Y0coJ$t|AJ{swq6!M!CRn(}idR;ZbX-=B0XdbLn)-vi6}JH8 zoaC}HmVLC&>!?m#60& z1f&PQ1M>@PEfwh{^_B_v!P{dbb=yM)$((Axis0K{;#aMHT_z%; z4aI4tFYW`<)Rr^v0(9HMCptcTu~oL1JljyVAx=v1GRuOutEQTo!C|Di(|*SoJQxg8#}TZ>qLW)7Zfo52|5%Z=*`Cr)#JCoK-=0B{sp zn8HNr&4uCdzo2n5KH^8SUy_uMgrst<5bm0wlRcYZyAddXb--$sm^Cpc(TqL@yj>!2 zTp?Kbl?=<+_Ngnf{WknE$!I#ZJ*kj*sWFA8vg^&!@u=J5O*O)G4;A6k9QR-Qn!&~C zK$aPHM2<-(`kE_#Pec4zg0)=f^eT4n3rtZKvF~u}HWwMtuve~?6l`OzW$}eAe!HQj z-#0FJc1@(cSGCa@CeVf023uY$qsBSUpiG-NK}RR2M2&Q2m~~$6`SjyMI-U7S<>1iJ zHRvzkoaR~F4c^%}8cDg_N-UYZVa0@HQqK4K{ynSLQo5Eu%p&I9yYiEMj^s^RzbQl;-OiWbHPdpDTgXFX!QUL!dfK4U4 z3ZW(f&TuS*-to&vNOlOc)8YKqt?#z7wYP8nBsm@a{y8+U_n#da7KXVJ6(!8Nv-&itaiU%`A8C% zY|)gZ%)kBbG?Yd{M3-X3=^$?LKn0J}Xg!z56!%k?QJsL_Z55r1go^YvhS4;2G}_u$ zJpM&O+FU|>T4m4cb$0hTl*P00thrAo3%LkB2WT5K4@IDpO|c01bbw3Rc_x7f`T&Nz zc@{M8g(3AimYwO>-4@4>@2F;ng1#BOVwwa@4ZJHq!ZLzv9>!}6^YfQepTH@rxgWeG z0iAaN;v2BGaTFCvtc?bqnlLoQwNuMe_@g5 z;P^Ovq=Mmv;I0n?mD$BEMx7M>Q4ksd()S812JXPrY2UuB$!|kA-8m&ucVpDYyUvxZ zYk~Lox@KC7j@Ht&|3Ibx*j!pk`w~@KvLNmM`9I0*X6g5VHLv&IE^lvJO{o}MTTzU$ zpOBjipNfzzvP`m(T4_k^=V{}M|LVGj`;ez(1UZT}s>?L83-q~7f10e6%JXn|WR z?f(D>v=7%{v&@5&)@J9^Ct`P#v_;=FMruvUV4cS|%RPf0>`#(Uepyg}KW^H&Wp||d z;}?Ook6#~DZ*Sw=fuXyqnku&gGo0X@O{foX=m-fD9(il>y;_g4Y%0};kVWMco`L;& zv*vcj3eHP`#{6uy2iuYq#@2+F&WQB^;t< zuk`<3er)`dfIz)(sd#;bj9H#`iBl>m&v0J#-6KgQPyX*aw)Z^5%l1UR`xujVFXx2B z3!M=iU)q02wZ;{{2K$ZuQl<1EX0H2@()D$(hOWyC5tha!7RO9lpCt*Kr_nIFnqr~ZI~TA5 zyv%p1U6}}?7oLhV2%Y`i7)l_-f(g{Z%&e2beX?`0fagu&zxbk-+Mp{ z9e@lX-^QiYLwAk~hlM3wbrpNi2+%x(wG0#*jcr4QpYgoOIV?PMvpQg;`vyw(E0*Iy zUZ*$qnHDeb0z9qsLB{Lg;IAZE#iO+>x+5_41I~oE)y9a$MlE&6nz}-SmQx?Em6=Tz z^zf@=Mdy}gvKj|RM)v1^0I3=fSi!vtdUR*n)#!pMhg$2Cb96p)MC~U0W7qxfT7dnT zbIlp zhj~cTDo^I~znm(bl`tZ^nrui$Eciwy0%*Od;dnlLeBRQ>32 zoJP!S$MGoU@nal`C_$87yIWnkLSpN(=#G&~P#UtxT3DG|n=k8ADzFwkCO6Iz?cpE zVMcm+u+yWnvkjnJc(d%eW?TT5225!psTL?s&=xm-Y}CE%R3Y)?iI}gS-*v~rZKuvm zz<4jwx<>L@!Je?stHJEAsFe~G>LMJTcs9r9v+P8kI-VM`WzOqX zc1XdZLD>3YI<|lqrlsY6eE%X|2?Gk0)XYx4V+Su3#Msd20G3?x*?WKZ(d9*ig zsWnOYY3Tn$)>npAxpiG{Ku`fiC8QApknRo@1qmtXZlpV;6#*rs8$?=KT2kp2kd%~` z?%2dPx92(WUf=oQnXz&FfI8 zfbA6mG)Z%P;$QlJ_B}psHQ}cCzlQMf83{9YLbtn?(p>q%Fa7i42bwh=;l590j=RaB ziu&_>uk+a-6C=3H-k`>UzP)Fs7ZH_U`zRep7UtM4Wa4H&Gn}=7INo1=0-?J5ewf!d z%&HKVm`{7TtODOYJ^M3_K)te~ANkL_dI!kqOAF58EdQ=mZr@hvyH$GKs6#njdNe>f zfqNp6nE=t%b;O8q^OAE!!#Lv4*koK#zOOHcf+u;V_e#c# zzK;u~yfZNV>SQ^mf+62m9k+w{feMnCPM!0sz;FyA;Yil^^!)KjX&@0by*DBNFRZEY{PJp%;2GSza6>#*Z-WhP=s|buY#B;CK7}nt*D(rw`w~^Y`8eFV{U+av+8L zZ^npvrP!DMix7?*>U70FgXe?iykaJ!MGYiTKrQSKqhT31iU3SrH~(vPHuxtjCv6%S z@LA@lYm=w`_r35${O6}SLj$Ju;yYTb{*OlHpA>6-KxJTejzK{JNHFZ%;*KE$^3vD$ z)V+P590fip6-umdAFw$r+3atKykO>PYHj6kTD?HIG8r-ENctV#cc|c)50HMsldZU7 zFi<1)NlKw;D?Aag;CRPNIOdNID23ugyk?$RzX=Wq_y{MXW%ziHtot6Pg+o@@`BIu} z&>@IRe-{x=caG}x0cAn)lV(!Ov-ll=N~rmd^NL$m+xpT3PrFKf-->9c4Or(3}BPna#dsk$_44N83~Z_>cO7WTcB%*ztGd>0VO(`%vLT zxas-84FWaS%k3PKw_xa}9~i@Ev=yG=us+TvZgd8ooIY>f1aelFVG!33bgWj$`O`Ju6MeBC9p+rqh zP1ij?pu}*z_^!*&VJV%oefglw+(P?N9R4X@~Yieug2o*c1r=WTzVo{R>$b|A`dV;9T-%%K<2haF_r{Z=v zpke7q?W+V13oxX_0QL$}RZzH*cD565i@UjO@|=P+80zSL_7;3IOSwq;Kw5vV_;_iZMEzbazjp1(Dc4rpP4M$;TRh45k7LjMtseXoSB_9Xac)Q5`GI_K=H*^Ve+-Ravmv~W(#>57_#UMCr3EJ zhh>*^O0c|P8_RPVjk%e|5sNrR%}oaL;XM6Wzp@~b9U??$OrdT|7Yi|HRaQr9lK|E< z*xJ~E&I;}+BMikj_KQa?PNvRlrSIR%7}$Hi;44PzJnRd)8xSkQ*18S&7{%)O?z=Nc@NEA|EwboeC7rT``S709cJs}F zcf6%3^=D)4V_aVSzt*dH9gjI>h4(nYu;A)SgYb+?=R&?amm5)7*u_|0+t;GPd8wz4 zU9MXHLMw9%{~-{xF_pSVI2nKqVE4%9mlz^Fr^#jJ%3I))NLmGfG%POeasy=l!_|@; zu>zfXVT-BqI33UAP8i9-wg5_9Yu@C&d5bg<)(f+enw*}Vs(p634%{R|_)(?fj&tM> z7C#M=^An&o2yDpQrk=TtaGn@U6j3SA-9|}1t4v$EpWHSk*v zrCRjtEw@bIE%L(ay+Wox2a)~AZhyFCOPtEfjFSp^c*yFue{*%D&ey}unGI;*Y3u5i zqcxb`a6sbNLui7i#9@n(?MajO*#*p~3m-Hp{9)LKCRPBIoZz9Z3F0ahpIH-t<=scI z#FCM8Ut{~MXI2t~Jif>zjlo_fGH(nd9`RNpz(H{*XLkc`vRKKGa9fW`PW=&q-2j}n zQ54V7u`Me-eREwP!p70jXsoo?1qSOuVtc@B-y?rVS$_-#TuZ*PLd7kOn0n==DtJN22 zmpSg-@rjMa5z~^C#D+kK$kVQk2HHCUG+i(EqGEzGb*QMQwB7aYLU{QIn(jnlM?XfX zXl5>}gY`XLsD6lB@toG1Y7a^|hrlZw&l`rlWLyv-eXFaRd96mbtsaSz5w`|<_7>D? z1(UNS!bTi4z!X|wH6JXra}0I|rK>Fxyuf*^M0UDvjg?tqcsJ|o{m51!r+K6k$RRI} z01`TIOvfiCOb35c+Q)*f8vXm(E2K0DolXHcVu*a;Rc91K=axa(NtucN)?~+hVP;D2 zAY8XQ)oN41wZR9OZ zH!$H*QM=E8?Z&Daa?shOFRDTRDwWI&0>0+~&M|`DWoE5(big z|L4W>zXD7%`s9e5Ie%RDYT!w3rEZGhivdpIFt0+1#x<|8-HnEBsBAmmhOvBhShl;P z{7LazFe!Js^m4!ZPQ7(Wpe1+FD6j3wn=PTsxSPGh2a7-2BSzRuvW6E-Cff073)Lf@ zL)GGRI^XKrytbmJZ7s~*y8R{CL$ctdrA>~4*y>4@y>7{M+od8=tz~j9HTp;v=e^db zz`69tg5wiw=Pc$0y<>L@YnfZMwCvg!i=80s)(E+h!$5&upD55*;L#%#6adpL&R@o_ z(9jm%&p>R3Pe2nN+a4hT@A(pR`n|}FXAao{j$@N^o+pQwLD4w#o>9=c1%+Qz{T3G0f=@IbV`t^n&lZPx>2+==eb#FPsTAGkYY#23& z!lvY5VF?JKdS`AU0j2FfB`xAU$3pl?kAppmX**|qFm+yM%1Jt$r{*IoauiF-^$3BA zh6W{DMj5ZANGc3iniksTnCRMUZ&+DaBsSVw0atX*=A^xqN+I45_7qgo0tH>k-Cq0vLvbUd&2o*_}ijj%L z#hvSGrG#%%wYxg%BX)(1)G@=y2W@GhnieJvlW!Mea&v2|n&K*5kVw7;3Hx2H_R6z^ z*tZ0TnDY=5i_fxUH=!wuC^qFG>0>*@{oT|2>byBR+L#&cXqZ{*{t1JVNfYZcT7OOfH?q^@c7;UhmO%6YM0v%(9AkTpJ60( z4l${hzJ*aQ0Kmp`=P9+wtLEFs=)7lFuI*oYp9%{0%6zxse8)j~4d>jUi7#eJ`T0XS z<|}z+>f%b{LqaPrR>Yd$X~;c9Ln9=(Msm4*BlPJlmJd%U@`X|s+wW}f9ZbHtn>~Io zSJTrY15EbrIk+;W0YrP5FXXME7D+TMyV=^+H9mLv6=MM1JXox;(Z82D8Ed54IfcC` zS-S9Hd-y#xI_>eF#26#BZlUdL4OCud8MyNRI;N&cSuo=?pI@z?XOMU{_&rUWhln{= z_Gfwi{e?BlaTbYz%=^Z3+F#L=p{~dB2L0am)YIyvJ=d1t)7Pb?L5_ih3|I|&D?CAL z8SlT#L7F`7>S^VBc{oiibF{5zYMLW`nflGOM{iXgxXp2H)wW8e7GHaGi8{#PdS6qm z2!V&i`uirg6Ewh*Lh$ZB@t22M&t~qlzk7mtXbDx=Q~vpA0QXBu!(dh$?EHi@l1896v)+zG0`g(NM>{>jn;W&u^rr=@7)N1g_~P0 z?fvt2?+T&=S?ZO1N+|S^?Xh0*BpfFA*KDv6Niwaa%N!uG$YVR#&bo@=<>Qk-kqBdK zs5!#nb=kb7OLybkv7H@KlTnvQ=&oa9;~j#yn;U1R#0Wr8vfGHs`Q24XI!fN**EjS` zCQHSzlyq5Fv1#WhJV_*{;PX7WKr2g{jO^khWdd)b86Uha^Eo5D{Kw3s!61yPiMs#S z6Qf_=?RblNu4l6j*PZ*YrWE{quURARa}&YGP|p#5`9)Ym;iK71xt_PY!g z1KtTvD7{6`&p+@2EJn~i#8&WtV-a@?1QzkV-(@e)eXM?b(n`U($&Bu|2d1#ZJWgFN zR@c_hpk+yOOmK$bnAW7}U^Fx$q^)I^KgUKgTuv(cMn*;uHV_jhtwthK8^vIN;|#+G zYsO>P*+;NZKQ&s}5(v&Jb{Wis1@VlK96ToZ0yrvtJ-Y!d?KSxrC+f4o_!%%Wx~}FwvVnS z-ro6j6wU3>akf^mM?+DR_A!lw^ALZ34T;}2#>CG8j$TJ6CD#!rtd{%->k}6nzJ5)< ze#Z~(dw#3`>UdS>p>{|>NTyCb8W;xqC}g<9dZ=i<;zgVNyj0a1ZLP)~`u{!Dzn<)T zxc2B@58BY+dH2;f^Im*0m^a}gXYE+2=nu~4WVe0=Q;392Qtfe3ijTj$`%C6-d3pKn z%5VhG8MzjmR>z-(QcfU1D}6z4=KAW0-ckV>iAPM^Qnjqur=rqQ!?Dth`#P!U%csrb zLV~f#9a*3U1wiefz@^@kc!jeEEnBnd2JrPb>J~7uUD=%1$;E zd)%Fl7>3s0gA7G-xxoh+;zpWxc7|WVQiz+Smc!Tcm2n=}Nz%_;cSbDz$iWv6J;o;_ z#1uTOgHQbNOXoROMTw4_y%JeLvpi*+UPB|sGV6+adA zN$C@-#S$$q-#m&!1cDv(BKKxVgW}?(Bnlu?nzV*!SxnR=<*F3e(X!vL-uQ@;K+QA? zXFQ#`9k+q)ZTu|3M)#bmD+$T*>L#llvRP&q*n?wFuHnS-9x`{1ViO=6z=V>PPXHTNH2cNkOI+$T>4YJ zFOJKPz-T5%yF{kvAm+aCR_A3GLdLhi<$2@lKl3|2MVU#1-D!1kWOURBx9K~H>#Uz6 zO7I9mFZ*@PTcJ1bb2M{qY}5{;(YCs;PQ=JtBx*bfAjkwWZE}9Q3;D_kdsUsw$fpOT zHNV@}E;~E(c%FL*3}kjsH@!o=F4SqQa4?Opc+uj0c536?Df`GBz1nGGMqQ58wU*hy zj#5=sb#tQP`O?U;2RQFYA6RK+YgH4OmBptlbY;RiT#Jd%!2knYN3w=M&+xs{_6`;v z>D#n4A6WaqXES}R07yI8^3UQrIFf!J1t$AmJEvS0nca4)yY^@uTRbj z2vOJ7Ae`}v%2SS1S!>x_Z(Jg>cU_N`iO!in(WZF5B#y~z`x8v(x*I9ezEg74rtisa zxPRj+?5AP#zF`}>>1t7Vf%8FWS^P;3X&1-f3vy?Bn!!f_gm5$jlVnfsVBa-9Nw{HZ z@}y{X$MI&qoyb+U;M5g{1|?d7^)Yo5(ybHY{m+;U!xfRY?=;mnWW@hrY$X_4m8WDWVEzN=<97tML*UD||K2AOD8 z??vumR+k3J){K}tj(11id~{;(@M0?Y{&_;jaWI*-dGS{Lgx{3}xm>2hA{AMq-_|*w z`7}vfKSiwJ=$+j#*G>yBEghqH@=yTBiSs})^Lz{UN(ILOU;EF93{7#&rDXg34ANOBxPqmG4bl0RH+RHCZcupKTy#I8)BZ>dX)|spe^UM`&sd!St*0l$ zC2Z`Lj*b8z8gb}1vARo{)E-M&f#fdYUZ;xiuqzu|aaST+MA}8X?P)!7Uu?pUqW;dy z1NBtK##Klg6FE6n3Vx@SZ{J=w*k?s9lMm)-q_`dEN6uJ)$NSUc1LTNHVq|2~i4M_? zYc}D~%|b2r1o#R|KeKpX2YDOkz>jzMXnE?jet>XYBP4v5`bZm`=wE}%Qs3OX^0)V2 zMF;%!Z>|*v5|H6gPtvL9&kSe~*)21mf+gw`zskTI>XugZfr6j?eAhC*i|YyUOOYAAd?glfRIWnVgG3`^W3V6QndjL{;cBO|9x!tOI;6Y|{-5W9`lT)UhwQtUhm+6# z`3T&Go6PkIWNSamp8MH1k9DY%ba1g0z6OLrlH^RBm4m}zA&5#9;Pp^Sp>LCu+I)}a z9%K!`4DnkcUkO0!@|nKh(!}54QgDZZiw;UnIVYh5dG|&>cktRzXV#;H^Q~c>i>gJs z*CGG1_Gr`^gHHHZ&(g9xRHP@}v({D6GqLUCllsrPGy1TM;_Ld9vmE#@N*_d*1npVG z@|h?X3_%BM+~7_9SWIkNrgK4>oCgb(ip;R2VVb%Td_QhYJv)7Me@eQ;K~}9hmM<_@ z#V2w`6ZrZJ{7C?r!ET$yQ@k&FMj4sUp`3=|AEe=lo12$Ne-yUPwd>5zE4_z*J1nC^ z)!m2t)j1$4AfrkNZz+S_6g~YJo{*;4Acd7${v<6^2kjf{EUmkQ?(|CElH!6DX<{UlT#XdQJ(rvP@w01~m!lkw4EAto?qoZO4_2+v+wud-#oe0UQR$cq7?^ zFu9g{<%_e}_j}?1CSVvTxFm0~O!g_hhaOV_k3=Nkr5pYY(ofPN10`QBoo_Ze0Q{*| zq)Q28Bn;gN>y;|cQ>^BiS{NxYM@L5^R5dgPst^3r(=*u(S{P5F3t5RISN2@%PS-CY zXgvA89*!X*>E|2!?*+b?{76{syg_ZZONUKBr`xp{t(^NLt?y|VB+tJC+@S`0K`70? z&lG<5c5(ak$X{`kT&AIxAms7Ao%K4p-xh%%hK4TjpOQ3>OWEEv`@9kEMn@kfPrJU; zv;OqAb?AqU)N1lT8TqRm0h0X+M)tf)-2Nq}`vyj^W8eq}0Y&PV>Wf6Rm7xt?Jv|xs zN9YySQ&`g=`xvjM@(B*UlCM#p7K^sw`1?}Bx{zDS*dC9T_V7;>e6~?;Zq7LJOr>Qw z#%{g?s>0D)w?YYKEiillYs$v2U&9J%pFu7t2L(>?%T8`s3X)drO*%bZ^#B2S^7r$$ z815sv7mbbSf$J&u=-VB{w{PE2+z^AVxD4@df5tx%VrjQ6zc(ua&%-);+@v`<%$)Q)XS{=Q>L(i2~5E7 zZSAL?bS!I_vwB&^qh|Uag;Q_gfY>d4*PEC7J8nXc>=zE*auZqew{Pe;IHqIF|Goh2 z{8{G+63-Linb1-K$D&*HWqj0I>d_bOv7Q`yIJgdMiD(BI92O%_9BJdKB}`*;6cd9b zjFyVsEUtD}z31hj6!Ap4lGJ5LF+4|P51Ms#b)h|dHi+_Dd4d2UK%8YUZ~)5WHB_(N zyeY2gQpuk5M0;ppWlyul<+3-p(3ntK{V1CGiO{Tkp{is0?WWFk3Xhofi29G-S^n&& z70^tBfo>frxq~myw#Lm_jaOdwR{6SGjrVh8WO{u;20&_<&|pn3fX_L$i>tnI^yzZE zesdtY&AtSiy%m#c(OYQ0Bce0Yf~-w%;Ny=Tw5IH?{S1YRH~`jfLxoe`+)P)<$jDy% z6mkInwAiqnJ;Jdk2`7DC!1B=Y6wAtY@^(rDJ|3m;Tw9;P?x_ocK_#D?gyrvN_;y(K zcUDv9`_uKPf6qDUcXr(@bg%4s%$&-14eg)HI+br4sy|cdQnWetq+irm2sXeIyEl6? z(Hnb7Wi0G8AIGlf*+XuworYnx?vJ@!BseDe)_N@s>>;>}vrkR45@m_IQ$%i_r}>)> z<>jzrHh@_Q_**ph^q?qrh-)`)U_bc!8j~ZqOV`vCMq3<+RM{5cXWB!sEQsd*{mb63 z`3TmC7^g2A=B11R(|4rAgay>BsgAjM6OrcyDW36Cpqdc%_k3MjYudVmj)@z}bNg3_ zgrF9ruY>g|j9ZM1=!jl)Km8?vurNNSKp84BBBIPT!3l7vSRTs9p~W~EEw|)Ao{ver zyU{EmHNfUHegPoV!=2FD)v>DJJN|gzQyZ%h(2(^QCfUx`KS=BQj!e@Ao_}Ja|AGDb zs)R7o;sWA%mW*gxUwqf~e}cv}Ys&wFO~5C-o!rv0Kl7dBRM^sPFl3?WS{CRV=byNY z84{rJ+biDaVb-F{r$L@&7l#vNx4!4EU(JSxM193F5T-MP1V2mpIO43Y-;+V|hPi1BAKckXHjj!8F*XM865F{YZmq}ioSa`v-SoryuY z;G<>r)yGf72R=OX@WZ7v?1CW{Lf6`4teA&GK%j9}QpVG1#$0~p>{ z@!rwYwB@L(FI!%;i(o^Eg+=sW9^~&J^k<7m%*vu&STJ64a5acAGKVz4*wweZ3+^$4 zhJxNxpYP?JTV4T8qeAB2S08EuGMbf;*#C3yx2Q`06r9`Tmg|-&r-D(x4xL`7n&-tN z@yW>YlQJmTv}+9iN+cKh-OKa5ZXT{;Y(hd`y^fCG6(m6!|14XUGp=W1&RdoHY<=IK zjqs(E+>TJv9H=hEo0j^8W3{IX@ zIWfHjQX$H_K)u|eKpF!CtG%mg10>w;cWGrVY-%qu(;T@vn&Xopy#dVBH!!yh^4gVW zDZs=mez~KZDSZ@u|9KtP0wWXC5wt~+@*7%)T8MkEEYkLR8PiA2oGvUx>x8k%2Mr!sPJjrn?L$WWvkRPD@}fq1_|8e?k?xQaq<%Nq~_>`C-%Q-34V8bcy(~} z=2eCXSvH4k$BLepHCdKJSKD8Pz2AJhQHM<4BjZMwtJkIwrV7yT%aJlJ4BD9G)S_s^C6Q? zI~CyKmE-Gw z{~z@`?d(TReg)0fL;IgT?XH{V<&{ap6^a4JsN>ybaaC1bCJk+PgMO5)ZA71)SE= zKY&MeYyuf}W(rN=3YYoV1@EM4Pw^}crk1Orm5o&m4=gM!P=sbgFllY@d0*q`B*>C4 zdj|*SvkjGqA9P6x5A3h`wT|Y+&DhT0J*yXo~drInT046lkarQa(3S9k`Mo z2d8Fp(@S(_ojF&XJ`H@yT{wCtIHY;_hN?T$tLekUYN|qw12TkGwfh78Uj{dBP|mwZ zu`DU&GA#`k;vo!LWp42v^6Sz?YE|#^0F4yP<@l&-&XZ?klHUdg8v&$@@^=N=4Dsws ztqmVMez0yusoIZVWkTNz+^NM=H#S^y-b+a`vT*c^Dk?bQVs3&S1@$Le7Nx+n@*Pw& zXmYjcTu+7+gGmSJ6x#wp`vC$f*+;Kabp`qP<9o$Jo2SlsDc7wWmNpC8YYzAw5!3#r z1AMMOs1U(ooshdg4TX;oKC8qdA|h(xE0pxzo|n^5wSg3$DVx$h+Zq~NwX?`l1m_X~ z5ymR$54`**!@uPj$el_;flmJmFz3Vr7Z2sim!?=oZag~SkOhW8w&2onV4Z~qqQr`urZ9dI4n4ITD2wHS) z*(x%K5eK{+z;P3y5>gK9{@p9Gj{=HOG}rETvuH4~dZV2GQywHGu;v*GQ_+mqh4shb z=$P2CU0~j81i2fHPNDSDJ*jBch5lgym%|qY!zCpp219wvyDL2JjXW7f4u4`{6K>z; z^W>p|!qPw`AQ zuSmA|S?7~f?KHEx+I5O#L(KJVbO17;YiW7>H5pYw)zJ}Js-PgH51m`IwrU%2TA5GyoBi&y(prgzD#WRbiA-Fr53mnka!M-vl;XBnmpce*az$<=B|(tln?E zc?+(8Vlp7K5|+Pz44?v_}bk3gb&l3 zgu@d&n)pE-k-WlF07V?A-IbR6(gZ+-pm^0Q1JJ-&NY#v53$5J=)Z?{ocwjl#5zQpJ zc);KPX;2_Gq4WGNF!#@+nRKA9J;PiLz}~{nNlv`?lGtQv^%m+gTh2nOY}jkX_B6#Q zlSgmNwKJaSBL>%7mZN^egmCwe zikm|r{tAYPZYqV^7$07+Q9Th8EKII-9mq9t2H{vhoOY;M=2X~j_S4Qrvgf{(_RaTk z^E$;+q!B?(qw2&H*Mg>s#3I(;IZf)BqU43jW14`o1h;GFb z?WL5hjm090lC5`N^lsRz3t>2ROVY4WPb$iUD0h4(lk)WW_pTp?(rNq%vDK=zb>uvi z>q<=%KlB*Z?sqV4(VvaEH?56bdsU2~v6u6cyRh)Sqv77(1)3DM2g5h^-S?FDDQ=(V z@2?_#UMD95oTMFhcnn{+V|DFRV0?JqQSH#!2gY0ba$8V`Hdj z{r9KL_uH#|U9yZl*@vn9-uM9*RrryWuW>+~rZIgHd#C!9oWT`|0kZAi7N72QPi~4u z>&?vUF7L3_ikxibG`G0cCUcn&7uzpxXx2D{0hT9;jLbAYz+2irL$)wHnpf5y#=!v( z)WoU_Sjme3>Z25Zn`>C$W&Y;2Z(wW?h2Y`ya*LnFH##NK{H~~#^~;j<^hM`usDTOt z%ELxLWquESVt3u8>xjOeMs7wx?2-(4f&K4YQ}BxhI_TpYYYj!b0#3A}KgH}(n{ zxWQ0*5p*0MAv1&Ju}++)slL9x%gG@dn2Bc(7A}O306hd{y#e8n?l}vferdqMRrEu% zadK*b4}+euAACv3Wf?Qvc9*V$EiRBbf}qNzANc#3zj1k%Nby7ZwDN-Q1iLVKVsv!N zM)iTsQd^(&9i3paqx?&&s;VRli=DP74tyc%>qCq=8wJv2QMTpjUZ&#oRds4HG|*7W@u$;Jp87)|k3UQQ4F{um zmbOU0nH~Z0uc)Z#*7TeNx=eBtm_cvUBbC`!23=c*lCH5tiKS(zvRQ+oYQSg^W<>wI z5$nkDq?gwifs7;UacqO!w>0{#(+dV1APOS32Ik*gT3UOM*h7l#0XOsf#PH_T(UL0X zjb!$}#<{3)Gn_4T2QNC9*+Ah1IUF2Q4&&PBN3%C)m{b_Vl-tB3UUPT+sF#5>ktLI2 z++>obx=^7R*Yhjk{`qWMSQ-I~a*(A4x82_Mlh*~>`o2@_z%&3$h?}{}h4M0;^4dc< zm#-MjK~D^H6~oP21=@9ONA_7&*TA`2cnL)T#=yGrFs+YtQ99|E>6MA>qKkm@0>NOe zaxYM+ZHUn&xrbc(}$1Brm1>;o;VW|hM$*r~IhD7WH@!$4IS%6Y235fS*g%8avPQGpk9s=NAi zZbD*j3?R%M0gqq1F@{tHiPU13Q?LM7nyh8u<~~2GkNNL|A4Lumk|B?eL5P1BINJr% z?*xzMB4kmv20<0T6)k=|Li_1&E&#BuVMUEUoC4q(p6m_lbeM_(+j$nalpRI?yiM)2 zWeoSF!90YSedE3?bVJJdTCJbTR^EDSK43fU-cik^U0V)*t&l*P~mao+PP8-##@d$s} zYnRfo-2b_?_yy6BiCN><(3u(Y3*~)DZtzY!$ga5aT3w@Y`HM*WGwRUqzdy5Fwfwwt zwT{e+<4iG#-RRpOmk7mJMb`7y5HL@Nkd?KOpD64``>o(AkTmR9)lNTFi<<3g8`xH&6_uN%l-EZ+`*z9 zaIojKW=fN;EBVqKxUT!FJ>^VfBy8^4>{0o!O+da#PTn(?+ehSlTsBzUkxnaz2S_(w*tye?T{{5d!l7*mR?F{pQV!u|%Y;t+cEL3lO|4o|#VBMKb8b zL5)4LSn(~r@jqQQysR)4qK*ev0CtCPnDjun#I8F(bL&YF2?f17xDLavqJ$6%fg6;R zJ<#J;+RbCbV3B}DI}u`{TPH+3X2_Qs;K%`JaRF#^$)G^7IodXYsfk?fAj%m4EY^V!=Hu(TJg~a4(F<(xDi0)g zD3wSHL~D>bqZUHt<#RBb$5Q~CvFRN?AN-3f$4d1^iu5|DSrkDShz0~sIYSt4Zo%F< z3|6xrbhGyVO!nRmr+Inl&?`HzQTqQ|q2RjuG^b~7&7{8g7|~B>X8lU}Vhm-v-SR{F zVSG-4;tJiDHt0Rd#&Z>%iwk$Ic!}M$6{d;OV#1#O&~vDH=0iY{f;rwQ!cT$_FC!tX zO)g=(4}%lEpKGmM`4Ln|Y5QbxFvdJI?O~kV;yFh~PfxGJiN$__*&D(a`=){+JL)Br zEcVzQcacnbR=;Amyv?#AdRe&M=JV9b&B2XBx z0UW&MGJ_610EX`H`UIlXD(Q6cV{NU_;3uJKvXcH{7{DNR1Q8vPTlaue2(&Nz-E5Hq z1AShH&1DVgBpS^4FQ4Q!c+X4Ghbze}}BKh8GkA|B1EE5h*}wLH^B{cSkb(Z4GmeRa4|qx=&X)}7 zzo@e31mGBLt9=}3YS8X;m&8Ntl5GCDT)Yi{l_j7?|sW?Wod3@NWRtX*m(4;B+OR3z}Vr<25j z5fqi%!Rf=+{WZ{tNp?7dSdG_Q_Kt~(xox0pWaOJ0hy%LQH{-aez%=EtdV`o&`;O01 zvWnAGn=DAnV@=wEuIZ(KScfS;toef_PZ*0mnA>!0Vepvy3bf44hCf-7r)a>jMjb?nbOmx_Q^GUUG;p(G@)?w<03^WL zhBvJd5jZQug(k367KWiwaH>%2M}C-bspm)crHR{vrW4jnnyzxXPdA{!S<046MF-!v z{NfU8J3DspELwo03S{Ldh|#1QJhV^>!7RrgC?O=nsQoF>D`CI}p$_|E+umd$5InOI zk(tz$&H*M1#WgB2yPs^ewnPv*uaE12i}C72bqvJb56ZbHhkVcgkNo_};k>RndpYdQ zn+sTYl+qxC=CPja+V79zPYQu+^J`;67WUbIRIAsHQPNZXDwS{-C{qxh9o(y&)<|L3 z00fz1PB2S@4j|%Wr=#;Ay!U<@&Tb>GXbkn=&*xWtruN3?NK*x3Li*hYK(p|iDv24n zh!-h(j!&cH$VNnpu}inyz@jo+m69m-^P5x4BLi>tBSurLwva2Sp_PQzBkq`@Jad*6 z`0dt~Js(}ydDTew-MSRz1}5TPI!-KYnh=nQQn6L$-NCfvC?8=jJio2%)(x0b?BEc*uOXILpe|Gp?sGTP{bb0Oq zj`Tnulur|Wo|!@~OikG^XZAv}hq(t*J6Y?3Sx5!;DU(-Q%Y%SCbBuh zUlCoX+K!?ob?`(an|pNv*sS^j7alnYpB=A8xh2MQCrc|CPOuXF(qul4$SG&c&x*#% zmmEJ-E!=v;TDv-m_RIuWIN*=8X5wE z@HfLGc%wkaGOuZx4P5y3>k%kjVDgYFp%VET6Gs7=O%&)E#yuFYD)!b*@GhQXpA01JR7 zeHGZKoFTF@G7Hrkb$0;1rqN3Hw^h)l#o-{_6J+1_?I$edy7$p#+8>bC(Q`g>z# z?uYU^4*V2|FQuH#t9jdBCS&{3*nkRk>K811sDGQsYJ3{f2w(|WvQwTXWgwrjh-Xgo z)>y8%LU!qDoeU64xAvJoN{c8-exBiTE~-#5WFPrSVkgn=hnH>A)m}wBJ~`8#*h)fW zCM(JA(3H$=K;v6eqgrBysluXjYx<3djx@2qdcN^8yNGh3nt5Ub8Pq$hM5I{^igX6` zMNdkMFPrQGk0?Ao-Yf`1`os=hpxiq38JXz7#lNrApK|essAH56~c|OWR$&s>ZVeKC~8$ay^W|5^D6vV`!8GXUH7J2?=P_VT4Vi zC9a(g)gGPbk^?`OPqu({$+KUSl$1dHLFGix(>)a!BbqYu7s#h!ahd&KCBgH1Gz&&` z?_d`z;yYAlUENn`ziLNHDsJfOk~8k?o*eB!G@6E&%TYJ!6{DSE`)EV0 zzq;uu&tKvV_$kms7;C%s(!w~8N^g|+450|k5WcsN>q5W>R^&SgHa0K#*3W3}tX4wn zcKIm(^Lmcqa4`4XuX1CQN;m4yLy#Uw`~7h~Fq1G3 zX8_C{3?7RCi2%IwnH^tyr>k|X(mSA){aTTjDsqUUl8QMpipK;(k0Ai}AXl-lEb$_T zL;>c$`hX#4rCe3cOu1i+u{FTYDYiZ}XMKQ9q;gso~~aqM*+Gk>*R{{t@`C!eh88{h=4#qz46|I+T025FhtppR!RJ@B`6>;h9zv5_Z_6mzAugp z;8n$LJ;^fKBLxaeE7lZXXkG4cX zm++mqvOZD0J(%7u=zjSAyG8}w!dgE0_P8CRs&dX}cinoU zzy*|eS*>KzX0A;hnCGbEh(@PgddcVF6&R2N<@75XKq08*k(?}N*ZDch)Y`kKr zKM98#5}T0zF{ENsa2f#gNweNF32q`o)*M|evNd-X)Ud@aTlxS}d*rB>(SxOZB)w7q zYy*bszj74;sLb@WIg>aFj`}K7T+drqzrHY8TCg9+w zHm;e*B>{7LAQN)D&=t8#A@psddOtjqAd_dNa!@w3Yn_;o z!zCx7$Y>*e!Kn^76B?b|ch;dbiHO%=E3@nBQQln9=bVPNx!X39z%Zxuc4?`a;J$hj zac9@qSo{g#khRXCZ{G^Aq>mitDAG6V7OUi|`$jQDhXGv~D9MSJc-voLT;y|Re$Qsm zitlm2oDxcE$}av7N{IT&T__7{M_hnK3L7d=g&QfCnPE5%9v%s z9SOBqk?WpWJhx?lzqnXTl*$@}z>aIWmADIFeS!v?5)};pNsC*bPaXHsyplW`cF}|* znanOe@cFVwr;|xuzcC>NHB~cis^X7HnH^P$u<;3^&cgvI-aHF)JzM#9jb=l=5VbkCs*8r~1l-Jai z2Z2Q0gd{$o+Y6!GqlH|}X>Bz^=B5n}bXe0%Gifi%#u&BuLnuUKpoOBOT(6wxao}WL zSPRa`xQ9n6IQ`8xP%%&StDy|wmb9|T=pR7x(c&^lZ;fhkt$g2*8pOe3UwaW6Ebs7p zQgokL0x4@LIpY;!=eP@CTMSO2TIg5-0qC;#J6DcGs&?yv~mx#24MGlFrzPOx{ysbuuhNi3YJX~h9xrX zceT@>UWZ}^ry)teFNAFw^fPQcJQqJ0wub|A+7AkP=+;cw*Of?M#~}Q|s2#*v>jIT&WZL_tAoD|W* zG0jsi@oNpGGKtqKa_v@!5(*QWpkarQ2MZ2osyuMoT&md)HpsA(Im|%~t-G}Q4@my! zDxFXimW=?5ofHTqFz^64hP4GK1h6qq7icty^z?LM`$CNctJ>ux-$Jcw<85Lp5zj8z z9oPm2X5K-q8{m?us!E)|W6ga(eQ4PMg#;rG4-FH$+GL>mwY9VR2Ecy=t?YlsAlJmq z55#x+@Ki2U$5;eG(G%7yLiW(d)sZ8ZAWo^<`iTJx+4{Y#K1VOkn^fr^2gvLI;ORea z6Ds8wS_>T2Gtv7}TmIV>y~tK`4Gp>LXF+*xkPs98yzuNI!#y!AZ2IQ;s*3^-OUfL5JALz0UtixYA9+gw;O~Z zzZwb=pZMaTn~1TwOq=pSAxE`zNu}Is>Xo*&i##-eot2k$&^Pr8DdVzGZjS1l)?z@R z5pwk~bWetp1QUiCuPGVK=>!RU4q3N*+Jy;@Z!CgA>A`Yd;SH+RUHC zC~e}u*K7ORE~!u9Lx54i3JrB_V875+;J-lM>Tp&>;ch zpOHAgJkzPh$c%K>wR=SHp$zCw!Q+Vpw6QS7Dk>>SgL(WR63O9l>;RO%hmf;? zTTeqPllTGtE@Oe1NhAlh zTN#P6F}=P*KO%d0+71Ei7 z=#-I>ivZv2!8;#2n1b!enDd+qU8iyIvR(I9_3RvrYw}cJ;ci`=;5$e;J9~51vB!;^ zd3k5OlOF!!YQ`Sq=RzD2lVq3Q5aryy^t#{*L34oW12xT`V=uTBA3N-oRTC8zMiD+4 zE<-OM%<$e^?SbzZoa<7hGa>1L2s1y#B|d+l=NZ)>tTaX zp@N2n2Ww+dG!mrT4%r2jCwBE*;k&y7vSw>v9HE=2I!hN07(Q?(JXAEn2xyq4pjQoy_a$J%?qHJLu`!lCU25pCB4R*5 zsnP|cH|aHitAc`nh9;p)uaVwCMLFvpNJS{4^ zmeQ(0BRAW74@lxWUtK)kXr7~%b_?pJiwj-7x3T`Ro94xblkaZpx^^KsZ!2#2cjZ5R zaWenHlu;8a56_Lf$}LUjpfWiiqQJd z5!;=)jBl%Uj`=c2z(+kkLa3Q$n(MdNHP*d^DxJ#~Sp)Vy{7YU1bq%wK^~I)pOJ@{2 zCr~Z31?qY{;c^(v=`;M(bsD7$h1|lN%+f#^Z$2QPA?0(b`z4onNA;ddYx%@0q0Nb4 ze&=P!xkk@4Nyb=;^g=tLUfK3bd~iI>2XZPp-WCn}DbtC#K>w(&kW9CCLNqq;?v z;dRGP8TCi3_#v{?fi(A(&RShvS1~cc+iaSuu_0RF4d-SaKE`(V`sm*wi6_gt`{4OKn;~S6B@^7*4yQ9*SNU!6h)U)g`S-=9hv2&e<~lj z`wHQBd)A;Kj+7PN}jY}xEuT@)KF~=WS zmROo3r&m_kIVqL<9iiItC1X-$XDrfVFIk>B zse6@i-pcMk#cmT1Eon7U3H=s3TtdwDv+5{jxXfT7+$AfT;4 zJt$S_Ss22ig#^QQ)88VWBoH}$F&kvzVGk@`E2B-4&3dEm*yB_0YHFt2RBh=-L=ejM z?5CSZJak`1Z`FVK(h10=il(Ny0)K>$_;5cT!Si5Jo1X2m@cA01_w`QNu>iTLZus`L z$Ev`=n=%!y7At()FADl!X&NZRKXcs}+Tb%;6xFe2!?ymr-E|1)iJ3RSEbWh$GFXTf@U|Gg?rcBhB(E-h?-9dPMGO^E~s~wU&@qzu72^ zUbtJGqJ_*TwN8IKTAYo&{9#e4NLN!U?#%dMN85qc1{a4oX{2Mo-8*-?sPrX-1?ef} zBiA1bc}Obn?pOySvm^*>ypYl9D>Oe1WOdzgTveA`D{|c``~m{H0y`Nz zwWYo;!iKAw&x~iuD=!^C=m5P{V4Cqb1ZIn_!axoTQI70 zNUQd%u^!1Zyp?<@bVkG5M~Q14Elv6opr3d8b5(%Y*e$hi82GFe5)zU)Hg>vVxmL?< zb!cPlBRSV>qIiX4153*HEUVAsS#tlhH*1++rXsy(<@PPMzYZ^HHE%s!qi64T7j}iUBLgM+!--$C{&PIG_qMFq@4m$S$PAfclm}-Aezut}aI@$CX$9gctWbgHz)MO`RRO*urja z_AT=cs(pONz|m~0f8-B91JCPtZYG8HoM*qE2-+DdbyrPo?QFZ8uZpf-k#$9(^^jW9 z+tDlH-(K0-+Y^>pY-T@riKkO0+XZ;acu!*03{- z-VhVp8bl1aHOgaw2u?^Rf=LXo2E{rnLT!r%vVep1RxEn=nUb<|YGUG@g??s>_uhwS z`B%=>eEOu)cl2PSOvU-=e#&;r2*24{`K-cQV_55quQNQh4RaWc!NPB=O;0wi>2S3q2EnWf3gG7=$gqABr?`Ul;u|_^Oz?>rTn^ z6@FCSvhUtz5V&nJEwW2AO=8huLOS)6b|}p0(_;>UwsjG?;_>kpF^P7*9bCf)3jtSI zu8UhNU$t(E;Z?_+rJ+e{ZT-z^xLn$6J4YI7plj`I+4(^6pF3;^a>6@VCXf5aa`~F7 zsui5$@Ter6ah@Tez?G7tzCU4oR6wkYt6N*C>Fu4e;Q?(#WKC;D#ajpIt?NezPAzac z&MtSAXue)(t#N;ieBa zJ~t2_MM@h6Zs;tXb99Ir-dkIi}mSa$7>f&oWttwh+<#p|> zgE>&g*p)J^h}18rca9B%q#jAW%ht)qn`hvEv*!0jpw1;sRRmrECv#7k@`y4{SIlvs zw{2?e&t(@;;`b`1IZ$+!|NQeYWGE26?ulixWfytG# z%C>aZPF5n?L5Ybl+aV)BS2cVvK=0$L`>q`{jZD(t*qYrs{Y9PoJSR#?`|d&+^V9F* z53c9syXD!sNG{9;8dLg8ow*_!2R^Zi>zhJ-mIA6?YmNhz=rTr+uS`TM0&SnAnvQ)_ zSLtfA$XNy^8&0d&5af|%Q?(PT>%Ks-Bj_G@j{Sb>QSx)L7yxb^CPURt2X&lT`S?2X zPaibH)D7f^={;@E5iXZE^9QYb^pFR+E0E$hV4fW|toEU@{*%yTfke7?nGELad8O5} z<6)`!`6>|_8kj_K&hneOGj;-~8qe7Vmpsi=tC64*@lTH+Z<9uSf zy_fO5ISGh*=XIPB%QbdB7;SjD5ofQiO#}y-E%ZBr84;>kYFQG-sgVxeQnMXm0P)+C zjfb7z20=q5Bl9*qzoTqQ>A`={N-{Fd%qh!iOS(mon#zn`=CXl|2Nnda0i~G`gySg@ zCu{&k_1zmcv?m?y^K2CqvPBtjz)4U2+XGb<6%`|EC}~d1yY3`JKUwPav4R%e*u;?> z(XAzyv9rD~&~y>>8XfB*Ee7Fyb*N?qEAd{1du*zfO@&6)UZtA4F8>W6VQ7V@pAe{= zrb~G+n0zcyHvQmNGT)n0S1Ec1-@1M;I9Zr3^;AAhniHK2(?AI?F1lIwYA@p49SVlT1GBG zonAXcIl0E`z9FHZNv^Bs-c*AswM^PJy68z}9gpQ_o+W1#=aax(o&=`)S<4rBF+Zj@ zF~0A@j~2GhQzjdAW)P8=ke!bgTXf6n=+kRo@hv_z9V)ZL>1yf4{gx3gC-RiF)!1a1*|H~ixo|}WDKfE^Y_Ac~L&EoUE*-f;?W=2Toeh3WdsI7Ec`41& zA`z^Vx}Khup4+yCr=)1bw^^yFsT4)_4*Aldg?kOzHy^v+b^SnJzf%ks>F{?l_FH~*TI@w)ew4Z3T#j(?}3(tIER zkES7XFjhF2mw!CW?nkx4pJyCW>`)VR$MMBGv-Y%Q)$|<-ONLCk(wj5Y9MW#Y9#OFk z_sX}2IpBa~M!mSGCWf}lJj~%JMtq_?zcLd*aTMdR#dxBrR!FGCZ8=FDfm#sBw^3OI zb$DRHZZoFn&uN`&Qp1{&0l}xw6u2yVRB5Bps|yeuKPTh3&_DT#kFHGHddQ>kT2f-- zXY&HnfkG>&%yn=8ZHA4|a7*zuiJA+_UF^0*JrlKOz|z{#QX zH->}4oyi04H)qGQY8qG_GrB6L#!Cl>cx6)xPIkX!knY$1suVbs1)2h$=1l7Ap-a>> zE?>p|SO69;dh>h4m`)i`WuY?X-MYX9euGbXj#~?c)O2SOz}q#G5*r3owj3*XU~nEpZkq5u1jiwSd%WCrTR+2g8Ct4kLlgWSHc^@A}qzV z9!k%%YQKMYBQ$PrKjVD`05PZ<_@>$us@-IAL?UO1bk<4_-hYtS)LmIMu)mGrbC{W= z6<%9um~)gD5P$W=wH#ML%oS3}c+$5dNn968>z-a+C>p-*dzOJA>%^H_i;qv2r_TOZ zeYY6~HH1jNV-T=ihWo^QlKSg}+%u%X!fdv9qdCHU-27QabGdJk9q06@<7nn7$324- zUCl@vg|3;EzOt?z|7h&m)`zYh<5uU&fgz1~o)j{&Cd-x>OTcgn-KEMnZIItU{|i~%AvMI! zNGXCqnfzZu4mXPvb>0m3-j7@AkaJYTl`>6dpAO%>_DB}PX-;R}aF{I6fpiEHx_YRo zY0?crmr=()8%+P~X?ynbjm<;<~ec?>gMf$_!!DC-8qT0W#qo#g?UP>9?e{Ssa z>`gVTbpK%rDUkNXz;D%G_H!t` z&;3S)RISEIz z0UumJb>f&S)5Zc@@+q=7mK~$Ula3Aj=9EXsyw7D?DU=E!mRY1F{SOqKyH6$tX+5tW zBl~*cE8lTi)7x8jt-d(VY~?ErIUD2)aiIDwoacH5?NIGCTgexXydQnA?DpkiT)nnk z+h*y++f}sscTEv?*2=oF`Q46?Lu7nOy%^D&u!)FjGP1At$BlmbrS){1<*9ve0fUGP z2uG5=|G@j#lP?#^$XaioFQVw07+DR~#kcos`?}Q}!^-g?6YenLIypO&&lsdn^Fmz^ zd#T3)=LGY!J~KxqCSK1tRzTTR5PkV9-9v7!w5f`wXKsrd9z*T?N@vVX-F6?zy^h)C zQ1n_l9e>=t?)#*uF313B%@DI5q6btk!zo=l!dGtihI8(>E0IaWr-6Tt7+tIAOwwnl zUTX;+hk|$qFVnson`^z*0VG)B$lfgyXlFY5F}74DY~rRp96x+FBJV~LYd42mS9h|N zl)Ft7e7MAmebQ>lox^7_Ogw9|>jtniu6;57eA!I7QWLS5hUZG?+hgH3ZvsulScUYX%4Ep+)?-2Iu4AsHFA_tm;SCQ)OSnaAsq?(2?(B=-hp6x4wZsKD&n zIA+Ug>+WPJ7l-L}tC4e|y8Rjl%xhgyvXlj8U?4~&cMX0TJR7sap(wZB>u6vZXB-or zNF83(mEJgD)~}I8nh&EsC$TN9O4kVM`P1wymN*tegP%Q6Ab~l5=vCXUt@TXwmv%%Q4rI{{!ZEbIyO4a!+fX_% zXKAZytmRhmJDoQ%L=Ht7BgUPWxw}i~;6P?G{bU_ZTqpM>$-27vAi4HMrEt3jF+a+D z{Alpl!z?>>w@<&!;uU^()7PmiJX^B8l%oG#&)i>CRmu|`Bc->%s_OAvUilb;m9=%l z6YYO78hha_l33KT>U%=)sB6uOQlUPD)unh*BQeTl$flR<*Do5=bn;q3BP?@B5suZ zM9#@O&-noo5-fa}h`mg4j&9$A)r>Oh*)U7;Y^u%cXhfi%3=or<)Vr>9CKJ>Pk-luy zf}$8i(NfTrqDRgVcorinq1 zj6Y5xaD&k$|F}mTw9L!o`&y>z!NC*Pcgl<@xhBG?NtUha(=ay=qHjo-qhW=gP7h|) z>oE!p9Ep`^SuR^M^%rt;EA*Nj=}vM(SDRL3&J?jwgC z{G*zQS!qFDqApbmf}_8XcErIe?gOYR`@c%&3E||Q|HbaZ(>O?Uv?)>WqiV;nmo;eX zCf)oc)k4MF^##IAx=^QOF9zr68rblr>zh3FB6#5RavL*LkXb`XTi5{mhJq_8ivuk7 zo8rS84>+`o?@vxTF7;giuw20R=QGJ@pTWTIzxX1rM@moUQ?_NUDWPL^(}TyJ@QEa- zVEBrynv?^2ri2|D$1H`!bag`L)BXE>(q-<%VX7^2Y1_T_*v)f|4a>^h@*Wwasy4jV z73J4`LUrA>q!MbRSyCPhB|L+^jeY9JgIhy5&6X^BcRbb8q8M>gN#Q6AXL{1>jbJNj zJGXM$*Qc|?6@z-D7jATk5B8X{qxjd3GrcMb;}`LZo_ZzxbSs&|hAlHDW#no3UK4%)*0M~?#+qH_aYsV)?}zZGXD#sjjX7JiF7BUx!Saa9 z<<3vt&9d~tNeNYS8a3SBqzp1%vhc{pfBEzP)t)loAG(w}e5X9)Q&JVj=XNUzBXt4E zpQsC2zRFJsCQjq1<|&<5^ngiE2i-umf7MK;l~W48rgvr~JdK$(okz$;F+yvDb0lLk zqVm|Nhi@5=dZfY@Eq3~02tMV@j(kI(Q_-R(R%iU-nCJ4J!bS4Wa~G_4tUSZICxs^Z zkTkBaBNIFu!WzfBs9E$+=55C6Zq2k@swl|0yULr6%&^Z}NC4|M0TBJz%x49s>5 zbNvu&^ws_B8Py^vPg0QS~D$o!F`QLvwV^& zbyJLxQh2f(W&0;EfoE%Z^(O@WE&zSC;&5nK2vrCqZ*PdQTjkl&i@m?V(%4-p>YI&? zOaq_+RpB~yCCU5X16JR zuE%(M65fcrC>V3NRa?P^I8I_(? z$>;&$n}GVNXungHRtPdS3BkAANJ($GGTdT*+h#+tRfLY5-`P3BDYSTE#l?s_F|za~ z=!JK6imo@7-^r57B2GlzlpoSgG;*KszyI}>x-KDrygihm+CjSRkp{&oE7_|=bV6&5 z|1Y40NImjA3GK5LjJaWdyVpfF!E2jGwv}T4mK0um_RPb3H#s_99ikTzup&xfplSwEIhh-_ejp$C=3gV23#@G#K!3YQ zl96Pl^nodq(W#&RuurWSL;e6r1&!}hq@jxgZW+eCEZylnwLdn;*ZY!l;NaMQ#i6(dHomzpRhSW{d0Qf?J;8mauj9PH zKlKi(#cKg*9uE$dx8O1-EDXqO4Du=r&AlOu4i3&B86p1XIFxifA45J*M!XbP4617x;l*gPsefll1Z8b#}Nj8RMwfb`@w8z=EaA#oztosw&5^Ny9 z0_-n+eSK&|q^`>-Gt@EB zQYTXeYim*TqAVEZ$dtg_BFfIP7+nur48RcD?2%_26pG2(mM0GkjBik8yPZ6!! zsjNp$=v0>$BdGp;$x+5ghEfIT3ybr!e7Ly?p4q z29xbjAf(4+Kz2=S?-!v#U3tw&oK6kr(fg+gD2nh=iN6i?GHdG}+A{IFuqD7@>$bKy zb!jI?D_i2E_Q^aNVR2!}%Ss;|%`@W62@yt|6fu()x#)7eOin8%R@92yj#gxaE{?}3 zrO}+fv(UliG)|F6MRA1iieAqdYgGGA9CztOeAEOkN!+7nawi)t!?cR__Z!l0?Xw}0k z?VhEkp4&0&;B#K|0H!cG0WEKE0%WlHQgnfjndOfMdwUW5VzG|#yRALyPyc#?_u7{F z_i+DjqhHA^yw3PH?){28Q8Q(``z-Fcs?GT|v(L0?MB!CJt!80eHdW!-`ngcyo4pf{ ze>6k{Sp|i!Pb>?htwn`y@hlVz;lVhC`jz696d%vZpyYtn8ajDM#>+3_N+fNESYj) zpmC01K2%Bo9I%=8f#xXhqpyAa@AMP8vsV}N&Vy}Am_s`{E)-es_&n*y#e)lsf4<>& z_}eS~7h$od4KB7WRpGiw|Fs)6uJvbdMTY;-9XQT4)e4$-O%EL+q=lwR{Dz;qg&s=9 z(GeUIDy=(v0*Iye^pOAD6lupFieu^O`eS~`nR}Wx_g1=lG=hfbVd~V2RQ@zg_2mt- zNdHb=0m+sfW(Cy6%ATGQ8Y67+C7BHSC?ZEsTIF)(72TA&jN_im{IpbNOzE~_31`ng z46PpTLW;-{KK6T{znkQg-a1Y)WoK(JXRY&0zbwDTBwF9w(yMOT5aD-BL9hsw1c+~K zlcTqnQ~E2>z5qXVR%jt9_U2S+9TtT=EedT$E~3nVSy)edWr6AJ*~H%73q}j|-GK`t zo$+XQBO{}<%*;EO&SZIuD&oodt@VuFsw>ol%AI-4_C`j!{9HE=Vxjl7=7Mcc^4Q4w zgE>7{f}X6LT!&f8t~7XKG9V%r)jn1uVK#5&3;|C}P?ygf&=qwh!t|bGDo)G3r)T54 z@BcD(|4##|b?FC)2trl=o|b=Hd}*Ywg%VUglMJYpXYMthsp%M_KL0T5C58E!x4fsH z5R{X3wEWkRbY6v7H_QBVgez2E#mt=7(qJ;>=F2pEQ;vzz4NfUSo+BdZj^F1?FT?c} zb3zIZ1_9exmK2hxf4i-XTUN2XU=7Ho;BlQQfP6A#uQHq)ir*8Rm|B77gAGVJhU^^civJrdUfM0Jll58)UsW_ zeiura1&DwDDiLbY(Ada~8r9bi<<0}bJQQYVF;ZMR=Zc^pgywPTk5=+ipLsRc-nqrE zQe)+@+EipGNbgw!VAafOi+H#6d7ZE4e;j3+x!9KOL+|f3Os?##Bo*r2f)2!4(<>e# z^OlNFKJh7_i{8CkrVcG%ql;ULx!{AVDM`LH-Dxnw-P4oCj19kM-a!0C^HgX_g)EyB z8%v^RopHD}&;6?eF;sM*+&x)YStC*Kv|N@MzEpjEZE!W1u)>C$^`XlSt}(dh;E<0< z%MZLVj5$5RH~I!$z|GB#L6lY8VGK&KleJT2uG#AT^u4$ako?Pkoz{BtKjy-_`fWVL$0waKu2{Em^K5?zr!Q$rAW_pi^wl3I*3Vqm zRhPo6xOyOg7NvCKMmYk6Bpjt6rd2hxE>^uSx}raV-Cr?|!*TY6z7(c8JzP~91nABJ z?ZAb zaH+UK`ImyLB3k2%rPbvJ%0@9qI>XOc05a3pl-8a5&J|K0)%SzI5! zH?ehFjyFhM^R-Skb>HMv=b_NBFg*CDJ37;r8fPK^4bhjS2)3wW)K4n+y|+H}qG?^jXnC$6qtH!|<;+*1AKW?}6< zl5@FhskC~uHH^c{9P(Z#bUf@DOB!prH=Z}r5$p>(7{xyL} zc>y}90XQRQ{l>xmYYn00eNjBs-Rfi3P(k_W`YD=QLqXs3)jnVnH<0J+b*iJc#IsZk zJsY|Q=*YjM9RWR|Yir3X&Ljq{tA{2O0Ah#b9+xW_VsxBo)wQ+F!6XJNE~d=Pm{Omk zHfyW&8BVX86 z0r?8OXZdS<_P0qc?2?ia&zu!IUrrjk2+mTM=XQ46_82}mB{7JU4lB_4y^}v~LO;A~ zM_nKs;-_IwCK;R&?^}rNH#hh1Nh;*0wyG+ar1G5;k+~+`n;x4tPdd5En6=(}OmmFMNJznBO z-0tQVdrV>-lY64U3s5j=g;tDF?MJ9Y6FIbYLlCZ_jOdkm6XKXZO>SQ%?Q%~BYtQZW z=f77|)6r#sW-trdKyb%w85A;|GF^KE5hCTK^SKvKQc)_uJz}G?il*4??kSY9vdB=7%PuX%drlP`FU^wT=rx{+P&OdE=PCg? zm+VSokmx;O=KC$VyHF&NHj;!q!YU}dlW1%ASZkv

VTo}oC%eVfh9JI=z%sUVOc?Vg)ANst9Kt~((})Uxu#!Pv#s z80D};bY#b;?U*ssU`WI+<-4eyOzoMg&GujX-$;wqFIaKP@)fyi@=cU)& z`uTOyoA7$`0z3qRc1kctrGo9odik=HE>7KqVg^Sg;i7l>)+rR0^v( zdE!JH6rt^`M7swt@XB^neOLMY_uVsCs>HO7;KbEq(HW2`Yc=vZ9qPT!frZo4={(Zb z%bn0w;jSJU8pi54t$bR{=P+a z_j2UE>j>^ZN<|<2;nqtoO@9Mvc@a z`cmg5PMH!ZZtgac8%6MjEr1r@39&tMT~rd=ad3{<%R>COx`Dx4p_f-aEt4FBKMvf~ z5KQ|Ac;sJ+$&27kU+Aw1Ii#8FzC>ekfaQ4`7aBLwqSxAFrrsgU8e_HA-hVp&MIThA zf4p=DU6Bk2}iKB1;3eqda3w=|CU-w^v>_o5VY^88@%-YKAvM?Jcwf^TG*2??pM_8FMD9E=x#mLsAgyB7@q=}lXyu=uyKWxWbWra3wdNO9n?$j& zmkc7BP!mzW*Kvq|G~ZmIE`?2R{^f^KAZhhCx=cpqtF$pgMlB+yS(q{ywZe<6Pyr+V zjyKEYnxh(y({%gEB6FE`y}p2>|4F`- zwlAP6lALe*uex9V{Z}+E{ydpOgN@Y>Zye`OVMdYjEZDgxt@c0teEljjU?W}5##o=G zdcKqpwXybKixr2i!8_5Ls->JU1zJTrQCZ9y{EAB1HWi2kcBk1F{H~?Oo?*?~tUBQ> z&7JV^{DfXJ&aC`v9obu9(=%flW&B^~9ENgDgTlIP=>Q=iL`Ni7@^-8dn)~VU=*HuH z8SjN>i{%^Ca5XeEw7=ml$}qtNul1Se-NrzIeFmqFyXl9&q?1F%i-w$1N*xyBh3uyz zlp%VeEfK{vgSD_}MWGe%?HxM8@Ak!t=vuF258{RN^1l4MiT>bMa8lkqnL~?@J)7BQ zW<(05gF%=&-86tCd>@?n4PBD(DI{wMe3Q30o5QNdLz@h)aejV#*jJ`;J;b6f#!{?R zMAJ)*v-|@Avo@5iB(`G_10D{IQ!H0Lrvp~B!0I$xrjz7`pSTYe)54)j-b|oK9SaAC4ume9 zPl3b>5|{`92(4K~gNY>9zuFI(DtH>!@xL9dWZphsT~30}f^+(+uepD%`e$ZtQHd7>KQb;t~^{>ZD(3nE5QiNq#^H zA=REWbji_b`jPZ8O_=|*J+06TmdKN;!3QCT{Fj{l&4+g63@8oBqzJZhm`!@ae~FL# zU;aL4C<}HWgCd6(w5KaYJrYHN8QPRV>Q69I6Bf&2f;9?3h18`Fxf9D2zVTDaVY*&f zFVe14_T(Kv6cCTq5Dl%o>u4dN<$a#VWGTGgTKth9hqRdAzqOrtaK6%H_cuN1*6HP zav66ENh=4TRI7SP$%~!dyCyRbc%(|Q>R7;Z^4ghGCDuJT!;HdJuH(`he=J)1i{|2n zKa?P{_ZQDv@;ewSy*tUknDv=+qp?YzD-;pkbET8Z^N$Z|Qfg8wE=e}do9n6gtlblC zdzosolmvO#G@FV6ww)+dPXCJmf}CGe7VM>cAEDAc?zqoeTX8wCyf>Js&Hi0gju~}P zK&o2V!-6t}yjcR5%Tk;()ui_agmDw+cFt^_qH}tdspjvrAgg?<*4lnQBAbM+IQ33H ztlHb%BF-cPkrX>K;Cz6J)K35C9(%#tbRT_N5xykTC-{ zyCLVSDea2w2zp}{=&5*pnNeVvlimR20HXGaC4ELExIRJPCm4^K?;bpE@92Q!rbMV+ zHVdU9x?vdgvKW6180EvecBv4gVW!}g2F@4+iq+YjU>~Mq`#~-Gf>xDTrGS4tNjFT8 z@Fz^*Mdb&y6Fbg4A^E|HwZ~bwd;3#~FO|Zz8F4o6Ui^LtQ$2nMo^S=cT2Iw>HcIx~|UlweX) zLI)(ru6-X5QlDCIDg!z#@f~2;!XyGQ7mZIm1)YE?u!9p}40iC3rRt5xjY!TYZg57y zvC^Rd8zPsqA(Fk;UcByl6}HHt9?_T~H*mbM-J9n4#=4(c&>Kl~dBN>D`{;fR?M%fY zBKEO+%~57E7jOF(h^oQXfI}ZBIl2>3;|U)nDqE!RR{iJBLQ@O;{(hDEDK| zQ@vc$T^EQ!uOMm+nj&}gz`A)Jk>;hU(gH%N2E zuRQK;+=+aarIrCjbV8z|6PP8qNXU%dkM;NdHBR4zylo7o#=b>NUCNg-6akK&bRMKd zH#@2w66L_j&yI=~c5GiL8sS6%ZHBJV&l8HW+qJ$jd8~vc$G?SEMYcH@~fsWZgH!kNow# zpeP9p0qh*5G;n>Sb8O9^$d5iIJt?&CtYB08<-i`T)$t)fdW8}x8H8mqP# zfk1#P_1W#!mY!D^rP2eK^>QD+Fm8@YgS^yq2;hbATUw};n_a#l3n@5TU09)L1+kUS zmo-c5l6+3lyai^Y2sv!0Z5_i^l&qp+IHGUC2_mi4f&Bsx;-yOor36{S%w3-CKLZ%> zc4`Li4s-ZS8Hc2KDLwa#Vx(BOa3w=OTlAftJwvJctS-ZOvySsmwy#oYQOvHXeb6i( z$o3N-C3X+6`+%&N4WG9|ubJeK0{Mz=VRSY0;<%hcAnN`$0~15)(+3%)w+8ZsMv~05 z{2b~|-18(@lzi7gXj6{dU7&qL8%7p5g*d=W{)twemDS#W>MU;oT>aT7D zci}CHv%htzkC=Ykp_-Wp-kN>cp}5-kQvuix?5(ebf}TC!m&XaKEfQ=1;y`k>in)2Z zuGfyE7zpCrfgK4ytCfj*tf*_Q8csJ0c*r%f)W8krOpCMzG%s(*eC{$>-H;yjL`g{r zfr4zkHVWh*_)tgA2IS}OnJf(_WM$87FH3BUetlfb@XOJoDxc&6zsb|pv`2lRk-U?_ zu9HBcHgn%kSTozKH2&gh^c4;c^D`yNbv%J3Oc#X z)@7Zwx_L*d$w-z6?HD{;5xNE zBWsCa!yrQVU2QbMdDT~AX1jjAc>L?2W6zM#YNmaAdoH4$ zIPltQ*#ETx`N*R)O?U;S$JA&vZ)rh>zYH=bt51F)&veK2INe(b_ow@?5e3G zqJY<85^}f|FoWQKFPRL}Y6pG~2;=7hqB4ctU8f-~&t-p&+=n}K;Wo8#e?9@8*Tj6N zN((%ULip5#bnCT>#nQQ4AfTdm9<&vj3{h@xeOFmk0Tc^Bj>lX@?85N?tsj*A=7mHe zbJ83p*^E?fHak}a>sR|Qqagh~7%*9}jS(-e7>|{DEb>@UQPH%8-j*ih%?DF?d_A%` zI4vHolvZ2Tq}2+rZ#12C@kP|iqH`4hpg6s%C^vjsw4ti!_Bhj36fh@h3GVNtUQsWR zd4>{uGmCRL&J?{CANt~Ir<%{kG=rU&Qt`da5-%pfYa&(S z1x4dj4E=1egBg0poSMNjtLoExTJMB+iYz`isCVyOi1jqt+y0(bz_3Z7%U(3S;+la$ zNV#o%j@d!(N{~4foKg7hyZv=<_d1-GO;0Zeeu;6Pf32FKo|0sQqNSB}-6-2=k)jgQ zCSZpv+@~W)im#c(u9kd>L0cHNRPqB~L_;H2k?}%5L83)jAtLH>kmy^yBk2fz->sj) zPS+A8#JjMtusLrcp`}_*s)BO9^4qgJSUwgI3Ff-Q1A8JTCksq_nmrrMq%a}?bZ5bk)^1L^?o)gv9-G~Ub~y8Xm}7D z92_j+jif&n@vE&T9$o@0HUbg}c?PVCmLsP^Wtm5|r;w|2dV9i4?ju_(n=M|I_`38S z$Y+VMz*Q8Em%z3L{<#*2K;KhGA7}D-6ewuWVY5f?zQU9_T6nLU6&oMQpWKKlyCZ<7cWt-niqRe_QlP>F~%*2gH6}EalL{zcf&i` z!=*@^Mq-{pwdmKXHV;hH^>mwcx_ljDv1NmES#JCsT!lvWV;MK~25iEEHx^@mM+^ZO?1^f(9(Fn#3K+MP?e7wMHkE{|rHLXc z@sLgObBRhQ;2#Ob#YpgTaL5U;(p|0p^LRj0i#5@!WP62e<#R%iaogLv_Ce7)vk%Z zXX5$gz?o|dK@1{=Q&#U`g2Hv(%&b8WAnJjwvmKYtLcpvIQO>NF>QxrrmG8 z{&lLLv%S`Kn@w`Xr={??eEWJQR$L4k$66~e+celd2og6i2#9t?qDk{aHt^S;FtI?6 zMtH6#@8SkJbzKAFVYK&nvtjp0hH=^YLWFN1jV}Md*9s}@%J4;zvvs&R*ntEjz2e^7 zg8#TYF*3el1u3QH9-EifcgC4DkSE`Hd~TP1FKX}m?uK6ic9)=+GO4%csA;Qh1Xw~N z@auxJ08))|K^6f^lBk$_c9q#?euwyw(6D}M+`?)o&Lc-~AN(t{)6(f9aaK2jW*3f# zxUPR=G`}B8)x81QCKFJ?`>cwF%!k(pT3cTMZY%@^hS(XcxJjZFE+gs~-Jwtq$L2WG z-W$lhemTjoX&iEYYj z5swsG@4XBJDK6Q?V}A55Umr8r^jn`G$>=|iS1fB??gkz&8eZdxFfD6TnhJ?^XeFY> z;|hAKa=q%er$zlf67B&3T0=!hGHYeAvX5U4J9u8xe%2D<@B zCf+diEt{odRb>CbKrjLrFu%0PFzE#CYozUjSYiM{P@*xwB^Z3*_o z*%Xr&u?_ZGfdL=nS7e&e~)%B7kXBXp^w3-_hRhc_H+kKeO|LLn{x7>CR^usKK)&F%N(TQ%I%Q8r8{WGX^u`*2Pe3uYNM0p5i zkkINE-r&rtNhi{~5S3%!qvK57UccnEx5ddu$E8~?2(lX}iso-gkIN=uj2gpKn~Ymx zqJepF+K0|M6}yz%$VuW~2u8s1ktRF`E(Ks)dHrw)*LY8cArySY{NZ|@uBwY!e4wdk z0_7NF!a42e6+~1s}AKupHa8lS5w|xg4>ocfL!`xorELtJpyu=_AvGLGx`c`g< z3QR{>DErG5avBb_4o(J!}fphXImFPFg<>E#T zy-HD%VsPu_h1UR*oeLV}%7*+b(ppbi0GV=X>nxch8pebiAPfB09TqZgVsFaKNv+AH zR5*l?bxd9!r0qvK)M&U3Zh~nU1&QNOsJ#HT(;e~koFK;uD&d(pD7$&FXwXisS$HlJ zR8h`Z7(zy`?d5%`%H-)H6S2{Rhy93|z{qjdLsDZXZ?oWK_-2ds5wAVb?QhjWi^b!) z!1I~}6w9mAEkJLBw)^(sI8?I`Dw%ofzp@N*)&Lnd{dt@rWNWdE3nX(hSo|@-v`QKu z5@R=yX=|JZXfa&dpS$4Hh*VY(VjlTki?r)A3ZG2R$d&~vApacB)n3CO$H9J&k0-94 z4HB7uS53LOUR0$+|IccBN72XHSc|e-slRFhBygL3EuLXlV_YtQ*!ltGACe z(_rcYcd*LA^y4mBC>&S;Ai@MvyGc7jL3H`kAP}9a_94eFGi~06*^W(#abwL;%aR3| zE}NtT$A9^Do!7PuNlWu(@)Rta&y@=lUHKg4GWJ5F#Lh@9TYFzHwur9Y0j(^O%WJc- zWHvd{4s-#ddJMCfg+*g)ywqxu*Vf){mdejqfFhywUPGvhqrDxEkySV1Q|m9ab0Kd(j; zf%Dj8i*A3nU=BQJfu&hJyNG{=narX6KD~p?SK0W^uq!6^uK43jGyo9ak7D2$cKhQ1*Z31#5_ch z65uiLuLff8BI~zEH*yr)s6+lv_=KH|{ZCi4{zts6Gz=4Lzc#ky`(6Uz4P~^+9fdOJpx_ z{QUS#F9Jz-WJtC|TcJdoGw={u@16C7y}W;3{b*I(-wq!5-p<7TaMgLAp3Jh5mJY3T zge=u}|3Lir_q}B+{+Rsl+iIi3U&^sm8r z;7{sOM~1VqnqF0*O9*#eAWNXPm1LSynu?!PFBX<20tKABqY%6O;}ti@&(BXSGo&VE z`|}q3|J5}ZjXVEcqX9Imtf6DS6&k}iiE4+EJ_iQU{_|$si}qqgCR_YHA^!fVdKMsK9aPb2?%IK1Cov9eoUw~M63=($#y-)XNNm~8U-y9pzhr7Xl2l4#(T`3#dw(5j5PO|Hi z@&Ut7Gt>$4ue13K)b1-;Ht2oi;n=AQuYjTP+-04yDEsDmsBN>Fz_fD>%$hqGpe+;t zwl)uJWMl{6L?79P-++0CtlAn{@*79gzkhs$*RuB=z3r#pX(T*7KyB#S1Ye}x4xwkd zr)t+xQ&Y1YjHll(-+DOq=>=#-gST8=GN3sKl%ZkK&N?I%CjA%dHhy5&S`+MzD~d5u zBvV{(2d_?QHOZs^N(vE@o}EkMkLRI7Zlva*CW2JL}__9LPmZ~$f2P@qLF{}L^NgxiWkRZ|BG zjP!GR(s3YnIV>Nb0z^G?xVF-U)km!alID7x!QpOKrc&BOpvdCqXXka_2-#0ftPj|< z02^KmVsn(r>kSDpm}!kK`Sy{V*I}j=Q4hA-Yv98mwWDNnD$*Vh1gG7Qf1YM(NbC3H zEkxj*ld&P4_YN>gAEeFj%dm}-Wa(f4noq?r}}{(&kP%tD3|k_}DzK$p7SK8jd0>}H{h zCUlYD0l-oe6@+IK)8FEcag~3&0sV}Cd>NpBWCl*aMxe-xR`eJ<(+4I1=$=jUjwi9a)O70{H!>n5+!6;Q;d)i(*1cp3d^)>-35enAX zhH>g(f!EoDgjI0Y(dV^Uec1aRwD*RjX6Ko-WMi+^XKLiFfQ8MaM{Ezb>(%$A4hlwq z<>;WPr4^jHM~LP&yagOqSO5m|Uk%zhf%6Q3l9Q9eBtrFZHnq&#_V)HLo?Sv%glkXP za45nR3;88q7s#(0!o!`p;x7-AjQO}v0yng zDS`#0gHi=)Haa3*il9=CfQU4a{;!Q9BypUXZ)X1T{U2A}7lFg!dG6=FcUf!gy)iMT z62~zXU6v`v7YA}LgpC|FIQda@FBqr7Q!;}u7&&OfXkEGylma64@5^H`sz-&T4_O}UhE^g!{-J$dX865AXhno>ssyt*(W-7V zSNOQ!t9@M8ye33DD!C;L$X9BMVJ#jmJC`RqA7)AjwD@@~3w7i`%_YHXoT|ron3Rwm zPFbm;_p>yxb87$&HvzjMxlej|h6hRUAY;Cs<3W1I^L;q3Ec8%qdN^A#pIf@)wEMOo zL~;25askuk(odn8SUzJ$v|XTHd#e+BeVsR_Rf^z>E$6a9tV%RlJ%n)(vTn!*RiJ}> ztZ_>K8%kRRWN&C^UcT#V1=dJ^^hmBcAo{^UwoKs^AT$cmA%#=9{VwVazMP-bTYJ>z z`EOI2DE||T*^DlH&yrr8~=y+q~+zB zn@O+*&Y$W*F(#cR>g)P0HlPjYF%w%82($pfACwe!hAKLRg?)#P>~GIsKgbdm7H)vd zx{x3c%*9$o+ioU^FCr;u6USnlcoWu3KdW_nkdky?j$#vpM!A?!U_(gMMcJ9n>s(T! z6T^`J8URPiN7k*PDy+bgohi=1q=WnzgS@aNt#HUtBr9}}T4dQ^Sv&>w{DH_Ym@`or zL0(}oJ3j6+xO4VO#PKg*6`C%25?){L5iXjh%K33uAouDwR}ob(t=o3@OQ0lCJ9L>-i=C19^WB0m1{YbUSs5><7)mCjKnHO% zC(m4T8Q@}aON22rD35l@b6T$F_sU}qJ66VRy|9F-)Rp=CC9cF6r~=6AivTvbQyqG8 z0p|JkrjHCQ5H;UT;46H~9KGU5+EnCnhP#7h3;>ac4|SKsq7CFc#DQZU<_}CgUC%<7 z;)ag1fhubZuvZ_|%U2!Pe{dIBS$&KoW9r^5I?`MmROvD@wBDN2{7%u42d>#JCS-=N zkp8b>ue_lN?(c2aJD0KyMtRlw1Wm$tv~zPX|I_OQc? zf?EKV8zBE6SXEOp7oBX0e)9uT`RGQ?p=t{eE50;%VmAglpojJMMkdx)U1~N*H%Ml= z%VMqLK))oJYY)DB=z1m-{PPZEo@EiSFfq4Vl7Lc;4AqV3h*bL&;IRPL$psdDg+vTR zmm}f6-`o*g%yCTY@x<`{Bi0~#*F5FLmU^gnuHG>AWsJgn!#&YP7$Mk5vJY=VNugS-H3BmE04!E6w6dmAcGmk z`RL|Yb1*?%*SW6|0Pas~_2pOyIF^L^2xukn(xYQFz>7ypRKN8hcy@6ttP42PGJC+w z>kWNxggfiHJVxhzn~|YjqlbU~;JxnPzK-$jb<8sG_PQfdK;o>(%o9E@-WFG~cHhH4 zzk_2(QE@JiSRuF($JhIp@}%(+cIPTqd%4>3E+653{?7f)FzG9CpmKoixB^hDk-;t#FmLqYlP3#WxAuauVbTH^VvmUfPZt8*wW+1x z=*U1_0P(Ug&8kZr-`+d7KxifTQZ%dg!h|WKhU`leK){T90s=NdltVWMFfol5fb6yq zXmfNZ{{@NHPTxTK2tE}`pHQi=e|Wy~nARzDM< zqHNkwo7H@f24f%QvZB#R12PKIeGNqPwA&XR1(e`JQ6kQP_gaH(!`M$!Fs{65jI2dA zJLvMHP^?X10->2;n%VMWVy_P&NUoSl z*y3OIY#;~U{pQ(vgveGgfb#bR@)r~oB)xxmwoA;VON_|^Y4$Ty!kLdQi-I`9jiHCQ zqCeDDz`k-=_Z`AENg(4%=pS2xv4t?Hpr{p^-`Dtd`uC=ApIT%sM_Y^>3zl47usBF` zP$CKe0FLUBXD&o+-v8L>kyheqx%(Q}wRJT=VrN_;B6{SV)rjY$nd`0|*};MV;dga) zU4Sd^_)~tB$2KO*BU}@J6aqVzBf-GcH7ye9 z|AWtXg_V-rG1{4*^_l-wsW=ZwdxZ4{i+V(FPU}yS`@;W1Y-;OzEp~kA6+B(TY3%|5 z?dQkHjtG@CTufwzNbag!Hg_HxjNZz@k?_qw9(Yf=$p;=|Q{6|6FFpM|Qh!11ZYl*S zh0Zy3O9UR7$-+Fj33<Yi+u?*`*(i!-$w}i?*W1T^|1f*;g3HN zo#@ce&~+^d{}YGu9|zW!2NYPNH4$HXgepH{>i)yfpz?o!De?SYHJl629xsST8uBp^ zQ%H9nI+Nuasm*VM0KOUJj3hjktTmNHwrP)?X)RDM=+a_8DCsFCzf4aG&z& zLG7vTx@jev)*5veld~G~{iW_6=|0H|)|=>I5R#gbRbko~WZ^AoZ%sku%OX)LfN~2& zOjWXEknU;W&|}zVjkaykW^n|V07iiivi)pEwLPb~{j(L;x4CaTKG^``AA<74a>vi@ zEenSdG0qYvzk`_z#9v)K9%cyD2q`!CQ{LVu79NWIqcbs&l#xT4ObRF=dM+Ti5F^G` zGX25yVjh`@O;dgj8rYNSET$`XoZH&NNjG=%DC~$h7;>9@!7K)o0^0Mjdc&P)v;`$Z z`VC`0N?#zES4PV;{K2e6wcEbDpYCrF<1EDG$sq6VMD=R}7AhB1vJD`zMxejO#IiEP zQM5o=GQ%4%8AWTrE}M{E%d-g383hgjHw^;nlmo9$5a&5gG5)QK?NkMlL%S9DYRkn5N3x+>421enRf$N_1TKPtxb$<)mdWmmlKA^ZtRegSD)7ul8W z>I-)158kJLwh9Q08)@uDtuhGka4+$*Uyf+m)WJt470zRbo{s9 zfByaJ+V2JI#V)PrESPk{YhIR?rm4x9CCVv(gnN2AeCM9BsLQ(epx00aAIMMV;|k5C!ZF%W_`j-F%nP+yN=K>0E-2{JRqmU z))cKaccYX5ql*imHIKo$5_Mv>n&W_vg62?y!NaduI`TfN3)5X7OkGX+q75K_BZzNS zrN3U<0)eFhbgYy$fLKr<*aLEdptLk;X6rlOhykgnK!A3X4w7&S*sCyQqxN}|#c&Q8 zIe6PE4XL-fP~;GR(qggZ{<{DJ&};zUbj;jO2cJV!H0V}GOIbLHeQ0bFLFy%U54SUN zkhBtRNAg+Vz+|SZfdDmmr4JeqrvNab0SXX4sprs#$0BK41f7a_{W&2eEiGS$#+?Iu zDa9P@uSg$FHPJ=d2o{5q?QMJOw{iaz9S&I~=}dE=`?`XAxZ9mV8XFQpAyW{8LTL5f zmZhz<4#=u_ci*}?N7T~cx|P|xYM-Opq`!fn$Zn;)4~@EmJanjK%1M@(JIOrheOUU) zxD{FEKqg4osVGcwNH>0NR6oLBMO784B73-|k()93NZUE4|aN@d!; zmF*|C7eZlz-L-zPgzc&_8M#EuXPqO%bt8GOYP+Y^uFOfOw)E9GRLMmY4@zw!$yVp6 zmT4XRx*bTDuT(p68PM?rLx~?$b+XiFD5_nGf8N<7fZ3F4Q=X3=S-WD{VdZ>@H7w-M zce|ZrWn}@Tv7q|{=rm)YJJQyaI9(>bJ*qzk%($&0O2-CI#4|DD)b*&S%HN38FR%*KlDk*&)2Elg zOXx$J-MF1IAIbMcGETG~gn&l8M*UT2baY`B#7dN&H@0!^Fnf1by7$g*-X`RJxg@z` zP?it8LcLt|3Nms}uVNVJ?$!!{Q%f9-&rA2e>TERBg;cY~wnW~0{b9?5D${V(u!_Rz z30Rr!y1koM!0Bpy@XzZf0&o@{_cIAEw=UW|zY$}C^042804?MVu2#DYzbunQeI8Ft?2a3s#I+gvj{*XD4z}>pT zr_>^LU!rUtn;7m@o-hkPyK;?YFo5Xe0nIx~-o{<$_Fl|A7Od+pPh5+`vcq3e zoVH-I?F$mdk`B)X5Ve<{T73DN!%lBHV2DOVbmZlrCmThmr|Z4n6ze~$8eUciQoR&x zNg_k&+(V<7o`OQJj~~ZecgNm3$g+J?BTiw)t-TLe46JBCR~_&>d*~!>Kvj?`ZS~!k z?X+`AeyL@CdozJ)C>1zgP!8PB*C{=%7PloVD(dOw#vlL->Zb~l&TGV*s`}$IL15(F zY_gY~(nn~)ldhi{+3u`mbKxkoglGAaS0Uk{7z5Y?yTSkHn;d|=&2;EWd-tMrZzF%& z`Axu!dV_6!v$YbcVr$$3H*+4p_EYo78b5Ix zIuB^}@z&{H^}gGe!KsLaN1nxT^z!NiVCCs$4P3PeG+Q>{&~5Q_xRe9>RR|wuXh244 zVj*kXMmy;+o)L9AW4%u4dQ2*5!iBUflY%trPMOr)IjE6fu0d=gkZ*-2BPk{ zOcD~*O0t;4!@Gezw*&NScWud(E6%UG?)b5E9qW5cUhY|y06|?8j1t~PddVw? zs#b#{wI|e2toFzXob|%_>F*yEpFoA$HWmqZNmoabz1J|MB;)O*vK*)Y3kjXW&{zWc z@Z#K$bsvrc)pd{QEl8mplx5%7=p}UM^;1&dlJXpqbKrZGTo`$nZxVdPbEvmY2Boih zU1*UXq;vOBrF9-B}Fpp;eV=rB_>SNQ7>2XaNrQzCLT_ zg(Xs>g+(9R4$*eZ*b?{*BIB*mLxxYTZmb0PI2sI~q=kgT=vLwXVCH&W=!Ew|4xpBN zz}F%n2k3PtTV{lm-}w*@KnfZF_-@sSP0KJuWeaH_`}_MH!Qtf-HAi(3z>c#45XXR( zY=kiUh{U1SmwkME@_j^jx|#~|0N;C&L=}f>fw1vTGG`dhOkRLoc=Dk(A1miBUvFtA zhiNA^otJ0{VYHvnnu1*p`L6-u8QMS#IXPo-VhIrL%Quo%c4%v*8IGjB?L@f1g$cC| z^n6PzDiR&iBa^aZz=X#{+~t??LTD-3LV zwO*7lMM*exbTIMEdMAytVwblb-)RJvWaJgF*HaHSOi~n~TzUbm$zr z?!S2T(m7`z?X&-3*t@K>l)+Ss)KpCv$8ev#YiVuG+wL1t)j9r6jt$EvVq=aRdnaJJ zSC{{1zS`pkuS$*;MdT4HJ}w3aN15P<722%z{X{@-&5vLBDs1(g= zu{dgs94Liy)y8*4m9CSMlf8*sqyGQ$0~mj^fA7Klzv@wi@d#vOUf;ROJuNEStZ!?Q ziPiOVKj$I&=88J^|H7wuyOEo(ZY1s(T?|+6F{|9C_SX@a>*)slS~vf@|8HbP@Xg)4 z^YYHVEsSUy8995`wlZu&K6Nai!r&}fgz$uv@n^(w@m~{h%B35g{`53(&a@Nwk1RoJ z_C%X9dW?$Z{V@@jKl7!pT5_jzaKDRD;-+gm zb%DMu_WqE2jGNrmRlnaF>(YvK*?H*;U+vPd|7y>EHFdVd434V{C+0Gv?{_BQ`Ty6> z4UYhad45Yv3OEau2cLP?eiQAo3gds~D|01yaQtV#fa92*KAJcD)vXv$zduphy7^0A z*)@K9%vD3@3Xw{t|8TFXtL{4tsiY!t#Bj~SB`Cm)LxGcHlKi4WPxlhQiK%vvJA=aJ zPCG&R2ZS{mln8b<%9mH9y^9}2i3+a(H#4EeI*&+K6m=XS(G4odgthcQjuOL6g))?> z!;pTAM3*BiIjT3-Ya4rvGoWW0XC303zBVftUlOT6D#W8#`-)d1hC%uy3MUcl;qU6A zA#^ctx#Hd#GiFfExEB2~2vS5jm+2urI9Cs;xg6 zPG#t$F*n)|P()SIy=vm2n6kME>iJFldDy~LCl%8|{#B^oAH_&}Hgxok!V5YLN zGT~32_XC;M>O+mGfkS)|p3IBqXRom9wnMdfto#xrNF+Z**M%J5pH&e?h-nFa z?`rb3G6do$4>XGv*XGR0$!UXV-4Fs}0(l6mC($2jSM#Y%aJIs_Wx)Z?4$Zf#a{+9) zF>spfSiN7@QehkJxi+x19qOzr3?}A1=S_%oo<1$*%E4qZcl@>lHqKG1el$<0G-R@{ zshVI12+i5Px86-rS{&zj)d^Q$f6JpN#|s9yNyUOQQ5B~&1nHC$$);cn?r6M!M`~dt z{5B7N*xEol22aonDO(LLp%2!>j{q5HeRzAK%aANk`6wMc0zk^5odE{`4`|gNM zJ#&H+7&)lo3Sdw^X9H=asD)V*5DF7kL&l_8yjtVuc+Y-iufd`2DiUqbF{PZ9v?9ZR zOq(d1Z~mr!8#W86c9Bru(s(O6DhY%!m4?2JzYMbsFH*@bJ&e^yd>NF&0n%^=V9*2t z04S!M4)UC2z!wDWLra$pK+*;&??kM_FnO1TSw6`^|BO*iBr_`ao?6Ue$P82#76S|eDtc$mfyKZ} zLX)_FpX33nQ`3D+ntPzm1_eGM1uZy{axjC-PAyWJRvV=X!$qJI0(gy!e@7*s4n$c@ z2fa~<(&fsCa?b%BU^FCsK0Z?-MB~YkNYYd@SzrOYA|nF+^Vnrkzu%5$@!PYCE4F(@ zn#PPLT^T2LdNBy}BMlL48DJp$N&aLGVL6Vi6P2vL2I16L;dGTu0T`~OYf-LAu8|Vw zGsyC^RzFV$so)-&5^Pox)fkljZ=;i)3{+%zq1$w6C75s;8%?Yg#fSDlE)klBOK^&QTR)q~ry7!tTM)JfMKy zC@#MVSPUmSLTSUm@d&AJD@`T@G#23+4tLOSfXX^;a9KBjrI9beuLl_U+piq$5CYln z&9(wE)Fi{kD=%n6lvTO!EMKmc4@EAl2HXUXmVF^iFbmYG)RaXBHZ%v;fDmf9ewKfm zg@pyxkHg!FkK(kbBbOj4+R;A}%CP6Es}T(Op-Y0nm62XLz9{WqM|~!Mk~ESgneW?U zFGe)Cr_aGLkum&Cj26$d?kW%)6%vYCRMpKlHH(GLjt!5ezydLPD^88n`Ygs)2!{uL zPXu^Z5T;t7cxYI#lH9k0W4D$dCO%V9$#6M_*o4$0~s z$K6%d>5v&(fX#{}76@Wz%h+0m{fiUxJ4u8OK9WbTe<#tERG1C+%NnDMr!E{PRLrlN zx&`7IV3CGdh=+nT?$|ZvdeH8`u+M>BfwBs8S2AdBjpv@8-d(&sNR!2jGIQ$rLhNX= zN_$1L9(e$;KR?r-`}P1p4^+J`rN%D=71Aav7HNdTii0kX>*MZJPOCAM@9nw}*XRsk z@2LBNjxJj)t-}>AXMIo>A+(lvUYtw;=uedMww_seuw|nyV&E^adcXEffN^{;CXe#C zUD+!xG*Fc^F@^+e!sH@^Q6tn0?<5c~*ucGgK4Qn!OzV1AF+<>xIoJojB=<%Y!K~4a zBjoD|<)Xh$np`S7Cx9FdQGp-Q=W{;^E+;i7>0s@%I-Pmpb1i9@nxyqFM zttIFzH%5yR#7M1th^Z)!(z#o_nLs;8k;%@&L;4e|MMxubjMwID(>+@J7Iwl7ld@M* z&_T*)oY0%LO6|m}l5fYsln0=?7RyvxUfzV>A=zkk<`3?~!@uh|r5V)wB^F{%JP?`H zdb->?$+kJ8VW(&{FIm_a)FO3y_L{T~l~+`-eaxapmz#8=#QI=~M~~GRo~Ud*Zrzrl zUm{b{UM(UAwGC*-O*CK3L^FHMKmwC<~v#R*a3XDp*C8&iO zEy~K0Hd7)5bapo~=&_Gu7gFTcm^(h2tKI;QzDgkR6_}rVaJqWBlSP%xTwYv_vUGDe zI*H@`lh34Kn?AssF9I}1@yzBJ;F-^tZb#1ckNg~vs25+$zl#{M^(Xnx$r_pTHyHFI zWbwaS8vDE32Mp%foNq7*?}HX#^vN9iA|CT4tmex<`%Qiug0^crSD|s%t@HUPsvkpC zKVZ%l8=sCdL>wV`J7%E%Jt*&k1sZ=O z9RIi2i1DlYuRPxWhIdg>*X58S5eJ)SavKEvY!rp9*5jvQs?nMYRAJ(LR3bdx-6L@&lfG927dP5h3U1CEowBH}6LFff zA5p19A_J5$FiLL9Z--KZs*0Mn>-8%Umx$R#%DA9||MA zZ*Ux&VTzm|;yPy*kN|OtlPLfoK?GHz#`KhHc_muivR`8|5Zhx05{u? zu_4yTR=?q0ycL zZRrb6ulEza1O$z`2L*v7rmWwmXA&-uwi|Ud!qO22iAq{=QisKvlyssD*9aLP#eCa_ zI8FVxY04~(PjDH0-!b~v!6c`&$A)6@NTuX_dOAmlrWrf5r%${2c-$drQ&TDx$L@>4 z)*+=3zn((OULzT42hI7uQ$+14u0w!r4FQY0fXHZ!j|Gv#4j3)pg$9F3ktX0&3DQ#n zsvJ-mwD7-403X516t@vLMYYDvP9SSd6gQII=rw|WYCFanRc8?PMZy#+D?_EVTSIDv zQyyR9=ozCjbL|87z71kumasD&X1fe?lj4EUP3j{0XrCZtG3r#xr;X=FqAcHUeZJx- zuQ(;87>`@DaA6T(z@$c{B*F#En{l!mt*e_*z_w%HT^pg7#V$3wlWk3?unEfA>GUpL zD3-oqJDLUeb+Oq(0ew5tIz6HT6B0y$Pij&!TZ5XyN*rSw=&U^YWwN16@oZ*cH}zA9qtA+Qt3(%8^5O?h%EK?kR^eicnhXhoJqoMk;HD$UsMtik2>iv=881 z!seQg&*i&dwGDxbHu8-^dQ8%pdHgQOjQJ>Opeq8(p%UhD5#JF2MNGBhtDzSrDGO34 zKcri1Bbfn>&|!$N`Ke|PINSDnml-B?X-?FjiqbnOZzHS>)oQKK#&py7m@HuTBz|f} zo=XQrUDrOSG(CzMxB*Fl`Fi_9kBi8sCk*8?lb{qO3t8g%kPR~6Ha3eaJJ&tE=+qW#wKMdi0kc@AIEMGwo}oKN6NFc>J!iQDt7d=?UGtyZcmk9&L@+ z-JELr?1#-JH*{BQ4zq}Vsq|v<^?wC!n783yzy0AfohfJ7)VuZagM6_%{k5{!m!`PjV_UFI~8>TxS@y z=xg}+EZ%l0v+nz60jQ{3a_M~Vb@6BwU%9NUthhb*zSe=vy}wbwCG`IN`xm8^WzP@o zI2W9@_zuG5tw765F`brK3=yy&lj~-2vC`c6^LfO@EBg*i7qxx06J4X6P=lO|Qu6#a z-_txJMytVlw9E_X?zXk4P0_$4plwWMsEs`unFIXU4TLY5nvuIMmCH-OkQwba|zvDnU1Ip6tdCHQ|Fx5vs&F4>{7X;g|tD~diaq>?%P)`O+Mf}m^jQW6M8fQF0r5PxWyqBqb={&ZN zuIU|Y9AW3dBQt*bWdqQ|vfsa*uA;3SfuTzvkQ9MF6(da#L9p}+vBX(x8yhYr-3Th8 zHS41x-ay>x=E;Qv%o*$bX-f>FPXZ67Y7O+ZtDl;=SlIZ<&pI%jBA$=L&SDY}LsgL% zlDafy8A$eW^Yg#ceCeKHb>kc72ag&$j?9@o`v#!5R)~y1fO#bSc>^;wmD*aT4RSPq zDN#a8{9p67D`D6hCNg@kSl3ai@-3eR-`lTJEUHCs1Rufz#?c+1nj;k5O>!SUE(bpn ziO7S@9VVuf!}^t7-#opsq2U$qUu9oW3t`9i2bg6p{as*gp7={0Exjf?1^Z49Lb4}d z2|cb{i9|yY6~w4f2cuDRAG&^arm|8)-u7VU%OwdGTA0xrhL-TRZ{Kbyx^ar08+yr0 zDuh=^Lv`H zhOV`aUGg}bh(9b?qZ>`T=gl)M6xWnP#8<%2EK{H2xzJg1Zu5uJ$HoMj=IemxjX_yU zI1k>zF%pJ(X%*9CS=Y>{@l_;0apB?eZwnbjd*x?wqu&4=A8rASZ;2ZLNuRW1$LAkm63Hm zB*OFCDr2{T8hZlVw8`<9p`jrrQ~MXeEL=mMSV1~&ofpc#LJ)(qQfWfr4xA&1cny_~5+RJF4 zn6bbZb@_%3@6qo(&ZuxnP*6}Pd@pPH`gY}tS#rxYF3jocRC<$hCJ%t`QoLUlT&P4as^RdMB`+@G=>4}Hib8qz^;Zsdw4rS zfybRMgiDXKATYLFnJHJ6ZXtd2ow<%pfymxov~pQzWB%BMZPxwq1NZE-T zt_WmqV^<$T5ZKxCYD2im{sJrK8&IVV0VF^8IQ3VFs8IR9*3-83&82;z@82ImZmtL| z!cFQYu|sMs6K(D-3w4m9aS^n) z5ZYwrbQIL1-2FOg9KIVHSA(3;-QPb7KGY1U4Te0$;#AzgAv!ViqZC{XMN?p_qL3|! z;&{>A$La(Nx4L$9l-Q~6-1$5r7%xr+@IevZ&~R@$I z+OJW{Ke?`H&^ev4xtseOn27yb(3v+@1;G_g_=yJ03|SV>uxqc?83AkGU8<~Tf~=6j zu{leZMk7jW0nsBulZgvdKJ5#Jyx+QA{9fDDwGW>7@y8$Gu8k=>RZ&$9f~eZ0==$bl zI6}Yb(x7FT%B*f2A4HfM$Qo7Pi@2C1p8nkvfTmH*%a{A4ph$r$@YuJ1e-CP))-)8# zieFS)IZfwt`hsHZ;7M zq8shpePd}e#4NV`D;8Ijt}O2PH93WW0YE#*x>NMksea!X^hp?eOMl_WpkgZW$j1Uv zr`-Jf7IjWLED*T_tEyg5}&am5r>c6qs6YIvT>zV4ojxCU8RKz1MC=x2&*f z)23}I?;2`%(W4u~o^}rih{g}LA4m2>!WyBc}4U-CNUDKl$G~c-?dMS##xRFcTMXj2OL}@G*Y(+f0D#rfa z(+)gYvREW{Yecg=UOq znc;dFTR=A^pnXC02H`oBMeE7TSS+Ce-=v1nFyHq%r^+gV^qe}c zd5VFooSfVf)bCtH3uoFXdX60@5Rbc8d26A!@;Nm8VfB<~O(OYOG?0^9+q3L0D*s*s04Y+st5(pA# z-W0Y@2oTa_Xl1%FO(siwhth+OmRmrzt_fMFgmuH8HUk_1g~7-n0p}@LCaw>R79`tz z)xL9J2$(={oO$J1E(XIG7R5){gk|^AOt^lb@94?yxrD$RQ9Ld)P}j|s^FH3v4hk$2 z;24^z4)LPqPp?w=h~{=kH;bqs>|*#YlQ@nEJ@Ed;|4e4Z!KIfFwU!}Nibm0xwuXj= zCZ;bTgrHCyXsZTt6U?fWE%1H8+1Ya?>6<4Ji)5U&VN4%$bsr}?%%ZJ&BlzZ-Gt)2! zJ1i_L_qCFVNhkugvSd3mcCH9)KXPqV_~YBQ@dD|CN-RoLm}Oqit&n#ht1uWU@Pmul z9_G*JvTH2~zHn19APOAwCZuX*7_h>3A?pU}uFSH|FK&8?$K-7i(xDPaQJ4TfznYqw z=b<+>3sNrEBf#nGb8M-KZ>7;EFpcywhzx3>4^zK zg?ndDtaE&SD^U4Db89hXt~+7=NC}C(aHfpWv3{Y1Z3t#+#vORE$p)R64@@==>2O!E z(N-pWqA0{nYsG>A5n(J06r==eQeX>YdIlLbJRG{&q;+W;JATUUo9%C#-a!wSnMlkQi5*B#qH0U ziQs_=y`d_cCl?cz1uN!J$X3P^I5@p|WVn32iS0;9XJC)8Z^o@=H%r((#b zFsfE>xg-cmN$HT0hciaB?SC;XCT1<|U*sYZ?I~T1&4<&PsfPlC&W^&oJz8nEKb%6? zYVwLo%F0*tmWOR0bKj0TcANu6B8{>|9b)U>UT1|B>dl=uFAN#5$ncq8_uHfJ7|jTF zAZb0>fB+Ed@5!Ke-0$@A#fqC9bVEe}CtNSuxj+4+5*8a6Rtr$t?{W~Mj6xM~t`bMy zUd08-_IDKQjS)N+0=36BRyP7gs1{`v{Jp(HFG%;xA?RMXR{u{obu~2!uOt~Hif)K@ zwc+%NFQE7^XS1KI7fk>Nihkd z8BV$smey|&ufe$VAuHe=8A;Zi6@Y6tLQ$g*uZ8H`tsRa3?iya+(g1{+ z7jGMAXm~?bDa6!kZ!p4yjbG3Xk)KI`+PNiwq^eAi2d-JOrWk7KrUqDs5{NffGncVF z7$yh>k#(wV+_+J0$vT&AV7q^uI}DZ8^TD+#)lMz84uNc6{-lN<30^|wy-eJpHcjrr zyu1>C_(2GFT^9&t=5TV-+-RTRj3X^bmS+u|)l#ZI7u>R5`iy=b!fn^#?zzrmT7X63 zN^|GV?S~}yDo(gjA4W(^feV5$@)5VW=_Zzd2#E4?U_OVkH~)bZgQ8THNoHf+?9CGX ztmQJ%w$C%Z0w#CLZv|^P=hDq9d~WOQr-1g%*|R;UDPFyP{rW&FxNZqp4d7w#;^m#U z99bfS29I|j5{ExuO0G2-Y*A^fks`Js1$SigT}FCcPC4#evi{HxSVRw%A@*bpwd7&~ z-Y7+x`E^KYwxC*i7jOV7BgSv8!NaA-Wv((`9e_5t8Qk9q^wyZlr<1G#OR*Cuoz0KS z4g_631~797IYAjE_`ud)K~zKJQn}?sT=FP#AGe{E?1*sW__yli=yX z3nLA&eK1386@b$e)Q=h)($*fz(?Dm=y^s98?SD{vBRvGwK3imHc?p zpJ6Q97ON0)I4JGE$RPZXAcU8X&i3FBgu)AvPdvXwDhw0{{r;?^HZ+xtckHvCfm4Pi z1L%^RG`5c zy!6jZN_G(y$y%0N9z}*EzvX}31-NDxUh7wMG#A)mSUZSW)lPFW-@RcTIl0VV`DG&Dr_cy)cn zvEjj=hIniHAYhoSP@C-p>VSl`rGc!}56w@b1(9UZU{nWSV8VbH0MdrR;G@npAg#ZJ z2T^)~zf^U~f)9Qt9z+t4kdShF`bZj{HHzVlnt{ibqUOBpraY%jhbHAeKzNG4enufm z%|QSQ;;#r&OsgT+I)Y9Jf`M#25aEFtybea5TmVYi;CRuOK(2U;YBkDB5w6TdnuQmt zpo4Na?2IvgZ3u*A(I7#TFaRe&W#`VTNMAzmXhtz|I1_}Pr6~}A7~foI`f)*z_Ot{+ ziK_;>u?S_#z@>x9(jb(lZ3OHs-&Z&+FpwdIn)-go{38&stq~AVoH}(X1yP672Xu5G zdOdUcw4fERaGzVx!|x$%5C{pr`JCq=gPYq72#o{C|DHZ}0EIIYW+1vGrIGzCKT{d28`X`wep7mE7~zjoZs)|)4sD4a8jp3YD;xuGzCn6VZG0a zt@(iW{3fXSR<9@~G$iD}aNW-O)vG%@b}{lmTWDfD+Qqd(LUaY;2yEaY$a4Tt3nuJ} zcf{@vxnrbFuh7PlZl5c*eTrgD*=dYC+;{)Vw|!_(+x z=sTc+B876C4B{N%YN@V#;rc@n#CJvy8C?wV&1>%+DENL{Px0Yjn?_a&{QY}=4=2`k zefX&wqM5(FZqK9t^RmAz$^Ti9zbpru$$f5zz;Ay1*ua9Lx=9Uq47CN2cb^A!y%}MP zF~O^W`T09ADs}<-r6RAf1^k)UcBmC_E-SBb?fkyX*LCT7XxAJG|8|2?E5osd)4!hM z`BYpP0ozv)Qj0F-e9fg2aNXs+D>0lKy!JJOpVV^SRB5&=*J?mJ;4J!~57O%!a*iEq z;k-Q4<5K~s8iMaKgm3#Y5fYXlsfE`71AwM3CcMjx_8nAJ^?-6a3f{P4_se_&44Q$| zmq6W7n&xyg_Ac`dCO^4fx^xZZG#s@3`1Sq)y6EFaI7lq6l`DO2!&{*3eWDgM=mk7= zj!OY9{u~4j`!kIKVVasjaXmrnk|?kb-p}N>?1||B6AXO+!y7%1ODY(9&tu8v;6qsO zP-^U7>zf|3yG&pSw;?l!S2H~%wi=nd8^faJPSLB660u|12R}6d-0&-wkZbj8RU1$q z;pi2z9o`FkmTf1DOe^~5??kmx4?4Y=61b3TOSxxbj{}_jO^97Ipu4$P*It3_rW^4( z(MK!_k)VB+QP$M-iG92WyzCPcC53~pGlrJ|HK7D8fv|n2{){D(PqkBYVK6`}5t2o_;aFc!2aEojYSyRrb8j49GZN^yG7KeZK-h+<%?jo|O# z@eplN3PU+MaLZC~ZAPhJkE2sM&wylT=yAA`l9HkgghUlN8cGW_fx6~3hhG>2`+$E7 zAAI?1?A@Xzg@mkA)`jd*VI@%&w8y|=h(OJBQJM*-Rs=Oi5a7pGpbSM;Zv80l-@ur7 zQyC96SNG>+;@4SW(T+9PcF)kniCABpAa|rIC=d+2eti?QIUsz1pk*jVP7`cN`xOCK z8DUNc$q`%CXtdKg0qMM*h3+KLgucK*Cm^hfM`j$U5xZr`iaNV1LpL|ezXMKC)=69Xh1GX9A^pe7d#txq`XM~ z5!Y%c^*BkQfMlVFsk=?kbEex$f;?nn5dUolofeH^ScRUpYIV>(m?~WX zl06z}1CxbrNh@$?e(MfyvP$jo#LIiaGL`BjX@q8OYmX_Kl_f*KV~4OJ9I!EHq7ul_ zt~637nn9V}2($xR+TgP3<5+N#{F2xE&6EY?Xh&aX74|TN4O~pXC&CWl zRxZeeFiAYY!d3Ufb#wgr-A4&Fz8<}9Iy2lV&rQ>jzCZSG;xd5=Si{5Pme*K}4j_J# zS8=k4=)u?6tp%1Al`)%rjsR zPp&eTn1gRsU5``4qAUm1+UL&IzgA}Gv!PHCpKT2#kTO#Va1_P2yjq-HE%)O)98w9y zV^FTU#VS7Yh$Qxk4CZ+Dpw0(4@D2)83ADa^IRv#=#*jl$yoIxLT)pvWx=T7E6j;O- zB;}(Yzn=eO<&QZ`-Eq$2zM|;kygWA*o0x+Z_8G{+8D(k18KU3R4I^=IXHb5O&{jd$ z7bnv9QW>$|$PnXAO18jL872m1+Do!*(LAdeIZ*|8`@o0@UV=QKxIj(<=tT+m0EWRb zTfb?_5WNCj+`oA(Slm<`OV`?+zwz|Hd-JT?$Jfnl($_;TR88G`RNv}nunFg0NxR@!rH|B!XZ_6?wmQL*^{uZt~gga5$<{7#0i!{&(T!f z;PmU)@AsvfpjrkfgcAJ7W0dp6R-2+mmlcSxdBN!Z(Kj+O10P|EyTd5eX-1WoFaJb( z2c(A;Kv!wx{7K1o$8vMGIe`&j1!Bhi!&e`>7aq$Vcc2_@6|JhsyBPxyEBE}y%t6*f z!WB0cly8yD!Q4Co+ww)jS12-8H82Qz`SN9H>e0ioT7W2VTFIlquP~S-oPa8TL0G}P zv+F@^byE+!l99o}kyX;twXdVfKCtQG>%=na`+&-gChf2<@Nk)YVcZ+0L^Vk6$>6x5 zJ}NHK)r~?iH(}i4d;i?{F1tl(Av^aT)C)omvfr-c^i?02(SXg5ECfc8 zSd|-z?|66=eT12;ZWQ_I5Hf^S3_?Hl5FBO5QOhAJ4uZ~!Xwk0uNAze0sW@U&h!&1M40}KV9bxUTH^M=0Hp$;Ntxkw!gr=m_yb3M<=|V7!f&-Gx zZV-H{*00y1_yD0JNlo0{=K!Hoz;D@k&PveF`LWMYTK9A(~o)Eh6e`_pQbMu zD^z%Ek@!(`h30cb5bYMcS!n}N=-g}_w=U&_32xt_r|Yu?3@4pPLCS}CtVG#hBipS zZ{6Nc$;`*K(K~zR&bk?k);&4r%e9iT{@jCK_mBSMtp&)7Yp6o34VCiWE$?G{rMSfH zch9k{&7tRIRieFxexnQ-gV2ZX8y`Z{^R8|E)#ehHo3W2g>l->c1Y0VF#EqF_kK(P} zs#U9gxnWpfF`2{X7d=(G*50ybRWZg|TJua=%`)u1)zmVq4e+iguZrnUXma{?WW*Lf zvM|fLZegJj7s+=vA>N=UA%VB5+;emFhu^~w^UEdGOwGn$5a;Yt!0S`)>1|?rEus*-`q}E2$D?+i)8G~Wps>HG0j>i2j>t{nP zXf%GnW}F$bP8()7^$yq}bYCoPo&EaL88Wv0luh~EIy0M_iV=+aI>ie3$FB0&7B*rO zXQ6L=Vu*tt&CWl1=&OCJ1q3Qk3KVsGO1Q9H3m+@+%o%Gwjn24}vt$$rdpS6L>}{T0 zhMI%HM2{0e2OsXJF&x70HzcyEyLL@53UtjG-hrKR#%4|NWeL367J2tSe9EtC_Xyi2 zD`c}EHo@S9sn)!xPoE6iqFE_f|HhA`bW@{$_s+W(b%(JwMLUpq5m;xBysb#IY?`S0>0oIF^Gyuw0Wf@Tx130G)X-nm_M ztL%!;z452p8oRq>v~8@P^$lGp-*olrm;)0(t&R0ebIQ7s7}4(}emUu!#y@^+ILJVF zbjU)pB_g)UZ(2dSON#$%Z;dX87$d{B^~>z@KEF5x&h&FWH^(3R&))v~q88ZCJ!)89 zYc1Vw?yVujf6K^laY53)f83*=O{|e&LE#T$e{RS^@>HI<*im{uh6M+W3iJD`kJNa_ z^9i+D)%$&8wP61itc>H7+eU^%7Ecw8%OwpzZj-Cce?q+`FJxJIvOxRMrz%>-Tc`nJpyH3bIP0M!$IV&^0@@nu40W=*~7GFJC4C7t6EGQ-ow zTUyYsxEOe-lzn&p+H6%;#eAPK_91zT1+~*R(a{)tNo6Wg<4~zv&&4S z{R_7L<;E7t8r80q@36_v$3=Ejt?%-@J-*VPz5G^RIyK|#Ss^yz3p|g%t(>7w-N$AB z_;8Zb{&Ml&f4|g!cm4i;0a`)<(e{3K%~!H>^Z)+l@gipYHuI0PohbeEWU-TU%+!|j zwM}>`#hlRo>3=ie#s+7)G_ z=cod#(Qy9QftZk(WK1nq8q)$Cm<+f;9XQT{F=N3Yyz0Uy2Py&lj#VA+R@f#o!N-pu z{FS_rTF2fjR8hFMH};cjyIS2@rse(R#*qyx{F!e-RBUnqTO?GkEWN! zUB{!da-UtZh^(G{#Qe7^^_ce3eLDiqt_?c5`}sN?CbsUfw^l|mn+xXpKJD5qu1e_RHat8wdQS?ig*0R z;+(dr4l`6Ns=}V$b80X>S{`a=jyKuN&P{B;80dE4wv|tV_3F#90SeMv*y8JwbmH4= zs?!n|Z+{cD*wkEohvv2(=Z=TjjUCxSw}QjYRs4D^{d)h7ApXuH6leVejjGoF) z8+6WT471bm?~XM~QEMHLJQJi;mmM9e* z-q0h_damK8X}v$~PqtI?ND5G>&Gb!|^|vXJ)eTR(!qw?l!QJrFnZ-3*hi7{%Ydugq zNzqaiRqt2#+BY`|&au3kKhzMtu5V#m5ug6zH*s@9W5OP{#VnQ$>r`8M_g>-t zCxQ*t8q3vBB`Bm@9M$d!u2sC!w%&Drb|pJ=!0~x}*yW#cTH~X5-a9>;JHPeyRvVq3 zs;H{-&2IT^VP+9?-Y=TwTo9JabGTw==Td1kC*P~=qR`{#uW9x=ZMa+!lH{n91>Hg) z@A^JxD`(GdGd-JzO8X^GRz^Ly`xXzrZi|$B}vy)5vz1tU+BWB?Obb%rdNr2;r>-`lYXLOFt~V` z)1@V{g6x~? zbm7`XJMV!s&-RT?f(;7Iug9Z}TxytJJ zQmfB2ym=W!C_qosIj~SEf?CkxP^{%z{yFT9q?BRwQ0)r2J zaHo?brg@;ow^t@^arW#lUQF2@iceH+$#}&}k`zx}LVDT8{`WJxOU7%wNG&8U9SXMY zwU?;v*=a1tKh7|dWWNrb8I-bKowKjeaI9+UqYZTp_Q^~SkmJUEBb|J6msfN8aM;bv z;Uayi41Z5e(U^RR|MERSN^A58O-Zo9Lg`#=ic5_32c1$U9Z9n8ca(|`3oog2F@9?! z%4rHA@0TA$e=~h|j`>-NNBGNTuN)OIt&ClvkWgkN3{F2mfR2c*cUcq#4l>G9#{ z>A$@wb4#hLlJrZ>=th|4tbn5P3CU6#S0dwAK^t-(_l;n?OhZ@B#bbGHSM-QDZSamVDO(rp= zuT+^(_g=;Jm4iaaH|eK-2_xifRlLN{C%0}S%rBM-sEsR5 zDTY{8ddB2=A&nMRu$)336y9Gapd zYH$3PBgXl1F@cZ#TfO3#Yb0%kBj8}OMETM4=PD5(8fDH z4Npufg*Ygr@HBeq6nirK+nM$=?Y~`BO%#Qv7`8aIt=j59LR7K%zJ{%=%Cke?|MAN& zyS|Yc{85ABm!G~r@Q?i&zY_5AJ32lsoDt=#fBW0P*8E$4(oUaH7M$R)X!VQd5W~s8 z8I=v=kiSg6^{KJr`;c#6e!J_^H+7dPzCEpZ>5|t8^IFvb@k+1S9(@-t{p*vPe!W%? zeHZ~=vjg+5Mkf68JPrN7v{_9AD(y4h^46JI8w&5(EW{H1X$y9~)7;gKw}xlLYRY87 ziq>s;7!C}}wh8U?E$DoF|CMo%LdkFm#ovM=kxY?ROVwfwf5tEEu7aLD0G|8 zgie~-rio^LKe@kq;M<8rpKS7K?(YuncIpFaeOa6eZGE@)#Oj^x>%OPr z^pU7|oO8yo)bZuRoAUF!dvBB~=<#*&HmLd~P}w~{|KhsAUl+97<6H4 z@K{lOf@74!%R7V*m&X~yR)m&RadKaBvRaO^%vEhiIlS_RDc9tYCro0Tf}Ujgo=KnQ zdvY0$VxH9gzI0=E$DPcvVfW=6*#{0ytDJiAi}pJSJ-6&{)+XNCGwa*%JD(R#!9IaI zUH7=`m+wM$83Xi8T^(sq96R{ZbA6oHe{WOY9!1rB|IiALlw(9h7L!{eaJbn9bDGYO z1oaOoCz|MC_Fqg{ZusjtdZzwzW-7N6?Ahk#mV&Jc>DJZ`nmgIj%q0VVyB0B=a!Ygn z*4M@39BqYh>yyV0GnZ(|y>~Wp(zEt#;w<%*YMC zm+slg)b8)-^1K?)ObUxb@{%!@ZDom(3MF0+^#PYD(u`6~vXj_UhlZD{;hmF=?u>$t zr`*_g{ao70jQhlq4SuQdNIQ4=!~)ewy|zBZs3M^$g{zULqR2HLIBjPhZ&c)F-S#c8 z4-6?5y7D49_uml?-s{DyApqWz&Ggj$$X%1OH ztL;81Icoh8OTVn&X<)ac>t;nTRn>DYo_9OukxptLu}MFfsXg!y{v2-ANI6A?;JRd6 zK|BWo!@HVTGqAgWcy~~!!F0evC!8(l+FOss;ddU&Fsgh-MqF7BX?#-|=N%)6@3tS& z*qsWG+%c7{`}OCO&%NB%z6lx^wIoxAE&|Lj+0DBkbN=JBag5cZYw z%#XBR;ytwU7n3k$d%K|CWOI8lVPO-m*(+)sbrpxr?Xz>stW!#4KGa--{u=PQ_g8;? z;d?`TUM|LSXgN3(<2iRpO}}($Np-o$T@`cks1wH8IsPxrJ!A?xws)VUeL?%_12RqPjF%$auRc}MBXOVWe zu&||Rnz6{jpTr_<#RpTaDu-J8vo=y?3zX@BMK znhdx%RK-lks3uWs1hIErI{v#}%uq-ZjPQ5rBnmZQRh-hS?;IYMC=+u0=dNVRO}=dJ zzhOm`Vr$8Iqb(5GcPEf)a94~&vv`)=k?~>A!0qX_FSi8eH0fvOX35Gq0nCSf0?WPx zFbJ+)Wm(k1x%e(&w*CS$)fvsg$ICqAjdwRyPbaQa-S5aQoWQ17?hI-mJ(uHi?@apQ zJ#yA(%YTX@ENnG+UnsKoVAnJ1&Mb}T2zx1p&WOB=TU|6pAy;7M3KO(yp8KtnKHPnj0ltP_OS_RB5O5J{UR7&COhI*g9!qdnKH) zoXl5nCbOXeu8YwcomSMVs#r2YRWjjH5aXoZ==U$fS)>^=@r_Z*TY1dW%E8&~Y-* z)KOiX=V*k^4{LcgDrHxOYDcJVDsIj6sV?OSb_QhdW3T8P>ceF>J4()+k>%txZ`~}p z@+c-`_|M{kU&oTvf*I3XyOLD976T-iuSk7HmjgR79!AVkQb5l$!40i8o%{R?m(`u9 z282Gvm_F%q8PtsHey7wvWd5#&8S+_KlJ3@N{_RF*d(h%qP+6CPbV3rdAP$py%Dxsp zk%UDMlLCtgnG&QR9gq@0kf6VcbItQqDbVye&T*j?BYfGXCimjwo%lsnzOQDXiQ~E# zeQU|F3{0kCRhfgExk&71ET_I+qMDjiw}M`HTzcS8gE>)oz8tSF-|BXd|m`+V9 zYSJRx-S-_`dg*YDpb#Ut`)$kd=fc|4O znI{unRnfM4^s$jW_*W)+%{Gs6mm~;UeJzvny;j*|J4tstF|4ReOkYgyy?ldcg$KEx zr}aqg$JEPoeACKfC(joGvs3r1@qLRCS>%k+3(@Jp!i@{#D z8(w!3C{EvQ!U+G2B-NMz{YuZvJ*VM%oSw`pFEh6aXKuW?q$kF^vC8Qi51aiis@EfU zan!@2iPiWto3$O+2CHO#*ymt&_d{3LCK_)YJNS-Mm^B2S+*6qKnB;n#PR$gAqyY{@;VXK&hrFDeO$3-Ivqc?Lr@ zz^P1NDKl}|vUEeL*W#_Z7IF z=;`#V6zbaSc@{U3(-TydTh+UR&g9EKUUY9}TIGT8+LOhF)>MaGd4@A{_%{JehN?l@ zmaO)f2Ud0)TFhvb!t^+w((athpPgdxJ^XI=>PwFucMkA}>~OMqpBm-Rr)7WVHPyt@ zkz=qd(PU~lFWOb5Wr%btc0h|V!>kZ=w|l;*Uumv-kbJ&(wS+fVMJF<3NF#Q!N0-x1 z1T&}=a^}G<3cv&7K!R zU9ur*g9g2hO^+Vzl4y2(9xygUIAXumDeHKUsnuB~65^%O;wVpHbUjswkvXsyyL?TO zkVWCh2(&-+R_de7(IT1Yms-nL?Ny7X_UI23?V##S__mqE4t(h|Xm!`NSC#v0bhz-| zo-seCBRi+`64Y{bP;BZu$!Z<#ve4Z5voEaFWS$n>dY>vCSN2eJKHVwfMN^)6boz^d z?Au}#z3UDVPG=66n~Zx7Xh;^R3S(YH^apKnKtQEQ5eVDi)J>DwLk=oCJ+9k$w=i}} z`N8$lZXw+)l7D+&kZee5cW;m^bICE=UCiH1(!$Tlaenk^T-*+i&JCkx0sj2Rs*qwQ zpeG1ry47}4vq=ilH(&9sm~;_~Z1!Y0Su5+r!PQ=3Mhor^E|oGewwUba`eMlGel(VG z63#d5wUzF%Ysux?mefxrPv*#a1{7cGJ)HWA7HU;(VvRS-6YtG*xr)z}yVP#>E)3c~ zN%fmE*1Wxy%h#U5Z=rT%XIYCpBllS8!!ntVr9F2ywJ6!$DLd#^9|dwJl1@xgG;W=z z`^$8K>Xx{$FjDA?iMYahsJQQ7Pk3aSs7chxUruW1@Z>^kRuipwqJ;J|7M8!Fn9u4ul zTJD|Yhnp5ZM_%Nx=rzpnF{Wwxx8**TOWr>X)6`-kG~3KTA$emE4$IM#lL%f>_NsqfsfJF+EA`y(dB6pZ{Ekd&ZayFutu$^Roeevc)1iq@{RyS+e^DJ|YM`k(Q)-2EA zDe5&asg>&ud>mh1%Tw!3wO^pHU><4A1Ni_ut08$LeWiPJ!};B2?I`q&F;6{A4m zUA^;>HZIZnJx-)AA=y zXio1r5-@SOyn&HCC-1-TNU8wy)iNvmopW0HbxR4SXi8%nu{P5veNhR6Nd>JJWsQ`Pr)i-I3W+$x4(_jh8v(O6-Ghl%FT^XKBHQ>xM~=<*Ojb3(o*o+mazk2aAdh993kFNY~9o1<77-< zYnlo)$S=}zy4_H0<&SP-Gruyp!E&TjhG9Qjn=PB??0M&=5I^?B@f^K4QWqH}MSj2C z=>gtDWXO!V;WnlKNRV)vm?G$ExF({;X8-dL@Jv3zQ3LDx9AXp$NxxEP`TLMBiIPT`j zk^)E5dtP2yx3<4k>2R6#knY^RW9mpY7dD(G$FV+QW5tiY?SnLO=Ff~%%x^UwXgck~ zM6K>&^EiW<`Pm+)IQ8WV?n3y9FqoAoyu^6dw(p-a6_&qYlg@fXy`AxM&^22VW=TS? ziX^An4RNH=$p6RfGT=Vqa0xQa_tB=)Q;IVK`tObIREYo;mw$G4R#SlW<96w=Wgg-L zsM`S_DCfxbmcf>yG)(^e?Tt6*-WuQG@Z}y++vA&z{mwT|;k%YPvp>dfso?hjAr#6< z_db?$xe{*oPZURRK_%zZ<&p#a|759wcQthK8PmY{%PExsrcfW}2>8{pZGLOkd%wJvZxq zSXYbwrJbLaCc-OODF)bo|Nr`PX_z}^ZjJk;fS*vZ9dth}H0flWqJ13(oQAKjFaIvq z>%Y77X(+ITVRyV5)E7zp)!laT;gPpaUKdN}zUg&_>sH%o_Ed$0@OGv56vvLdaGa^eg?%Fjg7>igOt8a{szsG)CWASThT) z_|fRn$SEu={4zIp^)2hV*gSgTSXUS{eoc(W3TiWmj(v>3@8;8SRsmzs(cX>@?r>gn z9)fb2%0FH8_8{%*kdlS+^soeFUx$DDQAvB&`RxJ@AI79rS1WU}1}VLhv zpz)qzlS^vw>Fe(d|3~goeJ4-2@-?xNbk?-wFX-R*$^T%Ga_b}3?i$AV|Kqoen&Q9h z(Ei=pAWbGLEbBa&5X1IbVCfQE_O?dAJ5jE0P7U0UI|R8;#)CK{ z`g_&(!lv13xC(3Utqx|eeZ~)i>O@YhEveVXNpDZTnV-|_CDUAFclGby|3meN^%&2d z*LJ_aJtZ~{KV=GPY1 zJlX#EUqA0()^GbX|2uBvU;06rFH=)tuopA~CKO-CKquR4yCtN*2PmAn6-IgO6JGiv zlo*l-fcf$Ux%0G)j3Y>w*$7nA1w6X1+4uaa3`B1l(#oUN@6)M=k_u`91|Ry5-Mq-Q zcja<((*RE10M(oP1P~&J+Hes(6spNX1+BQsJQO}s*;QA5Ra*O!Mgz6&2Gik%xGys^ z5v{GQCE{$|G3+VW@kbRvMY4iA6e0j;ycZvj2tcvEn;&Q)5`fE)*~8xRMJb_Ng)Ob^ zhK}-nuVJh$s-q)9iCY4Ip{nQwE|EF`3(>Bxcd)1TO#2XG<+#QRPI7&t+k)`zE1lvzqS@3^@Y|<}RoGK>ovkGu8W5U9p-LxA z9&o8$qhVlC*MaH~h5#A_Bn`C(6s!_JqC5#MrlU8m`i>fWeRyHK&Wgwf^%gqoP*EBV zNB~m0X0#R19b5sEK!Qrd&Z+&4!KY61zzL-|zlk_HG&DrK`{|9HlT#$B8n$=+0341V z0W5e!T^(uGaB~x(8XAR9O$rZxeGgVInXw8$TlbB+Kc1!-%| zaw~7pvk_+Ws07I1RRR3d73j<)D1{qEl6fWN|$xu6FGCqCS}r4D5#?#PN@O}2UmdP!LO{WECm%F2>^G7XdgrONbrAcGKsuS|d{AmiK-UYLo{@64JTk$LmkrLm%NAOq0bS$ydG-fo)r{G0#W-Qz}-XV=H}#K z3{e69>jpS_m5s88>^ri{tFU*WdXNVEPAN1)>>M4#jf-zL00fsGDn(738o>Ccqc;j`VYsut zIx{nK9MK{j6*eD_tY><|w|G^TQn~h9f-I9;6(|vJO%-kCvQ7&Ep2mYvrvl2;yin)Z zFbWhAnw71stt&7-H>P%;^680UF9(j>I*@VAlwhlT?Q+NN%NrYS`ewZ|f5@#_vH?uTDBy_91_AU$G@5am2e!T#IjKPRGz&wJQxL8F`kWO5cfGyj;1$cT<{WB#@AT6%e4}b>p4bq!{>x*H!AM_{HN!zVgw!aSVutqu4+Eqn zCvi90j-cR%=NcyDhntI>fSb<(BlIQDr~ z7U&1C_4|&s1cQNb1s)j*>UF8eiYWw^mXxsWE8*0w^GHDh3Q!H4CBmTtFPrkS@o^&E zM0pT(6@IoXdT5ua9YFm^E1>MTAEs&~lmn_r;`0hC#Vac-cdh5EU9g69X`prQ!Q87xKW>h)(wR$FU&nu##FB#?ZnJTSt9M$n0=f$nXBJgf**n5wst!UA!=hDm~| zY%1^>!vKpzM%MwO!wczYpg;i#Zmv)OO5VXP&|Xw<S3=q+357dc^Knpvs5cd((Oi>J^2!tCuh4 z!0?3H_DIlPAOU|D13%{Rirn-V<~9MwNOoZiI}(VDSqgaV2PJMKXb-e9xWbK1wEi_k zFTMj)8!yMj0RH2P3ZM~C0kvrdTAoJLJ|49Tb!gP1h+Q7kK4FzOxY2a2O2_Jl_8hHAc#MAl?}XyoFN;JxGE<47fbki)a@Fs3RXD zC@ow5l_Vz;L^p8;bB3bP)48|1u|FYO3Kz^;BCAF zTV%8-mmr*s2-+m|XpkXefm}N74(2Ty=phJjb0gJ?5$)5nlS&EtwfrzrJ|m$%Dri+6 z+v?DEfO2JMo{<3Nh5C;#kF+Tp41*gMQ2)_CmVE~fCqEW*gXP%4DP>B-XgFEDbbTHCyD)I>>VbrIfIE2N8E#;n2!3r9U1ZmMp3-;c)ayY#;c-Y4 z?@0>*xLG%WfR#^D0FTAJOOcactpY4Va>7OM_-b!?&3?!};fn(3dHS{0QkM&`vryQJ z#|=!&o#rY8*AY;Bd!c0cSM{ZUTMcLtg>^l`(9&N9WI0`84%`DMhLQ@f+(}n5+_774@_6PUaKhohPy8}D z846*@o|TuwQQ!co*B_$j`Ql3gBsa-hD?(!_zG5I|=kg{A6rff)={Ot+;gBI;2P?$x zIxsB_K+K|o9Hp4$xeH61GJ^=244w^Yl)z3=nnC*7OjQx(kkR2gazBw14s* z1T*(-F$1^eS(6xI7}!c$RspNtD5&9vK?!)eS?!NlT|sN>#$+3|m|1YpVWmaZj|rSQ zmg^3I8kiH9S1Xg#G5dLhS`ava#P6HK8zA^da7KuBP{{0M}e-WQ8O*3fFsb>)~zQ?VgZR z+o6%#Y00AviyIQ04LBeN5NkqltD*$@8tOhP08Wp+(3@}N2-yR0gW$m_Sv$q z!<#?8NA4|h2gfh;0Em-UQc{u#hWaARmbd|}jJ9hnHv_fxP3|fuAWbZ!Hw9Nw!;M1O zL6A;GCItC!qq8o7zDaIP$7K8u9cA-v*%;=F>n|z8)|-RA7NdA|f^tp-)ip_gSAE*>t+{ zSXOd~Bk1V0weQm*mP$`wzP%cA<7y~kBGCYm+FjBwuY>2;5`2Fn&c`ydg5Ed%0JwCxSMg50XoC=9zmxJsNiW+OTdW$TDxVcfJpaibBer-f{Od23} z8sS0``#%55#{aX6Ao@haYGvB`Gis>@D@>3$UUhRN;k1pG()!o8D7NG!L-I&icof#G zOIm9vGJ+Pl2)+@DyCm_*ZmHKJ31ocirn&fZ9?vvWI~u{4)4(Z0@sSv4n0l+%Lkz`_ z)asF#naoCsnlfv-W{n*PWa;5;2~r{|^c)>nR=}pN#qMY~L)jT_Zp7$`j52oxbu<(u8ruRG z#mEOv*I#RV{FjR|&vb#|sI(Ekuc^~a3I{3QNJ;HpS~C zE4^Hg*9Ebb%L+A+OG4RSNUtI5*+kPg`!Q%Iiu7oJs$qp2D3L@00lov=JqNOOJh$}+ zh4lznNTBP?A$75DYz6r>z9UQv^a=%tWH2`7Cnz@{hqs{-4Q2yI;vl%U$iM56>lO*| zBYZND+wGQ~=h!*|PpQ1h*9cz(pxcDcja0`Kj!*Cd#|u z>d(JwyeN}qdwtIKiH8A8YY@d%agwIIfEH(YoRFzLaQN_H+JQjY0V(^=bhiy;4_Rpl zPzmpxKbZ*UI|u|sS0srV3dl7QWtdUs4Qv<;Y&mBoSz+*8D?HTks(saEbKGSVO<*&C zXrn7ZIb4Wor6H>H`?TAV@2kd^O}sfT3jc)GKeE%1PzN$YD6cdM4;sTEs5uGeFQ?~? zTtXpx^i{=UC?Qb~tVKR_(KITVjHaCt#3e|Z6D4%+oncFS2S6JkAh$ZBbrqp&$e_~q zBhZr~;LQ_g`G`KM9fq)5ID3py8U^D;(&E90N4M>$EWZ=F%h>;tsvsn1)oclp7L4hzW>{m6RHC<%G zP`H2w!uw?9v&mo(ydlMbfk#fr1lDw1f*8KLPj~SnL83xZNopC7N0ytZXO+T1d zToJYF&9yKBb%zZEXO3{4j)KQLy1_8KSx#d4NPsUA(gd-=gJ^2PohYk-PVD&F_jO2O z6!{`Zo#r4&G=!r$0eyHdW%a%7>5;00GqzXPhekHc2SLx327)aYz}TTk86_srhbGFI zS04R10*?+@9vN&)WHmfmQ2nNEq=az=d=0jzhcMq9sM(9~Spt9uwNwIOMU?f1AUYg` ziK=Ntf`9&quoGKi7f7F=r*f{K+d+cd^C1+!15mU6pluTOL`q-)TAzl}pRrPRhqML1 zwL6jF64eh*-t|sPLqo&W5qj0O1A}Hldd$x7hS8vuLhaZZdj~PB`zcoJ4WV}|#}Tex zl;C6K+t3f7`3$qF2m?5J0%{i4a|w7f(5Ve(Z0dG4_AbEqXv^@N1Lb&6AscE0mZS=1 zAbz=e-X8@7eN{n*f-&p|6@F;wTu%C@$8bv>|5rWu` Date: Sat, 19 Sep 2026 15:48:42 +0200 Subject: [PATCH 14/16] Fix mypy error in assign_lanes by using .at instead of .loc pandas-stubs cannot type-check DataFrame.loc[idx, "col"] for a scalar row/column lookup here, since idx is typed as bare Hashable. The .at accessor is the intended scalar accessor and type-checks cleanly. Co-Authored-By: Claude Sonnet 5 --- python/src/execution_trace/diagram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/execution_trace/diagram.py b/python/src/execution_trace/diagram.py index 707f5a6..321e43e 100644 --- a/python/src/execution_trace/diagram.py +++ b/python/src/execution_trace/diagram.py @@ -280,7 +280,7 @@ def _infer_lane(ts: float) -> int: if containing.empty: return -1 idx = containing["priority"].idxmax() - lane = cast(SupportsFloat, containing.loc[idx, "lane"]) + lane = cast(SupportsFloat, containing.at[idx, "lane"]) return int(float(lane)) markers = markers.copy() From d09d1427eace831b65e2693fb4117f6c6c61e1be Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 17:51:05 +0200 Subject: [PATCH 15/16] Remove outdated section references from comments and documentation --- proto/tracing.proto | 2 +- python/src/execution_trace/decode.py | 4 ++-- python/src/execution_trace/stream.py | 2 +- python/tests/test_stream.py | 3 +-- src/encode.rs | 10 ++-------- src/lib.rs | 2 +- src/sink.rs | 2 +- src/types.rs | 2 +- 8 files changed, 10 insertions(+), 17 deletions(-) diff --git a/proto/tracing.proto b/proto/tracing.proto index 10d2039..b4e3041 100644 --- a/proto/tracing.proto +++ b/proto/tracing.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package tracing; -// Execution-trace wire format v2 (EXEC-TRACE-002 §6). +// Execution-trace wire format v2. // // Not wire-compatible with v1. A v2 stream is recognised by its leading // TRACE_START frame; see §6.4. diff --git a/python/src/execution_trace/decode.py b/python/src/execution_trace/decode.py index 687b70c..978388f 100644 --- a/python/src/execution_trace/decode.py +++ b/python/src/execution_trace/decode.py @@ -56,7 +56,7 @@ SEEN_HISTORY: int = 512 # How far a frame may arrive ahead of a missing one before that one is called -# lost (EXEC-TRACE-002 §5.7, §19.12). +# lost. # # Frames are numbered by their producer, before the queue the transport drains, # so an ISR preempting a task between those two points takes a later number and @@ -108,7 +108,7 @@ class TraceEvent: @dataclass class GapRecord: - """A stretch of the trace where frames were lost (EXEC-TRACE-002 §5.9). + """A stretch of the trace where frames were lost. Emitted as a row of its own so that missing data *looks* missing. Silent truncation is what made a 2 % frame-loss rate read as a broken instrument diff --git a/python/src/execution_trace/stream.py b/python/src/execution_trace/stream.py index 5dfaa37..e525d9e 100644 --- a/python/src/execution_trace/stream.py +++ b/python/src/execution_trace/stream.py @@ -43,7 +43,7 @@ class SequenceTracker: With a *reorder_window* above zero the tracker tolerates frames arriving slightly out of order before calling a hole loss. The execution-trace v2 format needs this: a frame is numbered by its producer, before it reaches - the queue the transport drains (EXEC-TRACE-002 §5.7), so an ISR that + the queue the transport drains, so an ISR that preempts a task between those two points takes a later number and reaches the wire first. The transport's own frames — the stream header and the dictionary — are numbered when written and can likewise overtake events diff --git a/python/tests/test_stream.py b/python/tests/test_stream.py index da5fb87..9ce97da 100644 --- a/python/tests/test_stream.py +++ b/python/tests/test_stream.py @@ -257,8 +257,7 @@ def test_the_count_is_right_when_several_are_missing(self, caplog): def test_a_window_does_not_hide_a_device_reset(self, caplog): # Detectable only when the pre-reset counter was low in the range: from # high in it a restart at zero is indistinguishable from a forward gap, - # which is why the trace decoder leans on TRACE_START instead - # (EXEC-TRACE-002 §19.6). + # which is why the trace decoder leans on TRACE_START instead. t = self._tracker() with caplog.at_level(logging.WARNING, logger="execution_trace.stream"): t.observe(10) diff --git a/src/encode.rs b/src/encode.rs index aeb686a..548717b 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -1,4 +1,4 @@ -//! Wire encoding for the v2 trace format (EXEC-TRACE-002 §6). +//! Wire encoding for the v2 trace format. //! //! Every frame is `[varint: byte length][protobuf-encoded TraceFrame]`, and every //! frame class — the three per-occurrence events, the dictionary entry and the @@ -19,13 +19,7 @@ pub const MAX_TRACE_FRAME_SIZE: usize = 128; /// frame ahead of the event when a name is seen for the first time. pub const MAX_TRACE_BURST_SIZE: usize = 2 * MAX_TRACE_FRAME_SIZE; -/// Distinct names the dictionary holds, against 27 in the firmware today. -/// -/// Sized close to that count on purpose. Each entry is ~52 B, so this array is -/// the single largest thing the trace format puts in RAM — and under flip-link -/// every byte of it comes out of the stack, which EXEC-TRACE-002 §23 found had -/// no room left to give. Raising it is not free; `TracingNameRegistryFull` and -/// `UNKNOWN_NAME_ID` (REQ-T11) are what guard the other side. +/// Distinct names the dictionary holds pub const NAME_REGISTRY_CAPACITY: usize = 40; /// The reserved "unknown" name id, emitted when the registry is full (REQ-T11). diff --git a/src/lib.rs b/src/lib.rs index 5d52451..ec54f10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ #[cfg(feature = "enabled")] pub mod encode; -/// The producer-side sequence counter (EXEC-TRACE-002 §5.7). +/// The producer-side sequence counter. /// /// Numbering at record time rather than in the transport is what makes loss at /// the producer queue visible: a frame dropped there has already burned a diff --git a/src/sink.rs b/src/sink.rs index f559820..e937610 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -71,7 +71,7 @@ pub trait TraceSink: TraceTransport { /// [`TraceEncoder::with_cycle_counter`]. A cycle source may be a /// free-running 32-bit counter returned widened: the encoder extends it, so /// that this can be a single volatile load with no critical section and no - /// division on the path an ISR takes (EXEC-TRACE-002 §5.8). + /// division on the path an ISR takes. /// /// This method has no default — every `TraceSink` implementor must wire up a real clock /// source. Returning a constant `0` is valid for stubs, but must be done explicitly to diff --git a/src/types.rs b/src/types.rs index 6020ead..16507f6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -49,7 +49,7 @@ impl TraceEvent { /// /// Zero until [`set_sequence`] is called; the transport forwards whatever is /// here rather than numbering the event itself, so that a frame lost between - /// the recording layer and the wire still leaves a gap (EXEC-TRACE-002 §5.7). + /// the recording layer and the wire still leaves a gap. /// /// [`set_sequence`]: TraceEvent::set_sequence #[must_use] From ad63c84eb6b5d38a6ce279d4eb37435b34caafc2 Mon Sep 17 00:00:00 2001 From: Ralf Rettig Date: Sat, 19 Sep 2026 17:51:10 +0200 Subject: [PATCH 16/16] Update heapless dependency to version 0.9.3 in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 88a64ad..1588148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ enabled = ["dep:heapless", "dep:micropb"] std = [] [dependencies] -heapless = { version = "0.9.2", optional = true } +heapless = { version = "0.9.3", optional = true } micropb = { version = "0.6.0", default-features = false, features = ["encode", "decode", "container-heapless-0-9", "enable-64bit"], optional = true } [build-dependencies]