Skip to content

feat(engine): a dropped bag is counted at its port, per link, and graph shows it - #2026

Merged
tato123 merged 6 commits into
mainfrom
feat/2023-counted-per-link-bag-drops
Aug 29, 2026
Merged

feat(engine): a dropped bag is counted at its port, per link, and graph shows it#2026
tato123 merged 6 commits into
mainfrom
feat/2023-counted-per-link-bag-drops

Conversation

@tato123

@tato123 tato123 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

PortMailbox::push evicted its oldest entry silently, whatever profile the port declared — measured on the audio playback path as 378 blocks published, 78 delivered, indistinguishable from a healthy run. The mailbox's queue element now carries the inbound link it arrived on, so an eviction is charged to the link whose bag was lost, not the link whose push made room — exact under fan-in, because the tag rides the entry rather than the port. The destination's graph node shares those counts live through ProcessorMetrics, which graph already renders.

  • New iceoryx2/dropped_bag_counters.rsInboundLinkDroppedBagCounter (one link's cumulative count) and DroppedBagCountsByInboundLink (one destination's registry). A link's counter is minted when its subscriber binds, so a wired link that has lost nothing reports 0 rather than going missing, and is forgotten on disconnect.
  • mailbox.rs — one eviction site; push_frame_from_inbound_link for the live path, push_frame_without_inbound_link_attribution for the manual-injection path (route, SDK e2e harness + unit tests only), which has no link to name.
  • input.rsPortBoundSubscriber carries its link's counter; receive_pending pushes with it; remove_channel_link forgets it.
  • open_iceoryx2_service_op.rswire_rust_dest publishes the counts onto the destination's graph node as its last step.
  • delivery_profile.rs and two built-in comments — corrected where this change falsified them.

newest is untouched. Passing over bags to reach the latest is the read policy working, not loss, and pop/read paths are unchanged.

Closes

Closes #2023

Exit criteria

  • A stalled-consumer run reports each link's own losses in graph, exact per link, and the sum equals published minus delivered.
  • A healthy run reports zero, and the graph snapshot of a dropping run carries the counts under the processor's metrics.
  • test_a_declared_port_carries_no_type_key_under_any_spelling still passes — port rendering untouched.
  • Every new test is named into test.yml's slice and the xtask mirror, individually.

Test plan

14 new tests, all in the engine-lib CI slice (67 pass locally, 6/6 repeated runs). The test.yml list and the xtask mirror are identical, checked programmatically.

  • Attribution — two producers fan in to one stalled ordered port at depth 2; A×5 then B×5 reports {L-first: 5, L-second: 3} and counted + delivered == published. Charging the pushing link would report {3, 5} — flipping it in the tree makes the test red, verified.
  • Rendering — a real channel service, the destination-side wiring the compiler op runs, the source's own write_raw, the destination's own receive_pending; the counts are read back off the node's serialize_components(), which is byte-exactly what ProcessorNodeOutput::from puts on /api/graph. Deleting the publish step from wire_rust_dest makes it red, verified.
  • Negatives — a port that keeps up renders 0 for every wired link; pop_latest passing over bags counts nothing; a disconnected link's count leaves with it; a helper-placed destination's node carries no metrics key at all — inserting a default one makes it red, so the gap in note 1 cannot be closed later with a zero.

Gates: cargo xtask check-all-source-gates 11/11, clippy clean on every touched file, fmt clean, rustdoc clean, wheel pytest 349 passed, stubtest and pyright clean, full local battery 44 pass / 0 fail.

Notes for owner

1. The plan entry is not fully met, and /ship-change must not fold it in as delivered. ARCHITECTURE.md's "No loss is silent" entry is unconditional, but a helper-placed processor — which is every Python processor — counts its evictions in its own process and the parent holds no handle on them (iceoryx2_input_mailboxes_inner returns None for a subprocess host). Such a node deliberately carries no metrics key rather than a false zero. Surfacing it needs a parent↔helper report path the plan does not state; escalate is child-initiated request/reply, so this is a genuine design call, not wiring. I flagged this before starting and you said proceed — recording it here so the ship doesn't quietly mark the entry done. My recommendation: either the plan gains that mechanism, or the graph clause is scoped to app-process destinations with the helper case named as an explicit OPEN.

2. A second loss site survives uncounted. A consumer parked inside process() pumps no receive_pending, so the iceoryx2 subscriber ring overflows instead of the mailbox — enable_safe_overflow(true), counted nowhere. The SpeakerSink room-wait is exactly that shape. This change counts the site the change file names and nothing else; the docs now say so in both places rather than claiming the 378-block gap is fully accounted for. Counting it would likely need a sequence number on the wire header (an IPC-ABI change) or an iceoryx2-side statistic — plan-level either way. Not filed as a ticket.

3. frames_dropped is now derived, and the uncomputed metric fields no longer render. ProcessorMetrics had no insert site anywhere in the tree, so this change gives metrics its first-ever appearance on the wire. Rendering throughput_fps, the two latencies and frames_processed — which nothing writes — would have served four permanent zeros a reader cannot distinguish from an idle processor. Only frames_dropped (the per-link counts summed, one snapshot so the two keys cannot disagree) and dropped_bags_by_link render. The change file says those placeholders "make no claim"; I read declining to publish them as honoring that rather than contradicting it, but it is your call.

