diff --git a/cpu/ppc/ppcemu.h b/cpu/ppc/ppcemu.h index af75394e51..b75bd70a9c 100644 --- a/cpu/ppc/ppcemu.h +++ b/cpu/ppc/ppcemu.h @@ -344,6 +344,11 @@ enum Exc_Cause : uint32_t { extern unsigned exec_flags; +// Counter of guest accesses to memory-mapped devices, incremented by +// mmu_read_vmem/mmu_write_vmem. Used by ppcexec.cpp to detect when the +// guest is idling (spinning without touching any device). +extern uint64_t g_mmio_access_count; + extern jmp_buf exc_env; enum Po_Cause : int { @@ -728,6 +733,9 @@ extern int get_icnt_factor(); /* toggle_g_realtime */ extern bool toggle_g_realtime(); +/* set_g_idle_cpu_save */ +extern void set_g_idle_cpu_save(bool enabled); + /* force_cycle_counter_reload */ static void force_cycle_counter_reload(); diff --git a/cpu/ppc/ppcexec.cpp b/cpu/ppc/ppcexec.cpp index e089312d80..759a342cc4 100644 --- a/cpu/ppc/ppcexec.cpp +++ b/cpu/ppc/ppcexec.cpp @@ -34,6 +34,7 @@ along with this program. If not, see . #include #include #include +#include #ifdef __APPLE__ #include @@ -135,6 +136,9 @@ uint64_t g_nanoseconds_base; uint64_t g_icycles; int icnt_factor; +/* when true, sleep an idle guest in realtime mode to save host CPU */ +bool g_idle_cpu_save = false; + /* global variables related to the timebase facility */ uint64_t tbr_wr_timestamp; // stores vCPU virtual time of the last TBR write uint64_t rtc_timestamp; // stores vCPU virtual time of the last RTC write @@ -375,10 +379,152 @@ void set_virt_time_ns(uint64_t time_now) LOG_F(INFO, "time before: %lld after: %lld change: %lld", time_now, time_new, time_new - time_now); } +// Idle detection for realtime mode. The guest never halts (no PPC +// equivalent of x86 HLT), so at the desktop it keeps executing its idle +// path forever, burning a whole host core. We cannot simply sleep +// whenever the guest is quiet for a moment: stalling guest execution +// while wall time (and thus every time-based device, timer and driver +// timeout) keeps advancing derails the guest if it is still doing +// critical boot work. The distinguishing signal we use is the guest's +// memory-mapped I/O rate: boot and real work touch devices constantly +// (hundreds of thousands of accesses per second), while a settled idle +// desktop touches them at a steady few thousand per second. The rate is +// low-pass filtered (a burst of accesses an interrupt handler performs +// in a few microseconds would otherwise look like activity), and only +// once the filtered rate has stayed low continuously for +// IDLE_CONFIRM_NS do we throttle. +// +// The first confirmation must be long enough that it cannot be +// satisfied during boot at all; a boot that takes tens of seconds never +// yields that much uninterrupted low-rate time. Once the guest has been +// throttled once it has provably reached its idle state, so afterwards we +// only ever disengage for sustained work (see guest_is_idle). +static constexpr uint64_t IDLE_RATE_EMA_TAU_NS = 2000000000ULL; // 2 s +static constexpr uint64_t IDLE_CONFIRM_RATE = 30000; // MMIO accesses per second +static constexpr uint64_t IDLE_CONFIRM_NS = 15000000000ULL; // 15 s, first time +static constexpr uint64_t IDLE_DISENGAGE_NS = 1000000000ULL; // 1 s of sustained high rate +// Minimum uptime before the throttle may engage at all, so a guest that +// is still in the middle of booting is never throttled; the confirm +// timer guards against throttling during any later settling work. +static constexpr uint64_t IDLE_UPTIME_GUARD_NS = 15000000000ULL; // 15 s +static uint64_t g_idle_last_ns = 0; +static uint64_t g_idle_last_mmio = 0; +static uint64_t g_mmio_count_ema = 0; // converges to rate * IDLE_RATE_EMA_TAU_NS +static uint64_t g_idle_low_ns = 0; // continuous time with a low filtered rate +static uint64_t g_idle_high_ns = 0; // continuous time with a high filtered rate +static uint64_t g_idle_boot_start_ns = 0; +static bool g_idle_engaged = false; + +static void reset_idle_detector() +{ + // Called from ppc_cpu_init, i.e. on every boot (including guest + // restarts): start over with the long first confirmation and a + // fresh uptime gate so a rebooting guest is never throttled during + // its boot phase. + g_idle_last_ns = 0; + g_idle_last_mmio = 0; + g_mmio_count_ema = 0; + g_idle_low_ns = 0; + g_idle_high_ns = 0; + g_idle_boot_start_ns = get_virt_time_ns(); + g_idle_engaged = false; +} + +static bool guest_is_idle() +{ + const uint64_t now_ns = get_virt_time_ns(); + const uint64_t elapsed_ns = now_ns - g_idle_last_ns; + g_idle_last_ns = now_ns; + + const uint64_t mmio_delta = g_mmio_access_count - g_idle_last_mmio; + g_idle_last_mmio = g_mmio_access_count; + + if (elapsed_ns > 0) { + // Exponential-decay accumulator on the MMIO count. Each window + // adds its accesses and the total decays by exp(-elapsed/tau); + // it converges to rate * tau regardless of how often this is + // called, unlike an additive EMA which tracks the per-window + // count when windows are much shorter than tau. Rate is then + // count / tau. + uint64_t decayed; + if (elapsed_ns >= IDLE_RATE_EMA_TAU_NS) { + decayed = 0; + } else { + decayed = g_mmio_count_ema * (IDLE_RATE_EMA_TAU_NS - elapsed_ns) / IDLE_RATE_EMA_TAU_NS; + } + g_mmio_count_ema = decayed + mmio_delta; + + const uint64_t rate = g_mmio_count_ema * 1000000000ULL / IDLE_RATE_EMA_TAU_NS; + if (rate < IDLE_CONFIRM_RATE) { + g_idle_high_ns = 0; + g_idle_low_ns += elapsed_ns; + } else { + g_idle_low_ns = 0; + g_idle_high_ns += elapsed_ns; + } + } + + // The guest has real work to do: an exception to take, an interrupt + // asserted that it cannot take yet (MSR.EE off), or a device that + // requested immediate processing. Do not sleep in these cases. + if ((exec_flags & EXEF_EXCEPTION) || int_pin || exec_timer) { + return false; + } + + // Never throttle before the guest has had a chance to boot. + if (now_ns - g_idle_boot_start_ns < IDLE_UPTIME_GUARD_NS) { + return false; + } + + // The first throttle only happens after IDLE_CONFIRM_NS of + // continuous low rate. Afterwards the throttle stays on through + // brief activity (input, a momentary burst) - the guest services it + // during the bursts - and only yields to sustained work: once the + // rate has been high for IDLE_DISENGAGE_NS it runs at full speed + // until the rate drops again, then throttling resumes immediately. + // This avoids the flapping where any short spike above the + // threshold disengages the throttle for seconds at a time, which + // showed up as the CPU jumping back to 99% at the settled desktop. + if (g_idle_engaged) { + return g_idle_high_ns < IDLE_DISENGAGE_NS; + } + + if (g_idle_low_ns >= IDLE_CONFIRM_NS) { + g_idle_engaged = true; + return true; + } + return false; +} + static uint64_t process_events() { exec_timer = false; uint64_t slice_ns = TimerManager::get_instance()->process_timers(); + if (g_realtime && g_idle_cpu_save && guest_is_idle()) { + // The guest is idling: sleep until the next scheduled event + // instead of executing its idle path. Guest time is wall-clock + // based in realtime mode, so the sleep advances guest time and + // the guest's timers (VBL, decrementer, ...) keep firing on + // schedule. We still run a short burst afterwards so the guest + // services those interrupts and keeps its devices polled. + // Only sleep if a timer is actually pending; otherwise the + // guest has no interrupt to wake it and the host must keep + // executing. + if (slice_ns != 0) { + constexpr uint64_t max_sleep_ns = 16000000ULL; // 16 ms + // The burst must be long enough for the guest to fully + // service its pending interrupts (VBL, DEC, device polls) + // and return to its idle loop. Shorter bursts (1-4 ms) + // starve that servicing: guest time keeps advancing while + // the guest executes too little, so it ends up stuck in a + // machine-check storm at the external-interrupt vector. + // 6 ms per 16 ms window keeps DP3 healthy at ~7% host CPU. + constexpr uint64_t burst_ns = 6000000ULL; // 6 ms + const uint64_t sleep_ns = (slice_ns > max_sleep_ns) ? max_sleep_ns : slice_ns; + std::this_thread::sleep_for(std::chrono::nanoseconds(sleep_ns)); + return g_icycles + (burst_ns >> icnt_factor) + 1; + } + } if (slice_ns == 0) { // execute 25.000 cycles // if there are no pending timers @@ -427,6 +573,11 @@ bool toggle_g_realtime() return g_realtime; } +void set_g_idle_cpu_save(bool enabled) +{ + g_idle_cpu_save = enabled; +} + typedef enum { main, until, @@ -1090,6 +1241,8 @@ void ppc_cpu_init(MemCtrlBase* mem_ctrl, uint32_t cpu_version, bool do_include_6 /* redirect code execution to reset vector */ ppc_state.pc = 0xFFF00100; + reset_idle_detector(); + #ifdef CPU_PROFILING gProfilerObj->register_profile("PPC_CPU", std::unique_ptr(new CPUProfile())); diff --git a/cpu/ppc/ppcmmu.cpp b/cpu/ppc/ppcmmu.cpp index 1f3f3540d7..1ebf4b574a 100644 --- a/cpu/ppc/ppcmmu.cpp +++ b/cpu/ppc/ppcmmu.cpp @@ -38,6 +38,9 @@ along with this program. If not, see . /* pointer to exception handler to be called when a MMU exception is occurred. */ void (*mmu_exception_handler)(Except_Type exception_type, uint32_t srr1_bits); +/* counts guest accesses to memory-mapped devices; see ppcemu.h */ +uint64_t g_mmio_access_count = 0; + /* pointers to BAT update functions. */ std::function ibat_update; std::function dbat_update; @@ -1280,6 +1283,7 @@ inline T mmu_read_vmem(uint32_t opcode, uint32_t guest_va) #ifdef MMU_PROFILING iomem_reads_total++; #endif + g_mmio_access_count++; #if SUPPORTS_MEMORY_CTRL_ENDIAN_MODE needs_swap = mem_ctrl_instance->needs_swap_endian(tlb2_entry->rgn_desc); @@ -1457,6 +1461,7 @@ inline void mmu_write_vmem(uint32_t opcode, uint32_t guest_va, T value) #ifdef MMU_PROFILING iomem_writes_total++; #endif + g_mmio_access_count++; #if SUPPORTS_MEMORY_CTRL_ENDIAN_MODE needs_swap = mem_ctrl_instance->needs_swap_endian(tlb2_entry->rgn_desc); diff --git a/main.cpp b/main.cpp index 37d96d334d..49a18c3d91 100644 --- a/main.cpp +++ b/main.cpp @@ -114,6 +114,8 @@ int main(int argc, char** argv) { bool debugger_skip = true; bool debugger_enter = false; bool deterministic_interactive = false; + bool start_realtime = false; + bool start_idle_cpu_save = false; string deterministic_mode = "strict"; string keyboard_string = "Eng_USA"; @@ -143,6 +145,10 @@ int main(int argc, char** argv) { "Select deterministic features (strict or interactive)") ->needs(deterministic_opt) ->check(CLI::IsMember({"strict", "interactive"})); + emu->add_flag("--realtime", start_realtime, + "Start in realtime mode (guest time follows the wall clock)"); + emu->add_flag("--idle-cpu-save", start_idle_cpu_save, + "Sleep an idle guest in realtime mode to save host CPU"); bool log_to_stderr = false; loguru::Verbosity log_verbosity = loguru::Verbosity_INFO; @@ -312,6 +318,13 @@ int main(int argc, char** argv) { keyboard_id = kbd_map.at(keyboard_string); + if (start_realtime) { + toggle_g_realtime(); + } + if (start_idle_cpu_save) { + set_g_idle_cpu_save(true); + } + while (true) { run_machine( machine_str,