From 29ac000b4fae9a0e25bd0208396dc8794be9d254 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Tue, 31 Mar 2026 07:52:56 -0700 Subject: [PATCH 1/2] [kernel] E: LAPIC calibration for WHP - Reduce PIT-based calibration window from 10 ms to 1 ms. - Replace PIT-based calibration with RDTSC spin loop when CPUID leaf 0x16 is available, eliminating ~100 VM exits during boot. - Fall back to PIT-based calibration (1 ms window) when leaf 0x16 is unavailable instead of guessing TSC frequency. - Add max-iteration guard to RDTSC spin loop to prevent hangs. - Fix CPUID helper to check max supported leaf and set ECX=0 explicitly for well-defined subleaf selection. --- .../arch/shared/cpu/interrupt/controller.rs | 162 ++++++++++++++++++ src/libs/arch/src/x86/cpu/cpuid.rs | 42 ++++- 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs b/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs index baff88944e..92c0a759e9 100644 --- a/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs +++ b/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs @@ -147,6 +147,168 @@ impl InterruptController { }); } + // On microvm/WHP the partition enables LAPIC emulation in + // xAPIC mode. Enable the LAPIC software-enable bit and + // configure the LAPIC periodic timer so timer interrupts + // fire entirely inside the WHP LAPIC emulator — zero VM + // exits for timer delivery. The LAPIC page at 0xFEE00000 + // is identity-mapped via the microvm platform init and + // handled by the WHP LAPIC emulator (not guest RAM). + #[cfg(all(feature = "microvm", feature = "whp"))] + { + use ::arch::cpu::xapic; + let lapic_base: usize = ::config::microvm::DEFAULT_LAPIC_BASE; + let lapic: xapic::Xapic = xapic::Xapic::new(lapic_base as *mut u32); + // SAFETY: The LAPIC MMIO page is identity-mapped during + // microvm platform init. Writes go through the WHP LAPIC + // emulator. + unsafe { + lapic.write(xapic::XAPIC_SVR, 0x1FF); + lapic.write(xapic::XAPIC_TPR, 0); + } + info!("lapic svr enabled for whp interrupt delivery"); + + // LAPIC timer calibration. + // + // When CPUID leaf 0x16 is available, an RDTSC-based spin + // loop is used. This eliminates ~100 PIT-polling VM exits + // that are extremely expensive during the first + // WHvRunVirtualProcessor call (WHP lazily initialises + // internal partition state). + // + // When leaf 0x16 is not available, we fall back to + // PIT-based calibration with a reduced 1 ms window. + + // SAFETY: LAPIC registers go through the WHP emulator. + // CPUID and RDTSC do not cause VM exits. + unsafe { + // 1. Mask the LAPIC timer during calibration. + lapic.write( + xapic::XAPIC_TIMER, + xapic::XapicTimer::new(0x20, false, true, 0).to_u32(), + ); + + // 2. Set LAPIC timer divide-by-128. + lapic.write(xapic::XAPIC_TDCR, 0x0A); + + // 3. Check CPUID leaf 0x16 for TSC frequency. + let base_freq: u32 = ::arch::cpu::cpuid::get_base_frequency_mhz(); + + let mut ticks_per_ms: u32 = if base_freq > 0 { + // RDTSC-based calibration (zero VM exits). + let tsc_freq_mhz: u64 = base_freq as u64; + let tsc_ticks_per_ms: u64 = tsc_freq_mhz * 1_000; + + // 4a. Start the LAPIC timer counting from max value. + lapic.write(xapic::XAPIC_TICR, 0xFFFF_FFFF); + + // 5a. Spin for ~1 ms using RDTSC (zero VM exits). + // A max-iteration guard prevents a hang if TSC + // does not advance (virtualisation quirk). + const RDTSC_MAX_ITERS: u64 = 1_000_000_000; + let tsc_start: u64 = ::arch::cpu::rdtsc(); + let mut iters: u64 = 0; + while (::arch::cpu::rdtsc() - tsc_start) < tsc_ticks_per_ms { + core::hint::spin_loop(); + iters += 1; + if iters >= RDTSC_MAX_ITERS { + warn!( + "rdtsc calibration timeout after {} iterations", + RDTSC_MAX_ITERS + ); + break; + } + } + + // 6a. Read remaining LAPIC count and actual TSC + // delta to correct for overshoot. + let current_count: u32 = lapic.read(xapic::XAPIC_TCCR); + let elapsed_ticks: u32 = 0xFFFF_FFFF_u32.wrapping_sub(current_count); + let tsc_elapsed: u64 = ::arch::cpu::rdtsc() - tsc_start; + + // ticks_per_ms = elapsed × (target / actual) so the + // result is independent of TSC frequency errors. + let tpm: u32 = + ((elapsed_ticks as u64 * tsc_ticks_per_ms) / tsc_elapsed) as u32; + + info!( + "lapic timer calibration (rdtsc): elapsed_ticks={}, ticks_per_ms={}, \ + tsc_freq_mhz={}", + elapsed_ticks, tpm, tsc_freq_mhz + ); + tpm + } else { + // PIT-based fallback (reduced 1 ms window). + use ::arch::cpu::pit; + const CALIBRATION_MS: u32 = 1; + let pit_reload: u16 = + ((pit::PIT_MAX_FREQUENCY as u64 * CALIBRATION_MS as u64 / 1000) + & 0xFFFF) as u16; + + warn!("cpuid leaf 0x16 unavailable, using pit-based calibration fallback"); + + // 4b. Program PIT channel 2 in one-shot mode. + let speaker: u8 = (::arch::io::in8(0x61) & 0xFC) | 0x01; + ::arch::io::out8(0x61, speaker); + ::arch::io::out8( + pit::PIT_CTRL, + pit::PIT_SEL2 + | pit::PIT_ACC_LOHI + | pit::PIT_MODE_TCOUNT + | pit::PIT_BINARY, + ); + ::arch::io::out8(pit::PIT_DATA + 2, (pit_reload & 0xFF) as u8); + ::arch::io::out8(pit::PIT_DATA + 2, (pit_reload >> 8) as u8); + + // Start the LAPIC timer counting from max value. + lapic.write(xapic::XAPIC_TICR, 0xFFFF_FFFF); + + // 5b. Wait for PIT channel 2 output (bit 5 of + // port 0x61) with a bounded busy-wait. + const PIT_CALIBRATION_MAX_ITERS: u32 = 10_000_000; + let mut pit_iters: u32 = 0; + while (::arch::io::in8(0x61) & 0x20) == 0 { + core::hint::spin_loop(); + pit_iters = pit_iters.wrapping_add(1); + if pit_iters >= PIT_CALIBRATION_MAX_ITERS { + warn!( + "pit calibration timeout after {} iterations", + PIT_CALIBRATION_MAX_ITERS + ); + break; + } + } + + // 6b. Read remaining LAPIC timer count. + let current_count: u32 = lapic.read(xapic::XAPIC_TCCR); + let elapsed_ticks: u32 = 0xFFFF_FFFF_u32.wrapping_sub(current_count); + let tpm: u32 = elapsed_ticks / CALIBRATION_MS; + + info!( + "lapic timer calibration (pit fallback): elapsed_ticks={}, \ + ticks_per_ms={}", + elapsed_ticks, tpm + ); + tpm + }; + + if ticks_per_ms == 0 { + warn!("lapic timer calibration underflow: using fallback ticks_per_ms=1"); + ticks_per_ms = 1; + } + + // 7. Program LAPIC timer in periodic mode with vector + // 0x20, initial count = ticks_per_ms (1 kHz). + lapic.write( + xapic::XAPIC_TIMER, + xapic::XapicTimer::new(0x20, false, false, 1).to_u32(), + ); + lapic.write(xapic::XAPIC_TICR, ticks_per_ms); + + info!("lapic periodic timer started (vector=0x20, period=1ms)"); + } + } + info!("using legacy pic"); return Ok(Self { intmap, diff --git a/src/libs/arch/src/x86/cpu/cpuid.rs b/src/libs/arch/src/x86/cpu/cpuid.rs index 0ce1bfa10c..68f3ad97a9 100644 --- a/src/libs/arch/src/x86/cpu/cpuid.rs +++ b/src/libs/arch/src/x86/cpu/cpuid.rs @@ -758,13 +758,53 @@ pub fn has_pbe() -> bool { /// The processor base frequency in MHz, or 0 if not supported. /// pub fn get_base_frequency_mhz() -> u32 { + // Check the maximum supported basic CPUID leaf. let (max_basic_leaf, _, _, _): (u32, u32, u32, u32) = cpuid(0); if max_basic_leaf < CPUID_FREQUENCY { + // Leaf 0x16 is not supported. return 0; } - let (eax, _, _, _): (u32, u32, u32, u32) = cpuid_subleaf(CPUID_FREQUENCY, 0); + // Issue CPUID with EAX = CPUID_FREQUENCY and ECX = 0 explicitly, so the + // subleaf selector is well-defined and does not depend on caller state. + let mut eax: u32 = CPUID_FREQUENCY; + let ebx: u32; + let mut ecx: u32 = 0; + let edx: u32; + + unsafe { + #[cfg(target_pointer_width = "32")] + ::core::arch::asm!( + "mov {ebx_backup}, ebx", + "cpuid", + "mov {ebx_out}, ebx", + "mov ebx, {ebx_backup}", + ebx_backup = out(reg) _, + ebx_out = out(reg) ebx, + inout("eax") eax, + inout("ecx") ecx, + out("edx") edx, + options(nomem, preserves_flags, nostack) + ); + + #[cfg(target_pointer_width = "64")] + ::core::arch::asm!( + "mov {ebx_backup}, rbx", + "cpuid", + "mov {ebx_out:e}, ebx", + "mov rbx, {ebx_backup}", + ebx_backup = out(reg) _, + ebx_out = out(reg) ebx, + inout("eax") eax, + inout("ecx") ecx, + out("edx") edx, + options(nomem, preserves_flags, nostack) + ); + } + + // Suppress unused-variable warnings for registers we must clobber. + let _ = (ebx, ecx, edx); // EAX contains the processor base frequency in MHz. eax From 8e1ee9455cec3636cce2d547293d84db996dd491 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Wed, 1 Apr 2026 18:45:48 -0700 Subject: [PATCH 2/2] [vmm][whp] E: Override CPUID leaf 0x16 with WHP clock frequency Hyper-V zeros out CPUID leaf 0x16 (Processor Frequency Information) even on CPUs that support it (e.g. Skylake-SP). This prevents the guest kernel from using RDTSC-based LAPIC timer calibration, forcing a fallback to the PIT busy-wait loop which generates ~908 VM exits. Query the host TSC frequency via WHvGetCapability(ProcessorClockFrequency) and inject it into CPUID leaf 0x16 EAX via WHvPartitionPropertyCodeCpuidResultList before partition setup. The guest kernel (on the LAPIC calibration branch) sees a non-zero base frequency and uses an RDTSC spin loop instead of PIT, eliminating ~908 PMIO read exits. Benchmark results (p50, 50 iterations): Before (dev): 81,106 us total, 59,384 us guest_exec After (this fix): 67,029 us total, 48,641 us guest_exec Improvement: 17.4% total, 18.1% guest_exec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/uservm/src/vmm/microvm/whp/partition.rs | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/uservm/src/vmm/microvm/whp/partition.rs b/src/uservm/src/vmm/microvm/whp/partition.rs index e9e2cd83e4..8ed755bcb4 100644 --- a/src/uservm/src/vmm/microvm/whp/partition.rs +++ b/src/uservm/src/vmm/microvm/whp/partition.rs @@ -8,13 +8,20 @@ use ::anyhow::Result; use ::log::{ error, + info, trace, + warn, }; use windows::Win32::System::Hypervisor::{ + WHV_CAPABILITY, WHV_PARTITION_HANDLE, WHV_PARTITION_PROPERTY, + WHV_X64_CPUID_RESULT, + WHvCapabilityCodeProcessorClockFrequency, WHvCreatePartition, WHvDeletePartition, + WHvGetCapability, + WHvPartitionPropertyCodeCpuidResultList, WHvPartitionPropertyCodeLocalApicEmulationMode, WHvPartitionPropertyCodeProcessorCount, WHvSetPartitionProperty, @@ -107,6 +114,49 @@ impl WhpPartition { })?; } + // Override CPUID leaf 0x16 (Processor Frequency Information) so + // the guest kernel can use RDTSC-based LAPIC timer calibration + // instead of the PIT busy-wait loop (~908 VM exits eliminated). + // Hyper-V zeros out leaf 0x16 even when the host CPU supports it. + unsafe { + let freq_hz: u64 = Self::query_processor_clock_frequency(); + let freq_mhz: u32 = (freq_hz / 1_000_000) as u32; + if freq_mhz > 0 { + let cpuid_entry = WHV_X64_CPUID_RESULT { + Function: 0x16, + Reserved: [0; 3], + Eax: freq_mhz, + Ebx: freq_mhz, + Ecx: 0, + Edx: 0, + }; + let entry_size: u32 = + u32::try_from(std::mem::size_of::()) + .map_err(|_| anyhow::anyhow!("CPUID result size overflow"))?; + WHvSetPartitionProperty( + handle, + WHvPartitionPropertyCodeCpuidResultList, + (&cpuid_entry as *const WHV_X64_CPUID_RESULT).cast::(), + entry_size, + ) + .map_err(|e| { + let reason: String = + format!("failed to set CPUID result list (error={e:?})"); + error!("WhpPartition::new(): {reason}"); + anyhow::anyhow!(reason) + })?; + info!( + "overriding CPUID leaf 0x16: base_freq={}MHz (from {}Hz)", + freq_mhz, freq_hz + ); + } else { + warn!( + "could not query processor clock frequency; \ + guest will use PIT-based calibration" + ); + } + } + // Setup the partition (finalizes configuration). unsafe { WHvSetupPartition(handle).map_err(|e| { @@ -127,6 +177,31 @@ impl WhpPartition { pub fn handle(&self) -> WHV_PARTITION_HANDLE { self.handle } + + /// Queries the host processor TSC clock frequency via WHP. + /// Returns 0 if the query fails or the frequency is unavailable. + unsafe fn query_processor_clock_frequency() -> u64 { + let mut cap: WHV_CAPABILITY = unsafe { std::mem::zeroed() }; + let cap_size: u32 = match u32::try_from(std::mem::size_of::()) { + Ok(s) => s, + Err(_) => return 0, + }; + let result = unsafe { + WHvGetCapability( + WHvCapabilityCodeProcessorClockFrequency, + (&mut cap as *mut WHV_CAPABILITY).cast::(), + cap_size, + None, + ) + }; + match result { + Ok(()) => unsafe { cap.ProcessorClockFrequency }, + Err(e) => { + warn!("WHvGetCapability(ProcessorClockFrequency) failed: {e:?}"); + 0 + } + } + } } impl Drop for WhpPartition {