Skip to content

Reports

just-some-entity edited this page Sep 29, 2026 · 2 revisions

A report (RuntimeReport) is the Runtime's periodic message to the controller. It carries everything that happened since the previous report: request results, resource history, measurement samples, runtime events and timing statistics. While a session is running, reports are the only thing the Runtime sends (see Protocol).

Code: qitech_framework_core/src/report/ (types) and Runtime::export_report_if_due in qitech_framework/src/runtime/mod.rs (assembly).

When reports are sent

At the end of every cycle, the Runtime checks whether export_interval has passed since the last export. The default is 1/32 s, about 31 ms. If it has, the Runtime builds and sends one report.

RuntimeConfiguration::new()
    .export_interval(Duration::from_millis(50))   // 20 reports per second

The interval sets:

  • how fresh the controller's view is. Everything reaches the controller at most one interval late.
  • how finely measurements are sampled. One snapshot per measurement per report.
  • how long requests take. A response arrives in the first report after the request was processed.
  • the load on the controller. Every report has to be read and applied.

The interval is measured on the Runtime's monotonic clock, and reports are only sent at cycle boundaries. The actual spacing is therefore the interval rounded up to the next cycle. With the default 100 µs cycle, that's negligible.

Structure

struct RuntimeReport {
    timestamp: DateTime<Utc>,        // wall-clock time when the report was built
    responses: Vec<RuntimeResponse>, // results of requests processed in this window
    timings:   TimingsReport,        // cycle statistics for this window
    machines:  MachinesReport,       // resource activity for this window
    events:    Vec<RuntimeEvent>,    // runtime-level events
    logs:      Vec<LogRecord>,       // log output
}

Every field covers only the window since the previous report. The Runtime resets all of it after sending (RuntimeReport::reset).

responses

struct RuntimeResponse { request_id: u64, result: Result<(), RuntimeRequestError> }

There is one entry for every request the Runtime processed in this window, in processing order. The request_id is the one the controller chose (see Protocol). A controller matches responses to its pending requests by id. There can be several responses per report, or none.

machines: resource activity

struct MachinesReport {
    config_property_records: Vec<EventRecord<ConfigPropertyEvent>>,
    state_property_records:  Vec<EventRecord<StatePropertyEvent>>,
    measurement_snapshots:   Vec<MeasurementSnapshot>,
    command_records:         Vec<EventRecord<CommandEvent>>,
    event_records:           Vec<EventRecord<String>>,   // machine events, payload as JSON
}

There are two kinds of content:

  • Records: config, state, command and event history. These are the journals, drained in full. Every change is included, in order, with its own timestamp. A controller applies them to rebuild the current state.

  • Snapshots: measurements. These are sampled once per report, not recorded:

    struct MeasurementSnapshot {
        machine: MachineInstanceIdentification,
        path:    String,        // e.g. "diameter", or "diameter.avg" for statistics
        value:   Option<f64>,   // in the schema's unit; None = no value (nullable)
    }

    All snapshots in a report share the report's timestamp. Values are converted to the unit declared in the schema (see Quantities and Units). Booleans become 0.0 or 1.0, and integers become f64.

Measurement statistics

If a measurement's schema enables statistics (min, max, avg, stddev), the Runtime tracks them across every set call within the report window, and exports them as extra snapshots with a suffix on the path:

diameter          ← the latest value
diameter.min      ← smallest value set during this window
diameter.max
diameter.avg
diameter.stddev

The statistics are reset for each window. The reset is triggered by the export counter (export_count) that every Measurement watches. That's how "one snapshot per 31 ms" doesn't hide what happened in between: a spike shows up in .max, even if the latest value is back to normal.

events: runtime events

enum RuntimeEvent {
    AddedMachine        { ident },
    RemovedMachine      { ident },                                  // after an Irrecoverable ActError
    SubscriptionAdded   { provider, subscriber, resources },
    SubscriptionRemoved { provider, subscriber },
}

These are changes to the Runtime's set of machines and subscriptions, rather than to individual resources. See Subscriptions and Creating a Machine.

timings: cycle statistics

struct TimingsReport {
    cycle_count:    u32,       // cycles run in this window
    duration_total: Duration,  // time spent working (excluding the sleep)
    duration_peak:  Duration,  // slowest cycle
    overrun_count:  u32,       // cycles that took longer than cycle_period
}

This is the health signal of the real-time loop:

  • cycle_count should be close to export_interval / cycle_period, about 312 at the defaults.
  • duration_peak should stay well below cycle_period.
  • overrun_count should be 0. Overruns mean that some act, driver or request took too long.

timestamp

This is the wall-clock time (Utc::now()) when the report was built. It is the timestamp for all measurement snapshots in the report. Records carry their own, earlier timestamps.

How a report is built

export_report_if_due runs after the machines, at the end of a cycle, in this order:

  1. set timestamp,
  2. drain the config and state property journals into machines,
  3. sample every measurement (including statistics slots) of every machine that's still running,
  4. check the command capabilities: call each command's can_execute, and record CapabilityChanged when the result differs from last time,
  5. drain the command and event journals,
  6. send the report,
  7. reset the report and increase export_count, which starts a new statistics window.

responses, events and timings are filled throughout the window: when requests are processed, when machines are removed or subscriptions change, and after every cycle.

Guarantees

  • Complete: every journaled change since the previous report is in this report. Nothing is dropped or merged.
  • Ordered: reports are sent in order over an ordered transport, and all content of report n happened before report n + 1. Within one record list, records are in the order they happened. Across lists, compare timestamps.
  • Deltas, not state: a report only says what changed. The current state is the result of applying every report since the session started, beginning with the first one, which contains all Registered records.
  • One window each: snapshots, statistics and timings describe exactly one export window.

So a controller must process every report, in order. A missing report silently corrupts its view of the machines. That's why the Runtime terminates instead of dropping reports when the controller can't keep up (see Protocol).

Consuming reports

A minimal loop that applies a report:

fn apply(&mut self, report: RuntimeReport) {
    for r in report.responses { self.complete_request(r.request_id, r.result); }

    for rec in report.machines.config_property_records {
        match rec.event {
            ConfigPropertyEvent::Registered { default, .. } => self.set(rec.machine, &rec.path, default),
            ConfigPropertyEvent::Written { value, outcome: ConfigPropertyWriteOutcome::Accepted { changed: true }, .. }
                => self.set(rec.machine, &rec.path, value),
            _ => { /* defaults, capabilities, constraints, rejected writes … */ }
        }
    }

    for rec in report.machines.state_property_records { /* Registered / ValueChanged */ }
    for s in report.machines.measurement_snapshots { self.sample(s.machine, &s.path, report.timestamp, s.value); }
    for rec in report.machines.command_records { /* capabilities, executions */ }
    for rec in report.machines.event_records { /* rec.event is the JSON payload */ }
    for ev in report.events { /* machine removed, subscriptions */ }
}

The TUI's Tui::on_report (qitech_framework_tui/src/lib.rs) is a complete reference implementation. In the Hub, reports arrive in Listener::on_report_received (see Hub).

Size

Reports scale with activity, not with the number of resources. The exception is measurements: every measurement, and every statistics slot, adds one snapshot to every report, whether or not it changed.

A rough per-report estimate:

snapshots ≈ Σ machines × (measurements + enabled statistics)
records   ≈ number of changes in the window

For example, 10 machines with 8 measurements each and no statistics send 80 snapshots per report, which at 32 Hz is 2,560 per second. Each snapshot carries its path as a String. Keep an eye on this before you add many machines or enable statistics everywhere.

Clone this wiki locally