Skip to content

v1.0.7 #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 59 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
de520be
next version
zendive Dec 22, 2024
17f4516
fix make dev
zendive Dec 22, 2024
7f9854a
css chore
zendive Dec 24, 2024
b3e9700
bitwise even/odd
zendive Dec 24, 2024
7956899
unite count of invocations and terminations under Called column
zendive Dec 27, 2024
9c3ae44
tune variable animation throttle to eye blinking frequency
zendive Dec 27, 2024
8b20156
deprecate individual hasError trate and decoration (there should be a…
zendive Dec 28, 2024
2749866
add menu option to toggle callstack type between full and short
zendive Dec 29, 2024
1ea1055
update deps
zendive Dec 29, 2024
f8fe52e
fix svelte-check complaints
zendive Dec 29, 2024
5920bf7
refactor type names
zendive Dec 30, 2024
104e3c2
optimise 🌳 imports
zendive Dec 30, 2024
7052ebf
replace webpack with deno-esbuild
zendive Dec 30, 2024
aa0d9e6
migrate svelte4 to 5
zendive Dec 31, 2024
e5b887d
migrate to svelte 5
zendive Jan 3, 2025
d014556
fix esm-env warning by adding conditions
zendive Jan 4, 2025
93dd419
add self-time measurement metric
zendive Jan 5, 2025
2914f0c
fix setters of `isOnline` to honor `handler` in context (single histo…
zendive Jan 6, 2025
9a53c09
sync naming
zendive Jan 6, 2025
b4b6051
update deps
zendive Jan 6, 2025
37d551a
fix saving `selfTime` for timeout that clears itself (doing polling)
zendive Jan 7, 2025
4bd33ec
update tests
zendive Jan 7, 2025
7e921d3
swap EvalMetrics columns
zendive Jan 9, 2025
40d4fdf
trim `selfTime` to microsecond
zendive Jan 9, 2025
a159311
pertner in crime of switchig to svelete 5
zendive Jan 9, 2025
a598364
add tooltip over `delay` column for large numbers in short time format
zendive Jan 11, 2025
d39a8c1
refactor panel column sortings
zendive Jan 12, 2025
f39286e
add Scheduled column for timers, idle callbacks
zendive Jan 14, 2025
3d52ef1
add `online` to semisorting field
zendive Jan 14, 2025
0b11c21
add autopause alert
zendive Jan 14, 2025
8cb29e9
alert about `selfTime` metric that exceeds 1/5 less of the 1/60 frame…
zendive Jan 14, 2025
3b6929f
trim also delays to microsecond
zendive Jan 15, 2025
1d8d8cf
make delay of missed clearTimers more visible
zendive Jan 15, 2025
b633c2a
add TraceBreakpoint
zendive Jan 16, 2025
fba6552
add tooltip to `BP` column
zendive Jan 16, 2025
4e40f0c
change autopause alert position and wording
zendive Jan 17, 2025
5e8e910
shrink trace link on narrow panel width
zendive Jan 17, 2025
2bc89a7
clear unused css
zendive Jan 17, 2025
0626dd9
rename `Sheduled` column to `Set`
zendive Jan 17, 2025
1327380
evolve AnimationRequestHistory
zendive Jan 18, 2025
cf78371
add `TraceBypass`
zendive Jan 19, 2025
dfbcf88
extract component `TimersSetHistoryMetric`
zendive Jan 19, 2025
4ac6f07
refactor content script
zendive Jan 20, 2025
c799d23
reorganize wrappers file
zendive Jan 20, 2025
aee3244
extract traceUtil
zendive Jan 20, 2025
38af74d
add media event `waitingforkey`
zendive Jan 21, 2025
0a7e255
add media prop `mediaKeys`
zendive Jan 21, 2025
a3c1475
extract ApiEval (+ commonSense)
zendive Jan 21, 2025
578690d
extract `IdleWrapper`
zendive Jan 21, 2025
2c817e8
extract `AnimationWrapper`
zendive Jan 22, 2025
3e7724a
extract `TimerWrapper`
zendive Jan 22, 2025
79f4df8
update deps
zendive Jan 22, 2025
34662eb
`mediaKeys` serialization cautious implementation
zendive Jan 22, 2025
68d78e2
ensure that `mediaKeys` is stringified in UI
zendive Jan 23, 2025
333fcf0
split tests
zendive Jan 23, 2025
790b1c0
update deps
zendive Jan 23, 2025
da7728f
leave refference for `mediaKeys`
zendive Jan 23, 2025
7d7a962
update readme
zendive Jan 23, 2025
abf2ed5
fix markup to see images
zendive Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ install:

dev:
rm -rf ./public/build
NODE_OPTIONS="--import=tsx" \
pnpm exec webpack --progress --watch --mode=development

lint:
pnpm exec prettier . --write
pnpm exec svelte-check
NODE_ENV=development \
deno run --watch --allow-env --allow-read --allow-run deno-bundle.ts

test:
pnpm exec prettier . --write
pnpm exec jest --config jest/jest.config.js
pnpm exec svelte-check

prod: lint test
prod: test
rm -rf ./public/build $(ZIP_CHROME_FILE)
NODE_OPTIONS="--import=tsx" \
time pnpm exec webpack --mode=production
NODE_ENV=production \
time deno run --allow-env --allow-read --allow-run deno-bundle.ts
zip -r $(ZIP_CHROME_FILE) ./public ./manifest.json > /dev/null

.PHONY: clean install dev lint prod test
.PHONY: clean install dev prod test
.DEFAULT_GOAL := dev
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,55 @@

- Available in Chrome Web Store as [API Monitor](https://chromewebstore.google.com/detail/api-monitor/bghmfoakiidiedpheejcjhciekobjcjp)

Whether you're developing a Single Page Application (SPA) and want to assess implementation correctness or are curious about how something works, this tool adds additional functionality to the Chrome browser DevTools to reveal timeouts, intervals, idle callbacks, animation frames and `eval` usages while mapping their invocation call stacks to a source code location. If the page has mounted `video` or `audio` media element's, their events and property state changes can be observed as they happen ([documentation](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement), [example](https://www.w3.org/2010/05/video/mediaevents.html)).
If you're web developer and want to assess implementation correctness - this tool adds additional panel to the browser’s DevTool that enables to see scheduled timeouts and active intervals, as well as to review and navigate to initiators of: eval, setTimeout, setInterval, requestAnimationFrame, requestIdleCallback and their terminator functions.

### Motivation
#### Allows:

To expedite issues discovery, here are some examples from experience:
- to measure callback execution self-time.
- to see `requestAnimationFrame` callback request frame rate.
- visit every function in the call stack (if available), bypass or pause while debugging.
- detect eval ­function usage, see its argument and return value, same for setTimeout and setInterval when called with a string instead of a function.
- for every mounted video or audio media element's to see it’s state and properties.

- A ~10ms delay interval constantly consuming approximately 10% of CPU from a third-party library, solely to check if the page was resized.
- A bundled dependency library that utilizes the eval function, thereby preventing the removal of `unsafe-eval` from the Content Security Policy (CSP) header. It can be arguably intentional, but also may reveal a configuration issue when the package was bundled with webpack config's [devtool: 'eval'](https://webpack.js.org/configuration/devtool/) in production mode.
- A substantial number of hidden video elements in the DOM that were consuming resources, unexpectedly limited to 100 medias per domain.
- An unattended interval that was unintentionally left running and contributed to a slowly growing memory.
#### Helps to spot:

To explore the internals of a complex systems.
- incorrect timeout delay.
- bad handler for terminator function.
- terminating non existing or elapsed timeout.

#### Motivation:

- To expedite issues discovery.

#### Wrapped native functions:

- eval (by default off)
- setTimeout
- clearTimeout
- setInterval
- clearInterval
- requestAnimationFrame
- cancelAnimationFrame
- requestIdleCallback
- cancelIdleCallback

##### Note:

- while measuring performance of your code – consider disabling this extension as it may affect the results.

<details>
<summary> <strong>Example</strong> </summary>

![screenshot](./doc/screenshot-01.png)
![screenshot](./doc/screenshot-02.png)
![screenshot](./doc/screenshot-03.png)
![screenshot](./doc/screenshot-04.png)

</details>

### Build requirements

- OS: Linux
- Node: 20.16.0 (LTS)
- Node: 22.12.0 (LTS)
- Deno: 2.1.7

### Build instructions

Expand Down
47 changes: 47 additions & 0 deletions deno-bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { build, context, type BuildOptions } from 'esbuild';
import esbuildSvelte from 'esbuild-svelte';
import { sveltePreprocess } from 'svelte-preprocess';
import manifest from './manifest.json' with { type: 'json' };

const isProd = process.env.NODE_ENV === 'production';
console.log('🚧', process.env.NODE_ENV);

const buildOptions: BuildOptions = {
plugins: [
esbuildSvelte({
preprocess: sveltePreprocess(),
compilerOptions: { dev: !isProd },
}),
],
entryPoints: [
'./src/api-monitor-devtools.ts',
'./src/api-monitor-cs-main.ts',
'./src/api-monitor-cs-isolated.ts',
'./src/api-monitor-devtools-panel.ts',
],
outdir: './public/build/',
define: {
__development__: `${!isProd}`,
__app_name__: `"browser-api-monitor@${manifest.version}"`,
__app_version__: `"${manifest.version}"`,
__home_page__: `"${manifest.homepage_url}"`,
},
bundle: true,
platform: 'browser',
format: 'iife',
target: 'esnext',
conditions: [`${process.env.NODE_ENV}`],
minify: isProd,
sourcemap: false,
treeShaking: true,
logLevel: 'debug',
};

if (isProd) {
build(buildOptions).catch((error) => {
console.error(error);
});
} else {
const ctx = await context(buildOptions);
await ctx.watch();
}
Binary file removed doc/screenshot-01.png
Binary file not shown.
Binary file removed doc/screenshot-03.png
Binary file not shown.
Binary file modified doc/screenshot-04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions jest/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,4 @@ export default {
},
rootDir: '..', // . - /
roots: ['jest/tests'], // . - /jest
moduleNameMapper: {
// duplicates tsconfig.json/compilerOptions/path & webpack.config.ts/resolve/alias
'@/(.*)': '<rootDir>/src/$1',
},
};
75 changes: 75 additions & 0 deletions jest/tests/AnimationWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
import { AnimationWrapper } from '../../src/wrapper/AnimationWrapper.ts';
import { TraceUtil } from '../../src/wrapper/TraceUtil.ts';
import { TAG_EXCEPTION } from '../../src/api/clone.ts';
import { TextEncoder } from 'node:util';

global.TextEncoder = TextEncoder;

describe('AnimationWrapper', () => {
const traceUtil = new TraceUtil();
let apiAnimation: AnimationWrapper;

beforeEach(() => {
apiAnimation = new AnimationWrapper(traceUtil);
apiAnimation.wrapRequestAnimationFrame();
apiAnimation.wrapCancelAnimationFrame();
});

afterEach(() => {
apiAnimation.unwrapRequestAnimationFrame();
apiAnimation.unwrapCancelAnimationFrame();
});

test('rafHistory - recorded', async () => {
let typeOfArgument = '';
const handler = await new Promise((resolve) => {
const handler = requestAnimationFrame((time) => {
typeOfArgument = typeof time;
resolve(handler);
});
});
const rec = Array.from(apiAnimation.rafHistory.values())[0];

expect(typeOfArgument).toBe('number');
expect(apiAnimation.rafHistory.size).toBe(1);
expect(rec.handler).toBe(handler);
expect(rec.calls).toBe(1);
expect(rec.trace.length).toBeGreaterThan(1);
expect(rec.traceId.length).toBeGreaterThan(1);
expect(rec.selfTime).not.toBeNull();
expect(apiAnimation.callCounter.requestAnimationFrame).toBe(1);
});

test('cafHistory - recorded', async () => {
const unchanged = 0,
changed = 1;
let changeable = unchanged;
const handler = requestAnimationFrame(() => {
changeable = changed;
});
cancelAnimationFrame(handler);

const rafRec = Array.from(apiAnimation.rafHistory.values())[0];
const cafRec = Array.from(apiAnimation.cafHistory.values())[0];

expect(changeable).toBe(unchanged);
expect(apiAnimation.rafHistory.size).toBe(1);
expect(apiAnimation.cafHistory.size).toBe(1);
expect(cafRec.handler).toBe(handler);
expect(cafRec.calls).toBe(1);
expect(cafRec.trace.length).toBeGreaterThan(1);
expect(cafRec.traceId.length).toBeGreaterThan(1);
expect(apiAnimation.callCounter.cancelAnimationFrame).toBe(1);
expect(rafRec.canceledByTraceIds?.length).toBe(1);
expect(rafRec.canceledCounter).toBe(1);
});

test('cafHistory - invalid handler', () => {
cancelAnimationFrame(0);

const rec = Array.from(apiAnimation.cafHistory?.values())[0];

expect(rec.handler).toBe(TAG_EXCEPTION(0));
});
});
89 changes: 89 additions & 0 deletions jest/tests/EvalWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
import { EvalWrapper } from '../../src/wrapper/EvalWrapper.ts';
import { TimerWrapper } from '../../src/wrapper/TimerWrapper.ts';
import { TraceUtil } from '../../src/wrapper/TraceUtil.ts';
import { TAG_UNDEFINED } from '../../src/api/clone.ts';
import { TextEncoder } from 'node:util';
import {
TAG_EVAL_RETURN_SET_INTERVAL,
TAG_EVAL_RETURN_SET_TIMEOUT,
} from '../../src/api/const.ts';

global.TextEncoder = TextEncoder;

describe('EvalWrapper', () => {
const traceUtil = new TraceUtil();
let apiEval: EvalWrapper;
let apiTimer: TimerWrapper;

beforeEach(() => {
apiEval = new EvalWrapper(traceUtil);
apiEval.wrap();
apiTimer = new TimerWrapper(traceUtil, apiEval);
apiTimer.wrapSetTimeout();
apiTimer.wrapSetInterval();
});

afterEach(() => {
apiEval.unwrap();
apiTimer.unwrapSetTimeout();
apiTimer.unwrapSetInterval();
});

test('evalHistory - recorded', () => {
const NUMBER_OF_INVOCATIONS = 2;
const CODE = '(1+2)';
const RESULT = 3;

for (let i = 0, I = NUMBER_OF_INVOCATIONS; i < I; i++) {
window.eval(CODE);
}
expect(apiEval.evalHistory.size).toBe(1);

const rec = Array.from(apiEval.evalHistory.values())[0];

expect(rec.calls).toBe(NUMBER_OF_INVOCATIONS);
expect(rec.usesLocalScope).toBe(false);
expect(rec.code).toBe(CODE);
expect(rec.returnedValue).toBe(RESULT);
expect(rec.trace.length).toBeGreaterThan(1);
expect(rec.traceId.length).toBeGreaterThan(0);
expect(rec.selfTime).not.toBeNull();
});

test('evalHistory - detects local scope usage', () => {
const local_variable = 0;
window.eval('(local_variable++)');

const rec = Array.from(apiEval.evalHistory.values())[0];

expect(rec.calls).toBe(1);
expect(local_variable).toBe(0);
expect(rec.usesLocalScope).toBe(true);
expect(rec.returnedValue).toBe(TAG_UNDEFINED);
});

test('setTimeoutHistory - isEval recorded', () => {
const CODE = '(1+2)';
setTimeout(CODE);
const timerRec = Array.from(apiTimer.setTimeoutHistory.values())[0];
const evalRec = Array.from(apiEval.evalHistory.values())[0];

expect(timerRec.isEval).toBe(true);
expect(evalRec.code).toBe(CODE);
expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_TIMEOUT);
});

test('setIntervalHistory - isEval recorded', () => {
const CODE = '(1+2)';
const handler = setInterval(CODE, 123);
const timerRec = Array.from(apiTimer.setIntervalHistory.values())[0];
const evalRec = Array.from(apiEval.evalHistory.values())[0];

expect(timerRec.isEval).toBe(true);
expect(evalRec.code).toBe(CODE);
expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_INTERVAL);

clearInterval(handler);
});
});
Loading
Loading