From 2b0b1ed561b6a802865f96388f64b0fa39fd362f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 17 Oct 2026 14:34:19 +0200 Subject: [PATCH] Add a JIT stats debug overlay Shows the number of blocks, how full the native code space and the IR arena are, and how many times each filling up cleared the cache. The "Ran out of block numbers" message was logged for both of those, and there is no block number limit. It now says which one filled up. Also hide the GPU profile and allocator overlays by value on backends that lack them. Cutting the last two off the list had been hiding the framebuffer list since it was added after them. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/CodeBlock.h | 4 ++++ Core/ConfigValues.h | 1 + Core/MIPS/IR/IRJit.cpp | 42 ++++++++++++++++++++++++++------- Core/MIPS/IR/IRJit.h | 16 ++++++++++++- Core/MIPS/IR/IRNativeCommon.cpp | 7 ++++++ Core/MIPS/IR/IRNativeCommon.h | 1 + Core/MIPS/JitCommon/JitCommon.h | 17 +++++++++++++ UI/DebugOverlay.cpp | 39 ++++++++++++++++++++++++++++++ UI/DevScreens.cpp | 8 ++++--- 9 files changed, 122 insertions(+), 13 deletions(-) diff --git a/Common/CodeBlock.h b/Common/CodeBlock.h index 853c15c149..2e3894dde2 100644 --- a/Common/CodeBlock.h +++ b/Common/CodeBlock.h @@ -40,6 +40,10 @@ public: return ptr - region; } + size_t GetRegionSize() const { + return region_size; + } + virtual const u8 *GetCodePtrFromWritablePtr(u8 *ptr) = 0; virtual u8 *GetWritablePtrFromCodePtr(const u8 *ptr) = 0; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index fd5eb1eae7..e9abd43dcb 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -276,6 +276,7 @@ enum class DebugOverlay : int { GPU_PROFILE, GPU_ALLOCATOR, FRAMEBUFFER_LIST, + JIT_STATS, }; // Android-only for now diff --git a/Core/MIPS/IR/IRJit.cpp b/Core/MIPS/IR/IRJit.cpp index f0836213dd..93264f4881 100644 --- a/Core/MIPS/IR/IRJit.cpp +++ b/Core/MIPS/IR/IRJit.cpp @@ -190,9 +190,15 @@ void IRJit::Compile(u32 em_address) { std::vector instructions; u32 mipsBytes; - if (!CompileBlock(em_address, instructions, mipsBytes)) { - // Ran out of block numbers - need to reset. - ERROR_LOG(Log::JIT, "Ran out of block numbers, clearing cache"); + const CompileResult result = CompileBlock(em_address, instructions, mipsBytes); + if (result != CompileResult::OK) { + if (result == CompileResult::ARENA_FULL) { + ERROR_LOG(Log::JIT, "IR arena full (%d blocks), clearing cache", blocks_.GetNumBlocks()); + irArenaFullClears_++; + } else { + ERROR_LOG(Log::JIT, "Native code space full (%d blocks), clearing cache", blocks_.GetNumBlocks()); + codeFullClears_++; + } ClearCache(); CompileBlock(em_address, instructions, mipsBytes); } @@ -205,7 +211,7 @@ void IRJit::Compile(u32 em_address) { } // WARNING! This can be called from IRInterpret / the JIT, through the function preload stuff! -bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes) { +IRJit::CompileResult IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes) { _dbg_assert_(compilerEnabled_); frontend_.DoJit(em_address, instructions, mipsBytes); @@ -214,8 +220,8 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 int block_num = blocks_.AllocateBlock(em_address, mipsBytes, instructions); if (block_num < 0) { WARN_LOG(Log::JIT, "Failed to allocate block for %08x (%d instructions)", em_address, (int)instructions.size()); - // Out of block numbers. Caller will handle. - return false; + // Caller will handle. + return CompileResult::ARENA_FULL; } IRBlock *b = blocks_.GetBlock(block_num); @@ -226,7 +232,7 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 } if (!CompileNativeBlock(&blocks_, block_num)) - return false; + return CompileResult::CODE_FULL; if (mipsTracer.tracing_enabled) { mipsTracer.prepare_block(b, blocks_); @@ -235,7 +241,16 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 // Updates stats, also enters the block in the block shadow. blocks_.FinalizeBlock(block_num); FinalizeNativeBlock(&blocks_, block_num); - return true; + return CompileResult::OK; +} + +void IRJit::GetStats(JitStats *stats) const { + stats->numBlocks = blocks_.GetNumBlocks(); + stats->numValidBlocks = blocks_.CountValidBlocks(); + stats->irArenaUsed = blocks_.GetArenaSize(); + stats->irArenaSize = IRBlockCache::MAX_ARENA_SIZE; + stats->irArenaFullClears = irArenaFullClears_; + stats->codeFullClears = codeFullClears_; } void IRJit::RunLoopUntil(u64 globalticks) { @@ -331,8 +346,17 @@ void IRBlockCache::Clear() { IRBlockCache::IRBlockCache(bool compileToNative) : compileToNative_(compileToNative) {} +int IRBlockCache::CountValidBlocks() const { + int count = 0; + for (const IRBlock &block : blocks_) { + if (block.IsValid()) { + count++; + } + } + return count; +} + int IRBlockCache::AllocateBlock(int emAddr, u32 origSize, const std::vector &insts) { - const u32 MAX_ARENA_SIZE = 0x1000000 - 1; if (arena_.empty()) { // Offset 0 would mean "no block" in the block shadow, and 1 a replacement hook. arena_.push_back(IRInst{ IROp::Bad }); diff --git a/Core/MIPS/IR/IRJit.h b/Core/MIPS/IR/IRJit.h index 1e24f36f63..e828ec30f5 100644 --- a/Core/MIPS/IR/IRJit.h +++ b/Core/MIPS/IR/IRJit.h @@ -111,10 +111,15 @@ public: Clear(); } + // In IR instructions. + static constexpr u32 MAX_ARENA_SIZE = 0x1000000 - 1; + void Clear(); std::vector FindInvalidatedBlockNumbers(u32 address, u32 length); void FinalizeBlock(int blockNum); int GetNumBlocks() const override { return (int)blocks_.size(); } + int CountValidBlocks() const; + size_t GetArenaSize() const { return arena_.size(); } int AllocateBlock(int emAddr, u32 origSize, const std::vector &inst); IRBlock *GetBlock(int blockNum) { if (blockNum >= 0 && blockNum < (int)blocks_.size()) { @@ -204,6 +209,7 @@ public: void ClearCache() override; void InvalidateCacheAt(u32 em_address, int length = 4) override; void UpdateFCR31() override; + void GetStats(JitStats *stats) const override; bool CodeInRange(const u8 *ptr) const override { return false; @@ -222,7 +228,12 @@ protected: void RunBlocks(); virtual void WriteProfileReport(); - bool CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes); + enum class CompileResult { + OK, + ARENA_FULL, + CODE_FULL, + }; + CompileResult CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes); virtual bool CompileNativeBlock(IRBlockCache *irBlockCache, int block_num) { return true; } virtual void FinalizeNativeBlock(IRBlockCache *irBlockCache, int block_num) {} virtual void InvalidateNativeBlock(IRBlockCache *irBlockCache, int block_num) {} @@ -240,6 +251,9 @@ protected: // Set by --jit-profile. The IR interpreter times every block, the native JITs sample the host PC. bool profiling_ = false; + int irArenaFullClears_ = 0; + int codeFullClears_ = 0; + // where to write branch-likely trampolines. not used atm // u32 blTrampolines_; // int blTrampolineCount_; diff --git a/Core/MIPS/IR/IRNativeCommon.cpp b/Core/MIPS/IR/IRNativeCommon.cpp index c1e872f843..c3dd2bf091 100644 --- a/Core/MIPS/IR/IRNativeCommon.cpp +++ b/Core/MIPS/IR/IRNativeCommon.cpp @@ -906,6 +906,13 @@ const u8 *IRNativeJit::GetCodeBase() const { return backend_->CodeBlock().GetBasePtr(); } +void IRNativeJit::GetStats(JitStats *stats) const { + IRJit::GetStats(stats); + const CodeBlockCommon &code = backend_->CodeBlock(); + stats->codeUsed = code.GetOffset(code.GetCodePtr()); + stats->codeSize = code.GetRegionSize(); +} + u32 IRNativeJit::GetBlockShadowHookValue() const { return (u32)backend_->OffsetFromCodePtr(backend_->GetNativeHooks().blockShadowHook); } diff --git a/Core/MIPS/IR/IRNativeCommon.h b/Core/MIPS/IR/IRNativeCommon.h index efeb2cd6c8..b1329bcc06 100644 --- a/Core/MIPS/IR/IRNativeCommon.h +++ b/Core/MIPS/IR/IRNativeCommon.h @@ -227,6 +227,7 @@ public: JitBlockCacheDebugInterface *GetBlockCacheDebugInterface() override; const u8 *GetCodeBase() const override; + void GetStats(JitStats *stats) const override; protected: void Init(IRNativeBackend &backend); diff --git a/Core/MIPS/JitCommon/JitCommon.h b/Core/MIPS/JitCommon/JitCommon.h index a3e60f4f8d..5f51de8b4a 100644 --- a/Core/MIPS/JitCommon/JitCommon.h +++ b/Core/MIPS/JitCommon/JitCommon.h @@ -114,10 +114,27 @@ namespace MIPSComp { virtual int Replace_fabsf() = 0; }; + // For the JIT stats debug overlay. + struct JitStats { + int numBlocks = 0; + int numValidBlocks = 0; + // In IR instructions. Invalidated blocks keep their space until the cache is cleared. + size_t irArenaUsed = 0; + size_t irArenaSize = 0; + // In bytes. Zero when not compiling to native code. + size_t codeUsed = 0; + size_t codeSize = 0; + // How many times the cache was cleared because one of them filled up. + int irArenaFullClears = 0; + int codeFullClears = 0; + }; + class JitInterface { public: virtual ~JitInterface() {} + virtual void GetStats(JitStats *stats) const {} + virtual const u8 *GetCodeBase() const = 0; // Just for displaying in the debugger. virtual bool CodeInRange(const u8 *ptr) const = 0; virtual bool DescribeCodePtr(const u8 *ptr, std::string &name) = 0; diff --git a/UI/DebugOverlay.cpp b/UI/DebugOverlay.cpp index 0bf44c726c..4ad819e980 100644 --- a/UI/DebugOverlay.cpp +++ b/UI/DebugOverlay.cpp @@ -7,6 +7,7 @@ #include "Common/Data/Text/StringWriter.h" #include "Core/MIPS/MIPS.h" +#include "Core/MIPS/JitCommon/JitCommon.h" #include "Core/HW/Display.h" #include "Core/FrameTiming.h" #include "Core/HLE/sceSas.h" @@ -66,6 +67,40 @@ static void DrawDebugStats(UIContext *ctx, const Bounds &bounds) { ctx->RebindTexture(); } +static void DrawJitStats(UIContext *ctx, const Bounds &bounds) { + FontID ubuntu24("UBUNTU24"); + + char statbuf[1024]; + StringWriter w(statbuf, sizeof(statbuf)); + if (MIPSComp::jit) { + MIPSComp::JitStats stats; + MIPSComp::jit->GetStats(&stats); + const double irPercent = stats.irArenaSize ? 100.0 * stats.irArenaUsed / stats.irArenaSize : 0.0; + w.F("Blocks: %d (%d valid)\n", stats.numBlocks, stats.numValidBlocks); + if (stats.codeSize) { + // The native JITs compile from the IR, and keep it for the debugger. + w.F("Native code: %0.2f / %0.2f MB (%0.1f%%)\n", stats.codeUsed / (1024.0 * 1024.0), stats.codeSize / (1024.0 * 1024.0), + 100.0 * stats.codeUsed / stats.codeSize); + w.F("IR (debug info): %d / %d instructions (%0.1f%%)\n", (int)stats.irArenaUsed, (int)stats.irArenaSize, irPercent); + w.F("Cleared because full: native code %d, IR %d\n", stats.codeFullClears, stats.irArenaFullClears); + } else { + w.F("IR arena: %d / %d instructions (%0.1f%%)\n", (int)stats.irArenaUsed, (int)stats.irArenaSize, irPercent); + w.F("Cleared because full: %d\n", stats.irArenaFullClears); + } + } else { + w.W("No JIT (interpreter)\n"); + } + + ctx->Flush(); + ctx->BindFontTexture(); + ctx->Draw()->SetFontScale(.7f, .7f); + ctx->Draw()->DrawTextRect(ubuntu24, w.as_view(), bounds.x + 11, bounds.y + 31, bounds.w - 20, bounds.h - 30, 0xc0000000, FLAG_DYNAMIC_ASCII); + ctx->Draw()->DrawTextRect(ubuntu24, w.as_view(), bounds.x + 10, bounds.y + 30, bounds.w - 20, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); + ctx->Draw()->SetFontScale(1.0f, 1.0f); + ctx->Flush(); + ctx->RebindTexture(); +} + static void DrawAudioDebugStats(UIContext *ctx, const Bounds &bounds) { FontID ubuntu24("UBUNTU24"); @@ -271,6 +306,10 @@ void DrawDebugOverlay(UIContext *ctx, const Bounds &bounds, DebugOverlay overlay if (inGame) DrawFramebufferList(ctx, gpu, layoutBounds); break; + case DebugOverlay::JIT_STATS: + if (inGame) + DrawJitStats(ctx, layoutBounds); + break; default: break; } diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index c652c919fa..8f1073904b 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -105,16 +105,18 @@ static const char *g_debugOverlayList[] = { "GPU Profile", "GPU Allocator Viewer", "Framebuffer list", + "JIT stats", }; void AddOverlayList(UI::ViewGroup *items, ScreenManager *screenManager) { using namespace UI; auto dev = GetI18NCategory(I18NCat::DEVELOPER); - int numOverlays = ARRAY_SIZE(g_debugOverlayList); + static_assert(ARRAY_SIZE(g_debugOverlayList) == (int)DebugOverlay::JIT_STATS + 1, "g_debugOverlayList must match DebugOverlay"); + PopupMultiChoice *choice = items->Add(new PopupMultiChoice((int *)&g_Config.iDebugOverlay, dev->T("Debug overlay"), g_debugOverlayList, 0, ARRAY_SIZE(g_debugOverlayList), I18NCat::DEVELOPER, screenManager)); if (!(g_Config.iGPUBackend == (int)GPUBackend::VULKAN || g_Config.iGPUBackend == (int)GPUBackend::OPENGL)) { - numOverlays -= 2; // skip the last 2. + choice->HideChoice((int)DebugOverlay::GPU_PROFILE); + choice->HideChoice((int)DebugOverlay::GPU_ALLOCATOR); } - items->Add(new PopupMultiChoice((int *)&g_Config.iDebugOverlay, dev->T("Debug overlay"), g_debugOverlayList, 0, numOverlays, I18NCat::DEVELOPER, screenManager)); } void SaveFrameDump() {