4. Counter lifetime differs from the change file's wording. The change file says the counter is "monotonic and cumulative"; as built, a count is cumulative for the life of one wiring — disconnect takes it with the link, and reconnecting the same id starts at zero, because a count outliving its link would name something graph no longer has. Stated in the type doc. If you meant strictly monotonic across reconnects, that is a one-line change plus a change-file reconciliation.

5. Ticket's "wheel test" became an engine test. to_json is the byte-exact GET /api/graph payload, and a wheel-level dropping pipeline is GPU-gated so it would run nowhere in CI. Worth knowing the two notes interact: the wheel case the ticket asked for is precisely the Python-destination case that renders nothing today (note 1).

6. Two of these tests appeared to flake in the reviewer's cargo test --workspace run. I could not reproduce in 14 repeated runs (8× all iceoryx2 lib tests, 6× the exact CI slice), and one of the two is pure in-memory with no threads, no I/O and no shared state, so a genuine race is not representable. The workspace run was killed on the documented DRM-probe hang in the same binary, which reports in-flight tests as failures.

7. A third review (Fable) returned APPROVE with zero findings. It independently verified the two things I was least sure of: the accounting is exact-once by construction (push_frame pops only on a real push failure, ArrayQueue::pop yields each frame at most once, and record_eviction consumes it, so neither double-count nor undercount is representable), and the fan-in test's {5, 3} is uniquely determined rather than accidentally order-dependent. Its non-gating nits are addressed except two I left alone deliberately: inspector.rs's bottleneck branch stays dead because frames_processed has no writer anywhere (pre-existing, faithfully preserved), and frames still queued when a mailbox tears down vanish uncounted (nothing claims otherwise). It also notes cargo clippy --tests fails crate-wide on ~275 pre-existing println!/eprintln! in test code — none in touched files, and CI lints default targets only, so it is not this PR's regression, but the debt is real.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added live dropped-frame tracking for each inbound link.
    • Graph metrics now display total dropped frames and per-link loss details.
    • Health monitoring uses current drop counts for bottleneck detection.
  • Bug Fixes

    • Mailbox evictions are attributed to the link whose frame was discarded.
    • Counters are cleaned up when inbound links disconnect.
  • Documentation

    • Clarified delivery-profile limitations and when frame losses are counted.
  • Tests

    • Expanded coverage for mailbox eviction, loss reporting, metrics rendering, and service behavior.

…g was lost

The consumer-side mailbox dropped its oldest entry silently: 378 blocks
published reached the audio playback path as 78, indistinguishable from a
healthy run. The queue element now carries the inbound link it arrived on, so
an eviction is charged to the link whose bag was lost rather than the one whose
push made room — exact under fan-in — and the destination's graph node shares
those counts live through ProcessorMetrics, which graph already renders.

frames_dropped becomes derived from the per-link counts rather than a hardcoded
zero beside them. Passing over bags to reach the newest is the read policy
working, not loss, and stays uncounted.
Fifteen tests named one by one into the engine-lib slice and its xtask mirror:
a module name would silently take whatever is added to it later, and this slice
is the only gate the counting has. The end-to-end half drives real iceoryx2
services, a real source's write_raw and a real destination's receive_pending,
and reads the counts back off the graph node's rendering — the payload
GET /api/graph serves.
Craftsmanship review round 1:
- ProcessorMetrics::to_json took the counts lock twice, so frames_dropped and
  dropped_bags_by_link could disagree inside one rendered object. One snapshot
  now feeds both.
- push_frame's pre-drain loop was subsumed by the retry loop below it
  (ArrayQueue::push hands the frame back only when full), and duplicated the
  evict-and-charge body. One eviction site now.
- The counts publish moved inside wire_rust_dest, so the destination-side
  wiring the compiler op actually runs is what the test drives — the call site
  was uncovered before.
- dropped_bag_count_by_inbound_link (a snapshot) and
  dropped_bag_counts_by_inbound_link (a live handle) differed by one letter;
  the snapshot is now dropped_bag_count_snapshot_by_inbound_link.
- The test module's third copy of the channel-open boilerplate is now the one
  shared helper, and two unit tests subsumed by the render tests are gone.
Correctness review round 2:
- ProcessorMetrics had no insert site anywhere in the tree, so drop counts give
  `metrics` its first-ever appearance on /api/graph. Rendering the four fields
  nothing writes would have put four permanent zeros there — a reader could not
  tell an idle processor from an uninstrumented one, and the wiring helper one
  function away already refuses exactly that for the helper case. Only
  frames_dropped and dropped_bags_by_link render; the tests lock the whole
  object so an uncomputed field cannot creep back.
