Skip to content

perf(net-inject): fewer guest IRQs under multi-flow load - #491

Open
dyxushuai wants to merge 5 commits into
arcboxlabs:masterfrom
dyxushuai:perf/net-inject-adaptive-coalesce
Open

perf(net-inject): fewer guest IRQs under multi-flow load#491
dyxushuai wants to merge 5 commits into
arcboxlabs:masterfrom
dyxushuai:perf/net-inject-adaptive-coalesce

Conversation

@dyxushuai

Copy link
Copy Markdown

Multi-flow host→VM still costs about half single-stream on the same
CPU. Most of the extra time is the inject thread kicking the guest:
GSO only merges within one flow, so more flows mean more frames and
more interrupts for the same bytes.

We already batch (256 / 200 µs) and skip kicks when the guest is
polling (EVENT_IDX). The fixed 200 µs window is fine for one fat
stream; under multi-flow load it's still too chatty.

This stretches the gather window when a full-window batch looks busy
(≥12 used entries, or we hit the 256 cap) up to ~800 µs, and shrinks
it when the gather is idle or very light (<4). Single-stream GSO
(~7 frames / 200 µs) holds. Notify math and fences are unchanged.

Batch size only — not wall-clock spend — so a timeout full of idle
recv_timeout does not look “busy”. Empty idle gathers still observe
so the window can decay after load. Descriptor-exhaustion flushes do
not adapt.

Inject debug logs 1 Hz interval frames / irqs / frames-per-IRQ and
the current window. Mechanism model (no VM):

cargo run -p arcbox-net-inject --example coalesce_sim --release
scenario fixed IRQ/s adaptive frames/IRQ Δ
P1 ~10 Gbps (37k fps) 5001 5001 (hold) 0%
P2 multi-flow ~74k fps 5001 1252 (−75%) +~320%
small-pkt 500k fps 5001 1955 (−61%) +~156%

System numbers (HV iperf) on follow-up when available.

Also depends on arcbox-virtio-core only (this crate only used core types).

Related: #242

Not touching multi-queue, retransmit, or the IRQ delivery path.

Stretch the inject gather window when batches look busy and shrink it
when idle or very light, so multi-flow host→VM fires fewer guest IRQs
without changing EVENT_IDX notify math. Add interval inject stats and a
mechanism-model example.
Copilot AI review requested due to automatic review settings July 22, 2026 07:34

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one design concern and a nit, both non-blocking.

Reviewed changes — an adaptive RX-inject coalesce window replacing the fixed 200 µs timeout, aiming to cut guest IRQs under multi-flow load where GSO can't merge across flows.

  • Add CoalescePolicy (coalesce.rs) — batch-size-driven window in [100, 800] µs: expand ×3/2 when busy (filled_early or batch >= 12), shrink ×3/4 when quiet (batch < 4), hold otherwise. Classification deliberately ignores wall-clock spent.
  • Rework the inject loop (inject.rs) — move BATCH_SIZE into coalesce, drop the COALESCE_TIMEOUT const, and centralize flush + stats + observe in a finish_batch closure covering the normal, exhaustion, disconnect, and idle exits. Exhaustion flushes pass allow_adapt=false.
  • Add InjectStats (stats.rs) — lifetime totals for shutdown logs plus a 1 Hz interval debug line (frames/irqs/frames-per-IRQ/current window).
  • Add coalesce_sim example — no-VM discrete-event model of the mechanism.
  • Swap dependency arcbox-virtioarcbox-virtio-core — the crate only used GuestMemWriter/QueueConfig/SplitQueue, all re-exported by core; queue.rs/lib.rs are import/doc updates only.

ℹ️ Nitpicks

  • inject.rs:184-211 — the exhaustion flush correctly passes allow_adapt=false, but when exhaustion truncates the gather to batch=0 and the next recv_timeout returns empty, the normal end-of-gather observe(0, …, true) at line 217 shrinks the window. That mildly contradicts the "descriptor-exhaustion flushes do not adapt" intent (shrinking during a storm kicks the guest more often). Self-correcting once load resumes, so low impact — worth a comment if the intent is stricter.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread virt/arcbox-net-inject/src/coalesce.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an adaptive RX inject coalescing gather window in virt/arcbox-net-inject to reduce guest IRQ/vCPU kick frequency under multi-flow (high frame-rate) host→VM traffic, while keeping single-stream GSO behavior stable. It also adds lightweight per-thread stats logging and a no-VM simulation example to help tune/validate the policy.

Changes:

  • Add CoalescePolicy to adapt the gather window between 100–800µs based on observed batch activity.
  • Add InjectStats for lifetime + 1Hz interval debug counters (frames/flushes/IRQs/window).
  • Switch arcbox-net-inject to depend on arcbox-virtio-core types and add a coalesce_sim discrete-event example.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
