From cd4c89f7549658034c2bd7cd68ea7bdd41279a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=95=B7=EF=B8=8F?= <3756473+zendive@users.noreply.github.com> Date: Tue, 13 May 2025 14:57:58 +0300 Subject: [PATCH 1/4] make it work in Chrome Incognito mode --- manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 0d15a73..7b2d975 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.2.0", + "version": "1.3.0", "name": "API Monitor", "manifest_version": 3, "description": "Show active intervals, scheduled timeouts, animation frames, idle callbacks, eval invocations, media events and properties", @@ -13,6 +13,7 @@ "64": "public/img/panel-icon64.png", "128": "public/img/panel-icon128.png" }, + "incognito": "split", "content_scripts": [ { "world": "MAIN", From 7e36c2f52eaaa3ddaae0cefd67717e3b5a821196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=95=B7=EF=B8=8F?= <3756473+zendive@users.noreply.github.com> Date: Thu, 22 May 2025 16:01:12 +0300 Subject: [PATCH 2/4] retrospective --- Makefile | 12 +- build.ts | 1 + manifest.json | 1 + public/mirror.html | 20 +++ src/api-monitor-cs-isolated.ts | 15 ++- src/api-monitor-cs-main.ts | 18 ++- src/api-monitor-devtools-panel.ts | 2 + src/api-monitor-devtools.ts | 7 +- src/api/communication.ts | 37 ++++-- src/api/const.ts | 3 +- src/api/{ => storage}/storage.local.ts | 36 +++--- src/api/{ => storage}/storage.session.ts | 18 +-- src/api/storage/storage.ts | 88 +++++++++++++ src/api/time.ts | 85 ++++++------ src/devtoolsPanelUtil.ts | 28 +++- src/global.d.ts | 2 +- src/mirror/mirror.ts | 36 ++++++ src/state/config.state.svelte.ts | 6 +- src/state/session.state.svelte.ts | 7 +- src/state/telemetry.state.svelte.ts | 55 ++++++-- src/view/App.svelte | 30 +---- src/view/menu/DevReload.svelte | 6 +- src/view/menu/Menu.svelte | 26 ++++ src/view/menu/SummaryBar.svelte | 2 +- src/view/menu/SummaryBarItem.svelte | 4 +- src/view/menu/TogglePanels.svelte | 4 +- src/view/menu/UpdatePace.svelte | 16 +-- src/view/menu/UpdatePaceTimeMap.ts | 121 +++++++++--------- src/view/menu/Version.svelte | 10 +- .../TimersClearHistoryMetric.svelte | 39 ------ .../AppPanels.svelte => panels/Panels.svelte} | 22 ++-- .../animation}/AnimationCancelHistory.svelte | 53 ++++---- .../animation}/AnimationRequestHistory.svelte | 55 ++++---- src/view/{panel => panels/eval}/Eval.svelte | 45 ++----- .../idle}/IdleCallbackCancelHistory.svelte | 50 +++----- .../idle}/IdleCallbackRequestHistory.svelte | 80 ++++++------ src/view/{panel => panels/media}/Media.svelte | 6 +- .../media}/MediaCommands.svelte | 0 .../media}/MediaMetrics.svelte | 2 +- .../online/Online.svelte} | 10 +- .../online/OnlineTimers.svelte} | 10 +- .../shared/CellBreakpoint.svelte} | 0 .../shared/CellBypass.svelte} | 0 .../shared/CellCallstack.svelte} | 5 +- .../shared/CellCancelable.svelte} | 2 +- .../shared/CellFacts.svelte} | 2 +- .../shared/CellFrameTimeSensitive.svelte} | 0 .../shared/ColumnSortable.svelte} | 2 +- .../shared}/TraceLink.svelte | 4 +- src/{api => view/panels/shared}/comparator.ts | 4 +- .../timers}/TimersClearHistory.svelte | 30 ++--- .../timers/TimersClearHistoryMetric.svelte | 31 +++++ .../timers}/TimersSetHistory.svelte | 41 +++--- .../timers}/TimersSetHistoryMetric.svelte | 49 +++---- src/view/{components => shared}/Alert.svelte | 0 src/view/{components => shared}/Dialog.svelte | 0 .../{components => shared}/Variable.svelte | 0 src/{api => view/shared}/canvas.ts | 21 ++- src/wrapper/AnimationWrapper.ts | 24 ++-- src/wrapper/EvalWrapper.ts | 35 +++-- src/wrapper/IdleWrapper.ts | 37 ++++-- src/wrapper/MediaWrapper.ts | 6 +- src/wrapper/TimerWrapper.ts | 47 ++++--- src/wrapper/Wrapper.ts | 12 +- src/wrapper/{ => shared}/Fact.ts | 6 +- src/wrapper/{ => shared}/TraceUtil.ts | 4 +- src/wrapper/{ => shared}/util.ts | 0 tests/AnimationWrapper_test.ts | 8 +- tests/EvalWrapper_test.ts | 8 +- tests/Facts_test.ts | 4 +- tests/IdleWrapper_test.ts | 8 +- tests/TimerWrapper_test.ts | 8 +- tests/TraceUtil_test.ts | 11 +- tests/canvas_test.ts | 18 +-- tests/time_test.ts | 40 +++--- tests/util.ts | 5 - 76 files changed, 902 insertions(+), 638 deletions(-) create mode 100644 public/mirror.html rename src/api/{ => storage}/storage.local.ts (85%) rename src/api/{ => storage}/storage.session.ts (62%) create mode 100644 src/api/storage/storage.ts create mode 100644 src/mirror/mirror.ts create mode 100644 src/view/menu/Menu.svelte delete mode 100644 src/view/panel/components/TimersClearHistoryMetric.svelte rename src/view/{panel/components/AppPanels.svelte => panels/Panels.svelte} (72%) rename src/view/{panel => panels/animation}/AnimationCancelHistory.svelte (62%) rename src/view/{panel => panels/animation}/AnimationRequestHistory.svelte (76%) rename src/view/{panel => panels/eval}/Eval.svelte (61%) rename src/view/{panel => panels/idle}/IdleCallbackCancelHistory.svelte (60%) rename src/view/{panel => panels/idle}/IdleCallbackRequestHistory.svelte (70%) rename src/view/{panel => panels/media}/Media.svelte (88%) rename src/view/{panel/components => panels/media}/MediaCommands.svelte (100%) rename src/view/{panel/components => panels/media}/MediaMetrics.svelte (98%) rename src/view/{panel/OnlineTimers.svelte => panels/online/Online.svelte} (73%) rename src/view/{panel/components/ActiveTimers.svelte => panels/online/OnlineTimers.svelte} (85%) rename src/view/{panel/components/TraceBreakpoint.svelte => panels/shared/CellBreakpoint.svelte} (100%) rename src/view/{panel/components/TraceBypass.svelte => panels/shared/CellBypass.svelte} (100%) rename src/view/{panel/components/CallstackCell.svelte => panels/shared/CellCallstack.svelte} (90%) rename src/view/{panel/components/CancelableCallMetric.svelte => panels/shared/CellCancelable.svelte} (91%) rename src/view/{panel/components/FactsCell.svelte => panels/shared/CellFacts.svelte} (89%) rename src/view/{panel/components/FrameSensitiveTime.svelte => panels/shared/CellFrameTimeSensitive.svelte} (100%) rename src/view/{panel/components/SortableColumn.svelte => panels/shared/ColumnSortable.svelte} (94%) rename src/view/{panel/components => panels/shared}/TraceLink.svelte (95%) rename src/{api => view/panels/shared}/comparator.ts (89%) rename src/view/{panel => panels/timers}/TimersClearHistory.svelte (73%) create mode 100644 src/view/panels/timers/TimersClearHistoryMetric.svelte rename src/view/{panel => panels/timers}/TimersSetHistory.svelte (75%) rename src/view/{panel/components => panels/timers}/TimersSetHistoryMetric.svelte (63%) rename src/view/{components => shared}/Alert.svelte (100%) rename src/view/{components => shared}/Dialog.svelte (100%) rename src/view/{components => shared}/Variable.svelte (100%) rename src/{api => view/shared}/canvas.ts (94%) rename src/wrapper/{ => shared}/Fact.ts (87%) rename src/wrapper/{ => shared}/TraceUtil.ts (97%) rename src/wrapper/{ => shared}/util.ts (100%) delete mode 100644 tests/util.ts diff --git a/Makefile b/Makefile index e933da6..5440ad0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean install dev valid test prod +.PHONY: clean install dev valid test prod serve_mirror .DEFAULT_GOAL := dev DENO_DEV = NODE_ENV=development deno run --watch DENO_PROD = NODE_ENV=production deno run @@ -30,5 +30,13 @@ test: valid prod: test rm -rf $(BUILD_DIR) $(CHROME_ZIP) $(DENO_PROD) $(DENO_OPTIONS) $(BUILD_SCRIPT) + zip -r $(CHROME_ZIP) $(OUTPUT_DIR) ./manifest.json > /dev/null - tree -Dis $(BUILD_DIR) *.zip + zip --delete $(CHROME_ZIP) "$(OUTPUT_DIR)mirror.html" "$(BUILD_DIR)mirror/*" > /dev/null + + tree -Dis $(BUILD_DIR) *.zip | grep -E "api|zip" + +serve_mirror: + @echo "🎗 reminder to switch extension off" + @echo "served at: http://localhost:5555/mirror.html" + python3 -m http.server 5555 -d ./public/ diff --git a/build.ts b/build.ts index f563414..cd06602 100644 --- a/build.ts +++ b/build.ts @@ -17,6 +17,7 @@ const buildOptions: BuildOptions = { './src/api-monitor-cs-main.ts', './src/api-monitor-cs-isolated.ts', './src/api-monitor-devtools-panel.ts', + './src/mirror/mirror.ts', ], outdir: './public/build/', define: { diff --git a/manifest.json b/manifest.json index 7b2d975..b8ee8bd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,6 @@ { "version": "1.3.0", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxQCaHgX3DkPnGmHr+rhWyPvYemxMhBbvulmj4RvEpAnGVprdPCUiHSY0jOcDn3vnU6zm8mR1mT3sdlYoUGikBIT19/Jf1iGlc2dySt2bmDQXlTrqllT/XB8HW/wruFej9waMw9yqtW1wOJtElxWnT11pzXkKeflH1Sh+//Jnplr577vOmWh9TU8JLJHS9WklPHJyXCCMGrg/0Sxqte5qWryE2yIm9375KGkKN4ZKjSIxaCg0qodhf5Ug9s2QD7/s5xt548gbEUm9LqQHkNoIH3KXuYOnLksJFxi7FDwhg+oXalsONr5eEvPjkwxYpMKJXfRSg8sB8N6cXLUfgLAKUwIDAQAB", "name": "API Monitor", "manifest_version": 3, "description": "Show active intervals, scheduled timeouts, animation frames, idle callbacks, eval invocations, media events and properties", diff --git a/public/mirror.html b/public/mirror.html new file mode 100644 index 0000000..c9993dd --- /dev/null +++ b/public/mirror.html @@ -0,0 +1,20 @@ + + + + + + + Mirror - Browser API Monitor + + + + + + + + + + + + + diff --git a/src/api-monitor-cs-isolated.ts b/src/api-monitor-cs-isolated.ts index ef18b12..ccc10b2 100644 --- a/src/api-monitor-cs-isolated.ts +++ b/src/api-monitor-cs-isolated.ts @@ -5,11 +5,14 @@ import { windowListen, windowPost, } from './api/communication.ts'; -import { loadLocalStorage, onLocalStorageChange } from './api/storage.local.ts'; +import { + loadLocalStorage, + onLocalStorageChange, +} from './api/storage/storage.local.ts'; import { loadSessionStorage, onSessionStorageChange, -} from './api/storage.session.ts'; +} from './api/storage/storage.session.ts'; Promise.all([loadLocalStorage(), loadSessionStorage()]).then( ([config, session]) => { @@ -23,11 +26,11 @@ Promise.all([loadLocalStorage(), loadSessionStorage()]).then( portListen(windowPost); windowListen(runtimePost); - onLocalStorageChange((newValue) => { - windowPost({ msg: EMsg.CONFIG, config: newValue }); + onLocalStorageChange((config) => { + windowPost({ msg: EMsg.CONFIG, config }); }); - onSessionStorageChange((newValue) => { - windowPost({ msg: EMsg.SESSION, session: newValue }); + onSessionStorageChange((session) => { + windowPost({ msg: EMsg.SESSION, session }); }); runtimePost({ msg: EMsg.CONTENT_SCRIPT_LOADED }); diff --git a/src/api-monitor-cs-main.ts b/src/api-monitor-cs-main.ts index e9079ed..afd0323 100644 --- a/src/api-monitor-cs-main.ts +++ b/src/api-monitor-cs-main.ts @@ -47,31 +47,29 @@ const tick = new Timer( ); windowListen((o) => { - if (o.msg === EMsg.TELEMETRY_ACKNOWLEDGED) { + if (EMsg.TELEMETRY_ACKNOWLEDGED === o.msg) { tick.delay = adjustTelemetryDelay(o.timeOfCollection); originalMetrics = currentMetrics; eachSecond.isPending() && tick.start(); - } else if ( - o.msg === EMsg.CONFIG && o.config && typeof o.config === 'object' - ) { + } else if (EMsg.CONFIG === o.msg) { applyConfig(o.config); - } else if (o.msg === EMsg.START_OBSERVE) { + } else if (EMsg.START_OBSERVE === o.msg) { originalMetrics = currentMetrics = null; tick.trigger(); eachSecond.start(); - } else if (o.msg === EMsg.STOP_OBSERVE) { + } else if (EMsg.STOP_OBSERVE === o.msg) { tick.stop(); eachSecond.stop(); originalMetrics = currentMetrics = null; - } else if (o.msg === EMsg.RESET_WRAPPER_HISTORY) { + } else if (EMsg.RESET_WRAPPER_HISTORY === o.msg) { originalMetrics = currentMetrics = null; cleanHistory(); !tick.isPending() && tick.trigger(); - } else if (o.msg === EMsg.TIMER_COMMAND) { + } else if (EMsg.TIMER_COMMAND === o.msg) { runTimerCommand(o.type, o.handler); - } else if (o.msg === EMsg.MEDIA_COMMAND) { + } else if (EMsg.MEDIA_COMMAND === o.msg) { runMediaCommand(o.mediaId, o.cmd, o.property); - } else if (o.msg === EMsg.SESSION) { + } else if (EMsg.SESSION === o.msg) { applySession(o.session); } }); diff --git a/src/api-monitor-devtools-panel.ts b/src/api-monitor-devtools-panel.ts index d951c71..8eda8d0 100644 --- a/src/api-monitor-devtools-panel.ts +++ b/src/api-monitor-devtools-panel.ts @@ -2,8 +2,10 @@ import { mount } from 'svelte'; import App from './view/App.svelte'; import { initConfigState } from './state/config.state.svelte.ts'; import { onHidePanel } from './devtoolsPanelUtil.ts'; +import { establishTelemetryReceiver } from './state/telemetry.state.svelte.ts'; initConfigState().then(() => { mount(App, { target: document.body }); + establishTelemetryReceiver(); globalThis.addEventListener('beforeunload', onHidePanel); }); diff --git a/src/api-monitor-devtools.ts b/src/api-monitor-devtools.ts index bc94884..a86f8ff 100644 --- a/src/api-monitor-devtools.ts +++ b/src/api-monitor-devtools.ts @@ -1,6 +1,9 @@ import { EMsg, portPost } from './api/communication.ts'; -import { loadLocalStorage, saveLocalStorage } from './api/storage.local.ts'; -import { enableSessionInContentScript } from './api/storage.session.ts'; +import { + loadLocalStorage, + saveLocalStorage, +} from './api/storage/storage.local.ts'; +import { enableSessionInContentScript } from './api/storage/storage.session.ts'; import { onHidePanel } from './devtoolsPanelUtil.ts'; // tabId may be null if user opened the devtools of the devtools diff --git a/src/api/communication.ts b/src/api/communication.ts index 6dcc477..f0cea7b 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -14,21 +14,26 @@ import { APPLICATION_NAME } from './env.ts'; import { ERRORS_IGNORED } from './const.ts'; import { ETimerType } from '../wrapper/TimerWrapper.ts'; import type { TTelemetry } from '../wrapper/Wrapper.ts'; -import type { TConfig } from './storage.local.ts'; +import type { TConfig } from './storage/storage.local.ts'; import type { TMediaCommand } from '../wrapper/MediaWrapper.ts'; import type { Delta } from 'jsondiffpatch'; -import type { TSession } from './storage.session.ts'; +import type { TSession } from './storage/storage.session.ts'; let port: chrome.runtime.Port | null = null; export function portPost(payload: TMsgOptions) { + if (!chrome.runtime) { + windowPost(payload); + return; + } + if (!port) { port = chrome.tabs.connect(chrome.devtools.inspectedWindow.tabId, { name: APPLICATION_NAME, }); - port.onDisconnect.addListener(() => void (port = null)); + port?.onDisconnect.addListener(() => void (port = null)); } - port.postMessage(payload); + port?.postMessage(payload); } export function portListen(callback: (payload: TMsgOptions) => void) { @@ -67,16 +72,20 @@ export function runtimePost(payload: TMsgOptions) { } export function runtimeListen(callback: (payload: TMsgOptions) => void) { - chrome.runtime.onMessage.addListener( - (payload, sender: chrome.runtime.MessageSender, sendResponse) => { - if ( - sender.tab?.id === chrome.devtools.inspectedWindow.tabId - ) { - callback(payload); - sendResponse(); - } - }, - ); + if (chrome?.runtime) { + chrome.runtime.onMessage.addListener( + (payload, sender: chrome.runtime.MessageSender, sendResponse) => { + if ( + sender.tab?.id === chrome.devtools.inspectedWindow.tabId + ) { + callback(payload); + sendResponse(); + } + }, + ); + } else { + windowListen(callback); + } } function handleRuntimeMessageResponse(): void { diff --git a/src/api/const.ts b/src/api/const.ts index 19e95d9..e453a7f 100644 --- a/src/api/const.ts +++ b/src/api/const.ts @@ -6,7 +6,8 @@ export const ERRORS_IGNORED = [ ]; export const TELEMETRY_FREQUENCY_30PS = 33.3333333333; // ms export const TELEMETRY_FREQUENCY_1PS = 1000; // ms -export const FRAME_1of60 = 0.0166666666667; // ms +export const TIME_60FPS_SEC = 0.0166666666667; // s +export const TIME_60FPS_MS = 16.666666666666668; export const VARIABLE_ANIMATION_THROTTLE = 3500; // eye blinking average frequency export const SELF_TIME_MAX_GOOD = 13.333333333333332; // ms diff --git a/src/api/storage.local.ts b/src/api/storage/storage.local.ts similarity index 85% rename from src/api/storage.local.ts rename to src/api/storage/storage.local.ts index 35f49ab..66058cf 100644 --- a/src/api/storage.local.ts +++ b/src/api/storage/storage.local.ts @@ -1,15 +1,16 @@ import type { TCancelIdleCallbackHistory, TRequestIdleCallbackHistory, -} from '../wrapper/IdleWrapper.ts'; +} from '../../wrapper/IdleWrapper.ts'; import type { TCancelAnimationFrameHistory, TRequestAnimationFrameHistory, -} from '../wrapper/AnimationWrapper.ts'; +} from '../../wrapper/AnimationWrapper.ts'; import type { TClearTimerHistory, TSetTimerHistory, -} from '../wrapper/TimerWrapper.ts'; +} from '../../wrapper/TimerWrapper.ts'; +import { CONFIG_VERSION, local } from './storage.ts'; type TPanelKey = | 'callsSummary' @@ -24,19 +25,20 @@ type TPanelKey = | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback'; -export type TPanelMap = { - [K in TPanelKey]: TPanel; -}; + export type TPanel = { key: TPanelKey; label: string; visible: boolean; wrap: boolean | null; }; +export type TPanelMap = { + [K in TPanelKey]: TPanel; +}; + export type TConfig = typeof DEFAULT_CONFIG; export type TConfigField = Partial; -const CONFIG_VERSION = '2025-04-25'; export const DEFAULT_PANELS: TPanel[] = [ { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, @@ -143,32 +145,36 @@ export function panelsArray2Map(panels: TPanel[]) { } export async function loadLocalStorage(): Promise { - let store = await chrome.storage.local.get([CONFIG_VERSION]); + let store = await local.get([CONFIG_VERSION]); const isEmpty = !Object.keys(store).length; if (isEmpty) { - await chrome.storage.local.clear(); // reset previous version - await chrome.storage.local.set({ [CONFIG_VERSION]: DEFAULT_CONFIG }); - store = await chrome.storage.local.get([CONFIG_VERSION]); + await local.clear(); // reset previous version + await local.set({ [CONFIG_VERSION]: DEFAULT_CONFIG }); + store = await local.get([CONFIG_VERSION]); } return store[CONFIG_VERSION]; } export async function saveLocalStorage(value: TConfigField) { - const store = await chrome.storage.local.get([CONFIG_VERSION]); + const store = await local.get([CONFIG_VERSION]); Object.assign(store[CONFIG_VERSION], value); - return await chrome.storage.local.set(store); + return await local.set(store); } export function onLocalStorageChange( callback: (newValue: TConfig, oldValue: TConfig) => void, ) { - chrome.storage.local.onChanged.addListener((change) => { + local.onChanged.addListener((change: { + [key: string]: chrome.storage.StorageChange; + }) => { if ( - change && change[CONFIG_VERSION] && change[CONFIG_VERSION].newValue + change && + change[CONFIG_VERSION] && + change[CONFIG_VERSION].newValue ) { callback( change[CONFIG_VERSION].newValue, diff --git a/src/api/storage.session.ts b/src/api/storage/storage.session.ts similarity index 62% rename from src/api/storage.session.ts rename to src/api/storage/storage.session.ts index 8a74cce..0fe618f 100644 --- a/src/api/storage.session.ts +++ b/src/api/storage/storage.session.ts @@ -1,4 +1,4 @@ -const SESSION_VERSION = '2025-04-25'; +import { session, SESSION_VERSION } from './storage.ts'; export type TSession = typeof DEFAULT_SESSION; type TSessionProperty = Partial; @@ -8,36 +8,36 @@ const DEFAULT_SESSION = { }; export function enableSessionInContentScript() { - return chrome.storage.session.setAccessLevel({ + return session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS', }); } export async function loadSessionStorage(): Promise { - let store = await chrome.storage.session.get([SESSION_VERSION]); + let store = await session.get([SESSION_VERSION]); const isEmpty = !Object.keys(store).length; if (isEmpty) { - await chrome.storage.session.clear(); // reset previous version - await chrome.storage.session.set({ [SESSION_VERSION]: DEFAULT_SESSION }); - store = await chrome.storage.session.get([SESSION_VERSION]); + await session.clear(); // reset previous version + await session.set({ [SESSION_VERSION]: DEFAULT_SESSION }); + store = await session.get([SESSION_VERSION]); } return store[SESSION_VERSION]; } export async function saveSessionStorage(value: TSessionProperty) { - const store = await chrome.storage.session.get([SESSION_VERSION]); + const store = await session.get([SESSION_VERSION]); Object.assign(store[SESSION_VERSION], value); - return await chrome.storage.session.set(store); + return await session.set(store); } export function onSessionStorageChange( callback: (newValue: TSession, oldValue: TSession) => void, ) { - chrome.storage.session.onChanged.addListener((change) => { + session.onChanged.addListener((change) => { if ( change && change[SESSION_VERSION] && change[SESSION_VERSION].newValue ) { diff --git a/src/api/storage/storage.ts b/src/api/storage/storage.ts new file mode 100644 index 0000000..c5c926e --- /dev/null +++ b/src/api/storage/storage.ts @@ -0,0 +1,88 @@ +export const CONFIG_VERSION = '2025-04-25'; +export const SESSION_VERSION = '2025-04-25'; + +export const local = /*@__PURE__*/ (() => { + return globalThis.chrome?.storage + ? chrome.storage.local + : mockChromeStorageWith(globalThis.localStorage, CONFIG_VERSION); +})(); + +export const session = /*@__PURE__*/ (() => { + return globalThis.chrome?.storage + ? chrome.storage.session + : mockChromeStorageWith(globalThis.sessionStorage, SESSION_VERSION); +})(); + +type TOnChangeSignature = (changes: { + [key: string]: chrome.storage.StorageChange; +}) => void; + +const LOCAL_KEY = 'mock'; + +/** + * @NOTE: minimalistic coverage, just to accommodate project's basic needs + */ +function mockChromeStorageWith( + storage: Storage, + APP_KEY: string, +): chrome.storage.LocalStorageArea { + const allListeners = new Set(); + + return { + QUOTA_BYTES: 10485760, + + clear() { + return new Promise((resolve) => { + storage.clear(); + resolve(); + }); + }, + + getBytesInUse() { + return new Promise((resolve) => { + resolve( + (storage.getItem(LOCAL_KEY) || '').length, + ); + }); + }, + + async setAccessLevel() { + // NOOP in window runtime + }, + + // @ts-expect-error partial implementation + onChanged: { + addListener(callback: TOnChangeSignature) { + allListeners.add(callback); + }, + + removeListener(callback: TOnChangeSignature) { + allListeners.delete(callback); + }, + }, + + async set(o: object) { + storage.setItem(LOCAL_KEY, JSON.stringify(o)); + + // dispatch `change` events + const oo = await this.get(LOCAL_KEY); + for (const listener of allListeners) { + listener({ + [APP_KEY]: { + newValue: oo[APP_KEY], + }, + }); + } + }, + + get() { + return new Promise((resolve, reject) => { + try { + resolve(JSON.parse(storage.getItem(LOCAL_KEY) || '{}') || {}); + } catch (e) { + reject(e); + } + }); + }, + }; +} diff --git a/src/api/time.ts b/src/api/time.ts index 2b56e4e..0ed0b88 100644 --- a/src/api/time.ts +++ b/src/api/time.ts @@ -4,9 +4,10 @@ import { requestAnimationFrame, setTimeout, TELEMETRY_FREQUENCY_30PS, + TIME_60FPS_MS, } from './const.ts'; -export function callingOnce< +export function callableOnce< T extends (...args: Parameters) => ReturnType, >( fn: T | null, @@ -52,34 +53,30 @@ export class Stopper { return this.#finish - this.#start; } + toString() { + return Stopper.toString(this.value()); + } + static toString(msTime: number | unknown) { if (typeof msTime !== 'number' || !Number.isFinite(msTime)) { return; } - if (msTime < 1) { - return `${Math.trunc(msTime * 1e3)}μs`; - } else if (msTime < 3) { + if (msTime <= TIME_60FPS_MS) { const ms = Math.trunc(msTime); - return `${ms}.${Math.trunc((msTime - ms) * 1e2)}ms`; + return `${ms}.${ + toPaddedString(Math.trunc((msTime - ms) * 1e2), 2) + }\u00a0ms`; } else if (msTime < 1e3) { - return `${Math.trunc(msTime)}ms`; + return `${Math.trunc(msTime)}\u00a0ms`; } else if (msTime < 60e3) { const s = Math.trunc(msTime / 1e3) % 60; const ms = Math.trunc(msTime % 1e3); - return `${s}.${ms.toString().padStart(3, '0')}s`; + return `${s}.${toPaddedString(ms, 3)}\u00a0s`; } - const h = Math.trunc(msTime / 3600e3); - const m = Math.trunc(msTime / 60e3) % 60; - const s = Math.trunc(msTime / 1e3) % 60; - - return `${h.toString().padStart(2, '0')}:${ - m - .toString() - .padStart(2, '0') - }:${s.toString().padStart(2, '0')}`; + return ms2HMS(msTime); } } @@ -91,38 +88,38 @@ interface ITimerOptions { /** act as requestAnimationFrame called from another requestAnimationFrame (default: false); if true - `delay` is redundant */ animation?: boolean; - /** populate `executionTime` with measured execution time of `callback` (default: false) */ + /** populate `callbackSelfTime` with measured execution time of `callback` (default: false) */ measurable?: boolean; } /** * A unification of ways to delay a callback to another time in javascript event-loop * - `repetitive: false` - will call `setTimeout` with constant `delay`. - * - `repetitive: true` - will call `setTimeout` but act as `setInterval` with changable `delay`. + * - `repetitive: true` - will call `setTimeout` but act as `setInterval` with changeable `delay`. * - `animation: true` - will call `requestAnimationFrame` in recursive way (means to follow the browser's frame-rate). * - `measurable: true` - measure the callback's execution time. */ export class Timer { - readonly options: ITimerOptions; - readonly #defaultOptions: ITimerOptions = { + delay: number = 0; + /** callback's self-time in milliseconds */ + callbackSelfTime: number = -1; + #handler: number = 0; + readonly #fn: (...args: unknown[]) => void; + readonly #stopper?: Stopper; + readonly #options: ITimerOptions; + static readonly DEFAULT_OPTIONS: ITimerOptions = { delay: 0, repetitive: false, animation: false, measurable: false, }; - delay: number = 0; - /** callback's self-time in milliseconds */ - executionTime: number = -1; - #fn: (...args: unknown[]) => void; - #handler: number = 0; - readonly #stopper?: Stopper; constructor(o: ITimerOptions, fn: (...args: unknown[]) => void) { - this.options = Object.assign(this.#defaultOptions, o); + this.#options = Object.assign({}, Timer.DEFAULT_OPTIONS, o); this.#fn = fn; - this.delay = this.options.delay || 0; + this.delay = this.#options.delay || 0; - if (this.options.measurable) { + if (this.#options.measurable) { this.#stopper = new Stopper(); } } @@ -132,12 +129,12 @@ export class Timer { this.stop(); } - if (this.options.animation) { + if (this.#options.animation) { this.#handler = requestAnimationFrame(() => { this.trigger(...args); this.#handler = 0; - if (this.options.repetitive) { + if (this.#options.repetitive) { this.start(...args); } }); @@ -146,7 +143,7 @@ export class Timer { this.trigger(...args); this.#handler = 0; - if (this.options.repetitive) { + if (this.#options.repetitive) { this.start(...args); } }, this.delay); @@ -159,7 +156,7 @@ export class Timer { this.#stopper?.start(); this.#fn(...args); if (this.#stopper) { - this.executionTime = this.#stopper.stop().value(); + this.callbackSelfTime = this.#stopper.stop().value(); } return this; @@ -167,7 +164,7 @@ export class Timer { stop() { if (this.#handler) { - if (this.options.animation) { + if (this.#options.animation) { cancelAnimationFrame(this.#handler); } else { clearTimeout(this.#handler); @@ -221,12 +218,24 @@ export class Fps { } } -export function trim2microsecond(ms: T) { - return typeof ms === 'number' ? Math.trunc(ms * 1e3) / 1e3 : ms; +export function wait(timeout: number) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +export function trim2ms(ms: T) { + return typeof ms === 'number' ? Math.trunc(ms * 1e2) / 1e2 : ms; +} + +export function ms2HMS(ms: number) { + return `${toPaddedString(Math.trunc(ms / 3600e3), 2)}:${ + toPaddedString(Math.trunc(ms / 60e3) % 60, 2) + }:${toPaddedString(Math.trunc(ms / 1e3) % 60, 2)}`; } -export function msToHms(delay: number | unknown): string | undefined { - return delay && Number(delay) > 10e3 ? Stopper.toString(delay) : undefined; +function toPaddedString(value: number, padding: number) { + return value.toString().padStart(padding, '0'); } const TICK_TIME_LAG_SCALAR = 3; diff --git a/src/devtoolsPanelUtil.ts b/src/devtoolsPanelUtil.ts index 3e23cb9..2912d1c 100644 --- a/src/devtoolsPanelUtil.ts +++ b/src/devtoolsPanelUtil.ts @@ -4,10 +4,36 @@ * as a dependency */ import { EMsg, portPost } from './api/communication.ts'; -import { saveLocalStorage } from './api/storage.local.ts'; +import { saveLocalStorage } from './api/storage/storage.local.ts'; +import { ms2HMS } from './api/time.ts'; export async function onHidePanel() { chrome.power.releaseKeepAwake(); portPost({ msg: EMsg.STOP_OBSERVE }); await saveLocalStorage({ devtoolsPanelShown: false }); } + +type TColourScheme = 'light' | 'dark'; + +export function onColourSchemeChange( + callback: (scheme: TColourScheme) => void, +) { + const devtoolsScheme = chrome?.devtools?.panels.themeName; + const osDarkScheme = globalThis.matchMedia('(prefers-color-scheme: dark)'); + + if (devtoolsScheme === 'dark' || osDarkScheme.matches) { + callback('dark'); + } else { + callback('light'); + } + + osDarkScheme.addEventListener('change', (e: MediaQueryListEvent) => { + callback(e.matches ? 'dark' : 'light'); + }); +} + +export function delayTooltip(delay: number | unknown) { + if (typeof delay == 'number' && Number.isFinite(delay) && delay > 1e4) { + return ms2HMS(delay); + } +} diff --git a/src/global.d.ts b/src/global.d.ts index b76eac5..268851e 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,5 @@ -/// export {}; + declare global { let __development__: boolean; let __app_name__: string; diff --git a/src/mirror/mirror.ts b/src/mirror/mirror.ts new file mode 100644 index 0000000..7480789 --- /dev/null +++ b/src/mirror/mirror.ts @@ -0,0 +1,36 @@ +import { mount } from 'svelte'; +import App from '../view/App.svelte'; +import { initConfigState } from '../state/config.state.svelte.ts'; +import { establishTelemetryReceiverMirror } from '../state/telemetry.state.svelte.ts'; +import { EMsg, windowPost } from '../api/communication.ts'; +import { + loadLocalStorage, + onLocalStorageChange, +} from '../api/storage/storage.local.ts'; +import { + loadSessionStorage, + onSessionStorageChange, +} from '../api/storage/storage.session.ts'; + +initConfigState().then(() => { + mount(App, { target: document.body }); + establishTelemetryReceiverMirror(); + + Promise.all([loadLocalStorage(), loadSessionStorage()]).then( + ([config, session]) => { + windowPost({ msg: EMsg.CONFIG, config }); + windowPost({ msg: EMsg.SESSION, session }); + + if (!config.paused) { + windowPost({ msg: EMsg.START_OBSERVE }); + } + + onLocalStorageChange((config) => { + windowPost({ msg: EMsg.CONFIG, config }); + }); + onSessionStorageChange((session) => { + windowPost({ msg: EMsg.SESSION, session }); + }); + }, + ); +}); diff --git a/src/state/config.state.svelte.ts b/src/state/config.state.svelte.ts index a9e5ec8..44c8eba 100644 --- a/src/state/config.state.svelte.ts +++ b/src/state/config.state.svelte.ts @@ -5,7 +5,7 @@ import { loadLocalStorage, saveLocalStorage, type TConfig, -} from '../api/storage.local.ts'; +} from '../api/storage/storage.local.ts'; let config: TConfig = $state(DEFAULT_CONFIG); @@ -33,9 +33,9 @@ export async function toggleKeepAwake() { await saveLocalStorage({ keepAwake: $state.snapshot(config.keepAwake) }); if (config.keepAwake) { - chrome.power.requestKeepAwake('display'); + chrome.power?.requestKeepAwake('display'); } else { - chrome.power.releaseKeepAwake(); + chrome.power?.releaseKeepAwake(); } } diff --git a/src/state/session.state.svelte.ts b/src/state/session.state.svelte.ts index fc2c186..38fee95 100644 --- a/src/state/session.state.svelte.ts +++ b/src/state/session.state.svelte.ts @@ -1,8 +1,9 @@ import { loadSessionStorage, saveSessionStorage, -} from '../api/storage.session.ts'; +} from '../api/storage/storage.session.ts'; import { SvelteSet } from 'svelte/reactivity'; +import { session } from '../api/storage/storage.ts'; export const sessionState = $state({ bypass: > new SvelteSet(), @@ -35,7 +36,7 @@ export async function toggleDebug(traceId: string) { } } -const QUOTA_THRESHOLD = chrome.storage.session.QUOTA_BYTES; +const QUOTA_THRESHOLD = session.QUOTA_BYTES; const MARGINAL_SIZE = 40; // for ASCII string in an array async function toggleSet(set: Set, traceId: string): Promise { if (set.has(traceId)) { @@ -44,7 +45,7 @@ async function toggleSet(set: Set, traceId: string): Promise { } const freeSpace = QUOTA_THRESHOLD - - await chrome.storage.session.getBytesInUse(); + await session.getBytesInUse(); if (freeSpace - traceId.length - MARGINAL_SIZE >= 0) { set.add(traceId); diff --git a/src/state/telemetry.state.svelte.ts b/src/state/telemetry.state.svelte.ts index c9da58a..3b7d81f 100644 --- a/src/state/telemetry.state.svelte.ts +++ b/src/state/telemetry.state.svelte.ts @@ -1,5 +1,11 @@ import type { TTelemetry } from '../wrapper/Wrapper.ts'; -import { EMsg, portPost, runtimeListen } from '../api/communication.ts'; +import { + EMsg, + portPost, + runtimeListen, + windowListen, + windowPost, +} from '../api/communication.ts'; import diff from '../api/diff.ts'; import { type Writable, writable } from 'svelte/store'; @@ -14,17 +20,19 @@ export function useTelemetryState() { return state; } -runtimeListen((o) => { - if (o.msg === EMsg.TELEMETRY) { - telemetryProgressive = structuredClone(o.telemetry); - state.telemetry = o.telemetry; - acknowledgeTelemetry(o.timeOfCollection); - } else if (o.msg === EMsg.TELEMETRY_DELTA) { - diff.patch(telemetryProgressive, o.telemetryDelta); - state.telemetry = structuredClone(telemetryProgressive); - acknowledgeTelemetry(o.timeOfCollection); - } -}); +export function establishTelemetryReceiver() { + runtimeListen((o) => { + if (o.msg === EMsg.TELEMETRY) { + telemetryProgressive = structuredClone(o.telemetry); + state.telemetry = o.telemetry; + acknowledgeTelemetry(o.timeOfCollection); + } else if (o.msg === EMsg.TELEMETRY_DELTA) { + diff.patch(telemetryProgressive, o.telemetryDelta); + state.telemetry = structuredClone(telemetryProgressive); + acknowledgeTelemetry(o.timeOfCollection); + } + }); +} function acknowledgeTelemetry(timeOfCollection: number) { portPost({ @@ -34,3 +42,26 @@ function acknowledgeTelemetry(timeOfCollection: number) { state.timeOfCollection.set(timeOfCollection); } + +export function establishTelemetryReceiverMirror() { + windowListen((o) => { + if (o.msg === EMsg.TELEMETRY) { + telemetryProgressive = structuredClone(o.telemetry); + state.telemetry = o.telemetry; + acknowledgeTelemetryMirror(o.timeOfCollection); + } else if (o.msg === EMsg.TELEMETRY_DELTA) { + diff.patch(telemetryProgressive, o.telemetryDelta); + state.telemetry = structuredClone(telemetryProgressive); + acknowledgeTelemetryMirror(o.timeOfCollection); + } + }); +} + +function acknowledgeTelemetryMirror(timeOfCollection: number) { + windowPost({ + msg: EMsg.TELEMETRY_ACKNOWLEDGED, + timeOfCollection, + }); + + state.timeOfCollection.set(timeOfCollection); +} diff --git a/src/view/App.svelte b/src/view/App.svelte index 44d80e8..3418dc5 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -1,36 +1,14 @@
- -
- {#if __development__} - -
- {/if} - -
- -
- -
- -
- -
+
-
- +
diff --git a/src/view/menu/DevReload.svelte b/src/view/menu/DevReload.svelte index 09512bd..193496d 100644 --- a/src/view/menu/DevReload.svelte +++ b/src/view/menu/DevReload.svelte @@ -1,8 +1,10 @@ + + +
+{#if __development__} + +
+{/if} + +
+ +
+ +
+ +
+ +
diff --git a/src/view/menu/SummaryBar.svelte b/src/view/menu/SummaryBar.svelte index 93cae4b..2a5c6c5 100644 --- a/src/view/menu/SummaryBar.svelte +++ b/src/view/menu/SummaryBar.svelte @@ -1,5 +1,5 @@ @@ -27,9 +26,6 @@