From d5aecc6e8a8ede144c20269f64d3b81806eb6ea7 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sun, 29 Jun 2025 20:10:06 -0500 Subject: [PATCH] Add timing stats to Maintenance tab --- background.js | 35 +++++++++++++++++++++++++++++ modules/AiClassifier.js | 9 +++++++- options/options.html | 3 +++ options/options.js | 49 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/background.js b/background.js index 781b9df..6ef7eb5 100644 --- a/background.js +++ b/background.js @@ -19,6 +19,8 @@ let queue = Promise.resolve(); let queuedCount = 0; let processing = false; let iconTimer = null; +let timingStats = { count: 0, mean: 0, m2: 0, total: 0 }; +let currentStart = 0; function setIcon(path) { if (browser.browserAction) { @@ -106,6 +108,7 @@ async function applyAiRules(idsInput) { updateActionIcon(); queue = queue.then(async () => { processing = true; + currentStart = Date.now(); queuedCount--; updateActionIcon(); try { @@ -141,9 +144,27 @@ async function applyAiRules(idsInput) { } } processing = false; + const elapsed = Date.now() - currentStart; + currentStart = 0; + const t = timingStats; + t.count += 1; + t.total += elapsed; + const delta = elapsed - t.mean; + t.mean += delta / t.count; + t.m2 += delta * (elapsed - t.mean); + await storage.local.set({ classifyStats: t }); showTransientIcon("resources/img/done.png"); } catch (e) { processing = false; + const elapsed = Date.now() - currentStart; + currentStart = 0; + const t = timingStats; + t.count += 1; + t.total += elapsed; + const delta = elapsed - t.mean; + t.mean += delta / t.count; + t.m2 += delta * (elapsed - t.mean); + await storage.local.set({ classifyStats: t }); logger.aiLog("failed to apply AI rules", { level: 'error' }, e); showTransientIcon("resources/img/error.png"); } @@ -199,6 +220,10 @@ async function clearCacheForMessages(idsInput) { logger.setDebug(store.debugLogging); await AiClassifier.setConfig(store); await AiClassifier.init(); + const savedStats = await storage.local.get('classifyStats'); + if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') { + Object.assign(timingStats, savedStats.classifyStats); + } aiRules = Array.isArray(store.aiRules) ? store.aiRules.map(r => { if (r.actions) return r; const actions = []; @@ -390,6 +415,16 @@ async function clearCacheForMessages(idsInput) { } } else if (msg?.type === "sortana:getQueueCount") { return { count: queuedCount + (processing ? 1 : 0) }; + } else if (msg?.type === "sortana:getTiming") { + const t = timingStats; + const std = t.count > 1 ? Math.sqrt(t.m2 / (t.count - 1)) : 0; + return { + count: queuedCount + (processing ? 1 : 0), + current: currentStart ? Date.now() - currentStart : -1, + average: t.mean, + total: t.total, + stddev: std + }; } else { logger.aiLog("Unknown message type, ignoring", {level: 'warn'}, msg?.type); } diff --git a/modules/AiClassifier.js b/modules/AiClassifier.js index de267b6..3c526f8 100644 --- a/modules/AiClassifier.js +++ b/modules/AiClassifier.js @@ -323,6 +323,13 @@ async function clearCache() { } } +async function getCacheSize() { + if (!gCacheLoaded) { + await loadCache(); + } + return gCache.size; +} + function classifyTextSync(text, criterion, cacheKey = null) { if (!Services?.tm?.spinEventLoopUntil) { throw new Error("classifyTextSync requires Services"); @@ -407,4 +414,4 @@ async function init() { await loadCache(); } -export { classifyText, classifyTextSync, setConfig, removeCacheEntries, clearCache, getReason, getCachedResult, buildCacheKey, buildCacheKeySync, init }; +export { classifyText, classifyTextSync, setConfig, removeCacheEntries, clearCache, getReason, getCachedResult, buildCacheKey, buildCacheKeySync, getCacheSize, init }; diff --git a/options/options.html b/options/options.html index 6ddefa7..ed1ef41 100644 --- a/options/options.html +++ b/options/options.html @@ -180,6 +180,9 @@ Rule count Cache entries Queue items + Current run time--:-- + Average run time--:-- + Total run time--:-- diff --git a/options/options.js b/options/options.js index 7c1e595..997d7f7 100644 --- a/options/options.js +++ b/options/options.js @@ -305,20 +305,59 @@ document.addEventListener('DOMContentLoaded', async () => { const ruleCountEl = document.getElementById('rule-count'); const cacheCountEl = document.getElementById('cache-count'); const queueCountEl = document.getElementById('queue-count'); + const currentTimeEl = document.getElementById('current-time'); + const averageTimeEl = document.getElementById('average-time'); + const totalTimeEl = document.getElementById('total-time'); + let timingLogged = false; ruleCountEl.textContent = (defaults.aiRules || []).length; cacheCountEl.textContent = defaults.aiCache ? Object.keys(defaults.aiCache).length : 0; - async function refreshQueueCount() { + function format(ms) { + if (ms < 0) return '--:--'; + return (ms / 1000).toFixed(1) + 's'; + } + + async function refreshMaintenance() { try { - const { count } = await browser.runtime.sendMessage({ type: 'sortana:getQueueCount' }); - queueCountEl.textContent = count; + const stats = await browser.runtime.sendMessage({ type: 'sortana:getTiming' }); + queueCountEl.textContent = stats.count; + currentTimeEl.classList.remove('has-text-success','has-text-danger'); + let arrow = ''; + if (stats.current >= 0) { + if (stats.stddev > 0 && stats.current - stats.average > stats.stddev) { + currentTimeEl.classList.add('has-text-danger'); + arrow = ' ▲'; + } else if (stats.stddev > 0 && stats.average - stats.current > stats.stddev) { + currentTimeEl.classList.add('has-text-success'); + arrow = ' ▼'; + } + currentTimeEl.textContent = format(stats.current) + arrow; + } else { + currentTimeEl.textContent = '--:--'; + } + averageTimeEl.textContent = stats.count ? format(stats.average) : '--:--'; + totalTimeEl.textContent = format(stats.total); + if (!timingLogged) { + logger.aiLog('retrieved timing stats', {debug: true}); + timingLogged = true; + } } catch (e) { queueCountEl.textContent = '?'; + currentTimeEl.textContent = '--:--'; + averageTimeEl.textContent = '--:--'; + totalTimeEl.textContent = '--:--'; + } + + ruleCountEl.textContent = document.querySelectorAll('#rules-container .rule').length; + try { + cacheCountEl.textContent = await AiClassifier.getCacheSize(); + } catch { + cacheCountEl.textContent = '?'; } } - refreshQueueCount(); - setInterval(refreshQueueCount, 2000); + refreshMaintenance(); + setInterval(refreshMaintenance, 2000); document.getElementById('clear-cache').addEventListener('click', async () => { await AiClassifier.clearCache();