virt/arcbox-net-inject/src/stats.rs New inject-thread stats + 1Hz debug logging for coalesce effectiveness.
virt/arcbox-net-inject/src/coalesce.rs New adaptive gather-window policy + unit tests.
virt/arcbox-net-inject/src/inject.rs Integrates adaptive window + unified batch finishing (flush/stats/policy).
virt/arcbox-net-inject/src/queue.rs Switches SplitQueue imports to arcbox_virtio_core.
virt/arcbox-net-inject/src/lib.rs Exposes new coalesce and stats modules.
virt/arcbox-net-inject/examples/coalesce_sim.rs Adds a no-VM coalescing simulation to model IRQ reduction.
virt/arcbox-net-inject/Cargo.toml Replaces arcbox-virtio dependency with arcbox-virtio-core.
Cargo.lock Updates lockfile for the dependency rename.
Comments suppressed due to low confidence (1)

virt/arcbox-net-inject/src/inject.rs:150

  • The gather remaining budget is computed once before the recv loop, so multiple recv_timeout(remaining) calls can block past the intended coalescing window (latency bound) once time has already been spent injecting/receiving. Recompute remaining from loop_start inside the loop (or use a deadline) so the window is a true upper bound per gather.
            // Phase 3: Drain channel frames (classifier / DHCP / DNS / ARP).
            // Use the remaining adaptive window after inline polling.
            let elapsed = loop_start.elapsed();
            let remaining = window.saturating_sub(elapsed);

            while (batch as usize) < BATCH_SIZE {
                // Use the remaining timeout for the first recv, then zero
                // for subsequent ones to drain without blocking.
                let timeout = if batch == 0 && inline_conns.is_empty() {
                    // No inline conns and nothing batched yet — block for
                    // the full coalescing window.
                    window
                } else if remaining.is_zero() {
                    // Timeout already consumed by inline polling — try_recv only.
                    Duration::ZERO
                } else {
                    remaining
                };

Comment thread virt/arcbox-net-inject/src/inject.rs Outdated
Comment on lines +99 to +104
// Always observe (incl. batch == 0 idle) so the window can decay.
policy.observe(batch, filled_early, allow_adapt);
stats.sync_policy_events(policy.expand_events(), policy.shrink_events());
stats.window_us = window_budget_us;
stats.maybe_log();
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in e84e059: sync_from_policy / note_window update window_us_max on every finish_batch, including empty idle gathers.

Comment on lines +56 to +57
pub fn maybe_log(&mut self) {
let now = Instant::now();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in e84e059: maybe_log returns immediately when !tracing::enabled!(Level::DEBUG) so the hot path skips Instant::now() under normal logging.

Comment thread virt/arcbox-net-inject/src/stats.rs Outdated
Comment on lines +90 to +93
irqs_fired = d_irqs,
irqs_suppressed = d_suppressed,
frames_per_irq = format!("{fpi:.1}"),
avg_batch = format!("{avg_batch:.1}"),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already fixed in 8f15143 (and kept): interval rates are logged as f64 fields without format!.

Replace the multi-arg finish_batch closure with a method + FinishBatch
args struct, keep InjectStats fields private behind getters, and sync
policy events in one place. Log interval rates as f64 without format!.
Copilot AI review requested due to automatic review settings July 22, 2026 07:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Comment thread virt/arcbox-net-inject/src/inject.rs Outdated
Comment on lines +130 to +134
// Use the remaining timeout for the first recv, then zero
// for subsequent ones to drain without blocking.
let timeout = if batch == 0 && inline_conns.is_empty() {
// No inline conns and nothing batched yet — block for
// the full coalescing timeout.
COALESCE_TIMEOUT
// the full coalescing window.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in e84e059: remaining is recomputed from loop_start.elapsed() each iteration of the recv loop so the gather window is a real deadline.

Comment thread virt/arcbox-net-inject/src/inject.rs Outdated
Comment on lines 219 to 223
// Final flush on shutdown if a partial batch was left mid-iteration
// (running flag cleared between pushes).
if fire {
self.flush_interrupt(&queue, fire);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in follow-up: removed the unreachable post-loop if fire flush. Every iteration ends in finish_batch, which flushes when batch > 0.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No new issues — one doc-comment nit inline, non-blocking.

Reviewed changes — this run covers the follow-up refactor commit that reshapes the inject-thread plumbing added in the first commit; no coalesce-policy logic changed.

  • Extract finish_batch into a method — the multi-arg closure became RxInjectThread::finish_batch taking a FinishBatch<'_> argument struct, replacing the nine positional parameters at each call site.
  • Encapsulate InjectStats — struct fields are now private with #[must_use] const fn getters (frames_injected/flushes/irqs_fired/window_us_max), and the two shutdown info! sites use them.
  • Fold window tracking into sync_from_policy — replaces sync_policy_events(expand, shrink), now also setting window_us so the closure no longer writes the private field directly.
  • Drop format! in the 1 Hz debug logfpi/avg_batch are passed to tracing::debug! as bare f64 instead of pre-formatted strings.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread virt/arcbox-net-inject/src/stats.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds adaptive interrupt coalescing to the guest RX injection path. The main changes are:

  • Adaptive gather windows based on observed frame rates.
  • Separate counters for flushes, fired IRQs, and suppressed IRQs.
  • A simulation example for fixed and adaptive coalescing.
  • A narrower dependency on arcbox-virtio-core.

Confidence Score: 5/5

This looks safe to merge.

The simulator now states that its IRQ figures model non-empty flushes with EVENT_IDX disabled. Production statistics distinguish flushes from IRQs that actually fire. No blocking issue related to the earlier reporting problem remains.

No files need additional attention.

T-Rex T-Rex Logs

What T-Rex did

  • The release model exited with status 0 and reported adaptive P1 at 37k-fps, with a peak/average window of 200/200 µs.
  • For the P2 same-bits scenario, IRQ rate and frames-per-IRQ metrics shifted from 5,001 IRQ/s and 14.0 frames/IRQ to 1,252 IRQ/s and 58.9 frames/IRQ.
  • P2 saturation metrics changed from 5,001 IRQ/s and 24.0 frames/IRQ to 1,252 IRQ/s and 95.8 frames/IRQ.
  • Small-packet stress metrics changed from 5,001 IRQ/s and 100.0 frames/IRQ to 1,955 IRQ/s and 255.9 frames/IRQ.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
virt/arcbox-net-inject/examples/coalesce_sim.rs Adds a coalescing model and clarifies that its IRQ count represents non-empty flushes with EVENT_IDX disabled.
virt/arcbox-net-inject/src/coalesce.rs Adds the adaptive gather-window policy and tests for expansion, decay, and bounds.
virt/arcbox-net-inject/src/inject.rs Integrates adaptive coalescing, batch finalization, and production IRQ statistics.
virt/arcbox-net-inject/src/stats.rs Adds lifetime and interval counters that distinguish flushes from fired and suppressed IRQs.

Reviews (2): Last reviewed commit: "docs(net-inject): note coalesce_sim coun..." | Re-trigger Greptile

Comment thread virt/arcbox-net-inject/examples/coalesce_sim.rs
Classify busy/quiet by implied fps over the gather budget so an elevated
window does not trap a later single stream as busy. Track window_us_max on
every finish_batch, gate maybe_log on DEBUG, recompute recv remaining each
iteration, and skip idle adapt once after descriptor exhaustion.
Copilot AI review requested due to automatic review settings July 22, 2026 07:44

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c85bba0b3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread virt/arcbox-net-inject/src/inject.rs Outdated
Comment on lines +216 to +217
let filled_early = (batch as usize) >= BATCH_SIZE;
finish_batch(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop treating batch-cap hits as early window fills

For sustained but non-bursty RX streams, this labels any BATCH_SIZE gather as filled_early, but the loop computes remaining once before the receive loop and then reuses it for every recv_timeout, so reaching 256 frames only proves each inter-arrival gap was below the current window, not that 256 frames arrived within the window. A single GSO stream around 37k fps can therefore fill the batch over several milliseconds, get classified as busy, and expand to the 800µs window even though the policy and commit message say that band should hold, adding much larger RX/ACK latency on the production RX engine.

AGENTS.md reference: virt/AGENTS.md:L16-L20

Useful? React with 👍 / 👎.

Comment thread virt/arcbox-net-inject/src/coalesce.rs Outdated
Comment on lines +94 to +95
let busy = filled_early || batch >= EXPAND_MIN_BATCH;
let quiet = batch < QUIET_MAX_BATCH;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Classify inline buffers separately from frames

When the inline RX path delivers one large host read, poll_inline_conns adds num_used to batch, so a single GSO frame that spans 12+ RX buffers is treated here as a multi-flow/high-fps batch and expands the window. That contradicts the intended single-stream hold behavior and can push ordinary single-flow inline traffic to the 800µs coalescing delay even though only one packet was delivered; the adaptive threshold needs packet/read count or an inline-specific signal rather than raw used-entry count.

AGENTS.md reference: virt/AGENTS.md:L71-L78

Useful? React with 👍 / 👎.

Comment thread virt/arcbox-net-inject/src/coalesce.rs Outdated
///
/// At 200 µs: ~12 frames ≈ 60k fps — above single-stream GSO (~7 @ 37k fps
/// / 10 Gbps) and into multi-flow territory.
pub const EXPAND_MIN_BATCH: u16 = 12;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scale the busy threshold with the window

Because the busy cutoff stays fixed at 12 entries while the window can grow to 800µs, a connection that should be in the documented single-stream band after a multi-flow burst no longer holds: ~37k fps is only ~7 frames at 200µs, but it is ~30 frames at 800µs, so batch >= EXPAND_MIN_BATCH keeps the policy pinned at the maximum until traffic becomes nearly idle. This leaves normal single-stream host→VM traffic paying the higher coalescing delay after any prior busy period instead of returning to the 200µs regime.

AGENTS.md reference: virt/AGENTS.md:L16-L20

Useful? React with 👍 / 👎.

…ption

Remove unreachable post-loop fire flush and note that coalesce_sim counts
flushes, not EVENT_IDX-suppressed guest IRQs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

virt/arcbox-net-inject/src/inject.rs:235

  • This "final flush on shutdown" block appears to be dead code: fire is reset to false at the end of every gather iteration, and the loop only breaks at the top before starting a new iteration. That means fire can never be true after the loop, so this branch is unreachable and misleading.
        // Every loop iteration ends in finish_batch (which flushes when
        // batch > 0), so no trailing flush is needed on exit.

        tracing::info!(
            frames = stats.frames_injected(),

virt/arcbox-net-inject/src/inject.rs:132

  • The comment says only the first recv_timeout should block and subsequent receives should be non-blocking, but the code blocks up to remaining on every iteration until the gather deadline is exhausted. This mismatch makes the gather/timeout behavior harder to reason about.
                // Use the remaining timeout for the first recv, then zero
                // for subsequent ones to drain without blocking.

Copilot AI review requested due to automatic review settings July 22, 2026 07:47

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — the three commits since the prior pullfrog review (8f15143): a rate-based rework of the coalesce busy/quiet test, an inject-loop cleanup, and a sim doc clarification. Both prior pullfrog threads are addressed and now resolved.

  • Rate-based classification (coalesce.rs) — replaced the raw-count EXPAND_MIN_BATCH with EXPAND_MIN_FPS=60_000 and an implied_fps(batch, window_budget_us) helper; observe now takes the gather budget. An elevated window decays once fps drops below multi-flow, so a single stream following a multi-flow burst no longer pins at the 800µs cap — the window-ratchet concern from the first review.
  • Post-exhaustion idle guard (inject.rs) — added skip_idle_adapt so the first empty gather after descriptor exhaustion passes allow_adapt=false instead of shrinking the window mid-storm, addressing the earlier exhaustion-shrink nit.
  • Per-gather deadline (inject.rs) — remaining is recomputed from loop_start inside the recv loop, making the window a true upper bound per gather.
  • Dead shutdown flush removed (inject.rs) — every loop iteration ends in finish_batch (which flushes when batch > 0) and resets fire, so the trailing post-loop flush was unreachable.
  • maybe_log DEBUG gate (stats.rs) — early-returns when DEBUG is disabled to keep Instant::now() off the hot path, and the stale allocation comment is gone; note_window folds the shared window high-water update.
  • Sim IRQ clarification (coalesce_sim.rs) — docs/print note that the model's irqs are non-empty flushes with EVENT_IDX off (production can suppress further).

Verified the mixed→single transition decays 800µs→200µs and that cold single-stream holds at the 200µs default. System-level (HV iperf) validation is still deferred to a follow-up per the PR description — sim-only so far, unchanged by these commits.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

virt/arcbox-net-inject/src/inject.rs:135

  • The timeout selection is effectively just remaining unless the window is already consumed; the batch == 0 && inline_conns.is_empty() branch is redundant, and the comment about “first recv then zero for subsequent” no longer matches the implementation. Simplify this to a single remaining-deadline check so the gather/deadline behavior is unambiguous.
                // Use the remaining timeout for the first recv, then zero
                // for subsequent ones to drain without blocking.
                let timeout = if batch == 0 && inline_conns.is_empty() {
                    // No inline conns and nothing batched yet — block for
                    // the full coalescing window (or what's left of it).

Comment on lines +253 to +257
// Always observe (incl. batch == 0 idle) so the window can decay.
f.policy
.observe(f.batch, f.window_budget_us, f.filled_early, f.allow_adapt);
f.stats.sync_from_policy(f.policy, f.window_budget_us);
f.stats.maybe_log();
@PeronGH PeronGH self-assigned this Jul 31, 2026
@PeronGH

PeronGH commented Jul 31, 2026

Copy link
Copy Markdown
Member

Thanks for your contribution! Could you resolve all bot reviews first? Remember to click "Resolve conversation" after replying to them.

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.

3 participants