Skip to content

devices: legacy: give the UART per-device metrics - #6191

Open
AdaAibaby wants to merge 2 commits into
firecracker-microvm:mainfrom
AdaAibaby:feat/serial-per-device-metrics
Open

AdaAibaby wants to merge 2 commits into
firecracker-microvm:mainfrom
AdaAibaby:feat/serial-per-device-metrics

Conversation

@AdaAibaby

Copy link
Copy Markdown

Changes

Moves the serial (UART) device off its module-global metrics static to the per-device metrics model, completing the legacy-device conversion started in #6190 (i8042 + RTC).

The UART is trickier than i8042/RTC because its metrics are touched from three places, so the device's Arc<SerialDeviceMetrics> is threaded to each:

  • SerialEventsWrapper gains a private metrics field and a new() constructor; its SerialEvents impl increments through it.
  • SerialOut is built by the caller before the owning SerialDevice exists, so it starts with a private default metrics instance; SerialDevice::new swaps in the shared, registered instance via set_metrics so the rate-limiter counter aggregates with the rest.
  • The BusDevice impl reaches the metrics via self.serial.events().metrics.

legacy::flush_metrics serializes the registered UART instance (falling back to a default() when none has been built), so the serialized metrics output shape is unchanged.

Stacking

This is stacked on #6190. The first commit here is #6190 (i8042 + RTC); the second is the UART work. Please merge #6190 first, after which this reduces to just the UART commit. Happy to rebase/split however is easiest to review.

Why

Another step towards #4709. With all three legacy devices (i8042, RTC, UART) off the process-wide metrics global, their unit tests can build isolated per-device metrics and run concurrently. This PR still does not flip RUST_TEST_THREADS=1 — that stays until the remaining logger globals (mmds, msix) are converted.

Testing

Built and tested in the fcuvm:v93 dev container (x86_64):

  • cargo build -p vmm — clean
  • cargo clippy -p vmm --all-targets — no warnings
  • cargo fmt --check — clean (no lines >100 chars, safe under nightly wrap_comments)
  • cargo test -p vmm devices::legacy — 12 passed (i8042 + serial)
  • cargo test -p vmm --test devices (external integration test) — 3 passed
  • cargo test -p vmm devices::legacy -- --test-threads=16, 3 consecutive runs — 12 passed / 0 failed each, confirming parallel-safety

Related: #4709

The i8042 and RTC devices kept their metrics in a module-global static,
shared process-wide. Because unit tests assert on metric values and each
test builds its own device, that shared state forces the vmm unit tests
to run single-threaded (RUST_TEST_THREADS=1) to avoid spurious failures.

Move both devices to the per-device metrics model already used by the
virtio devices: the metrics live behind a
`RwLock<Option<Arc<XxxDeviceMetrics>>>` that the device populates on
construction, and the device increments counters through its own
`Arc<XxxDeviceMetrics>` rather than the global. `flush_metrics` reads
the registered instance (falling back to a default when no device has
been built yet), so the serialized output shape is unchanged.

For the RTC this relies on vm-superio's `impl<EV: RtcEvents> RtcEvents
for Arc<EV>`, letting the inner `Rtc` hold the same `Arc` the module
tracks. Tests now build a device with a caller-owned metrics instance
and assert on absolute values instead of reading-then-diffing a global.

The UART (serial) device is left on its module-global metrics; it will
be converted in a follow-up because it needs the metrics threaded
through `SerialOut` and `SerialEventsWrapper` at their call sites.

Progresses firecracker-microvm#4709.

Signed-off-by: shaolila <shaolila@buaa.edu.cn>
The serial (UART) device kept its metrics in a module-global static,
shared process-wide, so unit tests that assert on metric values could
race each other and had to run single-threaded.

Move it to the per-device metrics model used by the other devices: the
device owns an `Arc<SerialDeviceMetrics>` and registers it in a module
`RwLock<Option<...>>` on construction. Because the serial metrics are
touched from three places, the `Arc` is threaded to each:

- `SerialEventsWrapper` gains a `metrics` field (and a `new()`
  constructor) and increments through it.
- `SerialOut` (built by the caller before the device exists) starts with
  a private default instance; `SerialDevice::new` swaps in the shared,
  registered metrics via `set_metrics` so the rate-limiter counter is
  aggregated with the rest.
- The `BusDevice` impl reaches the metrics through
  `self.serial.events().metrics`.

`legacy::flush_metrics` now serializes the registered UART instance
(falling back to a default when none has been built), so the output
shape is unchanged. Tests build a device with a caller-owned metrics
instance and assert on absolute values.

With this, all three legacy devices are off the process-wide metrics
global. Removing RUST_TEST_THREADS=1 still depends on the remaining
logger globals (mmds, msix), so this does not close the issue yet.

Progresses firecracker-microvm#4709.

Signed-off-by: shaolila <shaolila@buaa.edu.cn>
@AdaAibaby
AdaAibaby force-pushed the feat/serial-per-device-metrics branch from b59a2d1 to c8a2cbe Compare September 8, 2026 03:31
@marco-marangoni marco-marangoni self-assigned this Sep 16, 2026
@marco-marangoni

Copy link
Copy Markdown
Contributor

Same comment as #6190

We're definitely interested in this, and especially the final outcome of being able to run unit tests in parallel.

However, this PR seems premature since the approach is still being discussed in #5599.

I suggest waiting for #5599 to be merged, and then we can apply the same approach to other devices

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