Skip to content

perf(core): cache the hardware facades, hoist per-message reflection - #28

Merged
IamCoder18 merged 4 commits into
mainfrom
perf/core-hot-path
Sep 30, 2026
Merged

IamCoder18 merged 4 commits into
mainfrom
perf/core-hot-path

Conversation

@IamCoder18

@IamCoder18 IamCoder18 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two allocation/CPU cleanups on the core dispatch hot path, both behaviour-preserving:

  1. Orchestrator.hardware() returns a shared facade. HardwareActions was
    allocated on every call, and bulkRead allocated a fresh HardwareView per
    registration. Both are immutable single-reference views over the
    orchestrator, so they are now built once in the constructor and shared. The
    only observable change is object identity — which means it is now safe for
    callers to capture the facade once and reuse it on a hot path.
  2. @SubscribedTo dispatch no longer re-boxes the parameter type per message.
    AnnotationBinder recomputed paramType.isPrimitive() ? boxed(paramType) : paramType
    inside the delivery lambda. The normalisation is now done once at bind time
    and captured. Equivalent, because boxed() already returns non-primitives
    unchanged.

Also drops a dead handlerBody Runnable in the binder that was constructed
and immediately discarded — its own comment admitted it was unused.

Changes

  • OrchestratorImpl — cache hardwareActions / hardwareView as final fields;
    bulkRead reuses the shared view; hardware() returns the cached facade.
  • AnnotationBinder — hoist primitive→wrapper normalisation out of the
    delivery lambda; remove the dead handlerBody.
  • HardwareView — Javadoc corrected: instances are per-orchestrator and shared
    across bulkRead registrations, not per-callback as previously documented.
  • hardware() — Javadoc documenting the shared-identity guarantee.
  • CHANGELOG.md — both changes under [Unreleased] / Changed.

Tests

New CoreHotPathTest covering the specific behaviour these changes touch:

  • hardware() returns the same instance across calls, and the cached facade
    still reaches the hardware thread (guards against caching something frozen or
    detached).
  • Annotated subscribers still receive both primitive-typed and
    reference-typed messages — the two sides of the hoisted normalisation.
  • Mismatched messages are still silently dropped, with a matching-type message
    on the same topic still delivered. The topic is created as Object first
    because topic types are first-writer-wins, which is what makes the drop
    branch reachable.
  • bulkRead callbacks receive a working HardwareView (publish + getLatestValue).

Full suite passes: gradle test → BUILD SUCCESSFUL.

Review notes

Two things worth a look during review:

  • The HardwareView lifetime is now tied to the orchestrator rather than to a
    registration. That is fine because the view only holds a reference back to
    the orchestrator and nothing else, but it does mean a HardwareView is now
    valid after its BulkReadHandle is cancelled. Nothing in the tree relies on
    the old per-registration lifetime.
  • OrchestratorImpl assigns hardwareActions / hardwareView at the end of the
    constructor, after the executors. They only need this, but the ordering
    keeps the final-field initialisation block together.

Review in cubic

Two small allocations and one repeated computation on the per-message path.

`orchestrator.hardware()` built a new `HardwareActions` on every call. Hot
subscribers call it once per message, so a teleop handler allocated a facade
per gamepad event for no reason. `HardwareActions` and `HardwareView` are
immutable one-field views over the orchestrator, so both are now built once in
the constructor and shared. `scheduleHardwareBulkRead` likewise reused a single
`HardwareView` instead of allocating one per registration.

  Behaviour note: `hardware()` now returns the same instance on every call.
  The object holds no per-call state, so nothing could observe a difference
  except code that relied on getting a fresh object, which was never a
  documented guarantee.

In the annotation binder, the primitive-to-wrapper normalisation for a
`@SubscribedTo` parameter was recomputed on *every delivered message*:

    paramType.isPrimitive() ? boxed(paramType).isInstance(msg)
                            : paramType.isInstance(msg)

