///|
/// A bounded, caller-owned read-only trace over runtime Inspector snapshots.
///
/// The trace stores deltas, not history of whole snapshots. The first sample
/// establishes the baseline; later samples carry message/effect/cache deltas,
/// frame-pass counters, and a bounded semantics-tree diff.
const DEFAULT_TRACE_CAPACITY : Int32 = 64

///|
const DEFAULT_TRACE_SEMANTICS_DIFF_LIMIT : Int32 = 128

///|
const TRACE_TEXT_ENTRY_LIMIT : Int32 = 16

///|
const TRACE_TEXT_DIFF_LIMIT : Int32 = 4

///|
#derive(Eq, Debug, ToJson)
pub(all) enum SemanticsTreeChangeKind {
  SemanticsTreeAdded
  SemanticsTreeRemoved
  SemanticsTreeChanged
}

///|
pub extend SemanticsTreeChangeKind with Debug::{to_repr}

///|
pub extend SemanticsTreeChangeKind with Eq::{not_equal, equal}

///|
pub extend SemanticsTreeChangeKind with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) enum SemanticsTreeChangedField {
  SemanticsTreeFieldRole
  SemanticsTreeFieldLabel
  SemanticsTreeFieldValue
  SemanticsTreeFieldFrame
  SemanticsTreeFieldDepth
  SemanticsTreeFieldChildCount
  SemanticsTreeFieldActionCount
  SemanticsTreeFieldFocusable
  SemanticsTreeFieldSelected
  SemanticsTreeFieldChecked
  SemanticsTreeFieldExpanded
  SemanticsTreeFieldDisabled
}

///|
pub extend SemanticsTreeChangedField with Debug::{to_repr}

///|
pub extend SemanticsTreeChangedField with Eq::{not_equal, equal}

///|
pub extend SemanticsTreeChangedField with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct SemanticsTreeDiffEntry {
  kind : SemanticsTreeChangeKind
  node_id : String
  before : @runtime.SemanticsInspectorEntry?
  after : @runtime.SemanticsInspectorEntry?
  changed_fields : Array[SemanticsTreeChangedField]
}

///|
pub extend SemanticsTreeDiffEntry with Debug::{to_repr}

///|
pub extend SemanticsTreeDiffEntry with Eq::{not_equal, equal}

///|
pub extend SemanticsTreeDiffEntry with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct SemanticsTreeDiff {
  baseline : Bool
  added_count : Int32
  removed_count : Int32
  changed_count : Int32
  dropped_count : Int32
  entries : Array[SemanticsTreeDiffEntry]
}

///|
pub extend SemanticsTreeDiff with Debug::{to_repr}

///|
pub extend SemanticsTreeDiff with Eq::{not_equal, equal}

///|
pub extend SemanticsTreeDiff with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsMessageTrace {
  dispatched_delta : Int32
  updated_delta : Int32
  ignored_delta : Int32
  enqueued_delta : Int32
  drained_delta : Int32
  pending_after : Int32
  max_pending_after : Int32
}

///|
pub extend DevtoolsMessageTrace with Debug::{to_repr}

///|
pub extend DevtoolsMessageTrace with Eq::{not_equal, equal}

///|
pub extend DevtoolsMessageTrace with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsEffectTrace {
  planned_delta : Int32
  run_delta : Int32
  send_delta : Int32
  started_delta : Int32
  completed_delta : Int32
  cancelled_delta : Int32
  ignored_delta : Int32
  active_after : Int32
  descriptors : Array[@core.EffectDescriptor]
}

///|
pub extend DevtoolsEffectTrace with Debug::{to_repr}

///|
pub extend DevtoolsEffectTrace with Eq::{not_equal, equal}

///|
pub extend DevtoolsEffectTrace with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsFrameTrace {
  timed : Bool
  rebuild_passes : Int32
  layout_passes : Int32
  paint_passes : Int32
  draw_builds : Int32
  build_ms : Double
  layout_ms : Double
  paint_ms : Double
  draw_ms : Double
  total_ms : Double
  damage_kind : String
  damage_rect_count : Int32
}

///|
pub extend DevtoolsFrameTrace with Debug::{to_repr}

///|
pub extend DevtoolsFrameTrace with Eq::{not_equal, equal}

///|
pub extend DevtoolsFrameTrace with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsCacheTrace {
  layout_hits_delta : Int32
  layout_misses_delta : Int32
  render_hits_delta : Int32
  render_misses_delta : Int32
}

