diff --git a/Makefile b/Makefile index f71306d..d3c1d45 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ -.PHONY: clean install dev valid test prod serve_mirror +.PHONY: clean install dev valid test test-dev prod serve_mirror .DEFAULT_GOAL := dev -DENO_DEV = NODE_ENV=development deno run --watch -DENO_PROD = NODE_ENV=production deno run -DENO_OPTIONS = --allow-env --allow-read --allow-run -CHROME_ZIP="extension.chrome.zip" -OUTPUT_DIR = ./public/ -BUILD_DIR = ./public/build/ -BUILD_SCRIPT = ./build.ts +DENO_DEV := NODE_ENV=development deno run --watch +DENO_PROD := NODE_ENV=production deno run +DENO_OPTIONS := --allow-env --allow-read --allow-run +VERSION != deno eval "\ + import m from './manifest.json' with {type:'json'};\ + console.log(m.version);\ + " +CHROME_ZIP := "extension.chrome-$(VERSION).zip" +OUTPUT_DIR := ./public/ +BUILD_DIR := ./public/build/ +BUILD_SCRIPT := ./build.ts clean: rm -rf ./node_modules $(BUILD_DIR) $(CHROME_ZIP) diff --git a/README.md b/README.md index 7a4a22e..642898c 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ To assess Web Application implementation correctness and expedite issues discove - Count `requestAnimationFrame` calls per second (CPS). - If requested recursively - it reflects animation FPS. -- Detect `eval` function usage in runtime, as well as `setTimeout` and `setInterval` when called with a `string` callback instead of a `function`. By default - `off`, cause the fact of wrapping it, excludes the access to local scope variables from the `eval` script, and as a result, may brake the application if it does need it. +- Detect `eval` function usage in runtime, as well as `setTimeout` and `setInterval` when called with a `string` callback instead of a `function`. + - By default - `off`, cause the fact of wrapping it, excludes the access to local scope variables from the `eval` script, and as a result, may break the application if it does depend on it. + +- Monitor Worker's instance behaviour, its methods and event handlers. - Monitor mounted `video` and `audio` media elements in DOM. - Present control panel with basic media functions. @@ -56,6 +59,7 @@ To assess Web Application implementation correctness and expedite issues discove - `cancelAnimationFrame` - `requestIdleCallback` - `cancelIdleCallback` +- `Worker`
diff --git a/doc/issues.log.md b/doc/issues.log.md index 6a3dc7b..368bfe6 100644 --- a/doc/issues.log.md +++ b/doc/issues.log.md @@ -4,15 +4,11 @@ ### Issues, that could have been spotted during the development - Timers with short delays unjustified for the use case, wasting CPU time. - - A ~10ms delay interval, from an old third-party library, constantly consuming approximately 10% of CPU solely to check if the window was resized. - - A 150ms delay interval, displaying time in `H:MM:SS` format (1 second precision); and displaying it via `innerHTML`. -- A bundled dependency library that utilizes the `eval` function, thereby preventing the removal of `unsafe-eval` from the `Content-Security-Policy` header. - +- A bundled dependency library that utilizes the `eval` function, thereby preventing the removal of `unsafe-eval` from the `Content-Security-Policy` header. - Code that uses `eval` with modern syntax to check if it's supported by browser (not throws exception). - - Dependency package that was bundled with webpack's config option [`devtool: 'eval'`](https://webpack.js.org/configuration/devtool/) or [`mode: 'development'`](https://webpack.js.org/configuration/mode/). - A substantial number of hidden video elements in DOM stopped working, after Chrome unexpectedly limited them to 100 per domain (later the limit was lifted to 1000). @@ -21,10 +17,11 @@ - `setTimeout`, `setInterval` are used to animate instead of `requestAnimationFrame`. -- `setTimeout` with dynamically computed delay value ends to be called with `NaN`. +- Observed on multiple sites `setTimeout` with dynamically computed delay value, ends to be called with `NaN`, `-Infinity`, `-31`... - Hidden UI feature runs its logic in the background. - - Indirectly, discovered from the bursts of short timeouts, fired from `ResizeObserver` handler of invisible feature that appears to be: or for a power user only, or just partially deprecated. - - Animation still runs (plus network requests) in the background after a "paywall" fullscreen popup. Despite claiming "it's to conserve data bandwidth". CPU usage doesn't drop to 0%. + +- Workers on loose. + - A Government Geo Science related site crashes under 2.25 hours, topping 4GB of RAM with 271 instances of the same Worker code. diff --git a/manifest.json b/manifest.json index b8ee8bd..0111376 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.3.0", + "version": "1.4.0", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxQCaHgX3DkPnGmHr+rhWyPvYemxMhBbvulmj4RvEpAnGVprdPCUiHSY0jOcDn3vnU6zm8mR1mT3sdlYoUGikBIT19/Jf1iGlc2dySt2bmDQXlTrqllT/XB8HW/wruFej9waMw9yqtW1wOJtElxWnT11pzXkKeflH1Sh+//Jnplr577vOmWh9TU8JLJHS9WklPHJyXCCMGrg/0Sxqte5qWryE2yIm9375KGkKN4ZKjSIxaCg0qodhf5Ug9s2QD7/s5xt548gbEUm9LqQHkNoIH3KXuYOnLksJFxi7FDwhg+oXalsONr5eEvPjkwxYpMKJXfRSg8sB8N6cXLUfgLAKUwIDAQAB", "name": "API Monitor", "manifest_version": 3, diff --git a/public/global.css b/public/global.css index fcef669..3aefd3e 100644 --- a/public/global.css +++ b/public/global.css @@ -2,7 +2,7 @@ color-scheme: light dark; --bg: light-dark(rgb(100% 100% 100%), rgb(10% 10% 10%)); --bg-popover: light-dark(rgb(90%, 90%, 90%), rgb(20%, 20%, 20%)); - --bg-invert: light-dark(rgb(10% 10% 10%), rgb(100% 100% 100%)); + --bg-invert: light-dark(rgb(38% 38% 38%), rgb(62% 62% 62%)); --bg-table-even: light-dark(rgb(30% 30% 30% / 10%), rgb(100% 100% 100% / 8%)); --border: light-dark(rgb(0 0 0 / 35%), rgb(100% 100% 100% / 35%)); --text: light-dark(rgb(10% 10% 10%), rgb(100% 100% 100%)); @@ -95,6 +95,9 @@ th, .w-full { width: 100%; } +.d-none { + display: none; +} .divider { width: 1px; height: 100%; @@ -204,3 +207,17 @@ th .icon { .icon.-facts { mask-image: url(img/facts.svg); } +.scrolled-to { + animation: scrolledTo 0.256s alternate infinite; +} + +@keyframes scrolledTo { + 0% { + outline: 1px solid var(--attention); + outline-offset: -1px; + } + 100% { + outline: 5px solid var(--attention); + outline-offset: -5px; + } +} diff --git a/src/api-monitor-cs-isolated.ts b/src/api-monitor-cs-isolated.ts index ccc10b2..47411ca 100644 --- a/src/api-monitor-cs-isolated.ts +++ b/src/api-monitor-cs-isolated.ts @@ -23,7 +23,13 @@ Promise.all([loadLocalStorage(), loadSessionStorage()]).then( windowPost({ msg: EMsg.START_OBSERVE }); } - portListen(windowPost); + portListen((o) => { + if (o.msg === EMsg.CONFIRM_INJECTION) { + runtimePost({ msg: EMsg.INJECTION_CONFIRMED }); + } else { + windowPost(o); + } + }); windowListen(runtimePost); onLocalStorageChange((config) => { diff --git a/src/api-monitor-cs-main.ts b/src/api-monitor-cs-main.ts index afd0323..67f8a76 100644 --- a/src/api-monitor-cs-main.ts +++ b/src/api-monitor-cs-main.ts @@ -4,7 +4,6 @@ import { adjustTelemetryDelay, Timer } from './api/time.ts'; import { applyConfig, applySession, - cleanHistory, collectMetrics, onEachSecond, runMediaCommand, @@ -61,10 +60,6 @@ windowListen((o) => { tick.stop(); eachSecond.stop(); originalMetrics = currentMetrics = null; - } else if (EMsg.RESET_WRAPPER_HISTORY === o.msg) { - originalMetrics = currentMetrics = null; - cleanHistory(); - !tick.isPending() && tick.trigger(); } else if (EMsg.TIMER_COMMAND === o.msg) { runTimerCommand(o.type, o.handler); } else if (EMsg.MEDIA_COMMAND === o.msg) { diff --git a/src/api-monitor-devtools.ts b/src/api-monitor-devtools.ts index a86f8ff..34d7e0e 100644 --- a/src/api-monitor-devtools.ts +++ b/src/api-monitor-devtools.ts @@ -19,7 +19,7 @@ if (chrome.devtools.inspectedWindow.tabId !== null) { portPost({ msg: EMsg.START_OBSERVE }); } if (config.keepAwake) { - chrome.power.requestKeepAwake('display'); + chrome.power?.requestKeepAwake('display'); } await saveLocalStorage({ devtoolsPanelShown: true }); }); diff --git a/src/api/Mean.ts b/src/api/Mean.ts new file mode 100644 index 0000000..40740fe --- /dev/null +++ b/src/api/Mean.ts @@ -0,0 +1,52 @@ +export class Mean { + samples = 0; + mean = 0; + min = Infinity; + max = -Infinity; + sosd = 0; // sum of squared deltas + + reset() { + this.samples = 0; + this.mean = 0; + this.min = Infinity; + this.max = -Infinity; + this.sosd = 0; + + return this; + } + + add(value: number) { + ++this.samples; + + this.min = Math.min(this.min, value); + this.max = Math.max(this.max, value); + + const delta = value - this.mean; + this.mean += delta / this.samples; + this.sosd += delta * (value - this.mean); + + return this; + } + + sampleVariance() { + return (this.samples > 1) ? this.sosd / (this.samples - 1) : 0; + } + + sampleStdDev() { + return (this.samples > 1) ? Math.sqrt(this.sampleVariance()) : 0; + } + + populationVariance() { + return (this.samples > 0) ? this.sosd / this.samples : 0; + } + + populationStdDev() { + return (this.samples > 0) ? Math.sqrt(this.populationVariance()) : 0; + } + + stdError() { + return (this.samples > 1) + ? this.sampleStdDev() / Math.sqrt(this.samples) + : 0; + } +} diff --git a/src/api/communication.ts b/src/api/communication.ts index f0cea7b..1cdcf49 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -109,9 +109,10 @@ export enum EMsg { TELEMETRY_DELTA, TELEMETRY_ACKNOWLEDGED, MEDIA_COMMAND, - RESET_WRAPPER_HISTORY, TIMER_COMMAND, SESSION, + CONFIRM_INJECTION, + INJECTION_CONFIRMED, } export interface IMsgStartObserve { @@ -120,9 +121,6 @@ export interface IMsgStartObserve { export interface IMsgStopObserve { msg: EMsg.STOP_OBSERVE; } -export interface IMsgResetHistory { - msg: EMsg.RESET_WRAPPER_HISTORY; -} export interface IMsgTimerCommand { msg: EMsg.TIMER_COMMAND; type: ETimerType; @@ -159,6 +157,12 @@ export interface IMsgSession { msg: EMsg.SESSION; session: TSession; } +export interface IMsgConfirmInjection { + msg: EMsg.CONFIRM_INJECTION; +} +export interface IMsgInjectionConfirmed { + msg: EMsg.INJECTION_CONFIRMED; +} export type TMsgOptions = | IMsgTelemetry | IMsgTelemetryDelta @@ -166,8 +170,9 @@ export type TMsgOptions = | IMsgStartObserve | IMsgStopObserve | IMsgLoaded - | IMsgResetHistory | IMsgTimerCommand | IMsgConfig | IMsgMediaCommand - | IMsgSession; + | IMsgSession + | IMsgConfirmInjection + | IMsgInjectionConfirmed; diff --git a/src/api/const.ts b/src/api/const.ts index e453a7f..bca5675 100644 --- a/src/api/const.ts +++ b/src/api/const.ts @@ -8,8 +8,6 @@ export const TELEMETRY_FREQUENCY_30PS = 33.3333333333; // ms export const TELEMETRY_FREQUENCY_1PS = 1000; // 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 // state native functions export const setTimeout = /*@__PURE__*/ globalThis.setTimeout.bind(globalThis); diff --git a/src/api/storage/storage.local.ts b/src/api/storage/storage.local.ts index 66058cf..294e0ca 100644 --- a/src/api/storage/storage.local.ts +++ b/src/api/storage/storage.local.ts @@ -14,8 +14,9 @@ import { CONFIG_VERSION, local } from './storage.ts'; type TPanelKey = | 'callsSummary' - | 'eval' | 'media' + | 'worker' + | 'eval' | 'activeTimers' | 'setTimeout' | 'clearTimeout' @@ -43,6 +44,7 @@ export const DEFAULT_PANELS: TPanel[] = [ { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, { key: 'activeTimers', label: 'Active Timers', visible: true, wrap: null }, + { key: 'worker', label: 'Worker', visible: true, wrap: true }, { key: 'eval', label: 'eval', visible: true, wrap: false }, { key: 'setTimeout', label: 'setTimeout History', visible: true, wrap: true }, { diff --git a/src/api/storage/storage.ts b/src/api/storage/storage.ts index c5c926e..90cf89f 100644 --- a/src/api/storage/storage.ts +++ b/src/api/storage/storage.ts @@ -1,4 +1,4 @@ -export const CONFIG_VERSION = '2025-04-25'; +export const CONFIG_VERSION = '2025-06-28'; export const SESSION_VERSION = '2025-04-25'; export const local = /*@__PURE__*/ (() => { diff --git a/src/devtoolsPanelUtil.ts b/src/devtoolsPanelUtil.ts index 2912d1c..8f458a6 100644 --- a/src/devtoolsPanelUtil.ts +++ b/src/devtoolsPanelUtil.ts @@ -8,7 +8,7 @@ import { saveLocalStorage } from './api/storage/storage.local.ts'; import { ms2HMS } from './api/time.ts'; export async function onHidePanel() { - chrome.power.releaseKeepAwake(); + chrome.power?.releaseKeepAwake(); portPost({ msg: EMsg.STOP_OBSERVE }); await saveLocalStorage({ devtoolsPanelShown: false }); } diff --git a/src/view/App.svelte b/src/view/App.svelte index 3418dc5..7e74c27 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -1,15 +1,19 @@
-
+
+ +
diff --git a/src/view/panels/shared/CellBreakpoint.svelte b/src/view/panels/shared/CellBreakpoint.svelte index 22d22fc..2b1f843 100644 --- a/src/view/panels/shared/CellBreakpoint.svelte +++ b/src/view/panels/shared/CellBreakpoint.svelte @@ -17,7 +17,8 @@
diff --git a/src/view/panels/shared/CellCancelable.svelte b/src/view/panels/shared/CellCancelable.svelte index 540d525..f43e54d 100644 --- a/src/view/panels/shared/CellCancelable.svelte +++ b/src/view/panels/shared/CellCancelable.svelte @@ -17,8 +17,8 @@ {#if canceledCounter} { e.preventDefault(); diff --git a/src/view/panels/shared/CellFrameTimeSensitive.svelte b/src/view/panels/shared/CellFrameTimeSensitive.svelte deleted file mode 100644 index ed9e220..0000000 --- a/src/view/panels/shared/CellFrameTimeSensitive.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - {Stopper.toString(value)} - - - diff --git a/src/view/panels/shared/CellSelfTime.svelte b/src/view/panels/shared/CellSelfTime.svelte new file mode 100644 index 0000000..cff0682 --- /dev/null +++ b/src/view/panels/shared/CellSelfTime.svelte @@ -0,0 +1,63 @@ + + +{#if time !== null} + +{/if} + +{#snippet SelfTimeSlot(time: number, title: string, tail?: string)} + FRAME_TIME_SAFE_THRESHOLD} + >{Stopper.toString(time)}{tail} +{/snippet} + + diff --git a/src/view/panels/shared/CellSelfTimeStats.svelte b/src/view/panels/shared/CellSelfTimeStats.svelte new file mode 100644 index 0000000..7193568 --- /dev/null +++ b/src/view/panels/shared/CellSelfTimeStats.svelte @@ -0,0 +1,40 @@ + + +{@render SelfTimeSlot(vs.mean, 'mean ± SD', `\u00A0±${vs.stdDev}`)} +{@render SelfTimeSlot(vs.max, 'max', '')} diff --git a/src/view/panels/shared/ColumnSortable.svelte b/src/view/panels/shared/ColumnSortable.svelte index 270f039..0b69a70 100644 --- a/src/view/panels/shared/ColumnSortable.svelte +++ b/src/view/panels/shared/ColumnSortable.svelte @@ -31,7 +31,7 @@ {caption} Callstack [] - + - + diff --git a/src/view/panels/worker/CollapseExpand.svelte b/src/view/panels/worker/CollapseExpand.svelte new file mode 100644 index 0000000..cdbe820 --- /dev/null +++ b/src/view/panels/worker/CollapseExpand.svelte @@ -0,0 +1,26 @@ + + + + {#if isExpanded}□{:else}■{/if} + {@render children?.()} + diff --git a/src/view/panels/worker/Worker.svelte b/src/view/panels/worker/Worker.svelte new file mode 100644 index 0000000..92ca870 --- /dev/null +++ b/src/view/panels/worker/Worker.svelte @@ -0,0 +1,29 @@ + + +{#if telemetry.collection.length} +
+ + + {#each telemetry.collection as metric (metric.specifier)} + + {/each} +
+{/if} + + diff --git a/src/view/panels/worker/WorkerMetric.svelte b/src/view/panels/worker/WorkerMetric.svelte new file mode 100644 index 0000000..10d5806 --- /dev/null +++ b/src/view/panels/worker/WorkerMetric.svelte @@ -0,0 +1,46 @@ + + +
+ + + {#if metric.online} + [] + {/if} + + void (isExpanded = !isExpanded)} + /> + + +
+ + + + + + + +
+
+ + diff --git a/src/view/panels/worker/WorkerMetricAddEventListener.svelte b/src/view/panels/worker/WorkerMetricAddEventListener.svelte new file mode 100644 index 0000000..6eb794c --- /dev/null +++ b/src/view/panels/worker/WorkerMetricAddEventListener.svelte @@ -0,0 +1,57 @@ + + +{#if metric.ael.length} + + + + + + + + + + + + + + + {#each metric.ael as ael (ael.traceId)} + + + + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + addEventListener [] + + SelfCPSEventsCalled
+ + + + {ael.eventsCps || undefined}
+{/if} diff --git a/src/view/panels/worker/WorkerMetricConstructor.svelte b/src/view/panels/worker/WorkerMetricConstructor.svelte new file mode 100644 index 0000000..eee23b6 --- /dev/null +++ b/src/view/panels/worker/WorkerMetricConstructor.svelte @@ -0,0 +1,56 @@ + + +{#if metric.konstruktor.length} + + + + + + + + + + + + {#each metric.konstruktor as konstruktor (konstruktor.traceId)} + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + constructor [] + + Called
+ + + +
+{/if} diff --git a/src/view/panels/worker/WorkerMetricOnError.svelte b/src/view/panels/worker/WorkerMetricOnError.svelte new file mode 100644 index 0000000..9c516c9 --- /dev/null +++ b/src/view/panels/worker/WorkerMetricOnError.svelte @@ -0,0 +1,57 @@ + + +{#if metric.onerror.length} + + + + + + + + + + + + + + + {#each metric.onerror as onerror (onerror.traceId)} + + + + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + set onerror [] + + SelfCPSEventsCalled
+ + + + {onerror.eventsCps || undefined}
+{/if} diff --git a/src/view/panels/worker/WorkerMetricOnMessage.svelte b/src/view/panels/worker/WorkerMetricOnMessage.svelte new file mode 100644 index 0000000..0d63646 --- /dev/null +++ b/src/view/panels/worker/WorkerMetricOnMessage.svelte @@ -0,0 +1,57 @@ + + +{#if metric.onmessage.length} + + + + + + + + + + + + + + + {#each metric.onmessage as onmessage (onmessage.traceId)} + + + + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + set onmessage [] + + SelfCPSEventsCalled
+ + + + {onmessage.eventsCps || undefined}
+{/if} diff --git a/src/view/panels/worker/WorkerMetricPostMessage.svelte b/src/view/panels/worker/WorkerMetricPostMessage.svelte new file mode 100644 index 0000000..508f749 --- /dev/null +++ b/src/view/panels/worker/WorkerMetricPostMessage.svelte @@ -0,0 +1,53 @@ + + +{#if metric.postMessage.length} + + + + + + + + + + + + + + {#each metric.postMessage as postMessage (postMessage.traceId)} + + + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + postMessage [] + + SelfCPSCalled
+ + {postMessage.cps || undefined}
+{/if} diff --git a/src/view/panels/worker/WorkerMetricRemoveEventListener.svelte b/src/view/panels/worker/WorkerMetricRemoveEventListener.svelte new file mode 100644 index 0000000..064451d --- /dev/null +++ b/src/view/panels/worker/WorkerMetricRemoveEventListener.svelte @@ -0,0 +1,48 @@ + + +{#if metric.rel.length} + + + + + + + + + + + + {#each metric.rel as rel (rel.traceId)} + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + removeEventListener [] + + Called
+ +
+{/if} diff --git a/src/view/panels/worker/WorkerMetricTerminate.svelte b/src/view/panels/worker/WorkerMetricTerminate.svelte new file mode 100644 index 0000000..959ac0e --- /dev/null +++ b/src/view/panels/worker/WorkerMetricTerminate.svelte @@ -0,0 +1,48 @@ + + +{#if metric.terminate.length} + + + + + + + + + + + + {#each metric.terminate as terminate (terminate.traceId)} + + + + + + + {/each} + +
+ void (isExpanded = !isExpanded)} + > + terminate [] + + Called
+ +
+{/if} diff --git a/src/view/panels/worker/WorkerSpecifier.svelte b/src/view/panels/worker/WorkerSpecifier.svelte new file mode 100644 index 0000000..7a188e2 --- /dev/null +++ b/src/view/panels/worker/WorkerSpecifier.svelte @@ -0,0 +1,16 @@ + + +{specifier} diff --git a/src/view/shared/Alert.svelte b/src/view/shared/Alert.svelte index 509680e..369b35a 100644 --- a/src/view/shared/Alert.svelte +++ b/src/view/shared/Alert.svelte @@ -4,10 +4,12 @@ let { title = '', dismissable = true, + class: className = '', children, }: { title: string; dismissable?: boolean; + class?: string; children?: Snippet; } = $props(); let selfEl: HTMLElement | null = null; @@ -53,15 +55,21 @@ } -
+ { e.preventDefault(); hide(); diff --git a/src/view/shared/Variable.svelte b/src/view/shared/Variable.svelte index 1192bd0..f1efea4 100644 --- a/src/view/shared/Variable.svelte +++ b/src/view/shared/Variable.svelte @@ -1,6 +1,6 @@