perf(history): gate the unconditional History-tab reload + fix the diagnostics history regression (v3.3.101)
v3.3.100's interaction instrument pinpointed the residual UI lag exactly: every slow click was a tab switch into the History view (button.tab / nav.tab-bar, 200-840ms), each coupled to `ipc get-history wall=150-200ms sync` + `main-longtask blocked=242-289ms lastIpc=get-history` + a 200-248ms renderer long task. Uploads themselves are pristine (event-loop mean 11.7ms; the only spikes line up with the get-history tab switches). Root cause: the History tab handler called loadHistory() UNCONDITIONALLY on every activation (renderer/app.js) even though it tracks a `_historyDirty` flag it never checked. Each call synchronously readFileSync + JSON.parse's the ~185MB / 30000-entry electron-history.json (no cache), ships all 30000 batches across IPC, and the renderer flattens ~120000 row objects before .slice(-2000) for the DOM (the DOM is already capped at 2000). Fix (the adversary-verified safe subset): - Gate the History-tab load: only reload when `_historyDirty || !_historyEverLoaded`. Added `_historyEverLoaded`, and both flags are now set inside loadHistory() after the fetch succeeds (so a failed load retries). Dirty-coverage is complete — every history append routes through batch-done -> appendHistory and upload-batch-done -> handleBatchDone which sets _historyDirty=true. Result: repeat History tab switches with no new uploads do zero IPC/parse/flatten and are instant. (The first open after a new batch still parses once ~450ms; removing that needs a parse-cache or JSONL storage, deferred.) - Diagnostics history regression: getHistory() read loadConfig().history, but since the v3.3.99 history split load() returns history:[] in packaged mode, so remote diagnostics reported totalBatches:0 despite 30000 real batches. It now reads loadHistory() (injected via the collector deps), with a backward-compatible fallback to loadConfig().history when not provided. 407 tests pass (2 new diagnostics regression tests); clean Electron boot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
003e14dfe9
commit
bad1c665f5
@ -11,7 +11,7 @@ const READABLE_LOGS = {
|
|||||||
const QUEUE_STATUSES = ['preview', 'queued', 'getting-server', 'uploading', 'retrying', 'done', 'error', 'aborted', 'skipped'];
|
const QUEUE_STATUSES = ['preview', 'queued', 'getting-server', 'uploading', 'retrying', 'done', 'error', 'aborted', 'skipped'];
|
||||||
|
|
||||||
function createCollectors(deps) {
|
function createCollectors(deps) {
|
||||||
const { loadConfig, getAllLogPaths, support, stats, appInfo, systemInfo, agentInfo } = deps;
|
const { loadConfig, loadHistory, getAllLogPaths, support, stats, appInfo, systemInfo, agentInfo } = deps;
|
||||||
|
|
||||||
function _secrets() {
|
function _secrets() {
|
||||||
try { return support.collectSecretValues(loadConfig()); } catch { return []; }
|
try { return support.collectSecretValues(loadConfig()); } catch { return []; }
|
||||||
@ -205,8 +205,9 @@ function createCollectors(deps) {
|
|||||||
|
|
||||||
function getHistory(args) {
|
function getHistory(args) {
|
||||||
const a = args || {};
|
const a = args || {};
|
||||||
const cfg = loadConfig();
|
const history = typeof loadHistory === 'function'
|
||||||
const history = Array.isArray(cfg.history) ? cfg.history : [];
|
? (loadHistory() || [])
|
||||||
|
: (Array.isArray(loadConfig().history) ? loadConfig().history : []);
|
||||||
const limit = Math.min(Math.max(Number(a.limit) || 20, 1), 200);
|
const limit = Math.min(Math.max(Number(a.limit) || 20, 1), 200);
|
||||||
const perHoster = stats.summarizePerHoster(history);
|
const perHoster = stats.summarizePerHoster(history);
|
||||||
const recent = [...history].slice(-limit).reverse();
|
const recent = [...history].slice(-limit).reverse();
|
||||||
|
|||||||
1
main.js
1
main.js
@ -2617,6 +2617,7 @@ function _diagAgentInfo() {
|
|||||||
function _buildDiagnosticHandler() {
|
function _buildDiagnosticHandler() {
|
||||||
const collectors = createCollectors({
|
const collectors = createCollectors({
|
||||||
loadConfig: () => configStore.load(),
|
loadConfig: () => configStore.load(),
|
||||||
|
loadHistory: () => configStore.loadHistory(),
|
||||||
getAllLogPaths,
|
getAllLogPaths,
|
||||||
support: { sanitizeConfig, collectSecretValues, redactLogText, valueScrub, collectFile, REDACTED },
|
support: { sanitizeConfig, collectSecretValues, redactLogText, valueScrub, collectFile, REDACTED },
|
||||||
stats,
|
stats,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.100",
|
"version": "3.3.101",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -332,6 +332,7 @@ async function init() {
|
|||||||
|
|
||||||
// --- Tab switching ---
|
// --- Tab switching ---
|
||||||
let _historyDirty = false;
|
let _historyDirty = false;
|
||||||
|
let _historyEverLoaded = false;
|
||||||
function _isHistoryTabActive() {
|
function _isHistoryTabActive() {
|
||||||
const tab = document.querySelector('.tab.active');
|
const tab = document.querySelector('.tab.active');
|
||||||
return !!(tab && tab.dataset.view === 'history');
|
return !!(tab && tab.dataset.view === 'history');
|
||||||
@ -359,8 +360,7 @@ function _isHistoryTabActive() {
|
|||||||
const nextView = viewsById[`${tab.dataset.view}-view`];
|
const nextView = viewsById[`${tab.dataset.view}-view`];
|
||||||
if (nextView) nextView.classList.add('active');
|
if (nextView) nextView.classList.add('active');
|
||||||
activeTab = tab;
|
activeTab = tab;
|
||||||
if (tab.dataset.view === 'history') {
|
if (tab.dataset.view === 'history' && (_historyDirty || !_historyEverLoaded)) {
|
||||||
_historyDirty = false;
|
|
||||||
loadHistory();
|
loadHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -4630,6 +4630,8 @@ function _hideOtpField() {
|
|||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
const history = await window.api.getHistory();
|
const history = await window.api.getHistory();
|
||||||
window._historyForStats = history || [];
|
window._historyForStats = history || [];
|
||||||
|
_historyEverLoaded = true;
|
||||||
|
_historyDirty = false;
|
||||||
_invalidateHosterLifetimeCache();
|
_invalidateHosterLifetimeCache();
|
||||||
const retSel = document.getElementById('historyRetentionSelect');
|
const retSel = document.getElementById('historyRetentionSelect');
|
||||||
if (retSel) retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
if (retSel) retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
||||||
|
|||||||
@ -1,3 +1,41 @@
|
|||||||
|
# v3.3.101 — History-tab lag: gate the unconditional reload + fix the diagnostics history regression
|
||||||
|
|
||||||
|
v3.3.100's interaction instrument named the residual exactly: EVERY slow click was `button.tab`/`nav.tab-bar`
|
||||||
|
(200–840ms), each coupled to `ipc get-history wall=150-200ms sync` + `main-longtask blocked=242-289ms
|
||||||
|
lastIpc=get-history` + `renderer-longtask 200-248ms`. Uploads themselves pristine (ELD mean 11.7ms; the only
|
||||||
|
spikes line up with the get-history tab-switches). Workflow wgxr06myb (4 agents + adversarial verify):
|
||||||
|
- get-history fires ONLY entering the History tab (not every tab) — but the handler called loadHistory()
|
||||||
|
UNCONDITIONALLY (app.js:362-365) despite tracking `_historyDirty` and never checking it. Each call:
|
||||||
|
synchronous readFileSync+JSON.parse of the ~185MB / 30000-entry electron-history.json (no cache), ships all
|
||||||
|
30000 over IPC, renderer flattens ~120000 row objects then .slice(-2000) for the DOM (DOM already capped 2000).
|
||||||
|
- Adversary safe subset = STEP 1 ALONE (gate the load, dirty-coverage verified complete: every append routes
|
||||||
|
batch-done→appendHistory + upload-batch-done→handleBatchDone sets _historyDirty=true). Zero risk.
|
||||||
|
|
||||||
|
SHIPPED v3.3.101 (STEP 1 + a regression fix, both safe):
|
||||||
|
- renderer/app.js: gate `if (tab.dataset.view==='history' && (_historyDirty || !_historyEverLoaded)) loadHistory()`;
|
||||||
|
added `_historyEverLoaded`, set both flags inside loadHistory() AFTER the await succeeds (retry on failure).
|
||||||
|
→ REPEAT History tab-switches (no new uploads) now do ZERO ipc/parse/flatten = instant. (Honest limit: the
|
||||||
|
FIRST History open after a new batch still parses 185MB once ~450ms — needs the parse-cache, see below.)
|
||||||
|
- lib/diagnostics-collectors.js + main.js: getHistory now reads loadHistory() not load().history — fixes a
|
||||||
|
CORRECTNESS regression I introduced in v3.3.99 (migrated mode → load().history is [] → remote diagnostics
|
||||||
|
reported totalBatches:0 despite 30000 real batches). Backward-compatible fallback kept. +2 regression tests.
|
||||||
|
|
||||||
|
407 tests pass, clean boot.
|
||||||
|
|
||||||
|
DEFERRED (adversary-flagged, by design — do only if the next log/user still shows pain):
|
||||||
|
- STEP 2(1) ConfigStore parse-cache for history (mtime+size key, invalidate BOTH _writeHistoryFileAtomic AND
|
||||||
|
_writeHistoryFileDurable, slice-before-push for 'all'-retention same-ref aliasing). Makes first-after-upload
|
||||||
|
switch instant + appendHistory read-half free, but ADDS ~185MB resident in main (NOT a relocation — adversary
|
||||||
|
corrected the design's false RAM claim).
|
||||||
|
- STEP 2(2) slice get-history to last-N-batches: REGRESSION VECTOR (breaks browse/sort-all 30000), needs a
|
||||||
|
net-new paging/search-in-main IPC + UI that doesn't exist. Defer.
|
||||||
|
- JSONL append-only storage: the ONLY thing that kills appendHistory's 185MB-rewrite-per-batch-done AND the
|
||||||
|
parse entirely (tail-readable). On-disk format migration → own careful build.
|
||||||
|
- Two minor independent residuals from the sweep: ~625ms batch-start spin-up (synchronous 22-job build/prime
|
||||||
|
burst in one tick) + 107ms debug-log block mid-batch. Separate, low priority.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v3.3.100 — close the LAST measurement gap: renderer interaction timing (switches/clicks)
|
# v3.3.100 — close the LAST measurement gap: renderer interaction timing (switches/clicks)
|
||||||
|
|
||||||
User asked "haben wir wirklich ALLES gemessen, auch switches/wechsel?". Audit: main-side was already
|
User asked "haben wir wirklich ALLES gemessen, auch switches/wechsel?". Audit: main-side was already
|
||||||
|
|||||||
@ -53,6 +53,32 @@ test('getConfigRedacted strips password/apiKey/token/webhookUrl and value-scrubs
|
|||||||
assert.ok(!json.includes('WBHOOKSECRETTOKEN'), 'webhook secret must be redacted');
|
assert.ok(!json.includes('WBHOOKSECRETTOKEN'), 'webhook secret must be redacted');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getHistory reads loadHistory (migrated mode: loadConfig().history is empty)', () => {
|
||||||
|
const c = createCollectors({
|
||||||
|
loadConfig: () => ({ hosters: {}, globalSettings: {}, history: [] }),
|
||||||
|
loadHistory: () => [
|
||||||
|
{ timestamp: '2026-01-01T00:00:00.000Z', files: [{ name: 'a.mkv', results: [{ hoster: 'voe.sx', status: 'done', url: 'https://voe.sx/a' }] }] },
|
||||||
|
{ timestamp: '2026-01-02T00:00:00.000Z', files: [{ name: 'b.mkv', results: [{ hoster: 'byse.sx', status: 'done', url: 'https://byse.sx/b' }] }] }
|
||||||
|
],
|
||||||
|
getAllLogPaths: () => ({ logDir: os.tmpdir() }),
|
||||||
|
support, stats,
|
||||||
|
appInfo: () => ({}), systemInfo: () => ({}), agentInfo: () => ({})
|
||||||
|
});
|
||||||
|
const out = c.getHistory({ limit: 10 });
|
||||||
|
assert.equal(out.totalBatches, 2, 'must report real history from loadHistory, not the empty load().history');
|
||||||
|
assert.equal(out.returned, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getHistory falls back to loadConfig().history when loadHistory is absent (legacy mode)', () => {
|
||||||
|
const c = createCollectors({
|
||||||
|
loadConfig: () => ({ hosters: {}, globalSettings: {}, history: [{ timestamp: '2026-01-01T00:00:00.000Z', files: [] }] }),
|
||||||
|
getAllLogPaths: () => ({ logDir: os.tmpdir() }),
|
||||||
|
support, stats,
|
||||||
|
appInfo: () => ({}), systemInfo: () => ({}), agentInfo: () => ({})
|
||||||
|
});
|
||||||
|
assert.equal(c.getHistory({ limit: 10 }).totalBatches, 1, 'legacy path reads load().history when loadHistory not injected');
|
||||||
|
});
|
||||||
|
|
||||||
test('readLog redacts a planted token and a Bearer line; doodstream is NOT readable; unknown name rejected', () => {
|
test('readLog redacts a planted token and a Bearer line; doodstream is NOT readable; unknown name rejected', () => {
|
||||||
const { collectors } = makeFixture();
|
const { collectors } = makeFixture();
|
||||||
const dbg = collectors.readLog({ name: 'debug', tailKb: 64 });
|
const dbg = collectors.readLog({ name: 'debug', tailKb: 64 });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user