///|
pub extend DevtoolsCacheTrace with Debug::{to_repr}

///|
pub extend DevtoolsCacheTrace with Eq::{not_equal, equal}

///|
pub extend DevtoolsCacheTrace with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsMemoTrace {
  available : Bool
  builds_delta : Int32
  hits_delta : Int32
  misses_delta : Int32
  builds_after : Int32
  hits_after : Int32
  misses_after : Int32
}

///|
pub extend DevtoolsMemoTrace with Debug::{to_repr}

///|
pub extend DevtoolsMemoTrace with Eq::{not_equal, equal}

///|
pub extend DevtoolsMemoTrace with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsTraceEntry {
  sequence : Int32
  messages : DevtoolsMessageTrace
  effects : DevtoolsEffectTrace
  frame : DevtoolsFrameTrace
  caches : DevtoolsCacheTrace
  memo : DevtoolsMemoTrace
  semantics : SemanticsTreeDiff
}

///|
pub extend DevtoolsTraceEntry with Debug::{to_repr}

///|
pub extend DevtoolsTraceEntry with Eq::{not_equal, equal}

///|
pub extend DevtoolsTraceEntry with ToJson::{to_json}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct DevtoolsTraceReport {
  capacity : Int32
  semantics_diff_limit : Int32
  entries : Array[DevtoolsTraceEntry]
}

///|
pub extend DevtoolsTraceReport with Debug::{to_repr}

///|
pub extend DevtoolsTraceReport with Eq::{not_equal, equal}

///|
pub extend DevtoolsTraceReport with ToJson::{to_json}

///|
priv struct DevtoolsTraceWatermark {
  runtime : @runtime.RuntimeInspectorSnapshot
  semantics : @runtime.SemanticsInspectorSnapshot
  memo : @core.ViewMemoStats?
}

///|
pub struct DevtoolsTrace {
  priv capacity : Int32
  priv semantics_diff_limit : Int32
  priv entries : Array[DevtoolsTraceEntry]
  priv mut previous : DevtoolsTraceWatermark?
  priv mut next_sequence : Int32
}

///|
/// Create a caller-owned trace. Retention and per-entry semantics-diff detail
/// are bounded independently so a large tree cannot consume unbounded memory.
pub fn DevtoolsTrace::new(
  capacity~ : Int32 = DEFAULT_TRACE_CAPACITY,
  semantics_diff_limit~ : Int32 = DEFAULT_TRACE_SEMANTICS_DIFF_LIMIT,
) -> DevtoolsTrace {
  {
    capacity: capacity.max(1),
    semantics_diff_limit: semantics_diff_limit.max(0),
    entries: [],
    previous: None,
    next_sequence: 0,
  }
}

///|
pub fn DevtoolsTrace::capacity(self : DevtoolsTrace) -> Int32 {
  self.capacity
}

///|
pub fn DevtoolsTrace::semantics_diff_limit(self : DevtoolsTrace) -> Int32 {
  self.semantics_diff_limit
}

///|
pub fn DevtoolsTrace::length(self : DevtoolsTrace) -> Int32 {
  self.entries.length()
}

///|
pub fn DevtoolsTrace::is_empty(self : DevtoolsTrace) -> Bool {
  self.entries.is_empty()
}

///|
pub fn DevtoolsTrace::entries(
  self : DevtoolsTrace,
) -> Array[DevtoolsTraceEntry] {
  self.entries.copy()
}

///|
pub fn DevtoolsTrace::latest(self : DevtoolsTrace) -> DevtoolsTraceEntry? {
  if self.entries.is_empty() {
    None
  } else {
    Some(self.entries[self.entries.length() - 1])
  }
}

///|
/// Drop retained samples and the comparison baseline. The next sample starts a
/// new baseline rather than being diffed against discarded state.
pub fn DevtoolsTrace::clear(self : DevtoolsTrace) -> Unit {
  self.entries.clear()
  self.previous = None
}

///|
/// Record a sample after an event/message turn. Frame pass counters are always
/// captured; timing stays zero unless `record_frame` receives profiled timing.
pub fn DevtoolsTrace::record(
  self : DevtoolsTrace,
  snapshot : @runtime.InspectorSnapshot,
  memo_stats? : @core.ViewMemoStats,
) -> DevtoolsTraceEntry {
  self.record_internal(snapshot, None, memo_stats)
}

