mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
Merge pull request #1773 from ShaneMcGovernIE/codex/launcher-navigation-performance
perf(launcher): keep MODS and MOD INDEX navigation responsive
This commit is contained in:
@@ -3056,8 +3056,8 @@ local function buildFindKindRow(imp, x, y, w, m)
|
||||
end
|
||||
|
||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
imp._findVisibleEntries = nil
|
||||
imp:_ensureFind()
|
||||
imp:_ensureMods()
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
local sources = imp.findSources or {}
|
||||
@@ -3126,7 +3126,9 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
|
||||
if #rows == 0 then
|
||||
local empty
|
||||
if carts then
|
||||
if imp._findFetch then
|
||||
empty = Strings("Loading mod index...")
|
||||
elseif carts then
|
||||
empty = (total == 0) and Strings("This index lists no carts yet.")
|
||||
or Strings("No carts match that search.")
|
||||
else
|
||||
@@ -3191,6 +3193,11 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
local listTop = cy
|
||||
setPage(imp, "find", Kit.wheelPage(x, listTop, w, listH, cur, #rows, perPage))
|
||||
|
||||
local visible = imp._findVisibleEntries or {}
|
||||
for i = #visible, 1, -1 do visible[i] = nil end
|
||||
for i = first, last do visible[#visible + 1] = rows[i] end
|
||||
imp._findVisibleEntries = visible
|
||||
|
||||
for i = first, last do
|
||||
local entry = rows[i]
|
||||
local ry = listTop + (i - first) * (rowH + gap)
|
||||
@@ -5764,12 +5771,6 @@ local function loaderSpec(imp)
|
||||
return { title = b.title, detail = b.detail, progress = b.progress,
|
||||
onCancel = b.cancel }
|
||||
end
|
||||
-- The boot prewarm runs without an overlay (the user did not ask for it and
|
||||
-- must be able to use the launcher meanwhile), but if they reach the Find
|
||||
-- Mods tab before it lands, THEN they are waiting on it and it earns one.
|
||||
if imp.tab == "find" and imp._findFetch and not imp.findLoaded then
|
||||
return { title = Strings("Loading mod index") }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
+73
-12
@@ -1390,8 +1390,9 @@ function RomImporter.new(onComplete, opts)
|
||||
-- player's index list from options; findIndex is the merged listing;
|
||||
-- _findThumbs caches one image per mod id (false = fetched and failed).
|
||||
findLoaded = false, findSources = nil, findIndex = nil,
|
||||
findScroll = 0, findNotice = nil, findQuery = "", findCategory = nil,
|
||||
_findSearchFocus = false, _findThumbs = nil,
|
||||
findInstalled = nil, findScroll = 0, findNotice = nil, findQuery = "",
|
||||
findCategory = nil, _findSearchFocus = false, _findThumbs = nil,
|
||||
_findVisibleEntries = nil,
|
||||
-- Which half of the feed the panel is browsing. Mods by default: carts
|
||||
-- are the newer, much shorter list, and a feed may carry none at all.
|
||||
findKind = "mods", findBase = nil,
|
||||
@@ -2590,6 +2591,7 @@ function RomImporter:update(dt)
|
||||
-- before a tab switch still completes.
|
||||
self:_pumpFindFetch()
|
||||
self:_pumpModInfoFetch()
|
||||
self:_queueFindEnrichment()
|
||||
self:_pumpFindStats()
|
||||
self:_pumpFindThumbs()
|
||||
self:_pumpSkinFetch()
|
||||
@@ -3227,6 +3229,7 @@ end
|
||||
-- offset persists inside the view's per-tab scroll container.
|
||||
function RomImporter:_switchTab(id)
|
||||
self.tab = id
|
||||
if id ~= "find" then self._findVisibleEntries = nil end
|
||||
self._findSearchFocus = false
|
||||
self._skinUrlFocus = false
|
||||
self:_disarmTextInput()
|
||||
@@ -3824,6 +3827,7 @@ function RomImporter:_toggleSafeMode()
|
||||
SaveData.saveOptions(options)
|
||||
self.safeMode = enabled
|
||||
self.mods = nil
|
||||
self.findInstalled = nil
|
||||
self._modSortCache = nil
|
||||
self._modInfoFetch = nil
|
||||
self.modNotice = nil
|
||||
@@ -4799,6 +4803,7 @@ function RomImporter:_refreshMods()
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
self._cartPlan = nil
|
||||
self.findInstalled = nil
|
||||
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||
-- unzipped beside the executable, which an ordinary (non-portable) install
|
||||
@@ -5719,7 +5724,8 @@ end
|
||||
-- as long as the slowest index took -- measured at over two minutes on a
|
||||
-- cold open, with no spinner, because the frame that would have drawn one
|
||||
-- never ran. The fetch now starts here and completes across later frames in
|
||||
-- _pumpFindFetch; the loader overlay is up for the whole flight.
|
||||
-- _pumpFindFetch. Only an explicit Refresh is blocking; boot prewarm and the
|
||||
-- first visit keep the launcher interactive while the listing arrives.
|
||||
function RomImporter:_refreshFind(force)
|
||||
-- The notice is the fix, not the gate (#876). This branch used to return an
|
||||
-- empty listing silently, and because the player had by then added a source,
|
||||
@@ -5757,9 +5763,11 @@ function RomImporter:_refreshFind(force)
|
||||
carts = {}, cartSeen = {}, bases = {}, baseSeen = {},
|
||||
stale = false, oldest = nil, at = 1,
|
||||
}
|
||||
if force == true then
|
||||
self:_setBusy(Strings("Fetching mod index"),
|
||||
#sources == 1 and (sources[1].label or sources[1].feed)
|
||||
or Strings("%d indexes", #sources))
|
||||
end
|
||||
end
|
||||
|
||||
-- Drive the in-flight index fetch one frame at a time. Called from update().
|
||||
@@ -5959,9 +5967,17 @@ end
|
||||
|
||||
function RomImporter:_findInstalledMap()
|
||||
if self:findingCarts() then return self:_findInstalledCarts() end
|
||||
local map = {}
|
||||
for _, m in ipairs(self.mods or {}) do map[m.id] = m.version or true end
|
||||
return map
|
||||
if self.findInstalled then return self.findInstalled end
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
if self.mods then
|
||||
self.findInstalled = {}
|
||||
for _, m in ipairs(self.mods) do
|
||||
self.findInstalled[m.id] = m.version or true
|
||||
end
|
||||
else
|
||||
self.findInstalled = LauncherMods.installedVersions() or {}
|
||||
end
|
||||
return self.findInstalled
|
||||
end
|
||||
|
||||
-- id -> installed version for every cart on disk, whatever game it plays as.
|
||||
@@ -5979,10 +5995,9 @@ function RomImporter:_findInstalledCarts()
|
||||
return map
|
||||
end
|
||||
|
||||
-- One thumbnail per frame, and only for a card actually on screen: the fetch
|
||||
-- is a blocking curl, so downloading a whole listing's worth on open would
|
||||
-- stall the launcher for as many seconds as there are mods. A failure is
|
||||
-- remembered as `false` so a broken URL is tried once, not every frame.
|
||||
-- Read the cached thumbnail for a card. Starting a download is deliberately
|
||||
-- separate: immediate-mode draw may call this for every visible row, but it
|
||||
-- must not mutate the fetch queue or perform network work.
|
||||
function RomImporter:_findThumb(entry)
|
||||
self._findThumbs = self._findThumbs or {}
|
||||
local cached = self._findThumbs[entry.id]
|
||||
@@ -5993,6 +6008,20 @@ function RomImporter:_findThumb(entry)
|
||||
self._findThumbs[entry.id] = false
|
||||
return nil
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Queue one thumbnail after draw has recorded the visible rows. The fetch
|
||||
-- pool runs off-thread; only the finished image decode stays in update().
|
||||
function RomImporter:_startFindThumb(entry)
|
||||
self._findThumbs = self._findThumbs or {}
|
||||
if self._findThumbs[entry.id] ~= nil then return end
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local url = ModIndex.joinUrl(entry._base, entry.thumbnail)
|
||||
if not url then
|
||||
self._findThumbs[entry.id] = false
|
||||
return
|
||||
end
|
||||
-- ASYNC (was one blocking download per frame). Only rows on the current
|
||||
-- page ever ask, so pagination already bounds this to a page's worth of
|
||||
-- requests; the fetch pool runs them off-thread and the card shows its
|
||||
@@ -6113,9 +6142,9 @@ function RomImporter:_requestFindStats(entry)
|
||||
}
|
||||
end
|
||||
|
||||
-- Request-and-read, for a row that is being drawn and for the detail modal.
|
||||
-- Read-only accessor for a row being drawn or shown in the detail modal.
|
||||
-- Network work is scheduled by _queueFindEnrichment from update().
|
||||
function RomImporter:_findStats(entry)
|
||||
self:_requestFindStats(entry)
|
||||
return self:_findStatsCached(entry)
|
||||
end
|
||||
|
||||
@@ -6135,6 +6164,38 @@ function RomImporter:_findThumbPending(id)
|
||||
return (self._findThumbFetch and self._findThumbFetch[id]) ~= nil
|
||||
end
|
||||
|
||||
-- Draw records the visible page in _findVisibleEntries. Queue only a small
|
||||
-- batch from that snapshot during update(), keeping network scheduling out of
|
||||
-- the immediate-mode render path and preventing a large index from creating a
|
||||
-- burst of thumbnail/GitHub work in one frame.
|
||||
local FIND_ENRICH_PER_FRAME = 2
|
||||
|
||||
function RomImporter:_queueFindEnrichment()
|
||||
if self.tab ~= "find" or not self.findLoaded then return end
|
||||
local visible = self._findVisibleEntries
|
||||
if not visible then return end
|
||||
local thumbnails, stats = 0, 0
|
||||
for _, entry in ipairs(visible) do
|
||||
if thumbnails < FIND_ENRICH_PER_FRAME
|
||||
and self:_findThumb(entry) == nil
|
||||
and not self:_findThumbPending(entry.id) then
|
||||
self:_startFindThumb(entry)
|
||||
thumbnails = thumbnails + 1
|
||||
end
|
||||
if stats < FIND_ENRICH_PER_FRAME
|
||||
and self:_findStatsCached(entry) == nil
|
||||
and entry.github and entry.github ~= ""
|
||||
and not self:_findStatsPendingFor(entry.id) then
|
||||
self:_requestFindStats(entry)
|
||||
stats = stats + 1
|
||||
end
|
||||
if thumbnails >= FIND_ENRICH_PER_FRAME
|
||||
and stats >= FIND_ENRICH_PER_FRAME then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Drive in-flight FIND MODS stats lookups. Called from update().
|
||||
function RomImporter:_pumpFindStats()
|
||||
local pending = self._findStatsPending
|
||||
|
||||
@@ -471,6 +471,20 @@ discover = function()
|
||||
return out
|
||||
end
|
||||
|
||||
-- installedVersions() -> id -> installed version. MOD INDEX only needs to
|
||||
-- know whether a listing is already present; it does not need enablement,
|
||||
-- dependency/conflict status, required-import validation, or migration. Keep
|
||||
-- that cheap read separate from list(), whose richer work belongs to MODS.
|
||||
function LauncherMods.installedVersions()
|
||||
local out = {}
|
||||
local ok, manifests = pcall(discover)
|
||||
if not ok then return out end
|
||||
for _, manifest in ipairs(manifests or {}) do
|
||||
out[manifest.id] = manifest.version or true
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- list([version]) -> the mods-panel rows for the current install. Reads the
|
||||
-- same enable-state the loader persists, so a toggle here is what the game
|
||||
-- sees on its next boot; `version` narrows that to one game's answers.
|
||||
|
||||
@@ -75,7 +75,8 @@ end
|
||||
do
|
||||
local ri = launcher()
|
||||
check(ri:_findStats(entry("nulled", nil, "someone/nulled")) == nil,
|
||||
"a null count is not an answer; the repo is still consulted")
|
||||
"a null count is not an answer")
|
||||
ri:_requestFindStats(entry("nulled", nil, "someone/nulled"))
|
||||
eq(#fetched, 1, "which is the fetch the panel already made for dates")
|
||||
|
||||
local bare = launcher()
|
||||
@@ -83,7 +84,7 @@ do
|
||||
check(stats ~= nil and stats.total == nil,
|
||||
"a listing with neither counts nor a repo is resolved-but-unknown")
|
||||
check(stats.recent == nil, "and has nothing to trend on")
|
||||
eq(#fetched, 1, "and queues nothing of its own")
|
||||
eq(#fetched, 1, "and the explicit scheduler queues nothing without a repo")
|
||||
end
|
||||
|
||||
-- A real zero is not unknown: the index has seen the releases and counted
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Launcher navigation performance seams. MOD INDEX must not pay the full
|
||||
-- MODS validation pass, background index prefetch must not create a blocking
|
||||
-- overlay, and visible-row enrichment must be scheduled from update state
|
||||
-- rather than from immediate-mode draw calls.
|
||||
-- luajit tests/engine/launcher_navigation_perf.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
|
||||
check(type(LauncherMods.installedVersions) == "function",
|
||||
"LauncherMods exposes a lightweight installed-version scan")
|
||||
|
||||
do
|
||||
local old = LauncherMods.installedVersions
|
||||
local called = 0
|
||||
LauncherMods.installedVersions = function()
|
||||
called = called + 1
|
||||
return { alpha = "1.2.3" }
|
||||
end
|
||||
local imp = setmetatable({}, RomImporter)
|
||||
local installed = imp:_findInstalledMap()
|
||||
eq(called, 1, "MOD INDEX asks for the lightweight scan when MODS is cold")
|
||||
eq(installed.alpha, "1.2.3",
|
||||
"the lightweight scan supplies installed versions")
|
||||
LauncherMods.installedVersions = old
|
||||
end
|
||||
|
||||
do
|
||||
local imp = setmetatable({ tab = "find", findLoaded = false }, RomImporter)
|
||||
imp._refreshFindSources = function(self)
|
||||
self.findSources = { { feed = "https://example.invalid/index.json" } }
|
||||
end
|
||||
local oldBegin = ModIndex.beginFetch
|
||||
ModIndex.beginFetch = function() return { test = true } end
|
||||
imp:_refreshFind(false)
|
||||
check(imp._findFetch ~= nil, "background index refresh starts asynchronously")
|
||||
eq(imp._busy, nil,
|
||||
"background index refresh does not block navigation with a loader")
|
||||
imp:_clearBusy()
|
||||
imp._findFetch = nil
|
||||
ModIndex.beginFetch = oldBegin
|
||||
end
|
||||
|
||||
do
|
||||
local requests = 0
|
||||
local imp = setmetatable({ tab = "find", findLoaded = true,
|
||||
_findVisibleEntries = {
|
||||
{ id = "one", thumbnail = "one.png", github = "a/one" },
|
||||
{ id = "two", thumbnail = "two.png", github = "a/two" },
|
||||
{ id = "three", thumbnail = "three.png", github = "a/three" },
|
||||
} }, RomImporter)
|
||||
imp._findThumb = function() return nil end
|
||||
imp._findThumbPending = function() return false end
|
||||
imp._findStatsCached = function() return nil end
|
||||
imp._startFindThumb = function() requests = requests + 1 end
|
||||
imp._requestFindStats = function() requests = requests + 1 end
|
||||
imp:_queueFindEnrichment()
|
||||
eq(requests, 4,
|
||||
"update schedules a bounded thumbnail and stats batch for visible rows")
|
||||
end
|
||||
|
||||
do
|
||||
local requests = 0
|
||||
local imp = setmetatable({}, RomImporter)
|
||||
imp._findStatsCached = function() return nil end
|
||||
imp._requestFindStats = function() requests = requests + 1 end
|
||||
imp:_findStats({ id = "draw-only", github = "a/draw-only" })
|
||||
eq(requests, 0, "reading row stats during draw never starts a request")
|
||||
end
|
||||
|
||||
do
|
||||
local f = assert(io.open("src/import/LauncherView.lua", "rb"))
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
local start = assert(src:find("local function buildFindPanel", 1, true))
|
||||
local finish = assert(src:find("\nlocal function ", start + 1, true))
|
||||
local panel = src:sub(start, finish - 1)
|
||||
check(not panel:find("imp:_ensureMods()", 1, true),
|
||||
"MOD INDEX panel does not force the full MODS list")
|
||||
end
|
||||
|
||||
T.finish("launcher_navigation_perf")
|
||||
Reference in New Issue
Block a user