`boxed` returns non-primitives unchanged, so both branches already collapsed
to the same check; normalising once at bind time and testing the result is
equivalent and does the work once instead of per message. Also drops a dead
`handlerBody` lambda in that method, which was constructed, given an
unreachable try/catch, and never invoked.

No API or behaviour change. Covered by new tests pinning primitive-parameter
delivery, the silent-drop branch, the shared facade, and bulkRead.
…cycle

Follow-up to the facade-caching commit. Two documentation problems it
introduced or left behind.

The Javadoc on `OrchestratorImpl.hardware()` was placed *after* the
`@Override` annotation. Javadoc only associates a doc comment with a
declaration when nothing but annotations sit between them, so the block was
discarded entirely — the generated HTML showed only "Description copied from
interface: Orchestrator". Since the build publishes a `javadocJar`, the one
place meant to state that the facade is now safe to capture and reuse never
reached consumers. The `javadoc` task emits no warning for this, so the build
could not have caught it. The block now precedes `@Override`, matching every
other documented method in the class, and verifies in the regenerated output.

`HardwareView`'s class Javadoc still claimed "Instances are created
per-callback by `HardwareActions.bulkRead`". There is now one instance per
orchestrator, built in the constructor and shared by every registration, so
the description is corrected and links `bulkRead` properly.

Also:

- Move the two new facade fields into the state block beside `hardwareThread`
  and the other finals, rather than declaring them after the constructor.
- Trim comments to match CONTRIBUTING's "minimal comments" convention. The
  equivalence argument in the annotation binder is kept — it is the part a
  future "optimization" could plausibly try to undo — while the restatements
  of adjacent code are not.
- Drop a tautological `assertFalse(seen.isEmpty())` from the new bulkRead
  test, which only re-checked what the preceding latch assertion already
  guaranteed, along with its unused list.
- Add a CHANGELOG entry under [Unreleased] for both behavior changes, noting
  the caching is not a behavioral change beyond object identity.

No logic changes. Full suite green: 55 tests, 0 failures. `javadoc` builds
with no new warnings.
- Use explicit Float.valueOf assertion instead of relying on
  assertEquals overload resolution.
- Replace Thread.sleep with assertFalse(latch.await) for the
  mismatched-message negative case so a regression fails fast
  instead of sleeping then checking.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f73b6df6-a0d7-4daa-837d-076a7088876d

📥 Commits

Reviewing files that changed from the base of the PR and between d5cc6ac and b93d419.

📒 Files selected for processing (1)
  • src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java
 _______________________________________________________________________________________________________
< Make it easy to reuse. If it's easy to reuse, people will. Create an environment that supports reuse. >
 -------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5cb9871e-f50b-47c8-9da9-f44e2c7a8e3c

📥 Commits

Reviewing files that changed from the base of the PR and between b61731d and d5cc6ac.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/main/java/com/aaravlabs/synapse/OrchestratorImpl.java
  • src/main/java/com/aaravlabs/synapse/ftc/HardwareView.java
  • src/main/java/com/aaravlabs/synapse/internal/AnnotationBinder.java
  • src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java

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

📜 Recent review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Build & Test
  • GitHub Check: Build image (arm64)
  • GitHub Check: Build image (amd64)
🔇 Additional comments (5)
src/main/java/com/aaravlabs/synapse/internal/AnnotationBinder.java (1)

110-114: LGTM!

Also applies to: 123-125

src/main/java/com/aaravlabs/synapse/OrchestratorImpl.java (1)

72-76: LGTM!

Also applies to: 105-107, 445-445, 456-460, 463-463

src/main/java/com/aaravlabs/synapse/ftc/HardwareView.java (1)

10-12: LGTM!

CHANGELOG.md (1)

10-22: LGTM!

src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java (1)

1-110: LGTM!


📝 Walkthrough

Walkthrough