///|
/// Record a sample after `AppRuntime::profile_draw_frame`. The supplied timing
/// belongs to that frame; pass counters still come from the Inspector snapshot.
pub fn DevtoolsTrace::record_frame(
  self : DevtoolsTrace,
  snapshot : @runtime.InspectorSnapshot,
  timing~ : @runtime.RuntimeFrameTiming,
  memo_stats? : @core.ViewMemoStats,
) -> DevtoolsTraceEntry {
  self.record_internal(snapshot, Some(timing), memo_stats)
}

///|
fn DevtoolsTrace::record_internal(
  self : DevtoolsTrace,
  snapshot : @runtime.InspectorSnapshot,
  timing : @runtime.RuntimeFrameTiming?,
  memo_stats : @core.ViewMemoStats?,
) -> DevtoolsTraceEntry {
  let runtime = snapshot.runtime()
  let semantics = snapshot.semantics()
  let previous_runtime = match self.previous {
    Some(previous) => Some(previous.runtime)
    None => None
  }
  let previous_semantics = match self.previous {
    Some(previous) => Some(previous.semantics)
    None => None
  }
  let previous_memo = match self.previous {
    Some(previous) => previous.memo
    None => None
  }
  let entry = {
    sequence: self.next_sequence,
    messages: message_trace_between(previous_runtime, runtime),
    effects: effect_trace_between(previous_runtime, runtime),
    frame: frame_trace_between(previous_runtime, runtime, timing),
    caches: cache_trace_between(previous_runtime, runtime),
    memo: memo_trace_between(previous_memo, memo_stats),
    semantics: semantics_tree_diff(
      previous_semantics,
      semantics,
      self.semantics_diff_limit,
    ),
  }
  self.entries.push(entry)
  if self.entries.length() > self.capacity {
    ignore(self.entries.remove(0))
  }
  self.previous = Some({ runtime, semantics, memo: memo_stats, })
  self.next_sequence = self.next_sequence + 1
  entry
}

///|
pub fn DevtoolsTrace::report(self : DevtoolsTrace) -> DevtoolsTraceReport {
  {
    capacity: self.capacity,
    semantics_diff_limit: self.semantics_diff_limit,
    entries: self.entries.copy(),
  }
}

///|
fn trace_counter_delta(current : Int32, previous : Int32) -> Int32 {
  if current >= previous {
    current - previous
  } else {
    current
  }
}

///|
fn message_trace_between(
  previous : @runtime.RuntimeInspectorSnapshot?,
  current : @runtime.RuntimeInspectorSnapshot,
) -> DevtoolsMessageTrace {
  let dispatched = match previous {
    Some(previous) => previous.program_dispatched_count()
    None => 0
  }
  let updated = match previous {
    Some(previous) => previous.program_update_count()
    None => 0
  }
  let ignored = match previous {
    Some(previous) => previous.ignored_program_dispatch_count()
    None => 0
  }
  let enqueued = match previous {
    Some(previous) => previous.program_message_enqueue_count()
    None => 0
  }
  let drained = match previous {
    Some(previous) => previous.program_message_drain_count()
    None => 0
  }
  {
    dispatched_delta: trace_counter_delta(
      current.program_dispatched_count(),
      dispatched,
    ),
    updated_delta: trace_counter_delta(current.program_update_count(), updated),
    ignored_delta: trace_counter_delta(
      current.ignored_program_dispatch_count(),
      ignored,
    ),
    enqueued_delta: trace_counter_delta(
      current.program_message_enqueue_count(),
      enqueued,
    ),
    drained_delta: trace_counter_delta(
      current.program_message_drain_count(),
      drained,
    ),
    pending_after: current.program_pending_message_count(),
    max_pending_after: current.program_max_pending_message_count(),
  }
}

