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 @@