From 81ed4e98f738856ae0401732d492d7c5c56b56de 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: Sat, 5 Jul 2025 19:21:15 +0300 Subject: [PATCH 01/16] fix extracting a callstack trace link from a row like this `at async https://.../...:#:#` --- src/wrapper/shared/TraceUtil.ts | 9 +++++++-- tests/TraceUtil_test.ts | 6 +++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/wrapper/shared/TraceUtil.ts b/src/wrapper/shared/TraceUtil.ts index cc45024..a4534d9 100644 --- a/src/wrapper/shared/TraceUtil.ts +++ b/src/wrapper/shared/TraceUtil.ts @@ -30,7 +30,9 @@ export const TAG_INVALID_CALLSTACK_LINK = '⟪N/A⟫'; const REGEX_STACKTRACE_SPLIT = /*@__PURE__*/ new RegExp(/\n\s+at\s/); const REGEX_STACKTRACE_NAME = /*@__PURE__*/ new RegExp(/^(.+)\(.*/); +const REGEX_STACKTRACE_HAS_LINK = /*@__PURE__*/ new RegExp(/:\/\//); const REGEX_STACKTRACE_LINK = /*@__PURE__*/ new RegExp(/.*\((async )?(.*)\)$/); +const REGEX_STACKTRACE_LINK_REMOVE = /*@__PURE__*/ new RegExp(/async /); const REGEX_STACKTRACE_LINK_PROTOCOL = /*@__PURE__*/ new RegExp( /http[s]?\:\/\//, ); @@ -154,13 +156,16 @@ export class TraceUtil { return; } - const link = stackRow.replace(REGEX_STACKTRACE_LINK, '$2').trim(); + const link = stackRow + .replace(REGEX_STACKTRACE_LINK, '$2') + .replace(REGEX_STACKTRACE_LINK_REMOVE, '') + .trim(); if (link.indexOf('') >= 0) { return; } let name: string | 0 = stackRow.replace(REGEX_STACKTRACE_NAME, '$1').trim(); - if (name === link) { + if (name === link || REGEX_STACKTRACE_HAS_LINK.test(name)) { name = 0; } diff --git a/tests/TraceUtil_test.ts b/tests/TraceUtil_test.ts index 7e81982..86a9d12 100644 --- a/tests/TraceUtil_test.ts +++ b/tests/TraceUtil_test.ts @@ -13,6 +13,7 @@ describe('TraceUtil', () => { at async (:1:1) at call2 (async https://example2.com/bundle3.js:4:5) at call1 (https://example1.com/bundle2.js:3:4) + at async https://example1.com/bundle2.js:2:3 at self (${traceUtil.selfTraceLink}:77:19)`; const TEST_MISSING_STACK = `Error: ${TraceUtil.SIGNATURE} at self (${traceUtil.selfTraceLink}:77:19) @@ -22,6 +23,7 @@ describe('TraceUtil', () => { test('createCallstack full', () => { traceUtil.callstackType = EWrapperCallstackType.FULL; const standard = [ + { name: 0, link: 'https://example1.com/bundle2.js:2:3' }, { name: 'call1', link: 'https://example1.com/bundle2.js:3:4' }, { name: 'call2', link: 'https://example2.com/bundle3.js:4:5' }, ]; @@ -30,11 +32,13 @@ describe('TraceUtil', () => { null, ); - expect(trace.length).toBe(2); + expect(trace.length).toBe(3); expect(trace[0].name).toBe(standard[0].name); expect(trace[0].link).toBe(standard[0].link); expect(trace[1].name).toBe(standard[1].name); expect(trace[1].link).toBe(standard[1].link); + expect(trace[2].name).toBe(standard[2].name); + expect(trace[2].link).toBe(standard[2].link); }); test('createCallstack short', () => { From f067093baeb1983fa0a9d695048cbaa03de9ba81 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: Sat, 5 Jul 2025 19:43:50 +0300 Subject: [PATCH 02/16] add Worker telemetry --- README.md | 6 +- doc/issues.log.md | 11 +- manifest.json | 2 +- public/global.css | 5 +- src/api/storage/storage.local.ts | 4 +- src/api/storage/storage.ts | 2 +- src/view/menu/DevDumpTelemetry.svelte | 17 + src/view/menu/DevReload.svelte | 6 +- src/view/menu/Menu.svelte | 3 + src/view/menu/SummaryBar.svelte | 22 +- src/view/menu/SummaryBarItem.svelte | 4 +- src/view/panels/Panels.svelte | 4 +- src/view/panels/media/Media.svelte | 3 + src/view/panels/worker/CollapseExpand.svelte | 25 + src/view/panels/worker/Worker.svelte | 42 ++ src/view/panels/worker/WorkerMetric.svelte | 46 ++ .../WorkerMetricAddEventListener.svelte | 57 +++ .../worker/WorkerMetricConstructor.svelte | 45 ++ .../panels/worker/WorkerMetricOnError.svelte | 57 +++ .../worker/WorkerMetricOnMessage.svelte | 57 +++ .../worker/WorkerMetricPostMessage.svelte | 55 +++ .../WorkerMetricRemoveEventListener.svelte | 48 ++ .../worker/WorkerMetricTerminate.svelte | 48 ++ src/view/panels/worker/WorkerSpecifier.svelte | 15 + src/wrapper/AnimationWrapper.ts | 26 +- src/wrapper/EvalWrapper.ts | 13 +- src/wrapper/IdleWrapper.ts | 22 +- src/wrapper/MediaWrapper.ts | 5 +- src/wrapper/TimerWrapper.ts | 44 +- src/wrapper/WorkerWrapper.ts | 458 ++++++++++++++++++ src/wrapper/Wrapper.ts | 30 +- src/wrapper/shared/util.ts | 4 + tests/AnimationWrapper_test.ts | 4 +- tests/EvalWrapper_test.ts | 6 +- tests/IdleWrapper_test.ts | 4 +- tests/TimerWrapper_test.ts | 6 +- 36 files changed, 1101 insertions(+), 105 deletions(-) create mode 100644 src/view/menu/DevDumpTelemetry.svelte create mode 100644 src/view/panels/worker/CollapseExpand.svelte create mode 100644 src/view/panels/worker/Worker.svelte create mode 100644 src/view/panels/worker/WorkerMetric.svelte create mode 100644 src/view/panels/worker/WorkerMetricAddEventListener.svelte create mode 100644 src/view/panels/worker/WorkerMetricConstructor.svelte create mode 100644 src/view/panels/worker/WorkerMetricOnError.svelte create mode 100644 src/view/panels/worker/WorkerMetricOnMessage.svelte create mode 100644 src/view/panels/worker/WorkerMetricPostMessage.svelte create mode 100644 src/view/panels/worker/WorkerMetricRemoveEventListener.svelte create mode 100644 src/view/panels/worker/WorkerMetricTerminate.svelte create mode 100644 src/view/panels/worker/WorkerSpecifier.svelte create mode 100644 src/wrapper/WorkerWrapper.ts diff --git a/README.md b/README.md index 7a4a22e..ebece3b 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 brake the application if it does need 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..5ffeda5 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. - - 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..75f42ae 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%; diff --git a/src/api/storage/storage.local.ts b/src/api/storage/storage.local.ts index 66058cf..5468372 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' @@ -42,6 +43,7 @@ export type TConfigField = Partial; export const DEFAULT_PANELS: TPanel[] = [ { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, + { key: 'worker', label: 'Worker', visible: true, wrap: true }, { key: 'activeTimers', label: 'Active Timers', visible: true, wrap: null }, { 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/view/menu/DevDumpTelemetry.svelte b/src/view/menu/DevDumpTelemetry.svelte new file mode 100644 index 0000000..44d826a --- /dev/null +++ b/src/view/menu/DevDumpTelemetry.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/view/menu/DevReload.svelte b/src/view/menu/DevReload.svelte index 193496d..269e6f6 100644 --- a/src/view/menu/DevReload.svelte +++ b/src/view/menu/DevReload.svelte @@ -10,6 +10,10 @@ } - diff --git a/src/view/menu/Menu.svelte b/src/view/menu/Menu.svelte index d02d416..f2847ba 100644 --- a/src/view/menu/Menu.svelte +++ b/src/view/menu/Menu.svelte @@ -6,6 +6,7 @@ import DevReload from './DevReload.svelte'; import TogglePause from './TogglePause.svelte'; import UpdatePace from './UpdatePace.svelte'; + import DevDumpTelemetry from './DevDumpTelemetry.svelte'; @@ -13,6 +14,8 @@ {#if __development__}
+ +
{/if}
diff --git a/src/view/menu/SummaryBar.svelte b/src/view/menu/SummaryBar.svelte index 2a5c6c5..c1c0168 100644 --- a/src/view/menu/SummaryBar.svelte +++ b/src/view/menu/SummaryBar.svelte @@ -11,13 +11,6 @@
{#if ts.telemetry && panels.callsSummary.visible} - - + + + + a { padding: 0 0.4rem; - border-right: 1px solid var(--border); + &:not(:last-of-type) { + border-right: 1px solid var(--border); + } &.link-disabled { cursor: default; diff --git a/src/view/panels/Panels.svelte b/src/view/panels/Panels.svelte index 4e0aa99..47c6b5c 100644 --- a/src/view/panels/Panels.svelte +++ b/src/view/panels/Panels.svelte @@ -9,13 +9,15 @@ import IdleCallbackRequestHistory from './idle/IdleCallbackRequestHistory.svelte'; import Online from './online/Online.svelte'; import { useTelemetryState } from '../../state/telemetry.state.svelte.ts'; + import Worker from './worker/Worker.svelte'; const ts = useTelemetryState(); {#if ts.telemetry} - + + {#if ts.telemetry.setTimeoutHistory?.length} diff --git a/src/view/panels/media/Media.svelte b/src/view/panels/media/Media.svelte index 80aa406..5a089ab 100644 --- a/src/view/panels/media/Media.svelte +++ b/src/view/panels/media/Media.svelte @@ -51,7 +51,10 @@ } .label { + display: flex; + align-items: center; font-weight: bold; width: 100%; + height: 1.125rem; } diff --git a/src/view/panels/worker/CollapseExpand.svelte b/src/view/panels/worker/CollapseExpand.svelte new file mode 100644 index 0000000..1129355 --- /dev/null +++ b/src/view/panels/worker/CollapseExpand.svelte @@ -0,0 +1,25 @@ + + + + {#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..6bf6106 --- /dev/null +++ b/src/view/panels/worker/Worker.svelte @@ -0,0 +1,42 @@ + + +{#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..68dbc9b --- /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..fcae97b --- /dev/null +++ b/src/view/panels/worker/WorkerMetricConstructor.svelte @@ -0,0 +1,45 @@ + + +{#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..138612c --- /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..653691c --- /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..82adb44 --- /dev/null +++ b/src/view/panels/worker/WorkerMetricPostMessage.svelte @@ -0,0 +1,55 @@ + + +{#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..07b4762 --- /dev/null +++ b/src/view/panels/worker/WorkerSpecifier.svelte @@ -0,0 +1,15 @@ + + +{specifier} diff --git a/src/wrapper/AnimationWrapper.ts b/src/wrapper/AnimationWrapper.ts index 8e1a917..f6d2a0e 100644 --- a/src/wrapper/AnimationWrapper.ts +++ b/src/wrapper/AnimationWrapper.ts @@ -11,7 +11,7 @@ import { type TTrace, } from './shared/TraceUtil.ts'; import { trim2ms } from '../api/time.ts'; -import { validHandler } from './shared/util.ts'; +import { traceUtil, validHandler } from './shared/util.ts'; import { Fact, type TFact } from './shared/Fact.ts'; export type TRequestAnimationFrameHistory = { @@ -49,7 +49,6 @@ export const CafFacts = /*@__PURE__*/ (() => ]))(); export class AnimationWrapper { - traceUtil: TraceUtil; native = { requestAnimationFrame: requestAnimationFrame, cancelAnimationFrame: cancelAnimationFrame, @@ -65,8 +64,7 @@ export class AnimationWrapper { cancelAnimationFrame: 0, }; - constructor(traceUtil: TraceUtil) { - this.traceUtil = traceUtil; + constructor() { } #updateRafHistory(handler: number, callstack: TCallstack) { @@ -80,7 +78,7 @@ export class AnimationWrapper { this.rafHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), calls: 1, handler, online: 1, @@ -137,7 +135,7 @@ export class AnimationWrapper { this.cafHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), facts, calls: 1, handler, @@ -157,7 +155,9 @@ export class AnimationWrapper { } } - updateAnimationsFramerate() { + updateAnimationsFramerate(panel: TPanel) { + if (!panel.wrap || !panel.visible) return; + for (const [, rafRecord] of this.rafHistory) { const prevCalls = this.animationCallsMap.get(rafRecord.traceId) || 0; rafRecord.cps = rafRecord.calls - prevCalls; @@ -172,15 +172,15 @@ export class AnimationWrapper { fn: FrameRequestCallback, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err, fn); + const callstack = traceUtil.getCallstack(err, fn); this.callCounter.requestAnimationFrame++; const handler = this.native.requestAnimationFrame((...args) => { const start = performance.now(); let selfTime = null; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } fn(...args); @@ -201,13 +201,13 @@ export class AnimationWrapper { handler: number, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err); + const callstack = traceUtil.getCallstack(err); this.#updateCafHistory(handler, callstack); this.callCounter.cancelAnimationFrame++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } this.native.cancelAnimationFrame(handler); diff --git a/src/wrapper/EvalWrapper.ts b/src/wrapper/EvalWrapper.ts index b78c399..d450813 100644 --- a/src/wrapper/EvalWrapper.ts +++ b/src/wrapper/EvalWrapper.ts @@ -8,6 +8,7 @@ import { import { trim2ms } from '../api/time.ts'; import type { TPanel } from '../api/storage/storage.local.ts'; import { Fact, type TFact } from './shared/Fact.ts'; +import { traceUtil } from './shared/util.ts'; export type TEvalHistory = { traceId: string; @@ -47,13 +48,11 @@ export const EvalFacts = /*@__PURE__*/ (() => ]))(); export class EvalWrapper { - traceUtil: TraceUtil; evalHistory: Map = new Map(); callCounter = 0; nativeEval = lesserEval; - constructor(traceUtil: TraceUtil) { - this.traceUtil = traceUtil; + constructor() { } updateHistory( @@ -87,7 +86,7 @@ export class EvalWrapper { facts, traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), selfTime, }); } @@ -99,7 +98,7 @@ export class EvalWrapper { code: string, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err, code); + const callstack = traceUtil.getCallstack(err, code); let rv: unknown; let throwError = null; let usesLocalScope = false; @@ -109,8 +108,8 @@ export class EvalWrapper { this.callCounter++; const start = performance.now(); - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } rv = this.nativeEval(code); diff --git a/src/wrapper/IdleWrapper.ts b/src/wrapper/IdleWrapper.ts index 7d5a9a8..965cf8b 100644 --- a/src/wrapper/IdleWrapper.ts +++ b/src/wrapper/IdleWrapper.ts @@ -6,7 +6,7 @@ import { TraceUtil, type TTrace, } from './shared/TraceUtil.ts'; -import { validHandler, validTimerDelay } from './shared/util.ts'; +import { traceUtil, validHandler, validTimerDelay } from './shared/util.ts'; import { Fact, type TFact } from './shared/Fact.ts'; import { TAG_BAD_DELAY, TAG_BAD_HANDLER } from '../api/const.ts'; @@ -63,7 +63,6 @@ export const CicFacts = /*@__PURE__*/ (() => ]))(); export class IdleWrapper { - traceUtil: TraceUtil; onlineIdleCallbackLookup: Map = new Map(); ricHistory: Map = new Map(); @@ -77,8 +76,7 @@ export class IdleWrapper { cancelIdleCallback: cancelIdleCallback, }; - constructor(traceUtil: TraceUtil) { - this.traceUtil = traceUtil; + constructor() { } #ricFired( @@ -130,7 +128,7 @@ export class IdleWrapper { this.ricHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), facts, calls: 1, handler, @@ -175,7 +173,7 @@ export class IdleWrapper { this.cicHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), facts, calls: 1, handler, @@ -204,15 +202,15 @@ export class IdleWrapper { ) { const delay = options?.timeout; const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err, fn); + const callstack = traceUtil.getCallstack(err, fn); this.callCounter.requestIdleCallback++; const handler = this.native.requestIdleCallback((deadline) => { const start = performance.now(); let selfTime = null; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } fn(deadline); @@ -233,13 +231,13 @@ export class IdleWrapper { handler: number, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err); + const callstack = traceUtil.getCallstack(err); this.#updateCicHistory(handler, callstack); this.callCounter.cancelIdleCallback++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } this.native.cancelIdleCallback(handler); diff --git a/src/wrapper/MediaWrapper.ts b/src/wrapper/MediaWrapper.ts index e83e023..160f716 100644 --- a/src/wrapper/MediaWrapper.ts +++ b/src/wrapper/MediaWrapper.ts @@ -7,6 +7,7 @@ import { READY_STATE, TIME_60FPS_SEC, } from '../api/const.ts'; +import type { TPanel } from '../api/storage/storage.local.ts'; type TMediaElement = HTMLVideoElement | HTMLAudioElement; type TMediaModel = { @@ -113,7 +114,9 @@ export class MediaWrapper { return rv; } - meetMedia() { + meetMedia(panel: TPanel) { + if (!panel.visible) return; + const els: NodeListOf = document.querySelectorAll( 'video,audio', ); diff --git a/src/wrapper/TimerWrapper.ts b/src/wrapper/TimerWrapper.ts index 968f2b4..d341f59 100644 --- a/src/wrapper/TimerWrapper.ts +++ b/src/wrapper/TimerWrapper.ts @@ -17,7 +17,7 @@ import { } from '../api/const.ts'; import type { TPanel } from '../api/storage/storage.local.ts'; import type { EvalWrapper } from './EvalWrapper.ts'; -import { validHandler, validTimerDelay } from './shared/util.ts'; +import { traceUtil, validHandler, validTimerDelay } from './shared/util.ts'; import { trim2ms } from '../api/time.ts'; import { Fact, type TFact } from './shared/Fact.ts'; @@ -85,7 +85,6 @@ export const ClearTimerFacts = /*@__PURE__*/ (() => ]))(); export class TimerWrapper { - traceUtil: TraceUtil; apiEval: EvalWrapper; onlineTimers: Map = new Map(); setTimeoutHistory: Map = new Map(); @@ -105,8 +104,7 @@ export class TimerWrapper { clearInterval: 0, }; - constructor(traceUtil: TraceUtil, apiEval: EvalWrapper) { - this.traceUtil = traceUtil; + constructor(apiEval: EvalWrapper) { this.apiEval = apiEval; } @@ -124,7 +122,7 @@ export class TimerWrapper { delay, traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), }); } @@ -202,7 +200,7 @@ export class TimerWrapper { online: 1, traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), facts, canceledCounter: 0, canceledByTraceIds: null, @@ -248,7 +246,7 @@ export class TimerWrapper { delay: handlerDelay, traceId: callstack.traceId, trace: callstack.trace, - traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), facts, }); } @@ -274,7 +272,7 @@ export class TimerWrapper { ...args: unknown[] ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err, code); + const callstack = traceUtil.getCallstack(err, code); const isEval = typeof code !== 'function'; this.callCounter.setTimeout++; @@ -285,8 +283,8 @@ export class TimerWrapper { if (isEval) { this.apiEval.callCounter++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } // see https://developer.mozilla.org/docs/Web/API/setTimeout#code @@ -294,8 +292,8 @@ export class TimerWrapper { selfTime = performance.now() - start; } } else { - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } code(...params); @@ -342,7 +340,7 @@ export class TimerWrapper { handler: number | undefined, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err); + const callstack = traceUtil.getCallstack(err); this.#updateClearTimersHistory( this.clearTimeoutHistory, @@ -356,8 +354,8 @@ export class TimerWrapper { this.callCounter.clearTimeout++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } this.native.clearTimeout(handler); @@ -373,7 +371,7 @@ export class TimerWrapper { ...args: unknown[] ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err, code); + const callstack = traceUtil.getCallstack(err, code); const isEval = typeof code !== 'function'; this.callCounter.setInterval++; @@ -385,8 +383,8 @@ export class TimerWrapper { if (isEval) { this.apiEval.callCounter++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } // see https://developer.mozilla.org/docs/Web/API/setInterval @@ -394,8 +392,8 @@ export class TimerWrapper { selfTime = performance.now() - start; } } else { - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } code(...params); @@ -441,7 +439,7 @@ export class TimerWrapper { handler: number | undefined, ) { const err = new Error(TraceUtil.SIGNATURE); - const callstack = this.traceUtil.getCallstack(err); + const callstack = traceUtil.getCallstack(err); this.#updateClearTimersHistory( this.clearIntervalHistory, @@ -455,8 +453,8 @@ export class TimerWrapper { this.callCounter.clearInterval++; - if (this.traceUtil.shouldPass(callstack.traceId)) { - if (this.traceUtil.shouldPause(callstack.traceId)) { + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { debugger; } this.native.clearInterval(handler); diff --git a/src/wrapper/WorkerWrapper.ts b/src/wrapper/WorkerWrapper.ts new file mode 100644 index 0000000..a5ba83f --- /dev/null +++ b/src/wrapper/WorkerWrapper.ts @@ -0,0 +1,458 @@ +import { ETraceDomain, TraceUtil, type TTrace } from './shared/TraceUtil.ts'; +import { traceUtil } from './shared/util.ts'; +import type { TPanel } from '../api/storage/storage.local.ts'; +import { trim2ms } from '../api/time.ts'; + +export interface IWorkerTelemetry { + totalOnline: number; + collection: IWorkerTelemetryMetric[]; +} +export interface IWorkerTelemetryMetric { + specifier: string; + online: number; + konstruktor: IConstructorMetric[]; + terminate: ITerminateMetric[]; + postMessage: IPostMessageMetric[]; + onmessage: IOnMessageMetric[]; + onerror: IOnErrorMetric[]; + ael: IAddEventListenerMetric[]; + rel: IRemoveEventListenerMetric[]; +} + +interface IWorkerMetric { + specifier: string; + online: number; + callsPerSecond: Map; + konstruktor: Map; + terminate: Map; + postMessage: Map; + onmessage: Map; + onerror: Map; + ael: Map; + rel: Map; +} +interface IConstructorMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; +} +interface ITerminateMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; +} +interface IPostMessageMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; + selfTime: number | null; + cps: number; +} +interface IOnMessageMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; + events: number; + eventSelfTime: number | null; + eventsCps: number; +} +interface IOnErrorMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; + events: number; + eventSelfTime: number | null; + eventsCps: number; +} +interface IAddEventListenerMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; + events: number; + eventSelfTime: number | null; + eventsCps: number; +} +interface IRemoveEventListenerMetric { + traceId: string; + trace: TTrace[]; + traceDomain: ETraceDomain; + calls: number; +} + +const workerMap: Map = new Map(); + +class ApiMonitorWorkerWrapper extends Worker { + readonly #specifier: string; + #eventHandlerLink: WeakMap< + /*authored handler*/ EventListenerOrEventListenerObject, + /*actual handler*/ EventListener + > = new WeakMap(); + + constructor(specifier: string | URL, options?: WorkerOptions) { + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + const methodMetric: IConstructorMetric = { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + }; + + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + super(specifier, options); + + this.#specifier = String(specifier); + const workerMetric = workerMap.get(this.#specifier); + if (workerMetric) { + workerMetric.online++; + + const rec = workerMetric.konstruktor.get(methodMetric.traceId); + if (rec) { + rec.calls++; + } else { + workerMetric.konstruktor.set(methodMetric.traceId, methodMetric); + } + } else { + workerMap.set(this.#specifier, { + specifier: this.#specifier, + online: 1, + callsPerSecond: new Map(), + konstruktor: new Map([[methodMetric.traceId, methodMetric]]), + terminate: new Map(), + postMessage: new Map(), + onmessage: new Map(), + onerror: new Map(), + ael: new Map(), + rel: new Map(), + }); + } + } + + terminate() { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + const methodMetric = workerMetric && + workerMetric.terminate.get(callstack.traceId); + + if (methodMetric) { + methodMetric.calls++; + } else { + workerMetric!.terminate.set(callstack.traceId, { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + }); + } + + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { + debugger; + } + super.terminate(); + if (workerMetric!.online) { + workerMetric!.online--; + } + } + } + + // @ts-expect-error: `Parameters...` conflict with multiple signatures overrides + postMessage(...args: Parameters) { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + const methodMetric = workerMetric && + workerMetric.postMessage.get(callstack.traceId); + const start = performance.now(); + let selfTime = null; + + if (traceUtil.shouldPass(callstack.traceId)) { + if (traceUtil.shouldPause(callstack.traceId)) { + debugger; + } + super.postMessage(...args); + selfTime = trim2ms(performance.now() - start); + } + + if (methodMetric) { + methodMetric.calls++; + methodMetric.selfTime = selfTime; + } else { + workerMetric!.postMessage.set(callstack.traceId, { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + selfTime, + cps: 1, + }); + } + } + + set onmessage(rhs: (ev: MessageEvent) => unknown | null) { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + let methodMetric = workerMetric && + workerMetric.onmessage.get(callstack.traceId); + let eventSelfTime: null | number = null; + + if (methodMetric) { + methodMetric.calls++; + } else { + methodMetric = { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + events: 0, + eventSelfTime, + eventsCps: 1, + }; + workerMetric!.onmessage.set(callstack.traceId, methodMetric); + } + + if (typeof rhs === 'function') { + super.onmessage = function (...args) { + const start = performance.now(); + + if (traceUtil.shouldPass(methodMetric.traceId)) { + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + rhs(...args); + eventSelfTime = trim2ms(performance.now() - start); + methodMetric.events++; + } + + methodMetric.eventSelfTime = eventSelfTime; + }; + } else { + super.onmessage = rhs; + } + } + + set onerror(rhs: (e: ErrorEvent) => unknown | null) { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + let methodMetric = workerMetric && + workerMetric.onerror.get(callstack.traceId); + let eventSelfTime: null | number = null; + + if (methodMetric) { + methodMetric.calls++; + } else { + methodMetric = { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + events: 0, + eventSelfTime, + eventsCps: 1, + }; + workerMetric!.onerror.set(callstack.traceId, methodMetric); + } + + if (typeof rhs === 'function') { + super.onerror = function (...args) { + const start = performance.now(); + + if (traceUtil.shouldPass(methodMetric.traceId)) { + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + rhs(...args); + eventSelfTime = trim2ms(performance.now() - start); + methodMetric.events++; + } + + methodMetric.eventSelfTime = eventSelfTime; + }; + } else { + super.onerror = rhs; + } + } + + addEventListener(type: string, listener: unknown, options?: unknown) { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + let methodMetric = workerMetric && workerMetric.ael.get(callstack.traceId); + let eventSelfTime: null | number = null; + + if (methodMetric) { + methodMetric.calls++; + } else { + methodMetric = { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + events: 0, + eventSelfTime, + eventsCps: 1, + }; + workerMetric!.ael.set(callstack.traceId, methodMetric); + } + + /** + * If the function or object is already in the list of event listeners for this target, + * the function or object is not added a second time. + * -- https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + */ + if (this.#eventHandlerLink.get( listener)) return; + + if (typeof listener === 'function') { + const selfHandler = function ( + this: ApiMonitorWorkerWrapper, + e: Event, + ) { + const start = performance.now(); + + if (traceUtil.shouldPass(methodMetric.traceId)) { + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + ( listener).call(this, e); + eventSelfTime = trim2ms(performance.now() - start); + methodMetric.events++; + } + + methodMetric.eventSelfTime = eventSelfTime; + }.bind(this); + + this.#eventHandlerLink.set( listener, selfHandler); + // @ts-expect-error: expects known types + super.addEventListener(type, selfHandler, options); + } else if ( + listener && typeof listener === 'object' && 'handleEvent' in listener && + typeof listener.handleEvent === 'function' + ) { + const selfHandler = function (e: Event) { + const start = performance.now(); + + if (traceUtil.shouldPass(methodMetric.traceId)) { + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + ( listener).handleEvent(e); + eventSelfTime = trim2ms(performance.now() - start); + methodMetric.events++; + } + + methodMetric.eventSelfTime = eventSelfTime; + }; + + this.#eventHandlerLink.set( listener, selfHandler); + // @ts-expect-error: expects known types + super.addEventListener(type, selfHandler, options); + } + } + + removeEventListener(type: string, listener: unknown, options?: unknown) { + const workerMetric = workerMap.get(this.#specifier); + const callstack = traceUtil.getCallstack(new Error(TraceUtil.SIGNATURE)); + let methodMetric = workerMetric && + workerMetric.rel.get(callstack.traceId); + + if (methodMetric) { + methodMetric.calls++; + } else { + methodMetric = { + traceId: callstack.traceId, + trace: callstack.trace, + traceDomain: traceUtil.getTraceDomain(callstack.trace[0]), + calls: 1, + }; + workerMetric!.rel.set(callstack.traceId, methodMetric); + } + + const selfHandler = this.#eventHandlerLink.get( + listener, + ); + if (selfHandler) { + if (traceUtil.shouldPass(methodMetric.traceId)) { + if (traceUtil.shouldPause(methodMetric.traceId)) { + debugger; + } + // @ts-expect-error: expects known types + super.removeEventListener(type, selfHandler, options); + } + } + } +} + +export function updateWorkerFrameRateMetrics(panel: TPanel) { + if (!panel.wrap || !panel.visible) return; + + for (const [_, workerMetric] of workerMap) { + const cpsMap = workerMetric.callsPerSecond; + + for (const [_, methodMetric] of workerMetric.postMessage) { + const prevCalls = cpsMap.get(methodMetric.traceId) || 0; + + methodMetric.cps = methodMetric.calls - prevCalls; + cpsMap.set(methodMetric.traceId, methodMetric.calls); + } + + for (const [_, methodMetric] of workerMetric.onmessage) { + const prevEvents = cpsMap.get(methodMetric.traceId) || 0; + + methodMetric.eventsCps = methodMetric.events - prevEvents; + cpsMap.set(methodMetric.traceId, methodMetric.events); + } + + for (const [_, methodMetric] of workerMetric.onerror) { + const prevEvents = cpsMap.get(methodMetric.traceId) || 0; + + methodMetric.eventsCps = methodMetric.events - prevEvents; + cpsMap.set(methodMetric.traceId, methodMetric.events); + } + + for (const [_, methodMetric] of workerMetric.ael) { + const prevEvents = cpsMap.get(methodMetric.traceId) || 0; + + methodMetric.eventsCps = methodMetric.events - prevEvents; + cpsMap.set(methodMetric.traceId, methodMetric.events); + } + } +} + +export function wrapWorker() { + // @ts-expect-error: TS2322 - new class extends from `Worker` + globalThis.Worker = ApiMonitorWorkerWrapper; +} + +export function collectWorkerHistory(panel: TPanel): IWorkerTelemetry { + const rv: IWorkerTelemetry = { + totalOnline: 0, + collection: [], + }; + + for (const [_, metric] of workerMap) { + rv.totalOnline += metric.online; + } + + if (panel.visible) { + for (const [_, metric] of workerMap) { + rv.collection.push({ + specifier: metric.specifier, + online: metric.online, + konstruktor: Array.from(metric.konstruktor.values()), + terminate: Array.from(metric.terminate.values()), + postMessage: Array.from(metric.postMessage.values()), + onmessage: Array.from(metric.onmessage.values()), + onerror: Array.from(metric.onerror.values()), + ael: Array.from(metric.ael.values()), + rel: Array.from(metric.rel.values()), + }); + } + } + + return rv; +} diff --git a/src/wrapper/Wrapper.ts b/src/wrapper/Wrapper.ts index b6e5a3c..b1fe8e1 100644 --- a/src/wrapper/Wrapper.ts +++ b/src/wrapper/Wrapper.ts @@ -1,5 +1,4 @@ import { callableOnce } from '../api/time.ts'; -import { TraceUtil } from './shared/TraceUtil.ts'; import { EvalWrapper, type TEvalHistory } from './EvalWrapper.ts'; import { EWrapperCallstackType, @@ -25,6 +24,13 @@ import { } from './IdleWrapper.ts'; import { MediaWrapper, type TMediaTelemetry } from './MediaWrapper.ts'; import type { TSession } from '../api/storage/storage.session.ts'; +import { + collectWorkerHistory, + type IWorkerTelemetry, + updateWorkerFrameRateMetrics, + wrapWorker, +} from './WorkerWrapper.ts'; +import { traceUtil } from './shared/util.ts'; export type TTelemetry = { media: TMediaTelemetry; @@ -39,6 +45,7 @@ export type TTelemetry = { ricHistory: TRequestIdleCallbackHistory[] | null; cicHistory: TCancelIdleCallbackHistory[] | null; activeTimers: number; + worker: IWorkerTelemetry; callCounter: { setTimeout: number; clearTimeout: number; @@ -53,12 +60,11 @@ export type TTelemetry = { }; let panels: TPanelMap; -const traceUtil = new TraceUtil(); const apiMedia = new MediaWrapper(); -const apiEval = new EvalWrapper(traceUtil); -const apiTimer = new TimerWrapper(traceUtil, apiEval); -const apiAnimation = new AnimationWrapper(traceUtil); -const apiIdle = new IdleWrapper(traceUtil); +const apiEval = new EvalWrapper(); +const apiTimer = new TimerWrapper(apiEval); +const apiAnimation = new AnimationWrapper(); +const apiIdle = new IdleWrapper(); const setCallstackType = callableOnce((type: EWrapperCallstackType) => { traceUtil.callstackType = type; @@ -74,6 +80,7 @@ const wrapApis = callableOnce(() => { panels.cancelAnimationFrame.wrap && apiAnimation.wrapCancelAnimationFrame(); panels.requestIdleCallback.wrap && apiIdle.wrapRequestIdleCallback(); panels.cancelIdleCallback.wrap && apiIdle.wrapCancelIdleCallback(); + panels.worker.wrap && wrapWorker(); }); export function applyConfig(config: TConfig) { @@ -88,13 +95,9 @@ export function applySession(session: TSession) { } export function onEachSecond() { - apiMedia.meetMedia(); - if ( - panels.requestAnimationFrame.wrap && - panels.requestAnimationFrame.visible - ) { - apiAnimation.updateAnimationsFramerate(); - } + apiMedia.meetMedia(panels.media); + apiAnimation.updateAnimationsFramerate(panels.requestAnimationFrame); + updateWorkerFrameRateMetrics(panels.worker); } export function collectMetrics(): TTelemetry { @@ -117,6 +120,7 @@ export function collectMetrics(): TTelemetry { panels.cancelIdleCallback, ), activeTimers: apiTimer.onlineTimers.size, + worker: collectWorkerHistory(panels.worker), callCounter: panels.callsSummary.visible ? { eval: apiEval.callCounter, diff --git a/src/wrapper/shared/util.ts b/src/wrapper/shared/util.ts index cc840d3..f3c4d37 100644 --- a/src/wrapper/shared/util.ts +++ b/src/wrapper/shared/util.ts @@ -1,3 +1,5 @@ +import { TraceUtil } from './TraceUtil.ts'; + export function validHandler(handler: unknown): handler is number { return Number.isInteger(handler) && handler > 0; } @@ -5,3 +7,5 @@ export function validHandler(handler: unknown): handler is number { export function validTimerDelay(delay: unknown): delay is number { return delay === undefined || (Number.isFinite(delay) && delay >= 0); } + +export const traceUtil = /*@__PURE__*/ new TraceUtil(); diff --git a/tests/AnimationWrapper_test.ts b/tests/AnimationWrapper_test.ts index 6171675..d0ba498 100644 --- a/tests/AnimationWrapper_test.ts +++ b/tests/AnimationWrapper_test.ts @@ -2,17 +2,15 @@ import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; import './browserPolyfill.ts'; import { AnimationWrapper, CafFact } from '../src/wrapper/AnimationWrapper.ts'; -import { TraceUtil } from '../src/wrapper/shared/TraceUtil.ts'; import { TAG_BAD_HANDLER } from '../src/api/const.ts'; import { Fact } from '../src/wrapper/shared/Fact.ts'; import { wait } from '../src/api/time.ts'; describe('AnimationWrapper', () => { - const traceUtil = new TraceUtil(); let apiAnimation: AnimationWrapper; beforeEach(() => { - apiAnimation = new AnimationWrapper(traceUtil); + apiAnimation = new AnimationWrapper(); apiAnimation.wrapRequestAnimationFrame(); apiAnimation.wrapCancelAnimationFrame(); }); diff --git a/tests/EvalWrapper_test.ts b/tests/EvalWrapper_test.ts index 2c5e456..5daf552 100644 --- a/tests/EvalWrapper_test.ts +++ b/tests/EvalWrapper_test.ts @@ -3,7 +3,6 @@ import { expect } from '@std/expect'; import './browserPolyfill.ts'; import { EvalFact, EvalWrapper } from '../src/wrapper/EvalWrapper.ts'; import { SetTimerFact, TimerWrapper } from '../src/wrapper/TimerWrapper.ts'; -import { TraceUtil } from '../src/wrapper/shared/TraceUtil.ts'; import { TAG_UNDEFINED } from '../src/api/clone.ts'; import { TAG_EVAL_RETURN_SET_INTERVAL, @@ -13,14 +12,13 @@ import { Fact } from '../src/wrapper/shared/Fact.ts'; import { wait } from '../src/api/time.ts'; describe('EvalWrapper', () => { - const traceUtil = new TraceUtil(); let apiEval: EvalWrapper; let apiTimer: TimerWrapper; beforeEach(() => { - apiEval = new EvalWrapper(traceUtil); + apiEval = new EvalWrapper(); apiEval.wrap(); - apiTimer = new TimerWrapper(traceUtil, apiEval); + apiTimer = new TimerWrapper(apiEval); apiTimer.wrapSetTimeout(); apiTimer.wrapSetInterval(); }); diff --git a/tests/IdleWrapper_test.ts b/tests/IdleWrapper_test.ts index 5d65535..8951f84 100644 --- a/tests/IdleWrapper_test.ts +++ b/tests/IdleWrapper_test.ts @@ -2,17 +2,15 @@ import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; import './browserPolyfill.ts'; import { CicFact, IdleWrapper, RicFact } from '../src/wrapper/IdleWrapper.ts'; -import { TraceUtil } from '../src/wrapper/shared/TraceUtil.ts'; import { TAG_BAD_DELAY, TAG_BAD_HANDLER } from '../src/api/const.ts'; import { Fact } from '../src/wrapper/shared/Fact.ts'; import { wait } from '../src/api/time.ts'; describe('IdleWrapper', () => { - const traceUtil = new TraceUtil(); let apiIdle: IdleWrapper; beforeEach(() => { - apiIdle = new IdleWrapper(traceUtil); + apiIdle = new IdleWrapper(); apiIdle.wrapRequestIdleCallback(); apiIdle.wrapCancelIdleCallback(); }); diff --git a/tests/TimerWrapper_test.ts b/tests/TimerWrapper_test.ts index aaa714a..d55064b 100644 --- a/tests/TimerWrapper_test.ts +++ b/tests/TimerWrapper_test.ts @@ -6,7 +6,6 @@ import { TAG_BAD_HANDLER, TAG_DELAY_NOT_FOUND, } from '../src/api/const.ts'; -import { TraceUtil } from '../src/wrapper/shared/TraceUtil.ts'; import { ClearTimerFact, SetTimerFact, @@ -17,12 +16,11 @@ import { Fact } from '../src/wrapper/shared/Fact.ts'; import { wait } from '../src/api/time.ts'; describe('wrappers', () => { - const traceUtil = new TraceUtil(); - const apiEval = new EvalWrapper(traceUtil); + const apiEval = new EvalWrapper(); let apiTimer: TimerWrapper; beforeEach(() => { - apiTimer = new TimerWrapper(traceUtil, apiEval); + apiTimer = new TimerWrapper(apiEval); apiTimer.wrapSetTimeout(); apiTimer.wrapClearTimeout(); apiTimer.wrapSetInterval(); From 780aacf9f350f2beb6db0bb394bcc384b1392f4e 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: Sat, 5 Jul 2025 20:33:54 +0300 Subject: [PATCH 03/16] fix wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebece3b..642898c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ To assess Web Application implementation correctness and expedite issues discove - 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. + - 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. From f11ba01d9f049a2a30e9b9cc71a38faa573d4df6 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: Sat, 5 Jul 2025 20:34:48 +0300 Subject: [PATCH 04/16] fix order of menu items --- src/api/storage/storage.local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/storage/storage.local.ts b/src/api/storage/storage.local.ts index 5468372..294e0ca 100644 --- a/src/api/storage/storage.local.ts +++ b/src/api/storage/storage.local.ts @@ -43,8 +43,8 @@ export type TConfigField = Partial; export const DEFAULT_PANELS: TPanel[] = [ { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, - { key: 'worker', label: 'Worker', visible: true, wrap: true }, { 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 }, { From 1b0b356af3809602847a650543370879ef1729ca 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: Wed, 9 Jul 2025 23:32:07 +0300 Subject: [PATCH 05/16] workaround weird -5px document liftup in Chrome v138, caused by `scrollIntoView` --- src/view/App.svelte | 2 +- src/view/menu/SummaryBarItem.svelte | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view/App.svelte b/src/view/App.svelte index 3418dc5..ed67e24 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -7,7 +7,7 @@
-
+
diff --git a/src/view/menu/SummaryBarItem.svelte b/src/view/menu/SummaryBarItem.svelte index 5bfeaa2..dc790fc 100644 --- a/src/view/menu/SummaryBarItem.svelte +++ b/src/view/menu/SummaryBarItem.svelte @@ -29,13 +29,13 @@ XPathResult.FIRST_ORDERED_NODE_TYPE, null, ).singleNodeValue; + const main = document.querySelector('.scrollable'); - if (el instanceof HTMLElement) { - el.scrollIntoView({ - behavior: 'instant', - block: 'start', - inline: 'nearest', - }); + if (main && el instanceof HTMLElement) { + const elBcr = el.getBoundingClientRect(); + const mainBcr = main.getBoundingClientRect(); + + main.scrollBy(0, elBcr.y - mainBcr.y); } } From 31a2df350eefb8262deefd9754e5061e6e9b3138 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, 10 Jul 2025 00:28:29 +0300 Subject: [PATCH 06/16] add animation after clicking on summary bar selector to bring attention on position in the screen --- public/global.css | 14 ++++++++++++++ src/view/App.svelte | 3 ++- src/view/menu/SummaryBarItem.svelte | 22 +++++++++++++++++++++- src/view/shared/const.ts | 2 ++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/view/shared/const.ts diff --git a/public/global.css b/public/global.css index 75f42ae..3aefd3e 100644 --- a/public/global.css +++ b/public/global.css @@ -207,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/view/App.svelte b/src/view/App.svelte index ed67e24..8984907 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -1,13 +1,14 @@
-
+
diff --git a/src/view/menu/SummaryBarItem.svelte b/src/view/menu/SummaryBarItem.svelte index dc790fc..a7e497f 100644 --- a/src/view/menu/SummaryBarItem.svelte +++ b/src/view/menu/SummaryBarItem.svelte @@ -1,6 +1,11 @@ diff --git a/src/view/shared/const.ts b/src/view/shared/const.ts new file mode 100644 index 0000000..8bd172e --- /dev/null +++ b/src/view/shared/const.ts @@ -0,0 +1,2 @@ +export const SCROLLABLE_CLASSNAME = 'scrollable'; +export const AFTER_SCROLL_ANIMATION_CLASSNAME = 'scrolled-to'; From 851c2679f7ce75f8953ad32895e4771af8b13647 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, 10 Jul 2025 00:35:28 +0300 Subject: [PATCH 07/16] deprecate `` button (doesn't fit with Active timers and newly added Workers telemetry collection philosophy) --- src/api-monitor-cs-main.ts | 5 ----- src/api/communication.ts | 5 ----- src/view/menu/Menu.svelte | 7 ++----- src/view/menu/ResetHistory.svelte | 15 --------------- src/wrapper/AnimationWrapper.ts | 10 ---------- src/wrapper/EvalWrapper.ts | 5 ----- src/wrapper/IdleWrapper.ts | 9 --------- src/wrapper/TimerWrapper.ts | 13 ------------- src/wrapper/Wrapper.ts | 7 ------- 9 files changed, 2 insertions(+), 74 deletions(-) delete mode 100644 src/view/menu/ResetHistory.svelte 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/communication.ts b/src/api/communication.ts index f0cea7b..6a9de5d 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -109,7 +109,6 @@ export enum EMsg { TELEMETRY_DELTA, TELEMETRY_ACKNOWLEDGED, MEDIA_COMMAND, - RESET_WRAPPER_HISTORY, TIMER_COMMAND, SESSION, } @@ -120,9 +119,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; @@ -166,7 +162,6 @@ export type TMsgOptions = | IMsgStartObserve | IMsgStopObserve | IMsgLoaded - | IMsgResetHistory | IMsgTimerCommand | IMsgConfig | IMsgMediaCommand diff --git a/src/view/menu/Menu.svelte b/src/view/menu/Menu.svelte index f2847ba..984eb53 100644 --- a/src/view/menu/Menu.svelte +++ b/src/view/menu/Menu.svelte @@ -1,5 +1,4 @@ - - diff --git a/src/wrapper/AnimationWrapper.ts b/src/wrapper/AnimationWrapper.ts index f6d2a0e..703c582 100644 --- a/src/wrapper/AnimationWrapper.ts +++ b/src/wrapper/AnimationWrapper.ts @@ -233,14 +233,4 @@ export class AnimationWrapper { : null, }; } - - cleanHistory() { - this.rafHistory.clear(); - this.cafHistory.clear(); - this.onlineAnimationFrameLookup.clear(); - this.animationCallsMap.clear(); - - this.callCounter.requestAnimationFrame = 0; - this.callCounter.cancelAnimationFrame = 0; - } } diff --git a/src/wrapper/EvalWrapper.ts b/src/wrapper/EvalWrapper.ts index d450813..3053591 100644 --- a/src/wrapper/EvalWrapper.ts +++ b/src/wrapper/EvalWrapper.ts @@ -145,9 +145,4 @@ export class EvalWrapper { ? Array.from(this.evalHistory.values()) : null; } - - cleanHistory() { - this.evalHistory.clear(); - this.callCounter = 0; - } } diff --git a/src/wrapper/IdleWrapper.ts b/src/wrapper/IdleWrapper.ts index 965cf8b..658e734 100644 --- a/src/wrapper/IdleWrapper.ts +++ b/src/wrapper/IdleWrapper.ts @@ -263,13 +263,4 @@ export class IdleWrapper { : null, }; } - - cleanHistory() { - this.ricHistory.clear(); - this.cicHistory.clear(); - this.onlineIdleCallbackLookup.clear(); - - this.callCounter.requestIdleCallback = 0; - this.callCounter.cancelIdleCallback = 0; - } } diff --git a/src/wrapper/TimerWrapper.ts b/src/wrapper/TimerWrapper.ts index d341f59..4a65d69 100644 --- a/src/wrapper/TimerWrapper.ts +++ b/src/wrapper/TimerWrapper.ts @@ -510,17 +510,4 @@ export class TimerWrapper { globalThis.clearInterval(handler); } } - - cleanHistory() { - this.setTimeoutHistory.clear(); - this.clearTimeoutHistory.clear(); - this.setIntervalHistory.clear(); - this.clearIntervalHistory.clear(); - - this.callCounter.setTimeout = - this.callCounter.clearTimeout = - this.callCounter.setInterval = - this.callCounter.clearInterval = - 0; - } } diff --git a/src/wrapper/Wrapper.ts b/src/wrapper/Wrapper.ts index b1fe8e1..9f3859a 100644 --- a/src/wrapper/Wrapper.ts +++ b/src/wrapper/Wrapper.ts @@ -153,10 +153,3 @@ export function runTimerCommand( ) { apiTimer.runCommand(...args); } - -export function cleanHistory() { - apiEval.cleanHistory(); - apiTimer.cleanHistory(); - apiAnimation.cleanHistory(); - apiIdle.cleanHistory(); -} From a27de2bbe9cd58212087b6e74a6c111ccbee3951 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: Mon, 14 Jul 2025 12:00:56 +0300 Subject: [PATCH 08/16] add to alert if script is not yet injected after fresh install, or port is closed due to a newer version --- src/api-monitor-cs-isolated.ts | 8 ++++- src/api/communication.ts | 12 ++++++- src/api/const.ts | 2 -- src/view/App.svelte | 3 ++ src/view/ConnectionAlert.svelte | 32 +++++++++++++++++++ .../shared/CellFrameTimeSensitive.svelte | 2 +- src/view/shared/Alert.svelte | 10 +++++- src/view/shared/Variable.svelte | 2 +- src/view/shared/const.ts | 3 ++ 9 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 src/view/ConnectionAlert.svelte 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/communication.ts b/src/api/communication.ts index 6a9de5d..1cdcf49 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -111,6 +111,8 @@ export enum EMsg { MEDIA_COMMAND, TIMER_COMMAND, SESSION, + CONFIRM_INJECTION, + INJECTION_CONFIRMED, } export interface IMsgStartObserve { @@ -155,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 @@ -165,4 +173,6 @@ export type TMsgOptions = | 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/view/App.svelte b/src/view/App.svelte index 8984907..7e74c27 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -2,6 +2,7 @@ import Panels from './panels/Panels.svelte'; import Menu from './menu/Menu.svelte'; import { SCROLLABLE_CLASSNAME } from './shared/const.ts'; + import ConnectionAlert from './ConnectionAlert.svelte';
@@ -11,6 +12,8 @@
+ +
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/timers/TimersSetHistory.svelte b/src/view/panels/timers/TimersSetHistory.svelte index c2e1a2e..a33e407 100644 --- a/src/view/panels/timers/TimersSetHistory.svelte +++ b/src/view/panels/timers/TimersSetHistory.svelte @@ -47,7 +47,7 @@ {caption} Callstack [] - + - + diff --git a/src/view/panels/worker/WorkerMetricAddEventListener.svelte b/src/view/panels/worker/WorkerMetricAddEventListener.svelte index 68dbc9b..6eb794c 100644 --- a/src/view/panels/worker/WorkerMetricAddEventListener.svelte +++ b/src/view/panels/worker/WorkerMetricAddEventListener.svelte @@ -3,7 +3,7 @@ import type { IWorkerTelemetryMetric } from '../../../wrapper/WorkerWrapper.ts'; import Variable from '../../shared/Variable.svelte'; import CellCallstack from '../shared/CellCallstack.svelte'; - import CellFrameTimeSensitive from '../shared/CellFrameTimeSensitive.svelte'; + import CellSelfTime from '../shared/CellSelfTime.svelte'; import CellBypass from '../shared/CellBypass.svelte'; import CellBreakpoint from '../shared/CellBreakpoint.svelte'; @@ -24,7 +24,7 @@ addEventListener [] - Self + Self CPS Events Called @@ -43,7 +43,7 @@ /> - + {ael.eventsCps || undefined} diff --git a/src/view/panels/worker/WorkerMetricOnError.svelte b/src/view/panels/worker/WorkerMetricOnError.svelte index 138612c..9c516c9 100644 --- a/src/view/panels/worker/WorkerMetricOnError.svelte +++ b/src/view/panels/worker/WorkerMetricOnError.svelte @@ -1,6 +1,6 @@ {#if telemetry.collection.length}
{#each telemetry.collection as metric (metric.specifier)} diff --git a/src/view/panels/worker/WorkerMetricConstructor.svelte b/src/view/panels/worker/WorkerMetricConstructor.svelte index fcae97b..eee23b6 100644 --- a/src/view/panels/worker/WorkerMetricConstructor.svelte +++ b/src/view/panels/worker/WorkerMetricConstructor.svelte @@ -2,8 +2,12 @@ import CellBreakpoint from '../shared/CellBreakpoint.svelte'; import Variable from '../../shared/Variable.svelte'; import CellCallstack from '../shared/CellCallstack.svelte'; - import type { IWorkerTelemetryMetric } from '../../../wrapper/WorkerWrapper.js'; + import { + type IWorkerTelemetryMetric, + WorkerConstructorFacts, + } from '../../../wrapper/WorkerWrapper.js'; import CollapseExpand from './CollapseExpand.svelte'; + import CellFacts from '../shared/CellFacts.svelte'; let { metric }: { metric: IWorkerTelemetryMetric } = $props(); let isExpanded = $state(true); @@ -22,6 +26,7 @@ constructor [] + Called @@ -36,6 +41,12 @@ traceDomain={konstruktor.traceDomain} /> + + + diff --git a/src/wrapper/WorkerWrapper.ts b/src/wrapper/WorkerWrapper.ts index a5ba83f..84bd32a 100644 --- a/src/wrapper/WorkerWrapper.ts +++ b/src/wrapper/WorkerWrapper.ts @@ -2,6 +2,7 @@ import { ETraceDomain, TraceUtil, type TTrace } from './shared/TraceUtil.ts'; import { traceUtil } from './shared/util.ts'; import type { TPanel } from '../api/storage/storage.local.ts'; import { trim2ms } from '../api/time.ts'; +import { Fact, type TFact } from './shared/Fact.ts'; export interface IWorkerTelemetry { totalOnline: number; @@ -10,6 +11,7 @@ export interface IWorkerTelemetry { export interface IWorkerTelemetryMetric { specifier: string; online: number; + facts: TFact; konstruktor: IConstructorMetric[]; terminate: ITerminateMetric[]; postMessage: IPostMessageMetric[]; @@ -22,6 +24,7 @@ export interface IWorkerTelemetryMetric { interface IWorkerMetric { specifier: string; online: number; + facts: TFact; callsPerSecond: Map; konstruktor: Map; terminate: Map; @@ -86,6 +89,18 @@ interface IRemoveEventListenerMetric { } const workerMap: Map = new Map(); +const HARDWARE_CONCURRENCY = globalThis.navigator.hardwareConcurrency; +export const WorkerFact = /*@__PURE__*/ (() => ({ + MAX_ONLINE: Fact.define(1 << 0), +} as const))(); +export const WorkerConstructorFacts = /*@__PURE__*/ (() => + Fact.map([ + [WorkerFact.MAX_ONLINE, { + tag: 'N', + details: + `Number of online instances exceeds number of available CPUs [${HARDWARE_CONCURRENCY}]`, + }], + ]))(); class ApiMonitorWorkerWrapper extends Worker { readonly #specifier: string; @@ -113,6 +128,10 @@ class ApiMonitorWorkerWrapper extends Worker { if (workerMetric) { workerMetric.online++; + if (workerMetric.online > HARDWARE_CONCURRENCY) { + workerMetric.facts = Fact.assign(workerMetric.facts, WorkerFact.MAX_ONLINE); + } + const rec = workerMetric.konstruktor.get(methodMetric.traceId); if (rec) { rec.calls++; @@ -123,6 +142,7 @@ class ApiMonitorWorkerWrapper extends Worker { workerMap.set(this.#specifier, { specifier: this.#specifier, online: 1, + facts: 0, callsPerSecond: new Map(), konstruktor: new Map([[methodMetric.traceId, methodMetric]]), terminate: new Map(), @@ -443,6 +463,7 @@ export function collectWorkerHistory(panel: TPanel): IWorkerTelemetry { rv.collection.push({ specifier: metric.specifier, online: metric.online, + facts: metric.facts, konstruktor: Array.from(metric.konstruktor.values()), terminate: Array.from(metric.terminate.values()), postMessage: Array.from(metric.postMessage.values()), From 049c21e1bad9ab9e3eed1c5c4793767d20f0f31a 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: Sun, 20 Jul 2025 14:34:50 +0300 Subject: [PATCH 13/16] include version in zip file-name --- Makefile | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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) From 480b8035bef7a147c3c772fb977aab0ded4b49c9 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: Mon, 21 Jul 2025 20:02:45 +0300 Subject: [PATCH 14/16] minimize exceptions --- src/api-monitor-devtools.ts | 2 +- src/devtoolsPanelUtil.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 }); } From e51663b1fc61ce66bffd6b39e247a901851c02b9 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: Mon, 21 Jul 2025 20:02:56 +0300 Subject: [PATCH 15/16] cleanup --- src/view/menu/SummaryBarItem.svelte | 1 - src/wrapper/WorkerWrapper.ts | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/view/menu/SummaryBarItem.svelte b/src/view/menu/SummaryBarItem.svelte index 4deda75..8b9fe11 100644 --- a/src/view/menu/SummaryBarItem.svelte +++ b/src/view/menu/SummaryBarItem.svelte @@ -25,7 +25,6 @@ { delay: 512 }, (el: HTMLElement | unknown) => { if (el instanceof HTMLElement) { - console.log('NO'); el.classList.remove(AFTER_SCROLL_ANIMATION_CLASSNAME); } }, diff --git a/src/wrapper/WorkerWrapper.ts b/src/wrapper/WorkerWrapper.ts index 84bd32a..f1afca5 100644 --- a/src/wrapper/WorkerWrapper.ts +++ b/src/wrapper/WorkerWrapper.ts @@ -129,7 +129,10 @@ class ApiMonitorWorkerWrapper extends Worker { workerMetric.online++; if (workerMetric.online > HARDWARE_CONCURRENCY) { - workerMetric.facts = Fact.assign(workerMetric.facts, WorkerFact.MAX_ONLINE); + workerMetric.facts = Fact.assign( + workerMetric.facts, + WorkerFact.MAX_ONLINE, + ); } const rec = workerMetric.konstruktor.get(methodMetric.traceId); From e49a05bd7e0f8cdad5c599b8fe7a94423057b153 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: Mon, 21 Jul 2025 20:03:19 +0300 Subject: [PATCH 16/16] detect extension code update --- src/view/ConnectionAlert.svelte | 68 +++++++++++++++++++++++++++++---- src/view/shared/const.ts | 1 + 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/view/ConnectionAlert.svelte b/src/view/ConnectionAlert.svelte index acb5301..284dcfd 100644 --- a/src/view/ConnectionAlert.svelte +++ b/src/view/ConnectionAlert.svelte @@ -2,31 +2,83 @@ import { EMsg, portPost, runtimeListen } from '../api/communication.ts'; import { Timer } from '../api/time.ts'; import Alert from './shared/Alert.svelte'; - import { INJECTION_ALERT_TIMEOUT } from './shared/const.ts'; + import { + INJECTION_ALERT_TIMEOUT, + UPDATE_SENSOR_INTERVAL, + } from './shared/const.ts'; + import { onMount } from 'svelte'; + import { loadLocalStorage } from '../api/storage/storage.local.ts'; - let alertEl: Alert | null = null; + let tabReloadAlertEl: Alert | null = null; + let devtoolsReloadAlertEl: Alert | null = null; const delayedAlert = new Timer( { delay: INJECTION_ALERT_TIMEOUT }, - () => void alertEl?.show(), + () => void tabReloadAlertEl?.show(), + ); + const extensionUpdateSensor = new Timer( + { delay: UPDATE_SENSOR_INTERVAL, repetitive: true }, + () => { + whenUpdateDetected(() => { + devtoolsReloadAlertEl?.show(); + extensionUpdateSensor.stop(); + }); + }, ); runtimeListen((o) => { if (o.msg === EMsg.INJECTION_CONFIRMED) { delayedAlert.stop(); - alertEl?.hide(); + tabReloadAlertEl?.hide(); } else if (o.msg === EMsg.CONTENT_SCRIPT_LOADED) { - alertEl?.hide(); + tabReloadAlertEl?.hide(); } }); - portPost({ msg: EMsg.CONFIRM_INJECTION }); - delayedAlert.start(); + onMount(() => { + pingContentScript(); + extensionUpdateSensor.start(); + + return () => { + delayedAlert.stop(); + extensionUpdateSensor.stop(); + }; + }); + + function pingContentScript() { + portPost({ msg: EMsg.CONFIRM_INJECTION }); + delayedAlert.start(); + } + + /** + * Detect extension code update by waiting for an indirect + * symptom - when reading localStorage fails + * @NOTE: `chrome.runtime.onInstalled.addListener` doesn't work in + * devtools panel script context + */ + function whenUpdateDetected(callback: () => void) { + loadLocalStorage().catch((e: Error) => { + if (!e || e.message !== 'Extension context invalidated.') { + return; + } + try { + callback(); + } catch (_) {} + }); + }
Tab reload required to continue live inspection
+ + +
Devtools reload required to continue live inspection
+
diff --git a/src/view/shared/const.ts b/src/view/shared/const.ts index c95d828..0bbce27 100644 --- a/src/view/shared/const.ts +++ b/src/view/shared/const.ts @@ -3,6 +3,7 @@ export const AFTER_SCROLL_ANIMATION_CLASSNAME = 'scrolled-to'; export const VARIABLE_ANIMATION_THROTTLE = 3500; // eye blinking average frequency export const FRAME_TIME_SAFE_THRESHOLD = 13.333333333333332; // ms export const INJECTION_ALERT_TIMEOUT = 1e3; +export const UPDATE_SENSOR_INTERVAL = 2e3; export enum ESelfTimeDisplayMode { DEFAULT, STATS,