diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 6635e158..8df785b2 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -389,6 +389,7 @@ STR_DELETE_CONFIRM: "Delete this server?" STR_OPDS_SERVERS: "OPDS Servers" STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" +STR_RENDER_BENCHMARK: "Render Benchmark" STR_WEATHER: "Weather" STR_WEATHER_LOCATION: "Location" STR_WEATHER_UNITS: "Units" diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 8293f805..1db9674e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderFootnotesActivity.h" #include "EpubReaderPercentSelectionActivity.h" +#include "EpubRenderBenchmarkActivity.h" #include "GlobalBookmarkIndex.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" @@ -79,6 +81,20 @@ int clampPercent(int percent) { return percent; } +const char* orientationToString(const GfxRenderer::Orientation orientation) { + switch (orientation) { + case GfxRenderer::Portrait: + return "Portrait"; + case GfxRenderer::LandscapeClockwise: + return "Landscape CW"; + case GfxRenderer::PortraitInverted: + return "Portrait Inverted"; + case GfxRenderer::LandscapeCounterClockwise: + return "Landscape CCW"; + } + return "Unknown"; +} + } // namespace void EpubReaderActivity::onEnter() { @@ -593,6 +609,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction onGoHome(); return; } + case EpubReaderMenuActivity::MenuAction::RENDER_BENCHMARK: { + runRenderBenchmark(); + break; + } case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { { RenderLock lock(*this); @@ -620,6 +640,202 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } } +void EpubReaderActivity::runRenderBenchmark() { + if (!epub) { + return; + } + + if (!section) { + requestUpdateAndWait(); + if (!section) { + return; + } + } + + const LastRenderStats startSnapshot = lastRenderStats; + BenchmarkAggregate aggregate; + auto recordRender = [&aggregate](const LastRenderStats& snapshot) { + if (!snapshot.valid) { + return; + } + + aggregate.renderCount++; + aggregate.imagePageCount += snapshot.hadImages ? 1 : 0; + aggregate.cacheRebuildCount += snapshot.cacheRebuilt ? 1 : 0; + if (snapshot.footnoteCount > aggregate.maxFootnotes) { + aggregate.maxFootnotes = snapshot.footnoteCount; + } + + aggregate.totalRequestRenderMs += snapshot.requestRenderMs; + if (aggregate.renderCount == 1 || snapshot.requestRenderMs < aggregate.minRequestRenderMs) { + aggregate.minRequestRenderMs = snapshot.requestRenderMs; + } + if (snapshot.requestRenderMs > aggregate.maxRequestRenderMs) { + aggregate.maxRequestRenderMs = snapshot.requestRenderMs; + } + + aggregate.totalRenderMs += snapshot.phases.totalMs; + if (aggregate.renderCount == 1 || snapshot.phases.totalMs < aggregate.minRenderMs) { + aggregate.minRenderMs = snapshot.phases.totalMs; + } + if (snapshot.phases.totalMs > aggregate.maxRenderMs) { + aggregate.maxRenderMs = snapshot.phases.totalMs; + } + + aggregate.totalSectionLoadMs += snapshot.sectionLoadMs; + aggregate.totalPageLoadMs += snapshot.pageLoadMs; + aggregate.totalPhases.prewarmMs += snapshot.phases.prewarmMs; + aggregate.totalPhases.bwRenderMs += snapshot.phases.bwRenderMs; + aggregate.totalPhases.displayMs += snapshot.phases.displayMs; + aggregate.totalPhases.bwStoreMs += snapshot.phases.bwStoreMs; + aggregate.totalPhases.grayLsbMs += snapshot.phases.grayLsbMs; + aggregate.totalPhases.grayMsbMs += snapshot.phases.grayMsbMs; + aggregate.totalPhases.grayDisplayMs += snapshot.phases.grayDisplayMs; + aggregate.totalPhases.bwRestoreMs += snapshot.phases.bwRestoreMs; + aggregate.totalPhases.totalMs += snapshot.phases.totalMs; + + aggregate.totalFontCacheHits += snapshot.fontCacheHits; + aggregate.totalFontCacheMisses += snapshot.fontCacheMisses; + aggregate.totalFontDecompressMs += snapshot.fontDecompressMs; + aggregate.totalFontGetBitmapTimeUs += snapshot.fontGetBitmapTimeUs; + aggregate.totalFontGetBitmapCalls += snapshot.fontGetBitmapCalls; + + if (aggregate.renderCount == 1 || snapshot.freeHeapAfter < aggregate.minFreeHeapAfter) { + aggregate.minFreeHeapAfter = snapshot.freeHeapAfter; + } + if (snapshot.freeHeapAfter > aggregate.maxFreeHeapAfter) { + aggregate.maxFreeHeapAfter = snapshot.freeHeapAfter; + } + }; + const unsigned long startTime = millis(); + int forwardTurns = 0; + int backwardTurns = 0; + + for (int i = 0; i < 10; i++) { + if (!stepPageState(true)) { + break; + } + requestUpdateAndWait(); + recordRender(lastRenderStats); + forwardTurns++; + } + + const unsigned long forwardMs = millis() - startTime; + const unsigned long backwardStart = millis(); + + for (int i = 0; i < 10; i++) { + if (!stepPageState(false)) { + break; + } + requestUpdateAndWait(); + recordRender(lastRenderStats); + backwardTurns++; + } + + const unsigned long backwardMs = millis() - backwardStart; + + startActivityForResult( + std::make_unique( + renderer, mappedInput, + buildRenderBenchmarkReport(startSnapshot, aggregate, forwardTurns, forwardMs, backwardTurns, backwardMs)), + [this](const ActivityResult&) { requestUpdate(); }); +} + +std::string EpubReaderActivity::buildRenderBenchmarkReport(const LastRenderStats& startSnapshot, + const BenchmarkAggregate& aggregate, const int forwardTurns, + const unsigned long forwardMs, const int backwardTurns, + const unsigned long backwardMs) const { + const LastRenderStats& endSnapshot = lastRenderStats.valid ? lastRenderStats : startSnapshot; + + std::string report; + report.reserve(768); + + auto appendLine = [&report](const std::string& line) { + if (!report.empty()) { + report += '\n'; + } + report += line; + }; + + appendLine("Forward 10: " + std::to_string(forwardTurns) + " turns in " + std::to_string(forwardMs) + " ms"); + if (forwardTurns > 0) { + appendLine("Forward avg: " + std::to_string(forwardMs / static_cast(forwardTurns)) + " ms/turn"); + } + appendLine("Backward 10: " + std::to_string(backwardTurns) + " turns in " + std::to_string(backwardMs) + " ms"); + if (backwardTurns > 0) { + appendLine("Backward avg: " + std::to_string(backwardMs / static_cast(backwardTurns)) + " ms/turn"); + } + appendLine("Measured renders: " + std::to_string(aggregate.renderCount) + ", image pages " + + std::to_string(aggregate.imagePageCount) + ", cache rebuilds " + + std::to_string(aggregate.cacheRebuildCount)); + + appendLine("Start: spine " + std::to_string(startSnapshot.spineIndex) + ", page " + + std::to_string(startSnapshot.pageIndex + 1) + "/" + std::to_string(startSnapshot.pageCount)); + appendLine("End: spine " + std::to_string(endSnapshot.spineIndex) + ", page " + + std::to_string(endSnapshot.pageIndex + 1) + "/" + std::to_string(endSnapshot.pageCount)); + appendLine("Orientation: " + + std::string(orientationToString(static_cast(endSnapshot.orientation)))); + appendLine("Viewport: " + std::to_string(endSnapshot.viewportWidth) + "x" + + std::to_string(endSnapshot.viewportHeight) + " px, margins T/R/B/L " + + std::to_string(endSnapshot.marginTop) + "/" + std::to_string(endSnapshot.marginRight) + "/" + + std::to_string(endSnapshot.marginBottom) + "/" + std::to_string(endSnapshot.marginLeft)); + appendLine("Font: " + std::to_string(endSnapshot.effectiveFontId) + ", embedded CSS " + + std::string(endSnapshot.embeddedStyle ? "on" : "off") + ", images " + + std::to_string(endSnapshot.imageRendering) + ", AA " + + std::string(endSnapshot.textAntiAliasing ? "on" : "off")); + appendLine("Last page: images " + std::string(endSnapshot.hadImages ? "yes" : "no") + ", footnotes " + + std::to_string(endSnapshot.footnoteCount) + ", cache rebuilt " + + std::string(endSnapshot.cacheRebuilt ? "yes" : "no")); + if (aggregate.renderCount > 0) { + appendLine("Render avg/min/max: request " + + std::to_string(aggregate.totalRequestRenderMs / static_cast(aggregate.renderCount)) + + "/" + std::to_string(aggregate.minRequestRenderMs) + "/" + std::to_string(aggregate.maxRequestRenderMs) + + " ms, core " + + std::to_string(aggregate.totalRenderMs / static_cast(aggregate.renderCount)) + "/" + + std::to_string(aggregate.minRenderMs) + "/" + std::to_string(aggregate.maxRenderMs) + " ms"); + appendLine("Aggregate loads: section " + std::to_string(aggregate.totalSectionLoadMs) + " ms, page " + + std::to_string(aggregate.totalPageLoadMs) + " ms, max footnotes " + + std::to_string(aggregate.maxFootnotes)); + appendLine("Aggregate phases: prewarm " + std::to_string(aggregate.totalPhases.prewarmMs) + ", bw " + + std::to_string(aggregate.totalPhases.bwRenderMs) + ", display " + + std::to_string(aggregate.totalPhases.displayMs) + ", gray total " + + std::to_string(aggregate.totalPhases.grayLsbMs + aggregate.totalPhases.grayMsbMs + + aggregate.totalPhases.grayDisplayMs)); + appendLine("Aggregate font: hits " + std::to_string(aggregate.totalFontCacheHits) + ", misses " + + std::to_string(aggregate.totalFontCacheMisses) + ", decompress " + + std::to_string(aggregate.totalFontDecompressMs) + " ms"); + appendLine("Aggregate glyph lookups: " + std::to_string(aggregate.totalFontGetBitmapCalls) + " calls, " + + std::to_string(aggregate.totalFontGetBitmapTimeUs) + " us total"); + appendLine("Heap after render min/max: " + std::to_string(aggregate.minFreeHeapAfter) + "/" + + std::to_string(aggregate.maxFreeHeapAfter)); + } + appendLine("Last render: request " + std::to_string(endSnapshot.requestRenderMs) + " ms, section load " + + std::to_string(endSnapshot.sectionLoadMs) + " ms, page load " + std::to_string(endSnapshot.pageLoadMs) + + " ms, render total " + std::to_string(endSnapshot.phases.totalMs) + " ms"); + appendLine("Phases: prewarm " + std::to_string(endSnapshot.phases.prewarmMs) + ", bw " + + std::to_string(endSnapshot.phases.bwRenderMs) + ", display " + + std::to_string(endSnapshot.phases.displayMs) + ", store " + std::to_string(endSnapshot.phases.bwStoreMs) + + ", gray lsb " + std::to_string(endSnapshot.phases.grayLsbMs) + ", gray msb " + + std::to_string(endSnapshot.phases.grayMsbMs) + ", gray display " + + std::to_string(endSnapshot.phases.grayDisplayMs) + ", restore " + + std::to_string(endSnapshot.phases.bwRestoreMs)); + appendLine("Font cache: hits " + std::to_string(endSnapshot.fontCacheHits) + ", misses " + + std::to_string(endSnapshot.fontCacheMisses) + ", decompress " + + std::to_string(endSnapshot.fontDecompressMs) + " ms, groups " + + std::to_string(endSnapshot.fontUniqueGroups)); + appendLine("Font buffers: page " + std::to_string(endSnapshot.fontPageBufferBytes) + ", glyph table " + + std::to_string(endSnapshot.fontPageGlyphsBytes) + ", hot group " + + std::to_string(endSnapshot.fontHotGroupBytes) + ", peak temp " + + std::to_string(endSnapshot.fontPeakTempBytes)); + appendLine("Glyph lookups: " + std::to_string(endSnapshot.fontGetBitmapCalls) + " calls, " + + std::to_string(endSnapshot.fontGetBitmapTimeUs) + " us total"); + appendLine("Heap: before " + std::to_string(endSnapshot.freeHeapBefore) + "/" + + std::to_string(endSnapshot.largestFreeBlockBefore) + ", after " + + std::to_string(endSnapshot.freeHeapAfter) + "/" + std::to_string(endSnapshot.largestFreeBlockAfter)); + + return report; +} + void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) { if (!epub) { return; @@ -903,33 +1119,43 @@ int EpubReaderActivity::getEffectiveReaderFontId() const { } } -void EpubReaderActivity::pageTurn(bool isForwardTurn) { +bool EpubReaderActivity::stepPageState(const bool isForwardTurn) { + if (!epub || !section || section->pageCount <= 0) { + return false; + } + if (isForwardTurn) { if (section->currentPage < section->pageCount - 1) { section->currentPage++; + } else if (currentSpineIndex + 1 < epub->getSpineItemsCount()) { + RenderLock lock(*this); + nextPageNumber = 0; + currentSpineIndex++; + section.reset(); } else { - // We don't want to delete the section mid-render, so grab the semaphore - { - RenderLock lock(*this); - nextPageNumber = 0; - currentSpineIndex++; - section.reset(); - } + return false; } } else { if (section->currentPage > 0) { section->currentPage--; } else if (currentSpineIndex > 0) { - // We don't want to delete the section mid-render, so grab the semaphore - { - RenderLock lock(*this); - nextPageNumber = UINT16_MAX; - currentSpineIndex--; - section.reset(); - } + RenderLock lock(*this); + nextPageNumber = UINT16_MAX; + currentSpineIndex--; + section.reset(); + } else { + return false; } } + lastPageTurnTime = millis(); + return true; +} + +void EpubReaderActivity::pageTurn(bool isForwardTurn) { + if (!stepPageState(isForwardTurn)) { + return; + } requestUpdate(); } @@ -979,18 +1205,34 @@ void EpubReaderActivity::render(RenderLock&& lock) { const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; + lastRenderStats = {}; + lastRenderStats.orientation = static_cast(renderer.getOrientation()); + lastRenderStats.marginTop = orientedMarginTop; + lastRenderStats.marginRight = orientedMarginRight; + lastRenderStats.marginBottom = orientedMarginBottom; + lastRenderStats.marginLeft = orientedMarginLeft; + lastRenderStats.viewportWidth = viewportWidth; + lastRenderStats.viewportHeight = viewportHeight; + lastRenderStats.embeddedStyle = getEffectiveEmbeddedStyle(); + lastRenderStats.imageRendering = getEffectiveImageRendering(); + lastRenderStats.effectiveFontId = getEffectiveReaderFontId(); + lastRenderStats.textAntiAliasing = SETTINGS.textAntiAliasing; + lastRenderStats.freeHeapBefore = esp_get_free_heap_size(); + lastRenderStats.largestFreeBlockBefore = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); if (!section) { - const bool embeddedStyle = getEffectiveEmbeddedStyle(); - const uint8_t imageRendering = getEffectiveImageRendering(); + const bool embeddedStyle = lastRenderStats.embeddedStyle; + const uint8_t imageRendering = lastRenderStats.imageRendering; const auto filepath = epub->getSpineItem(currentSpineIndex).href; LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex); section = std::make_unique
(epub, currentSpineIndex, renderer); + const unsigned long sectionStart = millis(); if (!section->loadSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { LOG_DBG("ERS", "Cache not found, building..."); + lastRenderStats.cacheRebuilt = true; Rect popupRect{}; const auto progressFn = [this, &popupRect](int progress) { @@ -1011,6 +1253,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { } else { LOG_DBG("ERS", "Cache found, skipping build..."); } + lastRenderStats.sectionLoadMs = millis() - sectionStart; if (nextPageNumber == UINT16_MAX) { section->currentPage = section->pageCount - 1; @@ -1097,7 +1340,9 @@ void EpubReaderActivity::render(RenderLock&& lock) { } { + const unsigned long pageLoadStart = millis(); auto p = section->loadPageFromSectionFile(); + lastRenderStats.pageLoadMs = millis() - pageLoadStart; if (!p) { LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); section->clearCache(); @@ -1110,13 +1355,22 @@ void EpubReaderActivity::render(RenderLock&& lock) { // Collect footnotes from the loaded page currentPageFootnotes = std::move(p->footnotes); + lastRenderStats.hadImages = p->hasImages(); + lastRenderStats.footnoteCount = static_cast(currentPageFootnotes.size()); + lastRenderStats.spineIndex = currentSpineIndex; + lastRenderStats.pageIndex = section->currentPage; + lastRenderStats.pageCount = section->pageCount; const auto start = millis(); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - LOG_DBG("ERS", "Rendered page in %dms", millis() - start); + lastRenderStats.requestRenderMs = millis() - start; + LOG_DBG("ERS", "Rendered page in %dms", lastRenderStats.requestRenderMs); } silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight); saveProgress(currentSpineIndex, section->currentPage, section->pageCount); + lastRenderStats.freeHeapAfter = esp_get_free_heap_size(); + lastRenderStats.largestFreeBlockAfter = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); + lastRenderStats.valid = true; if (pendingScreenshot) { pendingScreenshot = false; @@ -1200,6 +1454,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing; bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage; pendingHalfRefreshAfterImagePage = false; + lastRenderStats.imagePageWithAA = imagePageWithAA; + lastRenderStats.forcedHalfRefresh = forceHalfRefreshThisPage; logReaderMemSnapshot("before_bw_render"); page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); @@ -1283,6 +1539,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or logReaderMemSnapshot("bw_restore_end"); const auto tEnd = millis(); + lastRenderStats.usedGrayscale = true; + lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, + tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb, + tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0}; LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums " "gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums", @@ -1296,11 +1556,31 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or logReaderMemSnapshot("bw_restore_end"); const auto tEnd = millis(); + lastRenderStats.usedGrayscale = false; + lastRenderStats.phases = { + tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, tBwRestore - tBwStore, + tEnd - t0}; LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums", tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore, tEnd - t0); } + + if (auto* cacheManager = renderer.getFontCacheManager()) { + if (auto* decompressor = cacheManager->getDecompressor()) { + const auto& stats = decompressor->getStats(); + lastRenderStats.fontCacheHits = stats.cacheHits; + lastRenderStats.fontCacheMisses = stats.cacheMisses; + lastRenderStats.fontDecompressMs = stats.decompressTimeMs; + lastRenderStats.fontUniqueGroups = stats.uniqueGroupsAccessed; + lastRenderStats.fontPageBufferBytes = stats.pageBufferBytes; + lastRenderStats.fontPageGlyphsBytes = stats.pageGlyphsBytes; + lastRenderStats.fontHotGroupBytes = stats.hotGroupBytes; + lastRenderStats.fontPeakTempBytes = stats.peakTempBytes; + lastRenderStats.fontGetBitmapTimeUs = stats.getBitmapTimeUs; + lastRenderStats.fontGetBitmapCalls = stats.getBitmapCalls; + } + } } void EpubReaderActivity::renderStatusBar() const { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 8a792860..1a43f58d 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -38,6 +38,81 @@ class EpubReaderActivity final : public Activity { unsigned long lastPageTurnTime = 0UL; unsigned long pageTurnDuration = 0UL; bool pendingHalfRefreshAfterImagePage = false; + struct RenderPhaseStats { + unsigned long prewarmMs = 0UL; + unsigned long bwRenderMs = 0UL; + unsigned long displayMs = 0UL; + unsigned long bwStoreMs = 0UL; + unsigned long grayLsbMs = 0UL; + unsigned long grayMsbMs = 0UL; + unsigned long grayDisplayMs = 0UL; + unsigned long bwRestoreMs = 0UL; + unsigned long totalMs = 0UL; + }; + struct LastRenderStats { + bool valid = false; + bool cacheRebuilt = false; + bool usedGrayscale = false; + bool hadImages = false; + bool imagePageWithAA = false; + bool forcedHalfRefresh = false; + uint8_t orientation = 0; + uint8_t imageRendering = 0; + bool embeddedStyle = false; + bool textAntiAliasing = false; + int effectiveFontId = 0; + int spineIndex = 0; + int pageIndex = 0; + int pageCount = 0; + int footnoteCount = 0; + int marginTop = 0; + int marginRight = 0; + int marginBottom = 0; + int marginLeft = 0; + uint16_t viewportWidth = 0; + uint16_t viewportHeight = 0; + unsigned long sectionLoadMs = 0UL; + unsigned long pageLoadMs = 0UL; + unsigned long requestRenderMs = 0UL; + RenderPhaseStats phases; + uint32_t freeHeapBefore = 0; + uint32_t largestFreeBlockBefore = 0; + uint32_t freeHeapAfter = 0; + uint32_t largestFreeBlockAfter = 0; + uint32_t fontCacheHits = 0; + uint32_t fontCacheMisses = 0; + uint32_t fontDecompressMs = 0; + uint16_t fontUniqueGroups = 0; + uint32_t fontPageBufferBytes = 0; + uint32_t fontPageGlyphsBytes = 0; + uint32_t fontHotGroupBytes = 0; + uint32_t fontPeakTempBytes = 0; + uint32_t fontGetBitmapTimeUs = 0; + uint32_t fontGetBitmapCalls = 0; + }; + struct BenchmarkAggregate { + int renderCount = 0; + int imagePageCount = 0; + int cacheRebuildCount = 0; + int maxFootnotes = 0; + unsigned long totalRequestRenderMs = 0UL; + unsigned long minRequestRenderMs = 0UL; + unsigned long maxRequestRenderMs = 0UL; + unsigned long totalRenderMs = 0UL; + unsigned long minRenderMs = 0UL; + unsigned long maxRenderMs = 0UL; + unsigned long totalSectionLoadMs = 0UL; + unsigned long totalPageLoadMs = 0UL; + RenderPhaseStats totalPhases; + uint32_t totalFontCacheHits = 0; + uint32_t totalFontCacheMisses = 0; + uint32_t totalFontDecompressMs = 0; + uint32_t totalFontGetBitmapTimeUs = 0; + uint32_t totalFontGetBitmapCalls = 0; + uint32_t minFreeHeapAfter = 0; + uint32_t maxFreeHeapAfter = 0; + }; + LastRenderStats lastRenderStats; // Signals that the next render should reposition within the newly loaded section // based on a cross-book percentage jump. bool pendingPercentJump = false; @@ -95,7 +170,12 @@ class EpubReaderActivity final : public Activity { bool getEffectiveEmbeddedStyle() const; uint8_t getEffectiveImageRendering() const; int getEffectiveReaderFontId() const; + bool stepPageState(bool isForwardTurn); void pageTurn(bool isForwardTurn); + void runRenderBenchmark(); + std::string buildRenderBenchmarkReport(const LastRenderStats& startSnapshot, const BenchmarkAggregate& aggregate, + int forwardTurns, unsigned long forwardMs, int backwardTurns, + unsigned long backwardMs) const; // Footnote navigation void navigateToHref(const std::string& href, bool savePosition = false); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 6e68462b..bb621e54 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -33,7 +33,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu } void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages) { - menuItems.reserve(19); + menuItems.reserve(20); // --- Navigation --- menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION)); @@ -161,6 +161,8 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa menuItems.push_back( SettingInfo::Action(StrId::STR_DELETE_CACHE, SettingAction::None).withSubmenu(StrId::STR_READER_TOOLS)); menuItems.push_back(SettingInfo::Action(StrId::STR_GO_HOME_BUTTON, SettingAction::None)); + menuItems.push_back( + SettingInfo::Action(StrId::STR_RENDER_BENCHMARK, SettingAction::None).withSubmenu(StrId::STR_READER_TOOLS)); } EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId nameId) { @@ -195,6 +197,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId return MenuAction::DISPLAY_QR; case StrId::STR_DELETE_CACHE: return MenuAction::DELETE_CACHE; + case StrId::STR_RENDER_BENCHMARK: + return MenuAction::RENDER_BENCHMARK; case StrId::STR_GO_HOME_BUTTON: return MenuAction::GO_HOME; default: diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index a380e99b..df39b99c 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -28,7 +28,8 @@ class EpubReaderMenuActivity final : public MenuListActivity { PUSH_LOCAL, STARRED_PAGES, STAR_PAGE, - DELETE_CACHE + DELETE_CACHE, + RENDER_BENCHMARK }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, diff --git a/src/activities/reader/EpubRenderBenchmarkActivity.cpp b/src/activities/reader/EpubRenderBenchmarkActivity.cpp new file mode 100644 index 00000000..c83acfff --- /dev/null +++ b/src/activities/reader/EpubRenderBenchmarkActivity.cpp @@ -0,0 +1,42 @@ +#include "EpubRenderBenchmarkActivity.h" + +#include + +#include "components/UITheme.h" +#include "fontIds.h" + +void EpubRenderBenchmarkActivity::onEnter() { + Activity::onEnter(); + requestUpdate(); +} + +void EpubRenderBenchmarkActivity::loop() { + if (mappedInput.wasAnyPressed() || mappedInput.wasAnyReleased()) { + finish(); + } +} + +void EpubRenderBenchmarkActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const Rect contentRect = UITheme::getContentRect(renderer, true, false); + const int titleY = contentRect.y + 12; + renderer.drawCenteredText(UI_12_FONT_ID, titleY, "Render Benchmark", true, EpdFontFamily::BOLD); + + const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); + const int textTop = titleY + renderer.getLineHeight(UI_12_FONT_ID) + 14; + const int textWidth = contentRect.width - 20; + const auto lines = renderer.wrappedText(UI_10_FONT_ID, report.c_str(), textWidth, 48); + + int y = textTop; + for (const auto& line : lines) { + if (y + lineHeight > contentRect.y + contentRect.height - 30) { + break; + } + renderer.drawText(UI_10_FONT_ID, contentRect.x + 10, y, line.c_str(), true); + y += lineHeight; + } + + renderer.drawCenteredText(UI_10_FONT_ID, contentRect.y + contentRect.height - 18, "Press any button to close"); + renderer.displayBuffer(); +} diff --git a/src/activities/reader/EpubRenderBenchmarkActivity.h b/src/activities/reader/EpubRenderBenchmarkActivity.h new file mode 100644 index 00000000..c30f38e4 --- /dev/null +++ b/src/activities/reader/EpubRenderBenchmarkActivity.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "activities/Activity.h" + +class EpubRenderBenchmarkActivity final : public Activity { + public: + explicit EpubRenderBenchmarkActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string report) + : Activity("EpubRenderBenchmark", renderer, mappedInput), report(std::move(report)) {} + + void onEnter() override; + void loop() override; + void render(RenderLock&&) override; + + private: + std::string report; +};