///|
fn effect_trace_between(
  previous : @runtime.RuntimeInspectorSnapshot?,
  current : @runtime.RuntimeInspectorSnapshot,
) -> DevtoolsEffectTrace {
  let planned = match previous {
    Some(previous) => previous.program_effect_plan_count()
    None => 0
  }
  let executed = match previous {
    Some(previous) => previous.program_effect_run_count()
    None => 0
  }
  let sent = match previous {
    Some(previous) => previous.program_effect_send_count()
    None => 0
  }
  let started = match previous {
    Some(previous) => previous.started_effect_task_count()
    None => 0
  }
  let completed = match previous {
    Some(previous) => previous.completed_effect_task_count()
    None => 0
  }
  let cancelled = match previous {
    Some(previous) => previous.cancelled_effect_task_count()
    None => 0
  }
  let ignored = match previous {
    Some(previous) => previous.ignored_effect_task_dispatch_count()
    None => 0
  }
  {
    planned_delta: trace_counter_delta(
      current.program_effect_plan_count(),
      planned,
    ),
    run_delta: trace_counter_delta(current.program_effect_run_count(), executed),
    send_delta: trace_counter_delta(current.program_effect_send_count(), sent),
    started_delta: trace_counter_delta(
      current.started_effect_task_count(),
      started,
    ),
    completed_delta: trace_counter_delta(
      current.completed_effect_task_count(),
      completed,
    ),
    cancelled_delta: trace_counter_delta(
      current.cancelled_effect_task_count(),
      cancelled,
    ),
    ignored_delta: trace_counter_delta(
      current.ignored_effect_task_dispatch_count(),
      ignored,
    ),
    active_after: current.active_effect_task_count(),
    descriptors: current.last_effect_summary().descriptors().copy(),
  }
}

///|
fn frame_trace_between(
  previous : @runtime.RuntimeInspectorSnapshot?,
  current : @runtime.RuntimeInspectorSnapshot,
  timing : @runtime.RuntimeFrameTiming?,
) -> DevtoolsFrameTrace {
  let rebuild_passes = match previous {
    Some(previous) => previous.rebuild_pass_count()
    None => 0
  }
  let layout_passes = match previous {
    Some(previous) => previous.layout_pass_count()
    None => 0
  }
  let paint_passes = match previous {
    Some(previous) => previous.paint_pass_count()
    None => 0
  }
  let draw_builds = match previous {
    Some(previous) => previous.draw_command_build_count()
    None => 0
  }
  let timing_text = match timing {
    Some(timing) =>
      (
        true,
        timing.build_ms(),
        timing.layout_ms(),
        timing.paint_ms(),
        timing.draw_ms(),
        timing.total_ms(),
      )
    None => (false, 0.0, 0.0, 0.0, 0.0, 0.0)
  }
  let (timed, build_ms, layout_ms, paint_ms, draw_ms, total_ms) = timing_text
  let dirty = current.dirty_state()
  {
    timed,
    rebuild_passes: trace_counter_delta(
      current.rebuild_pass_count(),
      rebuild_passes,
    ),
    layout_passes: trace_counter_delta(
      current.layout_pass_count(),
      layout_passes,
    ),
    paint_passes: trace_counter_delta(current.paint_pass_count(), paint_passes),
    draw_builds: trace_counter_delta(
      current.draw_command_build_count(),
      draw_builds,
    ),
    build_ms,
    layout_ms,
    paint_ms,
    draw_ms,
    total_ms,
    damage_kind: dirty.damage_kind(),
    damage_rect_count: dirty.damage_rect_count(),
  }
}

///|
fn cache_trace_between(
  previous : @runtime.RuntimeInspectorSnapshot?,
  current : @runtime.RuntimeInspectorSnapshot,
) -> DevtoolsCacheTrace {
  let layout_hits = match previous {
    Some(previous) => previous.layout_cache_hit_count()
    None => 0
  }
  let layout_misses = match previous {
    Some(previous) => previous.layout_cache_miss_count()
    None => 0
  }
  let render_hits = match previous {
    Some(previous) => previous.render_cache_hit_count()
    None => 0
  }
  let render_misses = match previous {
    Some(previous) => previous.render_cache_miss_count()
    None => 0
  }
  {
    layout_hits_delta: trace_counter_delta(
      current.layout_cache_hit_count(),
      layout_hits,
    ),
    layout_misses_delta: trace_counter_delta(
      current.layout_cache_miss_count(),
      layout_misses,
    ),
    render_hits_delta: trace_counter_delta(
      current.render_cache_hit_count(),
      render_hits,
    ),
    render_misses_delta: trace_counter_delta(
      current.render_cache_miss_count(),
      render_misses,
    ),
  }
}