The orchestrator now reuses one HardwareActions facade and one HardwareView. Subscribed handler parameter types are normalized at bind time, and tests cover hardware access, bulk-read publication, and annotated message delivery.

Changes

Hardware object reuse

Layer / File(s) Summary
Retained hardware objects
src/main/java/com/aaravlabs/synapse/OrchestratorImpl.java, src/main/java/com/aaravlabs/synapse/ftc/HardwareView.java, CHANGELOG.md, src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java
OrchestratorImpl retains and returns one HardwareActions facade and one HardwareView. The documentation and changelog describe the shared instances. A test checks repeated hardware() calls return the same facade.
Bulk-read reuse validation
src/main/java/com/aaravlabs/synapse/OrchestratorImpl.java, src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java
Bulk-read scheduling passes the retained view to each reader. Tests cover hardware-thread execution and publication from a bulk-read callback.

Subscribed message dispatch

Layer / File(s) Summary
Bind-time parameter normalization
src/main/java/com/aaravlabs/synapse/internal/AnnotationBinder.java, CHANGELOG.md, src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java
wireSubscribedTos boxes primitive handler parameter types once and uses the effective type for topic filtering and message checks. Tests cover primitive and reference handlers and mismatched messages.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to d5cc6

The changes reuse hardware wrappers and move type normalization out of message dispatch without an established behavioral regression. Mergeable after normal checks.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to d5cc6

The shared objects remain tied to their original orchestrator and retain no callback-specific mutable state. Hardware scheduling, cancellation, shutdown, and message-type filtering remain unchanged. No introduced or worsened security risk was identified in the reviewed changes.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The reuse scope is one orchestrator and its existing hardware and topic operations. No global wrapper, alternate owner, or additional device authority is introduced by caching.

Trust Boundaries and Controls

  • inferred — A caller holding the cached facade reaches the same orchestrator methods as a caller holding the former newly allocated facade. Hardware execution still passes through the dedicated executor; caching changes object identity, not caller authorization or scheduling enforcement.

Resilience and Maintainability Implications

  • observed — Bulk-read cancellation remains non-interrupting future cancellation, and close() remains responsible for hardware-executor shutdown. Cached wrappers do not acquire independent resources or cleanup authority; these lifecycle characteristics predate the PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies both main performance changes: caching hardware facades and moving per-message reflection work out of the hot path.
Description check ✅ Passed The description directly explains the shared hardware facades, bind-time type normalization, related documentation changes, and tests for the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown

We've triggered an ultrareview automatically — This rewrites the core dispatch hot path: hardware facades and views become shared long-lived objects used from the hardware thread, and the annotation binder's per-message type normalisation is hoisted, so a subtle lifetime, threading, or type-matching bug could silently affect every.... I'll post findings when complete.

An ultrareview is cubic's deepest review, catching hard-to-find bugs in the most critical PRs. It runs a longer, multi-pass analysis using cubic's most capable review models, and typically takes around 30 minutes. It consumes your team's reviewed-lines allowance at 3× the standard rate.

Automated ultrareviews are disabled by default. We triggered this run as part of your trial. Want cubic to do this for every high-risk PR? Enable auto-ultrareview in your settings.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultrareview completed in 4m 55s

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/test/java/com/aaravlabs/synapse/CoreHotPathTest.java
The drop test only asserted the handler did not run, which cannot tell an
intentional silent drop apart from a delivery that threw and was swallowed by
the binder's catch (Throwable). Removing the isInstance guard in
wireSubscribedTos makes m.invoke raise IllegalArgumentException, the handler
still never runs, and the test still passes — so the regression it guards
against was undetectable.

Build the orchestrator with a recording LogSink and assert no error was
logged after the mismatched publish. A dropped message leaves no trace; a
failed delivery is logged.
@IamCoder18
IamCoder18 merged commit 88fe64f into main Sep 30, 2026
3 of 4 checks passed
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.

1 participant