- The delivery-profile doc claimed the measured 78-of-378 loss 'is counted now'
  without qualification. Eviction at the mailbox is; loss at the iceoryx2
  subscriber ring, which overflows while a consumer parked inside process()
  pumps no receive, is counted nowhere. Said plainly in both places.
- The counter's lifetime is stated: cumulative for the life of one wiring, not
  of the link id — disconnect takes it, reconnect starts at zero.
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fcc6464-93a8-4796-8a99-5e719cb70dbf

📥 Commits

Reviewing files that changed from the base of the PR and between ecf3d6e and 1bb1aa5.

📒 Files selected for processing (4)
  • .github/workflows/test.yml
  • runtime/streamlib-engine/src/core/compiler/compiler_ops/open_iceoryx2_service_op.rs
  • runtime/streamlib-engine/src/core/graph/components/processor_metrics.rs
  • xtask/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • xtask/src/main.rs
  • runtime/streamlib-engine/src/core/graph/components/processor_metrics.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds per-inbound-link Iceoryx2 mailbox eviction counters. It attributes subscriber evictions, exposes live counts through ProcessorMetrics, updates health reporting, wires metrics to destination graph nodes, and adds tests.

Changes

Iceoryx2 drop accounting

Layer / File(s) Summary
Counter and mailbox eviction accounting
runtime/streamlib-engine/src/iceoryx2/dropped_bag_counters.rs, runtime/streamlib-engine/src/iceoryx2/mailbox.rs, runtime/streamlib-engine/src/iceoryx2/mod.rs
Shared per-link counters are attached to queued frames. Full mailboxes record eviction against the evicted frame.
Input link attribution and lifecycle
runtime/streamlib-engine/src/iceoryx2/input.rs, runtime/streamlib-engine/src/iceoryx2/delivery_profile.rs, runtime/streamlib-engine/tests/fixtures/*, runtime/streamlib-media-builtins/src/..., sdk/streamlib-python-wheel/tests/speaker_sink_probes.py
Subscriber delivery passes inbound-link counters into mailboxes. Manual routing remains unattributed. Disconnected links are removed from tracking. Related documentation names the new mailbox method and counting behavior.
Graph metrics and service wiring
runtime/streamlib-engine/src/core/graph/components/processor_metrics.rs, runtime/streamlib-engine/src/core/observability/inspector.rs, runtime/streamlib-engine/src/core/compiler/compiler_ops/open_iceoryx2_service_op.rs, .github/workflows/test.yml, xtask/src/main.rs
ProcessorMetrics renders per-link counts and their total. Rust destination wiring publishes live metrics. Helper-hosted destinations omit metrics. Health reporting uses the derived total. Tests are added to CI and local test selections.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 1bb1a

Dropped-bag counts are exposed for application-process destinations, but helper-hosted processors still do not report those losses in the graph, leaving a bounded observability gap. The change is otherwise mergeable with explicit owner awareness that the helper-process case must be scoped or addressed separately.

Sequence Diagram(s)

sequenceDiagram
  participant Iceoryx2Subscriber
  participant InputMailboxesInner
  participant PortMailbox
  participant ProcessorMetrics
  participant GraphNode
  Iceoryx2Subscriber->>InputMailboxesInner: deliver frame with inbound-link counter
  InputMailboxesInner->>PortMailbox: push_frame_from_inbound_link
  PortMailbox->>ProcessorMetrics: record eviction for the evicted frame's link
  ProcessorMetrics->>GraphNode: publish frames_dropped and dropped_bags_by_link
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: per-link dropped-bag counting and graph exposure.
Linked Issues check ✅ Passed The changes satisfy issue #2023. Mailbox entries retain their originating link, evictions increment cumulative per-link counters, graph metrics expose total and per-link counts, healthy runs report ze…
Out of Scope Changes check ✅ Passed The changes remain within issue #2023. Added implementation, tests, metric wiring, documentation, and renamed API references directly support per-link mailbox eviction counting and graph reporting.
Docstring Coverage ✅ Passed Docstring coverage is 88.68% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 14 files. (1 skipped: 1…
Full details: Linked Issues check

Explanation

The changes satisfy issue #2023. Mailbox entries retain their originating link, evictions increment cumulative per-link counters, graph metrics expose total and per-link counts, healthy runs report zero, disconnected links are cleaned up, manual injection remains unattributed, and related tests are registered in CI and xtask.

Full details: Docstring Coverage

Explanation

Docstring coverage is 88.68% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 14 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/2023-counted-per-link-bag-drops

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…and that is locked

Fable review round 3, non-gating. The absence was declared in the PR but
asserted nowhere: rendering an empty map or a zero for a destination whose
mailboxes live in another process would say 'this processor lost nothing',
which the parent cannot know. The missing key is what makes it read as
unanswered rather than healthy, so nobody closes the gap later with a zero.
Also drops a stray whitespace run inside an assertion message.
@tato123
tato123 merged commit da6c4b7 into main Aug 29, 2026
9 of 10 checks passed
@tato123
tato123 deleted the feat/2023-counted-per-link-bag-drops branch August 29, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(engine): a dropped bag is counted at its port, per link, and graph shows it

1 participant