///|
fn memo_trace_between(
  previous : @core.ViewMemoStats?,
  current : @core.ViewMemoStats?,
) -> DevtoolsMemoTrace {
  match current {
    Some(current) => {
      let builds = match previous {
        Some(previous) => previous.build_count()
        None => 0
      }
      let hits = match previous {
        Some(previous) => previous.hit_count()
        None => 0
      }
      let misses = match previous {
        Some(previous) => previous.miss_count()
        None => 0
      }
      {
        available: true,
        builds_delta: trace_counter_delta(current.build_count(), builds),
        hits_delta: trace_counter_delta(current.hit_count(), hits),
        misses_delta: trace_counter_delta(current.miss_count(), misses),
        builds_after: current.build_count(),
        hits_after: current.hit_count(),
        misses_after: current.miss_count(),
      }
    }
    None =>
      {
        available: false,
        builds_delta: 0,
        hits_delta: 0,
        misses_delta: 0,
        builds_after: 0,
        hits_after: 0,
        misses_after: 0,
      }
  }
}

///|
fn semantics_tree_diff(
  previous : @runtime.SemanticsInspectorSnapshot?,
  current : @runtime.SemanticsInspectorSnapshot,
  limit : Int32,
) -> SemanticsTreeDiff {
  let before = match previous {
    Some(before) => before
    None =>
      return {
        baseline: true,
        added_count: 0,
        removed_count: 0,
        changed_count: 0,
        dropped_count: 0,
        entries: [],
      }
  }
  let previous_nodes : Map[String, @runtime.SemanticsInspectorEntry] = Map([])
  for node in before.nodes() {
    previous_nodes[node.node_id()] = node
  }
  let current_ids : Map[String, Bool] = Map([])
  let entries : Array[SemanticsTreeDiffEntry] = []
  let mut added_count = 0
  let mut removed_count = 0
  let mut changed_count = 0
  for node in current.nodes() {
    let node_id = node.node_id()
    current_ids[node_id] = true
    match previous_nodes.get(node_id) {
      Some(previous_node) =>
        if previous_node != node {
          changed_count = changed_count + 1
          if entries.length() < limit {
            entries.push({
              kind: SemanticsTreeChanged,
              node_id,
              before: Some(previous_node),
              after: Some(node),
              changed_fields: semantics_changed_fields(previous_node, node),
            })
          }
        }
      None => {
        added_count = added_count + 1
        if entries.length() < limit {
          entries.push({
            kind: SemanticsTreeAdded,
            node_id,
            before: None,
            after: Some(node),
            changed_fields: [],
          })
        }
      }
    }
  }
  for node in before.nodes() {
    let node_id = node.node_id()
    if !current_ids.contains(node_id) {
      removed_count = removed_count + 1
      if entries.length() < limit {
        entries.push({
          kind: SemanticsTreeRemoved,
          node_id,
          before: Some(node),
          after: None,
          changed_fields: [],
        })
      }
    }
  }
  let total = added_count + removed_count + changed_count
  {
    baseline: false,
    added_count,
    removed_count,
    changed_count,
    dropped_count: total - entries.length(),
    entries,
  }
}

///|
fn semantics_changed_fields(
  before : @runtime.SemanticsInspectorEntry,
  after : @runtime.SemanticsInspectorEntry,
) -> Array[SemanticsTreeChangedField] {
  let fields : Array[SemanticsTreeChangedField] = []
  if before.role() != after.role() {
    fields.push(SemanticsTreeFieldRole)
  }
  if before.label() != after.label() {
    fields.push(SemanticsTreeFieldLabel)
  }
  if before.value() != after.value() {
    fields.push(SemanticsTreeFieldValue)
  }
  if before.frame() != after.frame() {
    fields.push(SemanticsTreeFieldFrame)
  }
  if before.depth() != after.depth() {
    fields.push(SemanticsTreeFieldDepth)
  }
  if before.child_count() != after.child_count() {
    fields.push(SemanticsTreeFieldChildCount)
  }
  if before.action_count() != after.action_count() {
    fields.push(SemanticsTreeFieldActionCount)
  }
  if before.focusable() != after.focusable() {
    fields.push(SemanticsTreeFieldFocusable)
  }
  if before.selected() != after.selected() {
    fields.push(SemanticsTreeFieldSelected)
  }
  if before.checked() != after.checked() {
    fields.push(SemanticsTreeFieldChecked)
  }
  if before.expanded() != after.expanded() {
    fields.push(SemanticsTreeFieldExpanded)
  }
  if before.disabled() != after.disabled() {
    fields.push(SemanticsTreeFieldDisabled)
  }
  fields
}

///|
fn semantics_field_label(field : SemanticsTreeChangedField) -> String {
  match field {
    SemanticsTreeFieldRole => "role"
    SemanticsTreeFieldLabel => "label"
    SemanticsTreeFieldValue => "value"
    SemanticsTreeFieldFrame => "frame"
    SemanticsTreeFieldDepth => "depth"
    SemanticsTreeFieldChildCount => "children"
    SemanticsTreeFieldActionCount => "actions"
    SemanticsTreeFieldFocusable => "focusable"
    SemanticsTreeFieldSelected => "selected"
    SemanticsTreeFieldChecked => "checked"
    SemanticsTreeFieldExpanded => "expanded"
    SemanticsTreeFieldDisabled => "disabled"
  }
}

///|
fn semantics_diff_entry_text(entry : SemanticsTreeDiffEntry) -> String {
  match (entry.before, entry.after) {
    (Some(before), Some(after)) => {
      let fields = entry.changed_fields.map(semantics_field_label).join(",")
      "  ~ node=\{entry.node_id} role=\{Repr(after.role())} label=\{before.label()} -> \{after.label()} fields=\{fields}"
    }
    (None, Some(after)) =>
      "  + node=\{entry.node_id} role=\{Repr(after.role())} label=\{after.label()}"
    (Some(before), None) =>
      "  - node=\{entry.node_id} role=\{Repr(before.role())} label=\{before.label()}"
    (None, None) => ""
  }
}

///|
fn trace_entry_text(entry : DevtoolsTraceEntry) -> String {
  let messages = entry.messages
  let effects = entry.effects
  let frame = entry.frame
  let caches = entry.caches
  let memo = entry.memo
  let semantics = entry.semantics
  let memo_text = if memo.available {
    "b/h/m=\{memo.builds_delta}/\{memo.hits_delta}/\{memo.misses_delta} totals=\{memo.builds_after}/\{memo.hits_after}/\{memo.misses_after}"
  } else {
    "unavailable"
  }
  let semantics_text = if semantics.baseline {
    "baseline"
  } else {
    "+/-/~=\{semantics.added_count}/\{semantics.removed_count}/\{semantics.changed_count} dropped=\{semantics.dropped_count}"
  }
  let timing_text = if frame.timed {
    " timing=\{frame.build_ms}/\{frame.layout_ms}/\{frame.paint_ms}/\{frame.draw_ms}/\{frame.total_ms}ms"
  } else {
    ""
  }
  "#\{entry.sequence} messages(dispatch/update/enqueue/drain/pending)=\{messages.dispatched_delta}/\{messages.updated_delta}/\{messages.enqueued_delta}/\{messages.drained_delta}/\{messages.pending_after}; effects(plan/run/send/start/complete/cancel/active)=\{effects.planned_delta}/\{effects.run_delta}/\{effects.send_delta}/\{effects.started_delta}/\{effects.completed_delta}/\{effects.cancelled_delta}/\{effects.active_after}; frame(rebuild/layout/paint/draw)=\{frame.rebuild_passes}/\{frame.layout_passes}/\{frame.paint_passes}/\{frame.draw_builds}\{timing_text}; damage=\{frame.damage_kind}/\{frame.damage_rect_count}; cache(layout/render)=\{caches.layout_hits_delta}/\{caches.layout_misses_delta},\{caches.render_hits_delta}/\{caches.render_misses_delta}; memo=\{memo_text}; semantics=\{semantics_text}"
}

///|
/// Format the most recent trace samples. Node-level diff detail is limited so
/// terminal and in-app diagnostics stay readable; JSON retains every stored
/// entry within the recorder's configured bounds.
pub fn trace_report_text(
  trace : DevtoolsTrace,
  max_entries~ : Int32 = TRACE_TEXT_ENTRY_LIMIT,
) -> String {
  let entries = trace.entries()
  let requested = max_entries.max(0)
  let start = if entries.length() > requested {
    entries.length() - requested
  } else {
    0
  }
  let lines : Array[String] = [
    "[trace] capacity=\{trace.capacity()}; entries=\{entries.length()}; semantics_limit=\{trace.semantics_diff_limit()}; shown=\{entries.length() - start}",
  ]
  for index in start..= TRACE_TEXT_DIFF_LIMIT {
        break
      }
      let line = semantics_diff_entry_text(diff)
      if line != "" {
        lines.push(line)
        shown_diff = shown_diff + 1
      }
    }
  }
  lines.join("\n")
}

///|
pub fn trace_report_json(trace : DevtoolsTrace, indent~ : Int32 = 2) -> String {
  ToJson::to_json(trace.report()).stringify(indent~)
}