diff --git a/README.md b/README.md index 8cea331..fb96f2b 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,8 @@ If you're web developer and want to assess implementation correctness - this too - requestIdleCallback - cancelIdleCallback -##### Note: - -- while measuring performance of your code – consider disabling this extension as it may affect the results. +> [!NOTE] +> While measuring performance of your code – consider disabling this extension as it may affect the results.
Example @@ -49,8 +48,8 @@ If you're web developer and want to assess implementation correctness - this too ### Build requirements - OS: Linux -- Node: 22.12.0 (LTS) -- [Deno](https://docs.deno.com/runtime/getting_started/installation/) 2.2.8 +- Node: 22.14.0 (LTS) +- [Deno](https://docs.deno.com/runtime/getting_started/installation/) 2.2.12 ### Build instructions diff --git a/build.ts b/build.ts index 5f29deb..f563414 100644 --- a/build.ts +++ b/build.ts @@ -33,7 +33,7 @@ const buildOptions: BuildOptions = { minify: isProd, sourcemap: false, treeShaking: true, - logLevel: 'warning', + logLevel: isProd ? 'warning' : 'debug', }; if (isProd) { diff --git a/deno.json b/deno.json index a90c42b..07bc20d 100644 --- a/deno.json +++ b/deno.json @@ -27,6 +27,8 @@ "include": [ "src/", "tests/", + "public/*.html", + "public/*.css", "build.ts", "*.json" ] @@ -34,11 +36,11 @@ "imports": { "esbuild": "https://deno.land/x/esbuild@v0.25.2/mod.js", "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@0.11.1", - "esbuild-svelte": "npm:esbuild-svelte@0.9.0", + "esbuild-svelte": "npm:esbuild-svelte@0.9.2", "svelte-preprocess": "npm:svelte-preprocess@6.0.3", - "@std/expect": "jsr:@std/expect@1.0.14", - "@std/testing": "jsr:@std/testing@1.0.10", + "@std/expect": "jsr:@std/expect@^1.0.15", + "@std/testing": "jsr:@std/testing@^1.0.11", "happy-dom": "npm:happy-dom@17.4.4" } } diff --git a/deno.lock b/deno.lock index 7055d4e..f0fdc3a 100644 --- a/deno.lock +++ b/deno.lock @@ -7,24 +7,24 @@ "jsr:@std/bytes@^1.0.2": "1.0.4", "jsr:@std/data-structures@^1.0.6": "1.0.6", "jsr:@std/encoding@^1.0.5": "1.0.6", - "jsr:@std/expect@1.0.14": "1.0.14", - "jsr:@std/fs@^1.0.15": "1.0.15", + "jsr:@std/expect@^1.0.15": "1.0.15", + "jsr:@std/fs@^1.0.16": "1.0.16", "jsr:@std/internal@^1.0.6": "1.0.6", "jsr:@std/path@^1.0.6": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8", - "jsr:@std/testing@1.0.10": "1.0.10", - "npm:@noble/hashes@1.7.1": "1.7.1", - "npm:@types/chrome@*": "0.0.313", - "npm:@types/chrome@0.0.313": "0.0.313", + "jsr:@std/testing@^1.0.11": "1.0.11", + "npm:@noble/hashes@1.8.0": "1.8.0", + "npm:@types/chrome@*": "0.0.317", + "npm:@types/chrome@0.0.317": "0.0.317", "npm:@types/deno@2.2.0": "2.2.0", - "npm:esbuild-svelte@0.9.0": "0.9.0_esbuild@0.25.2_svelte@5.25.7__acorn@8.14.1", + "npm:esbuild-svelte@0.9.2": "0.9.2_esbuild@0.25.3_svelte@5.28.2__acorn@8.14.1", "npm:happy-dom@17.4.4": "17.4.4", "npm:jsondiffpatch@0.7.3": "0.7.3", - "npm:sass@1.86.3": "1.86.3", - "npm:sv@0.8.0": "0.8.0", - "npm:svelte-check@4.1.5": "4.1.5_svelte@5.25.7__acorn@8.14.1_typescript@5.8.3", - "npm:svelte-preprocess@6.0.3": "6.0.3_sass@1.86.3_svelte@5.25.7__acorn@8.14.1_typescript@5.8.3", - "npm:svelte@5.25.7": "5.25.7_acorn@8.14.1", + "npm:sass@1.87.0": "1.87.0", + "npm:sv@0.8.3": "0.8.3", + "npm:svelte-check@4.1.6": "4.1.6_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3", + "npm:svelte-preprocess@6.0.3": "6.0.3_sass@1.87.0_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3", + "npm:svelte@5.28.2": "5.28.2_acorn@8.14.1", "npm:typescript@5.8.3": "5.8.3" }, "jsr": { @@ -54,15 +54,15 @@ "@std/encoding@1.0.6": { "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" }, - "@std/expect@1.0.14": { - "integrity": "27b8200267c97206e38050aff18badc82e73b3072c1a6bdd59393ddea6f69183", + "@std/expect@1.0.15": { + "integrity": "eca360007b5a7f13dbfa1294224baee7fb98dcd460d8461fe64eeae302902945", "dependencies": [ "jsr:@std/assert", "jsr:@std/internal" ] }, - "@std/fs@1.0.15": { - "integrity": "c083fb479889d6440d768e498195c3fc499d426fbf9a6592f98f53884d1d3f41", + "@std/fs@1.0.16": { + "integrity": "81878f62b6eeda0bf546197fc3daa5327c132fee1273f6113f940784a468b036", "dependencies": [ "jsr:@std/path@^1.0.8" ] @@ -73,8 +73,8 @@ "@std/path@1.0.8": { "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" }, - "@std/testing@1.0.10": { - "integrity": "8997bd0b0df020b81bf5eae103c66622918adeff7e45e96291c92a29dbf82cc1", + "@std/testing@1.0.11": { + "integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48", "dependencies": [ "jsr:@std/assert", "jsr:@std/async", @@ -96,80 +96,80 @@ "@dmsnell/diff-match-patch@1.1.0": { "integrity": "sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==" }, - "@esbuild/aix-ppc64@0.25.2": { - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==" + "@esbuild/aix-ppc64@0.25.3": { + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==" }, - "@esbuild/android-arm64@0.25.2": { - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==" + "@esbuild/android-arm64@0.25.3": { + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==" }, - "@esbuild/android-arm@0.25.2": { - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==" + "@esbuild/android-arm@0.25.3": { + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==" }, - "@esbuild/android-x64@0.25.2": { - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==" + "@esbuild/android-x64@0.25.3": { + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==" }, - "@esbuild/darwin-arm64@0.25.2": { - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==" + "@esbuild/darwin-arm64@0.25.3": { + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==" }, - "@esbuild/darwin-x64@0.25.2": { - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==" + "@esbuild/darwin-x64@0.25.3": { + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==" }, - "@esbuild/freebsd-arm64@0.25.2": { - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==" + "@esbuild/freebsd-arm64@0.25.3": { + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==" }, - "@esbuild/freebsd-x64@0.25.2": { - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==" + "@esbuild/freebsd-x64@0.25.3": { + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==" }, - "@esbuild/linux-arm64@0.25.2": { - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==" + "@esbuild/linux-arm64@0.25.3": { + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==" }, - "@esbuild/linux-arm@0.25.2": { - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==" + "@esbuild/linux-arm@0.25.3": { + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==" }, - "@esbuild/linux-ia32@0.25.2": { - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==" + "@esbuild/linux-ia32@0.25.3": { + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==" }, - "@esbuild/linux-loong64@0.25.2": { - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==" + "@esbuild/linux-loong64@0.25.3": { + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==" }, - "@esbuild/linux-mips64el@0.25.2": { - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==" + "@esbuild/linux-mips64el@0.25.3": { + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==" }, - "@esbuild/linux-ppc64@0.25.2": { - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==" + "@esbuild/linux-ppc64@0.25.3": { + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==" }, - "@esbuild/linux-riscv64@0.25.2": { - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==" + "@esbuild/linux-riscv64@0.25.3": { + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==" }, - "@esbuild/linux-s390x@0.25.2": { - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==" + "@esbuild/linux-s390x@0.25.3": { + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==" }, - "@esbuild/linux-x64@0.25.2": { - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==" + "@esbuild/linux-x64@0.25.3": { + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==" }, - "@esbuild/netbsd-arm64@0.25.2": { - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==" + "@esbuild/netbsd-arm64@0.25.3": { + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==" }, - "@esbuild/netbsd-x64@0.25.2": { - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==" + "@esbuild/netbsd-x64@0.25.3": { + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==" }, - "@esbuild/openbsd-arm64@0.25.2": { - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==" + "@esbuild/openbsd-arm64@0.25.3": { + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==" }, - "@esbuild/openbsd-x64@0.25.2": { - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==" + "@esbuild/openbsd-x64@0.25.3": { + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==" }, - "@esbuild/sunos-x64@0.25.2": { - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==" + "@esbuild/sunos-x64@0.25.3": { + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==" }, - "@esbuild/win32-arm64@0.25.2": { - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==" + "@esbuild/win32-arm64@0.25.3": { + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==" }, - "@esbuild/win32-ia32@0.25.2": { - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==" + "@esbuild/win32-ia32@0.25.3": { + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==" }, - "@esbuild/win32-x64@0.25.2": { - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==" + "@esbuild/win32-x64@0.25.3": { + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==" }, "@jridgewell/gen-mapping@0.3.8": { "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", @@ -195,8 +195,8 @@ "@jridgewell/sourcemap-codec" ] }, - "@noble/hashes@1.7.1": { - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==" + "@noble/hashes@1.8.0": { + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" }, "@parcel/watcher-android-arm64@2.5.1": { "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==" @@ -265,8 +265,8 @@ "acorn" ] }, - "@types/chrome@0.0.313": { - "integrity": "sha512-9R5T7gTaYZhkxlu+Ho4wk9FL+y/werWQY2yjGWSqCuiTsqS7nL/BE5UMTP6rU7J+oIG2FRKqrEycHhJATeltVA==", + "@types/chrome@0.0.317": { + "integrity": "sha512-ibKycbXX8ZZToFshjgWg98BTvFUSvQht8m53Xc+87ye3Z6ZoHJubLjoiDsil8rtW+noWE+Z0+7y0nwLxArU+CQ==", "dependencies": [ "@types/filesystem", "@types/har-format" @@ -317,16 +317,16 @@ "detect-libc@1.0.3": { "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" }, - "esbuild-svelte@0.9.0_esbuild@0.25.2_svelte@5.25.7__acorn@8.14.1": { - "integrity": "sha512-ebGQYTuM4U1Tfx9HdkNtfBjaxY7t7LirlD1yylpSIkhRW+zLzff1wOK1jhuM7ZCnBVCGpt6sGZqiPb5c99KzJg==", + "esbuild-svelte@0.9.2_esbuild@0.25.3_svelte@5.28.2__acorn@8.14.1": { + "integrity": "sha512-8Jq6+rh+g1E2mkBOZKdYZ8JtlbtDq2Fydwvn+/cBvUX9S0cdKv6AISZcEbErKQ0TpLC/Cv04l1vKaqXOBO8+VQ==", "dependencies": [ "@jridgewell/trace-mapping", "esbuild", "svelte" ] }, - "esbuild@0.25.2": { - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "esbuild@0.25.3": { + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", "dependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", @@ -364,8 +364,8 @@ "@jridgewell/sourcemap-codec" ] }, - "fdir@6.4.3": { - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==" + "fdir@6.4.4": { + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==" }, "fill-range@7.1.1": { "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", @@ -444,8 +444,8 @@ "mri" ] }, - "sass@1.86.3": { - "integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==", + "sass@1.87.0": { + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "dependencies": [ "@parcel/watcher", "chokidar", @@ -456,11 +456,11 @@ "source-map-js@1.2.1": { "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, - "sv@0.8.0": { - "integrity": "sha512-nWTFtF3Q8hDrQJB1wko50z1dwUgmjhXgyJFTV0Z6dkzGjSPTHM56evKoJaHar0VfF+ljCj5a8kpSC2uwxNT/Jw==" + "sv@0.8.3": { + "integrity": "sha512-y/RIbFUowsykbShu8rJnxILieNtV1yduSN6dPhmCqNa+oMDSroRtjXBWyZe8MoZetAPN+m1tpvW1aA/CJNmIMw==" }, - "svelte-check@4.1.5_svelte@5.25.7__acorn@8.14.1_typescript@5.8.3": { - "integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==", + "svelte-check@4.1.6_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3": { + "integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==", "dependencies": [ "@jridgewell/trace-mapping", "chokidar", @@ -471,7 +471,7 @@ "typescript" ] }, - "svelte-preprocess@6.0.3_sass@1.86.3_svelte@5.25.7__acorn@8.14.1_typescript@5.8.3": { + "svelte-preprocess@6.0.3_sass@1.87.0_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3": { "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", "dependencies": [ "sass", @@ -479,8 +479,8 @@ "typescript" ] }, - "svelte@5.25.7_acorn@8.14.1": { - "integrity": "sha512-0fzXbXaKfSvFUs6Wxev2h4CoEhexZotbTF9EJ4+Cg7MHW64ZnZ9+xUedZyEpgj0Tt9HrYGv9aASHkqjn9b/cPw==", + "svelte@5.28.2_acorn@8.14.1": { + "integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==", "dependencies": [ "@ampproject/remapping", "@jridgewell/sourcemap-codec", @@ -526,23 +526,23 @@ "workspace": { "dependencies": [ "jsr:@luca/esbuild-deno-loader@0.11.1", - "jsr:@std/expect@1.0.14", - "jsr:@std/testing@1.0.10", + "jsr:@std/expect@^1.0.15", + "jsr:@std/testing@^1.0.11", "npm:@types/chrome@*", - "npm:esbuild-svelte@0.9.0", + "npm:esbuild-svelte@0.9.2", "npm:happy-dom@17.4.4", "npm:svelte-preprocess@6.0.3" ], "packageJson": { "dependencies": [ - "npm:@noble/hashes@1.7.1", - "npm:@types/chrome@0.0.313", + "npm:@noble/hashes@1.8.0", + "npm:@types/chrome@0.0.317", "npm:@types/deno@2.2.0", "npm:jsondiffpatch@0.7.3", - "npm:sass@1.86.3", - "npm:sv@0.8.0", - "npm:svelte-check@4.1.5", - "npm:svelte@5.25.7", + "npm:sass@1.87.0", + "npm:sv@0.8.3", + "npm:svelte-check@4.1.6", + "npm:svelte@5.28.2", "npm:typescript@5.8.3" ] } diff --git a/manifest.json b/manifest.json index e21d07d..0d15a73 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { - "version": "1.1.0", + "version": "1.2.0", "name": "API Monitor", "manifest_version": 3, "description": "Show active intervals, scheduled timeouts, animation frames, idle callbacks, eval invocations, media events and properties", "minimum_chrome_version": "135.0", "homepage_url": "https://github.com/zendive/browser-api-monitor", - "permissions": ["storage"], + "permissions": ["storage", "power"], "host_permissions": ["*://*/*"], "devtools_page": "public/api-monitor-devtools.html", "icons": { diff --git a/package.json b/package.json index f12f5a6..0297f21 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "type": "module", "devDependencies": { - "@types/chrome": "0.0.313", + "@types/chrome": "0.0.317", "@types/deno": "2.2.0", - "sass": "1.86.3", - "sv": "0.8.0", - "svelte": "5.25.7", - "svelte-check": "4.1.5", + "sass": "1.87.0", + "sv": "0.8.3", + "svelte": "5.28.2", + "svelte-check": "4.1.6", "typescript": "5.8.3" }, "dependencies": { - "@noble/hashes": "1.7.1", + "@noble/hashes": "1.8.0", "jsondiffpatch": "0.7.3" } } diff --git a/public/api-monitor-devtools-panel.html b/public/api-monitor-devtools-panel.html index 1b18575..05bbf58 100644 --- a/public/api-monitor-devtools-panel.html +++ b/public/api-monitor-devtools-panel.html @@ -1,4 +1,4 @@ - + diff --git a/public/api-monitor-devtools.html b/public/api-monitor-devtools.html index 0dcde82..9283322 100644 --- a/public/api-monitor-devtools.html +++ b/public/api-monitor-devtools.html @@ -1,7 +1,8 @@ - - + + + DevtoolsTab diff --git a/public/global.css b/public/global.css index 04c6860..fcef669 100644 --- a/public/global.css +++ b/public/global.css @@ -11,7 +11,7 @@ --text-trace: light-dark(rgb(30% 30% 30%), rgb(70% 70% 70%)); --link: light-dark(rgb(0% 0% 0%), rgb(100% 100% 100%)); --link-visited-bg: rgb(79 189 36 / 0.24); - --error: rgb(100% 0% 0%); + --attention: rgb(100% 0% 0%); --small-icon-size: 0.6875rem; } @@ -30,8 +30,15 @@ body { padding: 0; box-sizing: border-box; font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, - Cantarell, 'Helvetica Neue', sans-serif; + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen-Sans, + Ubuntu, + Cantarell, + 'Helvetica Neue', + sans-serif; } a { @@ -73,6 +80,9 @@ th, .ta-r { text-align: right; } +.tc-attention { + color: var(--attention); +} .t-zebra:where(:nth-child(even)) { background-color: var(--bg-table-even); } @@ -103,6 +113,15 @@ th, margin-right: 0; } +.sticky-header { + /* @NOTE: unstable in Chrome v135 */ + /*position: sticky;*/ + /*top: 0;*/ + /*z-index: 1;*/ + height: 1rem; + vertical-align: middle; +} + .icon { display: inline-block; width: 1rem; @@ -173,9 +192,15 @@ th .icon { .icon.-trace-extension { mask-image: url(img/trace-extension.svg); } +.icon.-trace-webpack { + mask-image: url(img/trace-webpack.svg); +} .icon.-breakpoint { mask-image: url(img/breakpoint.svg); } .icon.-bypass { mask-image: url(img/bypass.svg); } +.icon.-facts { + mask-image: url(img/facts.svg); +} diff --git a/public/img/breakpoint.svg b/public/img/breakpoint.svg index 312a1a2..9a2dbf1 100644 --- a/public/img/breakpoint.svg +++ b/public/img/breakpoint.svg @@ -1,4 +1,7 @@ + + + diff --git a/public/img/facts.svg b/public/img/facts.svg new file mode 100644 index 0000000..410a07e --- /dev/null +++ b/public/img/facts.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/public/img/trace-webpack.svg b/public/img/trace-webpack.svg new file mode 100644 index 0000000..6ac0da0 --- /dev/null +++ b/public/img/trace-webpack.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/api-monitor-cs-isolated.ts b/src/api-monitor-cs-isolated.ts index 76a12b7..ef18b12 100644 --- a/src/api-monitor-cs-isolated.ts +++ b/src/api-monitor-cs-isolated.ts @@ -5,17 +5,34 @@ import { windowListen, windowPost, } from './api/communication.ts'; -import { getSettings, onSettingsChange } from './api/settings.ts'; +import { loadLocalStorage, onLocalStorageChange } from './api/storage.local.ts'; +import { + loadSessionStorage, + onSessionStorageChange, +} from './api/storage.session.ts'; + +Promise.all([loadLocalStorage(), loadSessionStorage()]).then( + ([config, session]) => { + windowPost({ msg: EMsg.CONFIG, config }); + windowPost({ msg: EMsg.SESSION, session }); + + if (config.devtoolsPanelShown && !config.paused) { + windowPost({ msg: EMsg.START_OBSERVE }); + } -getSettings().then((settings) => { - windowPost({ msg: EMsg.SETTINGS, settings: settings }); + portListen(windowPost); + windowListen(runtimePost); - onSettingsChange((newValue) => { - windowPost({ msg: EMsg.SETTINGS, settings: newValue }); - }); -}); + onLocalStorageChange((newValue) => { + windowPost({ msg: EMsg.CONFIG, config: newValue }); + }); + onSessionStorageChange((newValue) => { + windowPost({ msg: EMsg.SESSION, session: newValue }); + }); -portListen(windowPost); -windowListen(runtimePost); + runtimePost({ msg: EMsg.CONTENT_SCRIPT_LOADED }); -runtimePost({ msg: EMsg.CONTENT_SCRIPT_LOADED }); + __development__ && + console.log('api-monitor-cs-isolated.ts', performance.now()); + }, +); diff --git a/src/api-monitor-cs-main.ts b/src/api-monitor-cs-main.ts index e0d6439..e9079ed 100644 --- a/src/api-monitor-cs-main.ts +++ b/src/api-monitor-cs-main.ts @@ -1,14 +1,14 @@ import { EMsg, windowListen, windowPost } from './api/communication.ts'; -import { IS_DEV } from './api/env.ts'; import { TELEMETRY_FREQUENCY_1PS } from './api/const.ts'; import { adjustTelemetryDelay, Timer } from './api/time.ts'; import { + applyConfig, + applySession, cleanHistory, collectMetrics, onEachSecond, runMediaCommand, runTimerCommand, - setSettings, type TTelemetry, } from './wrapper/Wrapper.ts'; import diff from './api/diff.ts'; @@ -52,11 +52,9 @@ windowListen((o) => { originalMetrics = currentMetrics; eachSecond.isPending() && tick.start(); } else if ( - o.msg === EMsg.SETTINGS && - o.settings && - typeof o.settings === 'object' + o.msg === EMsg.CONFIG && o.config && typeof o.config === 'object' ) { - setSettings(o.settings); + applyConfig(o.config); } else if (o.msg === EMsg.START_OBSERVE) { originalMetrics = currentMetrics = null; tick.trigger(); @@ -73,7 +71,9 @@ windowListen((o) => { runTimerCommand(o.type, o.handler); } else if (o.msg === EMsg.MEDIA_COMMAND) { runMediaCommand(o.mediaId, o.cmd, o.property); + } else if (o.msg === EMsg.SESSION) { + applySession(o.session); } }); -IS_DEV && console.debug('cs-main.ts'); +__development__ && console.debug('api-monitor-cs-main.ts', performance.now()); diff --git a/src/api-monitor-devtools-panel.ts b/src/api-monitor-devtools-panel.ts index 9daf707..d951c71 100644 --- a/src/api-monitor-devtools-panel.ts +++ b/src/api-monitor-devtools-panel.ts @@ -1,4 +1,9 @@ import { mount } from 'svelte'; import App from './view/App.svelte'; +import { initConfigState } from './state/config.state.svelte.ts'; +import { onHidePanel } from './devtoolsPanelUtil.ts'; -mount(App, { target: document.body }); +initConfigState().then(() => { + mount(App, { target: document.body }); + globalThis.addEventListener('beforeunload', onHidePanel); +}); diff --git a/src/api-monitor-devtools.ts b/src/api-monitor-devtools.ts index 33f1211..bc94884 100644 --- a/src/api-monitor-devtools.ts +++ b/src/api-monitor-devtools.ts @@ -1,5 +1,7 @@ import { EMsg, portPost } from './api/communication.ts'; -import { getSettings, setSettings } from './api/settings.ts'; +import { loadLocalStorage, saveLocalStorage } from './api/storage.local.ts'; +import { enableSessionInContentScript } from './api/storage.session.ts'; +import { onHidePanel } from './devtoolsPanelUtil.ts'; // tabId may be null if user opened the devtools of the devtools if (chrome.devtools.inspectedWindow.tabId !== null) { @@ -9,16 +11,19 @@ if (chrome.devtools.inspectedWindow.tabId !== null) { '/public/api-monitor-devtools-panel.html', (panel) => { panel.onShown.addListener(async () => { - const settings = await getSettings(); - if (!settings.paused) { + const config = await loadLocalStorage(); + if (!config.paused) { portPost({ msg: EMsg.START_OBSERVE }); } - setSettings({ devtoolsPanelShown: true }); - }); - panel.onHidden.addListener(() => { - portPost({ msg: EMsg.STOP_OBSERVE }); - setSettings({ devtoolsPanelShown: false }); + if (config.keepAwake) { + chrome.power.requestKeepAwake('display'); + } + await saveLocalStorage({ devtoolsPanelShown: true }); }); + + panel.onHidden.addListener(onHidePanel); }, ); + + enableSessionInContentScript(); } diff --git a/src/api/canvas.ts b/src/api/canvas.ts new file mode 100644 index 0000000..1e950c7 --- /dev/null +++ b/src/api/canvas.ts @@ -0,0 +1,265 @@ +/** + * Module assumption: + * - Coordinate system of canvas html element + */ + +export const PI = Math.PI; +export const PI2 = 2 * PI; +export const PId2 = PI / 2; + +/** + * Round floating point with custom precision + */ +export function fround(n: number, precision?: number) { + precision ??= 1e6; + return Math.round(n * precision) / precision; +} + +/** + * Degrees to radians + */ +export function deg2rad(deg: number) { + return ((deg % 360) / 360) * PI2; +} + +/** + * Radians to degrees + */ +export function rad2deg(rad: number) { + return ((rad % PI2) / PI2) * 360; +} + +class XY { + x: number = 0; + y: number = 0; + + constructor(x: number, y: number) { + this.set(x, y); + } + + set(x: number, y: number) { + this.x = x; + this.y = y; + return this; + } + + clone() { + return new XY(this.x, this.y); + } + + toString() { + return `{${this.x}, ${this.y}}`; + } + + shiftX(x: number) { + this.x += x; + return this; + } + + shiftY(y: number) { + this.y += y; + return this; + } + + shiftXY(x: number, y: number) { + this.x += x; + this.y += y; + return this; + } + + hasSameXY(x: number, y: number) { + return (x === this.x && y === this.y); + } + + isEqualTo(xy: XY) { + return this.hasSameXY(xy.x, xy.y); + } + + round(precision?: number) { + return this.set( + fround(this.x, precision), + fround(this.y, precision), + ); + } +} + +export class Point extends XY { + constructor(x: number, y: number) { + super(x, y); + } + + clone() { + return new Point(this.x, this.y); + } + + /** + * Get distance between two points + */ + proximity(p: Point) { + return this.vectorTo(p).length; + } + + /** + * Convert point to vector + * Assuming this(x,y) is a v(0,0) + * @note this(0,0) is at top left corner + */ + vectorTo(to: XY) { + return new Vector(to.x - this.x, to.y - this.y); + } + + /** + * Rotate point over center `axis` point by an angle + * @note: positive `radAngle` means counterclockwise + */ + rotate(radAngle: number, axis: Point) { + const p = axis.vectorTo(this).rotate(radAngle).atBase(axis); + return this.set(p.x, p.y); + } +} + +/** + * Vector with virtual base of {0,0} that points to {x,y} + */ +export class Vector extends XY { + constructor(x: number, y: number) { + super(x, y); + } + + clone() { + return new Vector(this.x, this.y); + } + + /** + * Get point where vector points from `base` point of view + * Assuming base(x,y) refers to v(0,0) of `this` vector + */ + atBase(base: Point) { + return new Point(base.x + this.x, base.y + this.y); + } + + /** + * Rotate at specific angle + * produced vector may contain float epsilon errors + * @note: positive `radAngle` means counterclockwise + */ + rotate(radAngle: number) { + const cos = Math.cos(-radAngle); + const sin = Math.sin(-radAngle); + + return this.set( + this.x * cos - this.y * sin, + this.x * sin + this.y * cos, + ); + } + + rotateLeft() { + return this.set(-this.y, this.x); + } + + rotateRight() { + return this.set(this.y, -this.x); + } + + rotateBack() { + return this.set(-this.x, -this.y); + } + + mirrorOver(axis: Vector) { + const delta = this.angleWithX - axis.angleWithX; + const k = delta >= 0 ? -2 : 2; + return this.rotate(k * this.angle(axis)); + } + + get length() { + return Math.sqrt(this.dot(this)); + } + + setLength(newLength: number) { + const v = new Vector(newLength, 0).rotate(this.angleWithX); + return this.set(v.x, v.y); + } + + half() { + return this.set(this.x / 2, this.y / 2); + } + + /** + * Get angle of vector relative to X axis in range [0 ... α ... PI2] counterclockwise + */ + get angleWithX() { + const angle = Math.atan2(-this.y, this.x); + + return (angle < 0) ? angle + PI2 : angle; + } + + angle(v: Vector) { + return Math.acos(this.normalize().dot(v.normalize())); + } + + /** + * Return normalized vector + */ + normalize() { + const length = this.length; + return new Vector(this.x / length, this.y / length); + } + + /** + * Dot product of two vectors + */ + dot(v: Vector) { + return (this.x * v.x + this.y * v.y); + } +} + +export class Box { + w: number = 0; + h: number = 0; + // top-left + tl: Point = new Point(0, 0); + // top-right + tr: Point = new Point(0, 0); + // bottom-right + br: Point = new Point(0, 0); + // bottom-left + bl: Point = new Point(0, 0); + // center of a box + c: Point = new Point(0, 0); + + constructor(tl: Point, w: number, h: number) { + this.tl.set(tl.x, tl.y); + this.resize(w, h); + } + + clone() { + return new Box(this.tl, this.w, this.h); + } + + resize(w: number, h: number) { + this.w = w; + this.h = h; + this.tr = new Point(this.tl.x + w, this.tl.y); + this.br = new Point(this.tl.x + w, this.tl.y + h); + this.bl = new Point(this.tl.x, this.tl.y + h); + this.c = new Point(this.tl.x + this.w / 2, this.tl.y + h / 2); + + return this; + } + + toString() { + return JSON.stringify({ + w: this.w, + h: this.h, + tl: this.tl.toString(), + c: this.c.toString(), + }); + } + + contains(p: Point) { + return ( + this.tl.x <= p.x && p.x <= this.tr.x && + this.tl.y <= p.y && p.y < this.bl.y + ); + } +} diff --git a/src/api/communication.ts b/src/api/communication.ts index 922daaa..6dcc477 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -14,9 +14,10 @@ import { APPLICATION_NAME } from './env.ts'; import { ERRORS_IGNORED } from './const.ts'; import { ETimerType } from '../wrapper/TimerWrapper.ts'; import type { TTelemetry } from '../wrapper/Wrapper.ts'; -import type { TSettings } from './settings.ts'; +import type { TConfig } from './storage.local.ts'; import type { TMediaCommand } from '../wrapper/MediaWrapper.ts'; import type { Delta } from 'jsondiffpatch'; +import type { TSession } from './storage.session.ts'; let port: chrome.runtime.Port | null = null; export function portPost(payload: TMsgOptions) { @@ -91,7 +92,7 @@ function handleRuntimeMessageResponse(): void { } export enum EMsg { - SETTINGS, + CONFIG, CONTENT_SCRIPT_LOADED, START_OBSERVE, STOP_OBSERVE, @@ -101,57 +102,63 @@ export enum EMsg { MEDIA_COMMAND, RESET_WRAPPER_HISTORY, TIMER_COMMAND, + SESSION, } -export interface TMsgStartObserve { +export interface IMsgStartObserve { msg: EMsg.START_OBSERVE; } -export interface TMsgStopObserve { +export interface IMsgStopObserve { msg: EMsg.STOP_OBSERVE; } -export interface TMsgResetHistory { +export interface IMsgResetHistory { msg: EMsg.RESET_WRAPPER_HISTORY; } -export interface TMsgTimerCommand { +export interface IMsgTimerCommand { msg: EMsg.TIMER_COMMAND; type: ETimerType; handler: number; } -export interface TMsgLoaded { +export interface IMsgLoaded { msg: EMsg.CONTENT_SCRIPT_LOADED; } -export interface TMsgTelemetry { +export interface IMsgTelemetry { msg: EMsg.TELEMETRY; timeOfCollection: number; telemetry: TTelemetry; } -export interface TMsgTelemetryDelta { +export interface IMsgTelemetryDelta { msg: EMsg.TELEMETRY_DELTA; timeOfCollection: number; telemetryDelta: Delta; } -export interface TMsgTelemetryAcknowledged { +export interface IMsgTelemetryAcknowledged { msg: EMsg.TELEMETRY_ACKNOWLEDGED; timeOfCollection: number; } -export interface TMsgSettings { - msg: EMsg.SETTINGS; - settings: TSettings; +export interface IMsgConfig { + msg: EMsg.CONFIG; + config: TConfig; } -export interface TMsgMediaCommand { +export interface IMsgMediaCommand { msg: EMsg.MEDIA_COMMAND; mediaId: string; cmd: TMediaCommand; property?: keyof HTMLMediaElement; } +export interface IMsgSession { + msg: EMsg.SESSION; + session: TSession; +} export type TMsgOptions = - | TMsgTelemetry - | TMsgTelemetryDelta - | TMsgTelemetryAcknowledged - | TMsgStartObserve - | TMsgStopObserve - | TMsgLoaded - | TMsgResetHistory - | TMsgTimerCommand - | TMsgSettings - | TMsgMediaCommand; + | IMsgTelemetry + | IMsgTelemetryDelta + | IMsgTelemetryAcknowledged + | IMsgStartObserve + | IMsgStopObserve + | IMsgLoaded + | IMsgResetHistory + | IMsgTimerCommand + | IMsgConfig + | IMsgMediaCommand + | IMsgSession; diff --git a/src/api/comparator.ts b/src/api/comparator.ts index 88ce717..8ecbda2 100644 --- a/src/api/comparator.ts +++ b/src/api/comparator.ts @@ -1,5 +1,5 @@ import type { TOnlineTimerMetrics } from '../wrapper/TimerWrapper.ts'; -import { ESortOrder } from './settings.ts'; +import { ESortOrder } from './storage.local.ts'; const SEMISORTING_FIELDS = ['calls', 'delay', 'online']; diff --git a/src/api/const.ts b/src/api/const.ts index 1b5bb20..19e95d9 100644 --- a/src/api/const.ts +++ b/src/api/const.ts @@ -1,3 +1,5 @@ +import type { TWritableBooleanKeys } from './generics.ts'; + export const ERRORS_IGNORED = [ 'Could not establish connection. Receiving end does not exist.', 'The message port closed before a response was received.', @@ -8,7 +10,7 @@ export const FRAME_1of60 = 0.0166666666667; // ms export const VARIABLE_ANIMATION_THROTTLE = 3500; // eye blinking average frequency export const SELF_TIME_MAX_GOOD = 13.333333333333332; // ms -// store native functions +// state native functions export const setTimeout = /*@__PURE__*/ globalThis.setTimeout.bind(globalThis); export const clearTimeout = /*@__PURE__*/ globalThis.clearTimeout.bind( globalThis, @@ -24,7 +26,9 @@ export const requestAnimationFrame = /*@__PURE__*/ globalThis export const cancelAnimationFrame = /*@__PURE__*/ globalThis .cancelAnimationFrame.bind(globalThis); -export const TAG_MISFORTUNE = '❓\u00A0⟪N/A⟫'; +export const TAG_DELAY_NOT_FOUND = ''; +export const TAG_BAD_DELAY = (x: unknown) => `${x}`; +export const TAG_BAD_HANDLER = (x: unknown) => `${x}`; export const TAG_EVAL_RETURN_SET_TIMEOUT = '(N/A - via setTimeout)'; export const TAG_EVAL_RETURN_SET_INTERVAL = '(N/A - via setInterval)'; @@ -100,14 +104,7 @@ export const MEDIA_ELEMENT_PROPS = [ 'videoHeight', ]; -export type TWritableBooleanKeys = { - [K in keyof T]-?: boolean extends T[K] - ? (() => { [P in K]: T[K] } extends { -readonly [P in K]: T[K] } ? K - : never) extends (() => infer I) ? I - : never - : never; -}[keyof T]; -export type TToggableMediaProps = TWritableBooleanKeys< +type TToggableMediaProps = TWritableBooleanKeys< HTMLVideoElement & HTMLAudioElement >; export const MEDIA_ELEMENT_TOGGABLE_PROPS: Set = diff --git a/src/api/env.ts b/src/api/env.ts index 6e46e9c..af39508 100644 --- a/src/api/env.ts +++ b/src/api/env.ts @@ -1,4 +1,3 @@ -export const IS_DEV = __development__; export const APPLICATION_VERSION = __app_version__; export const APPLICATION_NAME = __app_name__; export const APPLICATION_HOME_PAGE = __home_page__; diff --git a/src/api/generics.ts b/src/api/generics.ts new file mode 100644 index 0000000..9b7beeb --- /dev/null +++ b/src/api/generics.ts @@ -0,0 +1,10 @@ +declare const __brand: unique symbol; +export type Brand = T & { [__brand]: B }; + +export type TWritableBooleanKeys = { + [K in keyof T]-?: boolean extends T[K] + ? (() => { [P in K]: T[K] } extends { -readonly [P in K]: T[K] } ? K + : never) extends (() => infer I) ? I + : never + : never; +}[keyof T]; diff --git a/src/api/hash.ts b/src/api/hash.ts index e3f4f95..67483de 100644 --- a/src/api/hash.ts +++ b/src/api/hash.ts @@ -1,7 +1,7 @@ import { sha256 } from '@noble/hashes/sha2'; import { bytesToHex } from '@noble/hashes/utils'; -const HASH_STRING_LENGTH = 64; +export const HASH_STRING_LENGTH = 64; export function hashString(str: string) { return ( diff --git a/src/api/settings.ts b/src/api/storage.local.ts similarity index 71% rename from src/api/settings.ts rename to src/api/storage.local.ts index 7ad6459..35f49ab 100644 --- a/src/api/settings.ts +++ b/src/api/storage.local.ts @@ -12,6 +12,7 @@ import type { } from '../wrapper/TimerWrapper.ts'; type TPanelKey = + | 'callsSummary' | 'eval' | 'media' | 'activeTimers' @@ -24,19 +25,20 @@ type TPanelKey = | 'requestIdleCallback' | 'cancelIdleCallback'; export type TPanelMap = { - [K in TPanelKey]: TSettingsPanel; + [K in TPanelKey]: TPanel; }; -export type TSettingsPanel = { +export type TPanel = { key: TPanelKey; label: string; visible: boolean; wrap: boolean | null; }; -export type TSettings = typeof DEFAULT_SETTINGS; -export type TSettingsProperty = Partial; +export type TConfig = typeof DEFAULT_CONFIG; +export type TConfigField = Partial; -const SETTINGS_VERSION = '1.0.7'; -export const DEFAULT_PANELS: TSettingsPanel[] = [ +const CONFIG_VERSION = '2025-04-25'; +export const DEFAULT_PANELS: TPanel[] = [ + { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, { key: 'activeTimers', label: 'Active Timers', visible: true, wrap: null }, { key: 'eval', label: 'eval', visible: true, wrap: false }, @@ -97,29 +99,29 @@ export enum ESortOrder { export const DEFAULT_SORT_SET_TIMERS = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; export const DEFAULT_SORT_CLEAR_TIMERS = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; export const DEFAULT_SORT_RAF = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; export const DEFAULT_SORT_CAF = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; export const DEFAULT_SORT_RIC = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; export const DEFAULT_SORT_CIC = { field: 'calls', order: ESortOrder.DESCENDING, -} as const; +}; -export const DEFAULT_SETTINGS = { +export const DEFAULT_CONFIG = { panels: DEFAULT_PANELS, sortSetTimers: DEFAULT_SORT_SET_TIMERS, sortClearTimers: DEFAULT_SORT_CLEAR_TIMERS, @@ -129,49 +131,48 @@ export const DEFAULT_SETTINGS = { sortCancelIdleCallback: DEFAULT_SORT_CIC, paused: false, devtoolsPanelShown: false, - trace4Debug: null, - trace4Bypass: null, wrapperCallstackType: EWrapperCallstackType.SHORT, + keepAwake: false, }; -export function panelsArray2Map(panels: TSettingsPanel[]) { +export function panelsArray2Map(panels: TPanel[]) { return panels.reduce( (acc, o) => Object.assign(acc, { [o.key]: o }), {} as TPanelMap, ); } -export async function getSettings(): Promise { - let store = await chrome.storage.local.get([SETTINGS_VERSION]); +export async function loadLocalStorage(): Promise { + let store = await chrome.storage.local.get([CONFIG_VERSION]); const isEmpty = !Object.keys(store).length; if (isEmpty) { - await chrome.storage.local.clear(); // rid off previous version settings - await chrome.storage.local.set({ [SETTINGS_VERSION]: DEFAULT_SETTINGS }); - store = await chrome.storage.local.get([SETTINGS_VERSION]); + await chrome.storage.local.clear(); // reset previous version + await chrome.storage.local.set({ [CONFIG_VERSION]: DEFAULT_CONFIG }); + store = await chrome.storage.local.get([CONFIG_VERSION]); } - return store[SETTINGS_VERSION]; + return store[CONFIG_VERSION]; } -export async function setSettings(value: TSettingsProperty) { - const store = await chrome.storage.local.get([SETTINGS_VERSION]); +export async function saveLocalStorage(value: TConfigField) { + const store = await chrome.storage.local.get([CONFIG_VERSION]); - Object.assign(store[SETTINGS_VERSION], value); + Object.assign(store[CONFIG_VERSION], value); return await chrome.storage.local.set(store); } -export function onSettingsChange( - callback: (newValue: TSettings, oldValue: TSettings) => void, +export function onLocalStorageChange( + callback: (newValue: TConfig, oldValue: TConfig) => void, ) { chrome.storage.local.onChanged.addListener((change) => { if ( - change && change[SETTINGS_VERSION] && change[SETTINGS_VERSION].newValue + change && change[CONFIG_VERSION] && change[CONFIG_VERSION].newValue ) { callback( - change[SETTINGS_VERSION].newValue, - change[SETTINGS_VERSION].oldValue, + change[CONFIG_VERSION].newValue, + change[CONFIG_VERSION].oldValue, ); } }); diff --git a/src/api/storage.session.ts b/src/api/storage.session.ts new file mode 100644 index 0000000..8a74cce --- /dev/null +++ b/src/api/storage.session.ts @@ -0,0 +1,50 @@ +const SESSION_VERSION = '2025-04-25'; + +export type TSession = typeof DEFAULT_SESSION; +type TSessionProperty = Partial; +const DEFAULT_SESSION = { + debug: [], + bypass: [], +}; + +export function enableSessionInContentScript() { + return chrome.storage.session.setAccessLevel({ + accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS', + }); +} + +export async function loadSessionStorage(): Promise { + let store = await chrome.storage.session.get([SESSION_VERSION]); + const isEmpty = !Object.keys(store).length; + + if (isEmpty) { + await chrome.storage.session.clear(); // reset previous version + await chrome.storage.session.set({ [SESSION_VERSION]: DEFAULT_SESSION }); + store = await chrome.storage.session.get([SESSION_VERSION]); + } + + return store[SESSION_VERSION]; +} + +export async function saveSessionStorage(value: TSessionProperty) { + const store = await chrome.storage.session.get([SESSION_VERSION]); + + Object.assign(store[SESSION_VERSION], value); + + return await chrome.storage.session.set(store); +} + +export function onSessionStorageChange( + callback: (newValue: TSession, oldValue: TSession) => void, +) { + chrome.storage.session.onChanged.addListener((change) => { + if ( + change && change[SESSION_VERSION] && change[SESSION_VERSION].newValue + ) { + callback( + change[SESSION_VERSION].newValue, + change[SESSION_VERSION].oldValue, + ); + } + }); +} diff --git a/src/api/time.ts b/src/api/time.ts index 9296f87..2b56e4e 100644 --- a/src/api/time.ts +++ b/src/api/time.ts @@ -83,7 +83,7 @@ export class Stopper { } } -interface TimerOptions { +interface ITimerOptions { /** a delay of setTimeout or setInterval (default: 0); irrelevant if `animation` is true */ delay?: number; /** act as setInterval by repeating setTimeout (default: false) */ @@ -103,8 +103,8 @@ interface TimerOptions { * - `measurable: true` - measure the callback's execution time. */ export class Timer { - readonly options: TimerOptions; - readonly #defaultOptions: TimerOptions = { + readonly options: ITimerOptions; + readonly #defaultOptions: ITimerOptions = { delay: 0, repetitive: false, animation: false, @@ -117,7 +117,7 @@ export class Timer { #handler: number = 0; readonly #stopper?: Stopper; - constructor(o: TimerOptions, fn: (...args: unknown[]) => void) { + constructor(o: ITimerOptions, fn: (...args: unknown[]) => void) { this.options = Object.assign(this.#defaultOptions, o); this.#fn = fn; this.delay = this.options.delay || 0; diff --git a/src/devtoolsPanelUtil.ts b/src/devtoolsPanelUtil.ts new file mode 100644 index 0000000..3e23cb9 --- /dev/null +++ b/src/devtoolsPanelUtil.ts @@ -0,0 +1,13 @@ +/** + * module for functions common for devtools-panel + * as well as for svelte component, BUT doesn't require svelte + * as a dependency + */ +import { EMsg, portPost } from './api/communication.ts'; +import { saveLocalStorage } from './api/storage.local.ts'; + +export async function onHidePanel() { + chrome.power.releaseKeepAwake(); + portPost({ msg: EMsg.STOP_OBSERVE }); + await saveLocalStorage({ devtoolsPanelShown: false }); +} diff --git a/src/state/config.state.svelte.ts b/src/state/config.state.svelte.ts new file mode 100644 index 0000000..a9e5ec8 --- /dev/null +++ b/src/state/config.state.svelte.ts @@ -0,0 +1,61 @@ +import { EMsg, portPost } from '../api/communication.ts'; +import { + DEFAULT_CONFIG, + EWrapperCallstackType, + loadLocalStorage, + saveLocalStorage, + type TConfig, +} from '../api/storage.local.ts'; + +let config: TConfig = $state(DEFAULT_CONFIG); + +export function useConfigState() { + return config; +} + +export async function initConfigState() { + config = await loadLocalStorage(); +} + +export async function togglePause() { + config.paused = !config.paused; + await saveLocalStorage({ paused: $state.snapshot(config.paused) }); + + if (config.paused) { + portPost({ msg: EMsg.STOP_OBSERVE }); + } else { + portPost({ msg: EMsg.START_OBSERVE }); + } +} + +export async function toggleKeepAwake() { + config.keepAwake = !config.keepAwake; + await saveLocalStorage({ keepAwake: $state.snapshot(config.keepAwake) }); + + if (config.keepAwake) { + chrome.power.requestKeepAwake('display'); + } else { + chrome.power.releaseKeepAwake(); + } +} + +export async function toggleWrapperCallstackType() { + config.wrapperCallstackType = + config.wrapperCallstackType === EWrapperCallstackType.FULL + ? EWrapperCallstackType.SHORT + : EWrapperCallstackType.FULL; + + await saveLocalStorage({ + wrapperCallstackType: $state.snapshot(config.wrapperCallstackType), + }); +} + +export async function togglePanelWrap(index: number) { + config.panels[index].wrap = !config.panels[index].wrap; + await saveLocalStorage({ panels: $state.snapshot(config.panels) }); +} + +export async function togglePanelVisibility(index: number) { + config.panels[index].visible = !config.panels[index].visible; + await saveLocalStorage({ panels: $state.snapshot(config.panels) }); +} diff --git a/src/state/session.state.svelte.ts b/src/state/session.state.svelte.ts new file mode 100644 index 0000000..fc2c186 --- /dev/null +++ b/src/state/session.state.svelte.ts @@ -0,0 +1,55 @@ +import { + loadSessionStorage, + saveSessionStorage, +} from '../api/storage.session.ts'; +import { SvelteSet } from 'svelte/reactivity'; + +export const sessionState = $state({ + bypass: > new SvelteSet(), + debug: > new SvelteSet(), +}); + +loadSessionStorage().then((session) => { + session.bypass.forEach((traceId) => { + sessionState.bypass.add(traceId); + }); + + session.debug.forEach((traceId) => { + sessionState.debug.add(traceId); + }); +}); + +export async function toggleBypass(traceId: string) { + if (await toggleSet(sessionState.bypass, traceId)) { + await saveSessionStorage({ + bypass: Array.from(sessionState.bypass.values()), + }); + } +} + +export async function toggleDebug(traceId: string) { + if (await toggleSet(sessionState.debug, traceId)) { + await saveSessionStorage({ + debug: Array.from(sessionState.debug.values()), + }); + } +} + +const QUOTA_THRESHOLD = chrome.storage.session.QUOTA_BYTES; +const MARGINAL_SIZE = 40; // for ASCII string in an array +async function toggleSet(set: Set, traceId: string): Promise { + if (set.has(traceId)) { + set.delete(traceId); + return true; + } + + const freeSpace = QUOTA_THRESHOLD - + await chrome.storage.session.getBytesInUse(); + + if (freeSpace - traceId.length - MARGINAL_SIZE >= 0) { + set.add(traceId); + return true; + } + + return false; +} diff --git a/src/state/telemetry.state.svelte.ts b/src/state/telemetry.state.svelte.ts new file mode 100644 index 0000000..c9da58a --- /dev/null +++ b/src/state/telemetry.state.svelte.ts @@ -0,0 +1,36 @@ +import type { TTelemetry } from '../wrapper/Wrapper.ts'; +import { EMsg, portPost, runtimeListen } from '../api/communication.ts'; +import diff from '../api/diff.ts'; +import { type Writable, writable } from 'svelte/store'; + +class TelemetryState { + telemetry: TTelemetry | null = $state.raw(null); + timeOfCollection: Writable = writable(0); +} +const state = new TelemetryState(); +let telemetryProgressive: TTelemetry | null = null; + +export function useTelemetryState() { + return state; +} + +runtimeListen((o) => { + if (o.msg === EMsg.TELEMETRY) { + telemetryProgressive = structuredClone(o.telemetry); + state.telemetry = o.telemetry; + acknowledgeTelemetry(o.timeOfCollection); + } else if (o.msg === EMsg.TELEMETRY_DELTA) { + diff.patch(telemetryProgressive, o.telemetryDelta); + state.telemetry = structuredClone(telemetryProgressive); + acknowledgeTelemetry(o.timeOfCollection); + } +}); + +function acknowledgeTelemetry(timeOfCollection: number) { + portPost({ + msg: EMsg.TELEMETRY_ACKNOWLEDGED, + timeOfCollection, + }); + + state.timeOfCollection.set(timeOfCollection); +} diff --git a/src/view/App.svelte b/src/view/App.svelte index 4feeb92..44d80e8 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -1,204 +1,41 @@ -
+
- {#if IS_DEV} - -
- {/if}
- + {#if __development__} + +
+ {/if} +
- +
- -
- {#if telemetry} - - {/if} -
- - {#if !paused} -
- - {/if} - + +
+
- {#if telemetry} - - - - - {#if telemetry.setTimeoutHistory?.length} - - {/if} - {#if telemetry.clearTimeoutHistory?.length} - - {/if} - - {#if telemetry.setIntervalHistory?.length} - - {/if} - {#if telemetry.clearIntervalHistory?.length} - - {/if} - - {#if telemetry.rafHistory?.length} - - {/if} - {#if telemetry.cafHistory?.length} - - {/if} - - {#if telemetry.ricHistory?.length} - - {/if} - {#if telemetry.cicHistory?.length} - - {/if} - {/if} +
diff --git a/src/view/components/TimersClearHistory.svelte b/src/view/components/TimersClearHistory.svelte deleted file mode 100644 index a3e1c2c..0000000 --- a/src/view/components/TimersClearHistory.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - {#each sortedMetrics as metric (metric.traceId)} - - {/each} - -
- {caption} -
Callstack - Called - - Handler - - Delay -
diff --git a/src/view/components/TimersClearHistoryMetric.svelte b/src/view/components/TimersClearHistoryMetric.svelte deleted file mode 100644 index 48210a7..0000000 --- a/src/view/components/TimersClearHistoryMetric.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - {metric.delay} - - - diff --git a/src/view/components/Trace.svelte b/src/view/components/Trace.svelte deleted file mode 100644 index 0344557..0000000 --- a/src/view/components/Trace.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{#each trace as { link, name }, index (index)} - {@const isLast = index === trace.length - 1} - - {#if !isLast}• {/if} -{/each} diff --git a/src/view/components/TraceDomain.svelte b/src/view/components/TraceDomain.svelte deleted file mode 100644 index 8be9741..0000000 --- a/src/view/components/TraceDomain.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -{#if traceDomain === ETraceDomain.SAME} - -{:else if traceDomain === ETraceDomain.EXTERNAL} - -{:else if traceDomain === ETraceDomain.EXTENSION} - -{:else if traceDomain === ETraceDomain.UNKNOWN} - -{/if} diff --git a/src/view/menu/DevReload.svelte b/src/view/menu/DevReload.svelte new file mode 100644 index 0000000..09512bd --- /dev/null +++ b/src/view/menu/DevReload.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/view/menu/ResetHistory.svelte b/src/view/menu/ResetHistory.svelte new file mode 100644 index 0000000..c5cd7f6 --- /dev/null +++ b/src/view/menu/ResetHistory.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/view/menu/SummaryBar.svelte b/src/view/menu/SummaryBar.svelte new file mode 100644 index 0000000..93cae4b --- /dev/null +++ b/src/view/menu/SummaryBar.svelte @@ -0,0 +1,104 @@ + + +
+ {#if ts.telemetry && panels.callsSummary.visible} + + + + + + + + + + + + + + + + + + + + + + {/if} +
+ + diff --git a/src/view/components/InfoBarItem.svelte b/src/view/menu/SummaryBarItem.svelte similarity index 88% rename from src/view/components/InfoBarItem.svelte rename to src/view/menu/SummaryBarItem.svelte index 58fcaaa..ad10212 100644 --- a/src/view/components/InfoBarItem.svelte +++ b/src/view/menu/SummaryBarItem.svelte @@ -1,6 +1,6 @@ @@ -53,8 +35,8 @@ @@ -62,9 +44,9 @@ @@ -134,13 +130,31 @@ padding: 0 0.375rem; .menu-content { + margin: 0.2rem 0; + .menu-item { - line-height: 1.4rem; + td { + line-height: 1rem; + padding: 0.1rem 0 0.1rem 0; - &.-dash { + &.-left { + max-width: 12rem; + } + &.-right { + display: flex; + align-items: center; + justify-content: center; + } + } + + &.-dash-bottom { border-bottom: 1px solid var(--border); } + &.-dash-top { + border-top: 1px solid var(--border); + } + .toggle-visibility { color: var(--text); text-wrap: nowrap; @@ -152,9 +166,8 @@ .btn-toggle { color: var(--text); - border-left: 1px solid var(--border); - border-right: none; margin-left: 0.375rem; + font-weight: bold; } } } diff --git a/src/view/menu/TogglePause.svelte b/src/view/menu/TogglePause.svelte new file mode 100644 index 0000000..75d8049 --- /dev/null +++ b/src/view/menu/TogglePause.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/view/menu/UpdatePace.svelte b/src/view/menu/UpdatePace.svelte new file mode 100644 index 0000000..8c89294 --- /dev/null +++ b/src/view/menu/UpdatePace.svelte @@ -0,0 +1,41 @@ + + +
+ +
+ + diff --git a/src/view/menu/UpdatePaceTimeMap.ts b/src/view/menu/UpdatePaceTimeMap.ts new file mode 100644 index 0000000..2713b1d --- /dev/null +++ b/src/view/menu/UpdatePaceTimeMap.ts @@ -0,0 +1,134 @@ +import { deg2rad, PI2, Point, Vector } from '../../api/canvas.ts'; + +interface IMemo { + whenOccurred: number; + whenAdded: number; + age: number; + timePoint: Point; + vector: Vector; +} + +let raf = 0; +let ctx: CanvasRenderingContext2D; +const R = 20; +const D = 2 * R; +const LINE_WIDTH = 4; +const SHADOW_WIDTH = 4; +const pRotationAxis = new Point(R, R); +const WHITE = 'rgb(100% 100% 100%)'; +const BLACK = 'rgb(0% 0% 0%)'; +const ANIMATION_DURATION = 2e3; +const ANIMATION_DELTA_PX = R / ANIMATION_DURATION; +let primaryColour: string = BLACK; +let shadowColour: string = WHITE; +const memory: IMemo[] = []; + +export function startAnimation(ctx: CanvasRenderingContext2D) { + initContext(ctx); + raf = requestAnimationFrame(draw); + + return function stopAnimation() { + if (raf) { + cancelAnimationFrame(raf); + raf = 0; + } + }; +} + +export function update(timeOfCollection: number) { + const whenAdded = Date.now(); + const angle = (timeOfCollection % 1000) * 360 / 1000; + const vector = new Vector(R, 0) + .rotate(deg2rad(-angle)) + // rotate left to adjust zero angle to point at {0,-1} (north) + .rotateLeft(); + const timePoint = vector.atBase(pRotationAxis); + + memory.unshift({ + whenOccurred: timeOfCollection, + whenAdded, + age: 0, + timePoint, + vector: vector.rotateBack(), + }); +} + +function initContext(_ctx: CanvasRenderingContext2D) { + ctx = _ctx; + ctx.canvas.width = D; + ctx.canvas.height = D; + ctx.lineCap = 'round'; + ctx.lineWidth = LINE_WIDTH; + ctx.shadowBlur = SHADOW_WIDTH; + + onColourSchemeChange((scheme) => { + primaryColour = scheme === 'dark' ? WHITE : BLACK; + shadowColour = scheme === 'dark' ? BLACK : WHITE; + ctx.strokeStyle = primaryColour; + ctx.shadowColor = shadowColour; + }); +} + +function draw() { + const drawTime = Date.now(); + ctx.clearRect(0, 0, D, D); + drawCenter(); + + for (let n = 0, N = memory.length; n < N; n++) { + const memo = memory[n]; + memo.age = drawTime - memo.whenAdded; + drawLine(memo); + } + + let n = memory.length; + while (n--) { + if (memory[n].age >= ANIMATION_DURATION) { + memory.pop(); + } else { + break; + } + } + + raf = requestAnimationFrame(draw); +} + +function drawCenter() { + ctx.save(); + ctx.beginPath(); + ctx.arc(pRotationAxis.x, pRotationAxis.y, 0.5, 0, PI2); + ctx.stroke(); + ctx.closePath(); + ctx.restore(); +} + +function drawLine(memo: IMemo) { + const length = R - memo.age * ANIMATION_DELTA_PX; + const p = memo.vector.setLength(length).atBase(memo.timePoint); + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(p.x, p.y); + ctx.lineTo(memo.timePoint.x, memo.timePoint.y); + ctx.stroke(); + ctx.closePath(); + ctx.restore(); +} + +type TColourScheme = 'light' | 'dark'; + +export function onColourSchemeChange( + callback: (scheme: TColourScheme) => void, +) { + const devtoolsScheme = chrome.devtools.panels.themeName; + const osDarkScheme = globalThis.matchMedia('(prefers-color-scheme: dark)'); + + if (devtoolsScheme === 'dark' || osDarkScheme.matches) { + callback('dark'); + } else { + callback('light'); + } + + osDarkScheme.addEventListener('change', (e: MediaQueryListEvent) => { + callback(e.matches ? 'dark' : 'light'); + }); +} diff --git a/src/view/components/Version.svelte b/src/view/menu/Version.svelte similarity index 100% rename from src/view/components/Version.svelte rename to src/view/menu/Version.svelte diff --git a/src/view/panel/AnimationCancelHistory.svelte b/src/view/panel/AnimationCancelHistory.svelte new file mode 100644 index 0000000..e331a7b --- /dev/null +++ b/src/view/panel/AnimationCancelHistory.svelte @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + {#each sortedMetrics as metric (metric.traceId)} + + + + + + + + + {/each} + +
+ {caption} Callstack [] + + + + Called + + Handler +
+ + + + {metric.handler}
diff --git a/src/view/components/AnimationRequestHistory.svelte b/src/view/panel/AnimationRequestHistory.svelte similarity index 64% rename from src/view/components/AnimationRequestHistory.svelte rename to src/view/panel/AnimationRequestHistory.svelte index 60e9fd2..46a8dc5 100644 --- a/src/view/components/AnimationRequestHistory.svelte +++ b/src/view/panel/AnimationRequestHistory.svelte @@ -3,24 +3,19 @@ TCancelAnimationFrameHistory, TRequestAnimationFrameHistory, } from '../../wrapper/AnimationWrapper.ts'; - import { - DEFAULT_SORT_RAF, - ESortOrder, - getSettings, - setSettings, - } from '../../api/settings.ts'; + import { ESortOrder, saveLocalStorage } from '../../api/storage.local.ts'; import { compareByFieldOrder } from '../../api/comparator.ts'; - import Variable from './Variable.svelte'; - import Trace from './Trace.svelte'; - import TraceDomain from './TraceDomain.svelte'; - import SortableColumn from './SortableColumn.svelte'; - import FrameSensitiveTime from './FrameSensitiveTime.svelte'; - import TraceBreakpoint from './TraceBreakpoint.svelte'; - import Dialog from './Dialog.svelte'; - import Alert from './Alert.svelte'; + import Variable from '../components/Variable.svelte'; + import SortableColumn from './components/SortableColumn.svelte'; + import FrameSensitiveTime from './components/FrameSensitiveTime.svelte'; + import TraceBreakpoint from './components/TraceBreakpoint.svelte'; + import Dialog from '../components/Dialog.svelte'; + import Alert from '../components/Alert.svelte'; import AnimationCancelHistory from './AnimationCancelHistory.svelte'; - import TraceBypass from './TraceBypass.svelte'; - import CancelableCallMetric from './CancelableCallMetric.svelte'; + import TraceBypass from './components/TraceBypass.svelte'; + import CancelableCallMetric from './components/CancelableCallMetric.svelte'; + import CallstackCell from './components/CallstackCell.svelte'; + import { useConfigState } from '../../state/config.state.svelte.ts'; let { rafHistory, @@ -31,28 +26,25 @@ cafHistory: TCancelAnimationFrameHistory[] | null; caption: string; } = $props(); - let sortField = $state(DEFAULT_SORT_RAF.field); - let sortOrder = $state(DEFAULT_SORT_RAF.order); + const { sortRequestAnimationFrame } = useConfigState(); let dialogEl: Dialog | null = null; let alertEl: Alert | null = null; let sortedMetrics = $derived.by(() => - rafHistory.toSorted(compareByFieldOrder(sortField, sortOrder)) + rafHistory.toSorted( + compareByFieldOrder( + sortRequestAnimationFrame.field, + sortRequestAnimationFrame.order, + ), + ) ); - getSettings().then((settings) => { - sortField = settings.sortRequestAnimationFrame.field; - sortOrder = settings.sortRequestAnimationFrame.order; - }); - function onChangeSort(_field: string, _order: ESortOrder) { - sortField = _field; - sortOrder = _order; + sortRequestAnimationFrame.field = + _field; + sortRequestAnimationFrame.order = _order; - setSettings({ - sortRequestAnimationFrame: { - field: $state.snapshot(sortField), - order: $state.snapshot(sortOrder), - }, + saveLocalStorage({ + sortRequestAnimationFrame: $state.snapshot(sortRequestAnimationFrame), }); } @@ -100,17 +92,16 @@ - - + - + @@ -118,36 +109,40 @@ + + {#each sortedMetrics as metric (metric.traceId)} @@ -159,7 +154,7 @@ onClick={onFindRegressors} /> - +
- {caption} -
Callstack + {caption} Callstack [] + Self Called Handler Set
- - + {metric.cps || undefined}{metric.handler} {#if metric.online} diff --git a/src/view/components/EvalMetrics.svelte b/src/view/panel/Eval.svelte similarity index 52% rename from src/view/components/EvalMetrics.svelte rename to src/view/panel/Eval.svelte index 3d44c37..18803f4 100644 --- a/src/view/components/EvalMetrics.svelte +++ b/src/view/panel/Eval.svelte @@ -1,13 +1,35 @@ + + + + + + + + + + + + + + + {#each sortedMetrics as metric (metric.traceId)} + + + + + + + + + {/each} + +
+ {caption} Callstack [] + + + + Called + + Handler +
+ + + + {metric.handler}
diff --git a/src/view/components/IdleCallbackRequestHistory.svelte b/src/view/panel/IdleCallbackRequestHistory.svelte similarity index 56% rename from src/view/components/IdleCallbackRequestHistory.svelte rename to src/view/panel/IdleCallbackRequestHistory.svelte index 7289048..b8c65a6 100644 --- a/src/view/components/IdleCallbackRequestHistory.svelte +++ b/src/view/panel/IdleCallbackRequestHistory.svelte @@ -1,27 +1,25 @@ + + + + + + + + + + + + + + + + {#each sortedMetrics as metric (metric.traceId)} + + {/each} + +
+ {caption} Callstack [] + + + + Called + + Handler + + Delay +
diff --git a/src/view/components/TimersSetHistory.svelte b/src/view/panel/TimersSetHistory.svelte similarity index 50% rename from src/view/components/TimersSetHistory.svelte rename to src/view/panel/TimersSetHistory.svelte index 7093200..b8db023 100644 --- a/src/view/components/TimersSetHistory.svelte +++ b/src/view/panel/TimersSetHistory.svelte @@ -3,16 +3,12 @@ TClearTimerHistory, TSetTimerHistory, } from '../../wrapper/TimerWrapper.ts'; - import { - DEFAULT_SORT_SET_TIMERS, - ESortOrder, - getSettings, - setSettings, - } from '../../api/settings.ts'; + import { ESortOrder, saveLocalStorage } from '../../api/storage.local.ts'; import { compareByFieldOrder } from '../../api/comparator.ts'; - import Variable from './Variable.svelte'; - import SortableColumn from './SortableColumn.svelte'; - import TimersSetHistoryMetric from './TimersSetHistoryMetric.svelte'; + import Variable from '../components/Variable.svelte'; + import SortableColumn from './components/SortableColumn.svelte'; + import TimersSetHistoryMetric from './components/TimersSetHistoryMetric.svelte'; + import { useConfigState } from '../../state/config.state.svelte.ts'; let { setTimerHistory, @@ -25,81 +21,83 @@ clearIntervalHistory: TClearTimerHistory[] | null; caption?: string; } = $props(); - let sortField = $state(DEFAULT_SORT_SET_TIMERS.field); - let sortOrder = $state(DEFAULT_SORT_SET_TIMERS.order); - let sortedMetrics = $derived.by(() => - setTimerHistory.toSorted(compareByFieldOrder(sortField, sortOrder)) + let { sortSetTimers } = useConfigState(); + const sortedMetrics = $derived.by(() => + setTimerHistory.toSorted( + compareByFieldOrder(sortSetTimers.field, sortSetTimers.order), + ) ); - getSettings().then((settings) => { - sortField = settings.sortSetTimers.field; - sortOrder = settings.sortSetTimers.order; - }); + function onChangeSort(field: string, order: ESortOrder) { + sortSetTimers.field = field; + sortSetTimers.order = order; - function onChangeSort(_field: string, _order: ESortOrder) { - sortField = _field; - sortOrder = _order; - - setSettings({ - sortSetTimers: { - field: $state.snapshot(sortField), - order: $state.snapshot(sortOrder), - }, + saveLocalStorage({ + sortSetTimers: $state.snapshot(sortSetTimers), }); } - - + - + + + + {#each sortedMetrics as metric (metric.traceId)} - import type { TOnlineTimerMetrics } from '../../wrapper/TimerWrapper.ts'; - import { EMsg, portPost } from '../../api/communication.ts'; - import { msToHms } from '../../api/time.ts'; - import Variable from './Variable.svelte'; - import Trace from './Trace.svelte'; - import TraceDomain from './TraceDomain.svelte'; + import type { TOnlineTimerMetrics } from '../../../wrapper/TimerWrapper.ts'; + import { EMsg, portPost } from '../../../api/communication.ts'; + import { msToHms } from '../../../api/time.ts'; + import Variable from '../../components/Variable.svelte'; + import CallstackCell from './CallstackCell.svelte'; let { metrics, @@ -21,19 +20,25 @@
- {caption} -
Callstack + {caption} Callstack [] + Self + + Called Handler Delay Set
- - + - - - + + + + + {#each metrics as metric (metric.handler)} - + - + {/each} diff --git a/src/view/panel/components/AppPanels.svelte b/src/view/panel/components/AppPanels.svelte new file mode 100644 index 0000000..0f3b75a --- /dev/null +++ b/src/view/panel/components/AppPanels.svelte @@ -0,0 +1,78 @@ + + +{#if ts.telemetry} + + + + + {#if ts.telemetry.setTimeoutHistory?.length} + + {/if} + {#if ts.telemetry.clearTimeoutHistory?.length} + + {/if} + + {#if ts.telemetry.setIntervalHistory?.length} + + {/if} + {#if ts.telemetry.clearIntervalHistory?.length} + + {/if} + + {#if ts.telemetry.rafHistory?.length} + + {/if} + {#if ts.telemetry.cafHistory?.length} + + {/if} + + {#if ts.telemetry.ricHistory?.length} + + {/if} + {#if ts.telemetry.cicHistory?.length} + + {/if} +{/if} diff --git a/src/view/panel/components/CallstackCell.svelte b/src/view/panel/components/CallstackCell.svelte new file mode 100644 index 0000000..3c74216 --- /dev/null +++ b/src/view/panel/components/CallstackCell.svelte @@ -0,0 +1,30 @@ + + +{#if traceDomain === ETraceDomain.SAME} + +{:else if traceDomain === ETraceDomain.EXTERNAL} + +{:else if traceDomain === ETraceDomain.EXTENSION} + +{:else if traceDomain === ETraceDomain.WEBPACK} + +{:else if traceDomain === ETraceDomain.UNKNOWN} + +{/if} + +{#each trace as { link, name }, index (index)} + {@const isLast = index === trace.length - 1} + + {#if !isLast}• {/if} +{/each} diff --git a/src/view/components/CancelableCallMetric.svelte b/src/view/panel/components/CancelableCallMetric.svelte similarity index 91% rename from src/view/components/CancelableCallMetric.svelte rename to src/view/panel/components/CancelableCallMetric.svelte index 3350347..8ec7beb 100644 --- a/src/view/components/CancelableCallMetric.svelte +++ b/src/view/panel/components/CancelableCallMetric.svelte @@ -1,5 +1,5 @@ + + + {Fact.getTags(facts, factsMap)} + + + diff --git a/src/view/components/FrameSensitiveTime.svelte b/src/view/panel/components/FrameSensitiveTime.svelte similarity index 69% rename from src/view/components/FrameSensitiveTime.svelte rename to src/view/panel/components/FrameSensitiveTime.svelte index 1397ea5..ed9e220 100644 --- a/src/view/components/FrameSensitiveTime.svelte +++ b/src/view/panel/components/FrameSensitiveTime.svelte @@ -1,6 +1,6 @@ diff --git a/src/view/components/MediaMetrics.svelte b/src/view/panel/components/MediaMetrics.svelte similarity index 94% rename from src/view/components/MediaMetrics.svelte rename to src/view/panel/components/MediaMetrics.svelte index 437d7d3..8d72feb 100644 --- a/src/view/components/MediaMetrics.svelte +++ b/src/view/panel/components/MediaMetrics.svelte @@ -2,9 +2,9 @@ import { isToggableMediaProp, type TMediaMetrics, - } from '../../wrapper/MediaWrapper.ts'; - import { EMsg, portPost } from '../../api/communication.ts'; - import Variable from './Variable.svelte'; + } from '../../../wrapper/MediaWrapper.ts'; + import { EMsg, portPost } from '../../../api/communication.ts'; + import Variable from '../../components/Variable.svelte'; import MediaCommands from './MediaCommands.svelte'; let { metrics }: { metrics: TMediaMetrics } = $props(); diff --git a/src/view/components/SortableColumn.svelte b/src/view/panel/components/SortableColumn.svelte similarity index 89% rename from src/view/components/SortableColumn.svelte rename to src/view/panel/components/SortableColumn.svelte index 7dc724b..4053dbb 100644 --- a/src/view/components/SortableColumn.svelte +++ b/src/view/panel/components/SortableColumn.svelte @@ -1,6 +1,6 @@ {@render children?.()} {#if field === currentField} diff --git a/src/view/panel/components/TimersClearHistoryMetric.svelte b/src/view/panel/components/TimersClearHistoryMetric.svelte new file mode 100644 index 0000000..0a920db --- /dev/null +++ b/src/view/panel/components/TimersClearHistoryMetric.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/src/view/components/TimersSetHistoryMetric.svelte b/src/view/panel/components/TimersSetHistoryMetric.svelte similarity index 70% rename from src/view/components/TimersSetHistoryMetric.svelte rename to src/view/panel/components/TimersSetHistoryMetric.svelte index 4c1e8cd..633d819 100644 --- a/src/view/components/TimersSetHistoryMetric.svelte +++ b/src/view/panel/components/TimersSetHistoryMetric.svelte @@ -1,19 +1,21 @@
diff --git a/src/view/components/TraceBypass.svelte b/src/view/panel/components/TraceBypass.svelte similarity index 52% rename from src/view/components/TraceBypass.svelte rename to src/view/panel/components/TraceBypass.svelte index 780946c..9d55311 100644 --- a/src/view/components/TraceBypass.svelte +++ b/src/view/panel/components/TraceBypass.svelte @@ -1,36 +1,24 @@
diff --git a/src/view/components/TraceLink.svelte b/src/view/panel/components/TraceLink.svelte similarity index 90% rename from src/view/components/TraceLink.svelte rename to src/view/panel/components/TraceLink.svelte index 4cbd4ea..ebb81ad 100644 --- a/src/view/components/TraceLink.svelte +++ b/src/view/panel/components/TraceLink.svelte @@ -4,7 +4,7 @@ REGEX_STACKTRACE_COLUMN_NUMBER, REGEX_STACKTRACE_LINE_NUMBER, TAG_INVALID_CALLSTACK_LINK, - } from '../../wrapper/TraceUtil.ts'; + } from '../../../wrapper/TraceUtil.ts'; let { name, @@ -82,17 +82,17 @@ } } - @media only screen and (max-width: 45rem) { + @media only screen and (width <= 45rem) { a { max-width: 15rem; } } - @media only screen and (max-width: 35rem) { + @media only screen and (width <= 35rem) { a { max-width: 8rem; } } - @media only screen and (max-width: 27rem) { + @media only screen and (width <= 27rem) { a { max-width: 4rem; } diff --git a/src/wrapper/AnimationWrapper.ts b/src/wrapper/AnimationWrapper.ts index 62e0814..7d27a5f 100644 --- a/src/wrapper/AnimationWrapper.ts +++ b/src/wrapper/AnimationWrapper.ts @@ -1,5 +1,9 @@ -import type { TSettingsPanel } from '../api/settings.ts'; -import { cancelAnimationFrame, requestAnimationFrame } from '../api/const.ts'; +import type { TPanel } from '../api/storage.local.ts'; +import { + cancelAnimationFrame, + requestAnimationFrame, + TAG_BAD_HANDLER, +} from '../api/const.ts'; import { ETraceDomain, type TCallstack, @@ -8,7 +12,7 @@ import { } from './TraceUtil.ts'; import { trim2microsecond } from '../api/time.ts'; import { validHandler } from './util.ts'; -import { TAG_EXCEPTION } from '../api/clone.ts'; +import { Fact, type TFact } from './Fact.ts'; export type TRequestAnimationFrameHistory = { traceId: string; @@ -26,10 +30,16 @@ export type TCancelAnimationFrameHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; handler: number | undefined | string; }; +export const CafFact = /*@__PURE__*/ { + NOT_FOUND: Fact.define(1 << 0), + BAD_HANDLER: Fact.define(1 << 1), +} as const; + export class AnimationWrapper { traceUtil: TraceUtil; native = { @@ -92,38 +102,50 @@ export class AnimationWrapper { #updateCafHistory(handler: number | string, callstack: TCallstack) { const existing = this.cafHistory.get(callstack.traceId); - const hasError = !validHandler(handler); + let facts = 0; + let rafTraceId; + + if (validHandler(handler)) { + rafTraceId = this.onlineAnimationFrameLookup.get(handler); - if (hasError) { - handler = TAG_EXCEPTION(handler); + if (rafTraceId) { + this.onlineAnimationFrameLookup.delete(handler); + } else { + facts = Fact.assign(facts, CafFact.NOT_FOUND); + } + } else { + handler = TAG_BAD_HANDLER(handler); + facts = Fact.assign(facts, CafFact.BAD_HANDLER); } if (existing) { existing.calls++; existing.handler = handler; + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { this.cafHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + facts, calls: 1, handler, }); } - const rafTraceId = this.onlineAnimationFrameLookup.get(Number(handler)); const rafRecord = rafTraceId && this.rafHistory.get(rafTraceId); if (rafRecord) { - this.onlineAnimationFrameLookup.delete(Number(handler)); - rafRecord.online--; + rafRecord.canceledCounter++; if (rafRecord.canceledByTraceIds === null) { rafRecord.canceledByTraceIds = [callstack.traceId]; } else if (!rafRecord.canceledByTraceIds.includes(callstack.traceId)) { rafRecord.canceledByTraceIds.push(callstack.traceId); } - rafRecord.canceledCounter++; } } @@ -193,7 +215,7 @@ export class AnimationWrapper { globalThis.cancelAnimationFrame = this.native.cancelAnimationFrame; } - collectHistory(rafPanel: TSettingsPanel, cafPanel: TSettingsPanel) { + collectHistory(rafPanel: TPanel, cafPanel: TPanel) { return { rafHistory: rafPanel.wrap && rafPanel.visible ? Array.from(this.rafHistory.values()) diff --git a/src/wrapper/EvalWrapper.ts b/src/wrapper/EvalWrapper.ts index 3d99b43..745e879 100644 --- a/src/wrapper/EvalWrapper.ts +++ b/src/wrapper/EvalWrapper.ts @@ -6,21 +6,26 @@ import { type TTrace, } from './TraceUtil.ts'; import { trim2microsecond } from '../api/time.ts'; -import type { TSettingsPanel } from '../api/settings.ts'; +import type { TPanel } from '../api/storage.local.ts'; +import { Fact, type TFact } from './Fact.ts'; export type TEvalHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; returnedValue: unknown; code: unknown; - usesLocalScope: boolean; selfTime: number | null; }; // https://rollupjs.org/troubleshooting/#avoiding-eval const lesserEval = /*@__PURE__*/ globalThis.eval.bind(globalThis); +export const EvalFact = /*@__PURE__*/ { + USES_GLOBAL_SCOPE: Fact.define(1 << 0), + USES_LOCAL_SCOPE: Fact.define(1 << 1), +}; export class EvalWrapper { traceUtil: TraceUtil; @@ -40,19 +45,27 @@ export class EvalWrapper { selfTime: number | null, ) { const existing = this.evalHistory.get(callstack.traceId); + let facts = EvalFact.USES_GLOBAL_SCOPE; + + if (usesLocalScope) { + facts = Fact.assign(facts, EvalFact.USES_LOCAL_SCOPE); + } if (existing) { existing.code = cloneObjectSafely(code); existing.returnedValue = cloneObjectSafely(returnedValue); existing.calls++; - existing.usesLocalScope = usesLocalScope; existing.selfTime = trim2microsecond(selfTime); + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { this.evalHistory.set(callstack.traceId, { calls: 1, code: cloneObjectSafely(code), returnedValue: cloneObjectSafely(returnedValue), - usesLocalScope, + facts, traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), @@ -109,7 +122,7 @@ export class EvalWrapper { // noop - it's impossible to restore native eval afterwards } - collectHistory(evalPanel: TSettingsPanel) { + collectHistory(evalPanel: TPanel) { return evalPanel.wrap && evalPanel.visible ? Array.from(this.evalHistory.values()) : null; diff --git a/src/wrapper/Fact.ts b/src/wrapper/Fact.ts new file mode 100644 index 0000000..182d06d --- /dev/null +++ b/src/wrapper/Fact.ts @@ -0,0 +1,51 @@ +import type { Brand } from '../api/generics.ts'; + +export type TFact = Brand; +interface IFactDescriptor { + tag: string; + details: string; +} +export type TFactsMap = Map; + +export class Fact { + /** + * @param(n): 53 bits number in range [0 ... Number.MAX_SAFE_INTEGER] + */ + static define(n: number): TFact { + if (Number.isInteger(n) && 0 < n && n <= Number.MAX_SAFE_INTEGER) { + return n as TFact; + } else { + throw new Error('Fact must be in range [0 .. Number.MAX_SAFE_INTEGER]'); + } + } + + static assign(data: number, fact: TFact): TFact { + return (data | fact) as TFact; + } + + static check(data: number, fact: TFact): boolean { + return !!(data & fact); + } + + static getDetails(factsMap: TFactsMap): string { + const rv: string[] = []; + + for (const [_fact, descriptor] of factsMap) { + rv.push(`${descriptor.tag}: ${descriptor.details}`); + } + + return rv.join('\n'); + } + + static getTags(data: number, factsMap: TFactsMap): string { + let rv = ''; + + for (const [fact, descriptor] of factsMap) { + if (Fact.check(data, fact)) { + rv += descriptor.tag; + } + } + + return rv; + } +} diff --git a/src/wrapper/IdleWrapper.ts b/src/wrapper/IdleWrapper.ts index 34f3761..35033e5 100644 --- a/src/wrapper/IdleWrapper.ts +++ b/src/wrapper/IdleWrapper.ts @@ -1,5 +1,4 @@ -import type { TSettingsPanel } from '../api/settings.ts'; -import { TAG_EXCEPTION } from '../api/clone.ts'; +import type { TPanel } from '../api/storage.local.ts'; import { trim2microsecond } from '../api/time.ts'; import { type ETraceDomain, @@ -8,11 +7,14 @@ import { type TTrace, } from './TraceUtil.ts'; import { validHandler, validTimerDelay } from './util.ts'; +import { Fact, type TFact } from './Fact.ts'; +import { TAG_BAD_DELAY, TAG_BAD_HANDLER } from '../api/const.ts'; export type TRequestIdleCallbackHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; handler: number | undefined | string; delay: number | undefined | string; @@ -26,6 +28,7 @@ export type TCancelIdleCallbackHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; handler: number | undefined | string; }; @@ -36,6 +39,13 @@ const requestIdleCallback = /*@__PURE__*/ globalThis.requestIdleCallback.bind( const cancelIdleCallback = /*@__PURE__*/ globalThis.cancelIdleCallback.bind( globalThis, ); +export const RicFact = { + BAD_DELAY: Fact.define(1 << 0), +} as const; +export const CicFact = { + NOT_FOUND: Fact.define(1 << 0), + BAD_HANDLER: Fact.define(1 << 1), +} as const; export class IdleWrapper { traceUtil: TraceUtil; @@ -82,8 +92,14 @@ export class IdleWrapper { callstack: TCallstack, ) { const existing = this.ricHistory.get(callstack.traceId); - const hasError = !validTimerDelay(delay); - delay = hasError ? TAG_EXCEPTION(delay) : trim2microsecond(delay); + let facts = 0; + + if (validTimerDelay(delay)) { + delay = trim2microsecond(delay); + } else { + delay = TAG_BAD_DELAY(delay); + facts = Fact.assign(facts, RicFact.BAD_DELAY); + } if (existing) { existing.calls++; @@ -91,11 +107,16 @@ export class IdleWrapper { existing.didTimeout = undefined; existing.delay = delay; existing.online++; + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { this.ricHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + facts, calls: 1, handler, didTimeout: undefined, @@ -112,39 +133,51 @@ export class IdleWrapper { #updateCicHistory(handler: number | string, callstack: TCallstack) { const existing = this.cicHistory.get(callstack.traceId); - const hasError = !validHandler(handler); + let facts = 0; + let ricTraceId; + + if (validHandler(handler)) { + ricTraceId = this.onlineIdleCallbackLookup.get(handler); - if (hasError) { - handler = TAG_EXCEPTION(handler); + if (ricTraceId) { + this.onlineIdleCallbackLookup.delete(handler); + } else { + facts = Fact.assign(facts, CicFact.NOT_FOUND); + } + } else { + handler = TAG_BAD_HANDLER(handler); + facts = Fact.assign(facts, CicFact.BAD_HANDLER); } if (existing) { existing.calls++; existing.handler = handler; + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { this.cicHistory.set(callstack.traceId, { traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + facts, calls: 1, handler, }); } - const ricTraceId = this.onlineIdleCallbackLookup.get(Number(handler)); const ricRecord = ricTraceId && this.ricHistory.get(ricTraceId); if (ricRecord) { - this.onlineIdleCallbackLookup.delete(Number(handler)); - ricRecord.online--; ricRecord.didTimeout = undefined; + ricRecord.canceledCounter++; if (ricRecord.canceledByTraceIds === null) { ricRecord.canceledByTraceIds = [callstack.traceId]; } else if (!ricRecord.canceledByTraceIds.includes(callstack.traceId)) { ricRecord.canceledByTraceIds.push(callstack.traceId); } - ricRecord.canceledCounter++; } } @@ -207,7 +240,7 @@ export class IdleWrapper { globalThis.cancelIdleCallback = this.native.cancelIdleCallback; } - collectHistory(ricPanel: TSettingsPanel, cicPanel: TSettingsPanel) { + collectHistory(ricPanel: TPanel, cicPanel: TPanel) { return { ricHistory: ricPanel.wrap && ricPanel.visible ? Array.from(this.ricHistory.values()) diff --git a/src/wrapper/TimerWrapper.ts b/src/wrapper/TimerWrapper.ts index d5291f7..436101c 100644 --- a/src/wrapper/TimerWrapper.ts +++ b/src/wrapper/TimerWrapper.ts @@ -9,15 +9,17 @@ import { clearTimeout, setInterval, setTimeout, + TAG_BAD_DELAY, + TAG_BAD_HANDLER, + TAG_DELAY_NOT_FOUND, TAG_EVAL_RETURN_SET_INTERVAL, TAG_EVAL_RETURN_SET_TIMEOUT, - TAG_MISFORTUNE, } from '../api/const.ts'; -import type { TSettingsPanel } from '../api/settings.ts'; +import type { TPanel } from '../api/storage.local.ts'; import type { EvalWrapper } from './EvalWrapper.ts'; -import { TAG_EXCEPTION } from '../api/clone.ts'; import { validHandler, validTimerDelay } from './util.ts'; import { trim2microsecond } from '../api/time.ts'; +import { Fact, type TFact } from './Fact.ts'; export enum ETimerType { TIMEOUT, @@ -30,16 +32,15 @@ export type TOnlineTimerMetrics = { type: ETimerType; delay: number | undefined | string; handler: number; - isEval: boolean; }; export type TSetTimerHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; handler: number | string; delay: number | undefined | string; - isEval: boolean | undefined; online: number; canceledCounter: number; canceledByTraceIds: string[] | null; @@ -49,11 +50,21 @@ export type TClearTimerHistory = { traceId: string; trace: TTrace[]; traceDomain: ETraceDomain; + facts: TFact; calls: number; handler: number | string; delay: number | undefined | string; }; +export const SetTimerFact = /*@__PURE__*/ { + NOT_A_FUNCTION: Fact.define(1 << 0), + BAD_DELAY: Fact.define(1 << 1), +} as const; +export const ClearTimerFact = /*@__PURE__*/ { + NOT_FOUND: Fact.define(1 << 0), + BAD_HANDLER: Fact.define(1 << 1), +} as const; + export class TimerWrapper { traceUtil: TraceUtil; apiEval: EvalWrapper; @@ -85,17 +96,15 @@ export class TimerWrapper { handler: number, delay: number | undefined | string, callstack: TCallstack, - isEval: boolean, ) { delay = validTimerDelay(delay) ? trim2microsecond(delay) - : TAG_EXCEPTION(delay); + : TAG_BAD_DELAY(delay); this.onlineTimers.set(handler, { type, handler, delay, - isEval, traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), @@ -145,26 +154,39 @@ export class TimerWrapper { callstack: TCallstack, isEval: boolean, ) { + let facts = 0; const existing = history.get(callstack.traceId); - const hasError = !validTimerDelay(delay); - delay = hasError ? TAG_EXCEPTION(delay) : trim2microsecond(delay); + + if (validTimerDelay(delay)) { + delay = trim2microsecond(delay); + } else { + delay = TAG_BAD_DELAY(delay); + facts = Fact.assign(facts, SetTimerFact.BAD_DELAY); + } + + if (isEval) { + facts = Fact.assign(facts, SetTimerFact.NOT_A_FUNCTION); + } if (existing) { existing.handler = handler; existing.delay = delay; existing.calls++; - existing.isEval = isEval; existing.online++; + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { history.set(callstack.traceId, { handler, calls: 1, delay, - isEval, online: 1, traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + facts, canceledCounter: 0, canceledByTraceIds: null, selfTime: null, @@ -178,22 +200,30 @@ export class TimerWrapper { callstack: TCallstack, ) { const existing = history.get(callstack.traceId); - const hasError = !validHandler(handler); - const onlineTimer = hasError - ? null - : this.onlineTimers.get( handler); - const handlerDelay: string | number | undefined = onlineTimer - ? onlineTimer.delay - : TAG_MISFORTUNE; - - if (hasError) { - handler = TAG_EXCEPTION(handler); + let handlerDelay: string | number | undefined = TAG_DELAY_NOT_FOUND; + let facts = 0; + + if (validHandler(handler)) { + const onlineTimer = this.onlineTimers.get(handler); + + if (onlineTimer) { + handlerDelay = onlineTimer.delay; + } else { + facts = Fact.assign(facts, ClearTimerFact.NOT_FOUND); + } + } else { + handler = TAG_BAD_HANDLER(handler); + facts = Fact.assign(facts, ClearTimerFact.BAD_HANDLER); } if (existing) { existing.handler = handler; existing.delay = handlerDelay; existing.calls++; + + if (facts) { + existing.facts = Fact.assign(existing.facts, facts); + } } else { history.set(callstack.traceId, { handler: handler, @@ -202,6 +232,7 @@ export class TimerWrapper { traceId: callstack.traceId, trace: callstack.trace, traceDomain: this.traceUtil.getTraceDomain(callstack.trace[0]), + facts, }); } } @@ -227,7 +258,7 @@ export class TimerWrapper { ) { const err = new Error(TraceUtil.SIGNATURE); const callstack = this.traceUtil.getCallstack(err, code); - const isEval = typeof code === 'string'; + const isEval = typeof code !== 'function'; this.callCounter.setTimeout++; const handler = this.native.setTimeout( @@ -266,7 +297,7 @@ export class TimerWrapper { ...args, ); - this.#timerOnline(ETimerType.TIMEOUT, handler, delay, callstack, isEval); + this.#timerOnline(ETimerType.TIMEOUT, handler, delay, callstack); this.#updateSetTimersHistory( this.setTimeoutHistory, handler, @@ -326,7 +357,7 @@ export class TimerWrapper { ) { const err = new Error(TraceUtil.SIGNATURE); const callstack = this.traceUtil.getCallstack(err, code); - const isEval = typeof code === 'string'; + const isEval = typeof code !== 'function'; this.callCounter.setInterval++; @@ -365,7 +396,7 @@ export class TimerWrapper { ...args, ); - this.#timerOnline(ETimerType.INTERVAL, handler, delay, callstack, isEval); + this.#timerOnline(ETimerType.INTERVAL, handler, delay, callstack); this.#updateSetTimersHistory( this.setIntervalHistory, handler, @@ -432,11 +463,11 @@ export class TimerWrapper { } collectHistory( - activeTimers: TSettingsPanel, - setTimeout: TSettingsPanel, - clearTimeout: TSettingsPanel, - setInterval: TSettingsPanel, - clearInterval: TSettingsPanel, + activeTimers: TPanel, + setTimeout: TPanel, + clearTimeout: TPanel, + setInterval: TPanel, + clearInterval: TPanel, ) { return { onlineTimers: activeTimers.visible diff --git a/src/wrapper/TraceUtil.ts b/src/wrapper/TraceUtil.ts index 634e557..d61fe74 100644 --- a/src/wrapper/TraceUtil.ts +++ b/src/wrapper/TraceUtil.ts @@ -1,5 +1,5 @@ import { hashString } from '../api/hash.ts'; -import { EWrapperCallstackType } from '../api/settings.ts'; +import { EWrapperCallstackType } from '../api/storage.local.ts'; export type TTrace = { name: string | 0; @@ -13,6 +13,7 @@ export enum ETraceDomain { SAME, EXTERNAL, EXTENSION, + WEBPACK, UNKNOWN, } @@ -37,8 +38,8 @@ const REGEX_STACKTRACE_LINK_PROTOCOL = /*@__PURE__*/ new RegExp( export class TraceUtil { selfTraceLink = ''; callstackType: EWrapperCallstackType = EWrapperCallstackType.FULL; - trace4Debug: string | null = null; - trace4Bypass: string | null = null; + debug: Set = new Set(); + bypass: Set = new Set(); #fullCallstackCacheTrace: Map = new Map(); static readonly SIGNATURE = 'browser-api-monitor'; @@ -61,17 +62,19 @@ export class TraceUtil { return ETraceDomain.EXTERNAL; } else if (trace.link.startsWith('chrome-extension://')) { return ETraceDomain.EXTENSION; + } else if (trace.link.startsWith('webpack://')) { + return ETraceDomain.WEBPACK; } return ETraceDomain.UNKNOWN; } shouldPass(traceId: string) { - return this.trace4Bypass !== traceId; + return !this.bypass.has(traceId); } shouldPause(traceId: string) { - return this.trace4Debug === traceId; + return this.debug.has(traceId); } #getSelfTraceLink() { diff --git a/src/wrapper/Wrapper.ts b/src/wrapper/Wrapper.ts index 0891cae..ef85e05 100644 --- a/src/wrapper/Wrapper.ts +++ b/src/wrapper/Wrapper.ts @@ -4,9 +4,9 @@ import { EvalWrapper, type TEvalHistory } from './EvalWrapper.ts'; import { EWrapperCallstackType, panelsArray2Map, + type TConfig, type TPanelMap, - type TSettings, -} from '../api/settings.ts'; +} from '../api/storage.local.ts'; import { type TClearTimerHistory, TimerWrapper, @@ -24,6 +24,7 @@ import { type TRequestIdleCallbackHistory, } from './IdleWrapper.ts'; import { MediaWrapper, type TMediaTelemetry } from './MediaWrapper.ts'; +import type { TSession } from '../api/storage.session.ts'; export type TTelemetry = { media: TMediaTelemetry; @@ -75,14 +76,17 @@ const wrapApis = callingOnce(() => { panels.cancelIdleCallback.wrap && apiIdle.wrapCancelIdleCallback(); }); -export function setSettings(settings: TSettings) { - panels = panelsArray2Map(settings.panels); - traceUtil.trace4Debug = settings.trace4Debug; - traceUtil.trace4Bypass = settings.trace4Bypass; - setCallstackType(settings.wrapperCallstackType); +export function applyConfig(config: TConfig) { + panels = panelsArray2Map(config.panels); + setCallstackType(config.wrapperCallstackType); wrapApis(); } +export function applySession(session: TSession) { + traceUtil.debug = new Set(session.debug); + traceUtil.bypass = new Set(session.bypass); +} + export function onEachSecond() { apiMedia.meetMedia(); if ( @@ -113,12 +117,24 @@ export function collectMetrics(): TTelemetry { panels.cancelIdleCallback, ), activeTimers: apiTimer.onlineTimers.size, - callCounter: { - eval: apiEval.callCounter, - ...apiTimer.callCounter, - ...apiAnimation.callCounter, - ...apiIdle.callCounter, - }, + callCounter: panels.callsSummary.visible + ? { + eval: apiEval.callCounter, + ...apiTimer.callCounter, + ...apiAnimation.callCounter, + ...apiIdle.callCounter, + } + : { + eval: 0, + setTimeout: 0, + clearTimeout: 0, + setInterval: 0, + clearInterval: 0, + requestAnimationFrame: 0, + cancelAnimationFrame: 0, + requestIdleCallback: 0, + cancelIdleCallback: 0, + }, }; } diff --git a/src/wrapper/util.ts b/src/wrapper/util.ts index ffca4eb..cc840d3 100644 --- a/src/wrapper/util.ts +++ b/src/wrapper/util.ts @@ -1,5 +1,5 @@ export function validHandler(handler: unknown): handler is number { - return Number.isFinite(handler) && handler > 0; + return Number.isInteger(handler) && handler > 0; } export function validTimerDelay(delay: unknown): delay is number { diff --git a/tests/AnimationWrapper_test.ts b/tests/AnimationWrapper_test.ts index e1c4281..5271044 100644 --- a/tests/AnimationWrapper_test.ts +++ b/tests/AnimationWrapper_test.ts @@ -2,9 +2,10 @@ import './browserPolyfill.ts'; import { wait } from './util.ts'; import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; -import { AnimationWrapper } from '../src/wrapper/AnimationWrapper.ts'; +import { AnimationWrapper, CafFact } from '../src/wrapper/AnimationWrapper.ts'; import { TraceUtil } from '../src/wrapper/TraceUtil.ts'; -import { TAG_EXCEPTION } from '../src/api/clone.ts'; +import { TAG_BAD_HANDLER } from '../src/api/const.ts'; +import { Fact } from '../src/wrapper/Fact.ts'; describe('AnimationWrapper', () => { const traceUtil = new TraceUtil(); @@ -70,8 +71,17 @@ describe('AnimationWrapper', () => { const rec = Array.from(apiAnimation.cafHistory?.values())[0]; - expect(rec.handler).toBe(TAG_EXCEPTION(0)); + expect(rec.handler).toBe(TAG_BAD_HANDLER(0)); + expect(Fact.check(rec.facts, CafFact.BAD_HANDLER)).toBe(true); + }); + + test('cafHistory - raf not found', () => { + cancelAnimationFrame(404); + + const rec = Array.from(apiAnimation.cafHistory?.values())[0]; + + expect(Fact.check(rec.facts, CafFact.NOT_FOUND)).toBe(true); }); }); -await wait(1e3); +await wait(1e1); diff --git a/tests/EvalWrapper_test.ts b/tests/EvalWrapper_test.ts index 584b365..955e3f6 100644 --- a/tests/EvalWrapper_test.ts +++ b/tests/EvalWrapper_test.ts @@ -2,14 +2,15 @@ import './browserPolyfill.ts'; import { wait } from './util.ts'; import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; -import { EvalWrapper } from '../src/wrapper/EvalWrapper.ts'; -import { TimerWrapper } from '../src/wrapper/TimerWrapper.ts'; +import { EvalFact, EvalWrapper } from '../src/wrapper/EvalWrapper.ts'; +import { SetTimerFact, TimerWrapper } from '../src/wrapper/TimerWrapper.ts'; import { TraceUtil } from '../src/wrapper/TraceUtil.ts'; import { TAG_UNDEFINED } from '../src/api/clone.ts'; import { TAG_EVAL_RETURN_SET_INTERVAL, TAG_EVAL_RETURN_SET_TIMEOUT, } from '../src/api/const.ts'; +import { Fact } from '../src/wrapper/Fact.ts'; describe('EvalWrapper', () => { const traceUtil = new TraceUtil(); @@ -43,7 +44,8 @@ describe('EvalWrapper', () => { const rec = Array.from(apiEval.evalHistory.values())[0]; expect(rec.calls).toBe(NUMBER_OF_INVOCATIONS); - expect(rec.usesLocalScope).toBe(false); + expect(Fact.check(rec.facts, EvalFact.USES_GLOBAL_SCOPE)).toBe(true); + expect(Fact.check(rec.facts, EvalFact.USES_LOCAL_SCOPE)).toBe(false); expect(rec.code).toBe(CODE); expect(rec.returnedValue).toBe(RESULT); expect(rec.trace.length).toBeGreaterThan(1); @@ -59,7 +61,8 @@ describe('EvalWrapper', () => { expect(rec.calls).toBe(1); expect(local_variable).toBe(0); - expect(rec.usesLocalScope).toBe(true); + expect(Fact.check(rec.facts, EvalFact.USES_GLOBAL_SCOPE)).toBe(true); + expect(Fact.check(rec.facts, EvalFact.USES_LOCAL_SCOPE)).toBe(true); expect(rec.returnedValue).toBe(TAG_UNDEFINED); }); @@ -69,7 +72,7 @@ describe('EvalWrapper', () => { const timerRec = Array.from(apiTimer.setTimeoutHistory.values())[0]; const evalRec = Array.from(apiEval.evalHistory.values())[0]; - expect(timerRec.isEval).toBe(true); + expect(Fact.check(timerRec.facts, SetTimerFact.NOT_A_FUNCTION)).toBe(true); expect(evalRec.code).toBe(CODE); expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_TIMEOUT); }); @@ -80,7 +83,7 @@ describe('EvalWrapper', () => { const timerRec = Array.from(apiTimer.setIntervalHistory.values())[0]; const evalRec = Array.from(apiEval.evalHistory.values())[0]; - expect(timerRec.isEval).toBe(true); + expect(Fact.check(timerRec.facts, SetTimerFact.NOT_A_FUNCTION)).toBe(true); expect(evalRec.code).toBe(CODE); expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_INTERVAL); @@ -88,4 +91,4 @@ describe('EvalWrapper', () => { }); }); -await wait(1e3); +await wait(1e1); diff --git a/tests/Facts_test.ts b/tests/Facts_test.ts new file mode 100644 index 0000000..e04b828 --- /dev/null +++ b/tests/Facts_test.ts @@ -0,0 +1,46 @@ +import { describe, test } from '@std/testing/bdd'; +import { expect } from '@std/expect'; +import { Fact } from '../src/wrapper/Fact.ts'; + +const fact_1 = Fact.define(0b0001); +const fact_2 = Fact.define(0b0010); + +describe('Facts', () => { + test('define', () => { + expect(() => { + Fact.define(-1); + }).toThrow(/Number.MAX_SAFE_INTEGER/); + + expect(() => { + Fact.define(Number.MAX_SAFE_INTEGER + 1); + }).toThrow(/Number.MAX_SAFE_INTEGER/); + }); + + test('assign/check', () => { + let data = 0; + + data = Fact.assign(data, fact_1); + expect(Fact.check(data, fact_1)).toBeTruthy(); + expect(data).toBe(0b0001); + + data = Fact.assign(data, fact_2); + expect(Fact.check(data, fact_2)).toBeTruthy(); + expect(data).toBe(0b0011); + }); + + test('getDetails', () => { + let data = 0; + const factsMap = new Map([ + [fact_1, { tag: '1', details: 'fact_1' }], + [fact_2, { tag: '2', details: 'fact_2' }], + ]); + + for (const [fact, _detail] of factsMap) { + data = Fact.assign(data, fact); + } + + const details = Fact.getDetails(factsMap); + + expect(details).toBe(`1: fact_1\n2: fact_2`); + }); +}); diff --git a/tests/IdleWrapper_test.ts b/tests/IdleWrapper_test.ts new file mode 100644 index 0000000..2388a21 --- /dev/null +++ b/tests/IdleWrapper_test.ts @@ -0,0 +1,99 @@ +import './browserPolyfill.ts'; +import { wait } from './util.ts'; +import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; +import { expect } from '@std/expect'; +import { CicFact, IdleWrapper, RicFact } from '../src/wrapper/IdleWrapper.ts'; +import { TraceUtil } from '../src/wrapper/TraceUtil.ts'; +import { TAG_BAD_DELAY, TAG_BAD_HANDLER } from '../src/api/const.ts'; +import { Fact } from '../src/wrapper/Fact.ts'; + +describe('IdleWrapper', () => { + const traceUtil = new TraceUtil(); + let apiIdle: IdleWrapper; + + beforeEach(() => { + apiIdle = new IdleWrapper(traceUtil); + apiIdle.wrapRequestIdleCallback(); + apiIdle.wrapCancelIdleCallback(); + }); + + afterEach(() => { + apiIdle.unwrapRequestIdleCallback(); + apiIdle.unwrapCancelIdleCallback(); + }); + + test('ricHistory - recorded', async () => { + let typeOfArgument = ''; + const handler = await new Promise((resolve) => { + const handler = requestIdleCallback((o) => { + typeOfArgument = typeof o.didTimeout; + resolve(handler); + }); + }); + const rec = Array.from(apiIdle.ricHistory.values())[0]; + + expect(typeOfArgument).toBe('boolean'); + expect(apiIdle.ricHistory.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(apiIdle.callCounter.requestIdleCallback).toBe(1); + }); + + test('cicHistory - recorded', () => { + const unchanged = 0, + changed = 1; + let changeable = unchanged; + const handler = requestIdleCallback(() => { + changeable = changed; + }); + cancelIdleCallback(handler); + + const ricRec = Array.from(apiIdle.ricHistory.values())[0]; + const cicRec = Array.from(apiIdle.cicHistory.values())[0]; + + expect(changeable).toBe(unchanged); + expect(apiIdle.ricHistory.size).toBe(1); + expect(apiIdle.cicHistory.size).toBe(1); + expect(cicRec.handler).toBe(handler); + expect(cicRec.calls).toBe(1); + expect(cicRec.trace.length).toBeGreaterThan(1); + expect(cicRec.traceId.length).toBeGreaterThan(1); + expect(apiIdle.callCounter.cancelIdleCallback).toBe(1); + expect(ricRec.canceledByTraceIds?.length).toBe(1); + expect(ricRec.canceledCounter).toBe(1); + }); + + test('ricHistory - invalid delay', () => { + const BAD_DELAY = -1; + const handler = requestIdleCallback(() => {}, { timeout: BAD_DELAY }); + cancelIdleCallback(handler); + + const rec = Array.from(apiIdle.ricHistory?.values())[0]; + + expect(rec.delay).toBe(TAG_BAD_DELAY(BAD_DELAY)); + expect(Fact.check(rec.facts, RicFact.BAD_DELAY)).toBe(true); + }); + + test('cicHistory - invalid handler', () => { + const BAD_HANDLER = 0; + cancelIdleCallback(BAD_HANDLER); + + const rec = Array.from(apiIdle.cicHistory?.values())[0]; + + expect(rec.handler).toBe(TAG_BAD_HANDLER(BAD_HANDLER)); + expect(Fact.check(rec.facts, CicFact.BAD_HANDLER)).toBe(true); + }); + + test('cicHistory - ric not found', () => { + cancelIdleCallback(404); + + const rec = Array.from(apiIdle.cicHistory?.values())[0]; + + expect(Fact.check(rec.facts, CicFact.NOT_FOUND)).toBe(true); + }); +}); + +await wait(1e1); diff --git a/tests/TimerWrapper_test.ts b/tests/TimerWrapper_test.ts index 5d1fc85..cdb746b 100644 --- a/tests/TimerWrapper_test.ts +++ b/tests/TimerWrapper_test.ts @@ -2,11 +2,19 @@ import './browserPolyfill.ts'; import { wait } from './util.ts'; import { afterEach, beforeEach, describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; -import { TAG_EXCEPTION } from '../src/api/clone.ts'; -import { TAG_MISFORTUNE } from '../src/api/const.ts'; +import { + TAG_BAD_DELAY, + TAG_BAD_HANDLER, + TAG_DELAY_NOT_FOUND, +} from '../src/api/const.ts'; import { TraceUtil } from '../src/wrapper/TraceUtil.ts'; -import { TimerWrapper } from '../src/wrapper/TimerWrapper.ts'; +import { + ClearTimerFact, + SetTimerFact, + TimerWrapper, +} from '../src/wrapper/TimerWrapper.ts'; import { EvalWrapper } from '../src/wrapper/EvalWrapper.ts'; +import { Fact } from '../src/wrapper/Fact.ts'; describe('wrappers', () => { const traceUtil = new TraceUtil(); @@ -114,22 +122,24 @@ describe('wrappers', () => { expect(rec.calls).toBe(1); expect(rec.delay).toBe(DELAY); - expect(rec.isEval).toBe(false); expect(rec.trace.length).toBeGreaterThan(1); expect(rec.traceId.length).toBeGreaterThan(0); + expect(Fact.check(rec.facts, SetTimerFact.BAD_DELAY)).toBe(false); + expect(Fact.check(rec.facts, SetTimerFact.NOT_A_FUNCTION)).toBe(false); globalThis.clearTimeout(handler); expect(rec.selfTime).toBeNull(); }); test('setTimeoutHistory - invalid delay', () => { - globalThis.setTimeout(() => {}, -1); + const BAD_DELAY = -1; + globalThis.setTimeout(() => {}, BAD_DELAY); const rec = Array.from(apiTimer.setTimeoutHistory.values())[0]; expect(rec.calls).toBe(1); - expect(rec.delay).toBe(TAG_EXCEPTION('-1')); - expect(rec.isEval).toBe(false); + expect(rec.delay).toBe(TAG_BAD_DELAY(BAD_DELAY)); + expect(Fact.check(rec.facts, SetTimerFact.BAD_DELAY)).toBe(true); }); test('setTimeout - poling registers selfTime', async () => { @@ -166,6 +176,8 @@ describe('wrappers', () => { expect(rec.handler).toBe(handler); expect(rec.delay).toBe(1e3); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(false); + expect(Fact.check(rec.facts, ClearTimerFact.NOT_FOUND)).toBe(false); }); test('clearTimeoutHistory - non existent handler', () => { @@ -173,7 +185,9 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.clearTimeoutHistory.values())[0]; - expect(rec.delay).toBe(TAG_MISFORTUNE); + expect(rec.delay).toBe(TAG_DELAY_NOT_FOUND); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(false); + expect(Fact.check(rec.facts, ClearTimerFact.NOT_FOUND)).toBe(true); }); test('clearTimeoutHistory - invalid handler', () => { @@ -181,8 +195,10 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.clearTimeoutHistory.values())[0]; - expect(rec.delay).toBe(TAG_MISFORTUNE); - expect(rec.handler).toBe(TAG_EXCEPTION(0)); + expect(rec.delay).toBe(TAG_DELAY_NOT_FOUND); + expect(rec.handler).toBe(TAG_BAD_HANDLER(0)); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(true); + expect(Fact.check(rec.facts, ClearTimerFact.NOT_FOUND)).toBe(false); }); test('setIntervalHistory & clearIntervalHistory - recorded', () => { @@ -219,10 +235,11 @@ describe('wrappers', () => { expect(rec.calls).toBe(1); expect(rec.delay).toBe(DELAY); - expect(rec.isEval).toBe(false); expect(rec.trace.length).toBeGreaterThan(1); expect(rec.traceId.length).toBeGreaterThan(0); expect(rec.selfTime).not.toBeNull(); + expect(Fact.check(rec.facts, SetTimerFact.BAD_DELAY)).toBe(false); + expect(Fact.check(rec.facts, SetTimerFact.NOT_A_FUNCTION)).toBe(false); globalThis.clearInterval(handler); }); @@ -232,8 +249,9 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.setIntervalHistory.values())[0]; expect(rec.calls).toBe(1); - expect(rec.delay).toBe(TAG_EXCEPTION('-1')); - expect(rec.isEval).toBe(false); + expect(rec.delay).toBe(TAG_BAD_DELAY('-1')); + expect(Fact.check(rec.facts, SetTimerFact.BAD_DELAY)).toBe(true); + expect(Fact.check(rec.facts, SetTimerFact.NOT_A_FUNCTION)).toBe(false); globalThis.clearInterval(handler); }); @@ -245,6 +263,7 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.clearIntervalHistory.values())[0]; expect(rec.delay).toBe(1e3); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(false); }); test('clearIntervalHistory - non existent handler', () => { @@ -252,7 +271,9 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.clearIntervalHistory.values())[0]; - expect(rec.delay).toBe(TAG_MISFORTUNE); + expect(rec.delay).toBe(TAG_DELAY_NOT_FOUND); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(false); + expect(Fact.check(rec.facts, ClearTimerFact.NOT_FOUND)).toBe(true); }); test('clearIntervalHistory - invalid handler', () => { @@ -260,8 +281,10 @@ describe('wrappers', () => { const rec = Array.from(apiTimer.clearIntervalHistory.values())[0]; - expect(rec.delay).toBe(TAG_MISFORTUNE); + expect(rec.delay).toBe(TAG_DELAY_NOT_FOUND); + expect(Fact.check(rec.facts, ClearTimerFact.BAD_HANDLER)).toBe(true); + expect(Fact.check(rec.facts, ClearTimerFact.NOT_FOUND)).toBe(false); }); }); -await wait(1e3); +await wait(1e1); diff --git a/tests/TraceUtil_test.ts b/tests/TraceUtil_test.ts index be187d7..09d45fb 100644 --- a/tests/TraceUtil_test.ts +++ b/tests/TraceUtil_test.ts @@ -1,5 +1,5 @@ import { wait } from './util.ts'; -import { EWrapperCallstackType } from '../src/api/settings.ts'; +import { EWrapperCallstackType } from '../src/api/storage.local.ts'; import { TAG_INVALID_CALLSTACK_LINK, TraceUtil, @@ -75,4 +75,4 @@ describe('TraceUtil', () => { }); }); -await wait(1e3); +await wait(1e1); diff --git a/tests/canvas_test.ts b/tests/canvas_test.ts new file mode 100644 index 0000000..1eda19f --- /dev/null +++ b/tests/canvas_test.ts @@ -0,0 +1,188 @@ +import { describe, test } from '@std/testing/bdd'; +import { expect } from '@std/expect'; +import { + Box, + deg2rad, + fround, + PI, + PI2, + PId2, + Point, + rad2deg, + Vector, +} from '../src/api/canvas.ts'; + +describe('module exports', () => { + test('fround', () => { + expect(fround(1.33333333333)).toBe(1.333333); + expect(fround(1.33333333333, 1e3)).toBe(1.333); + }); + + test('rad2deg', () => { + expect(rad2deg(0)).toBe(0); + expect(rad2deg(PId2)).toBe(90); + expect(rad2deg(PI)).toBe(180); + expect(rad2deg(PI + PId2)).toBe(270); + expect(rad2deg(PI2)).toBe(0); + }); + + test('deg2rad', () => { + expect(deg2rad(0)).toBe(0); + expect(deg2rad(90)).toBe(PId2); + expect(deg2rad(180)).toBe(PI); + expect(deg2rad(270)).toBe(PI + PId2); + expect(deg2rad(360)).toBe(0); + }); +}); + +describe('Point', () => { + test('clone', () => { + const v = new Point(Infinity, -Infinity); + const clone = v.clone(); + + expect(clone).not.toBe(v); + expect(clone.isEqualTo(v)).toBe(true); + }); + + test('proximity', () => { + const v = new Point(0, 0); + + expect(v.proximity(new Point(0, 0))).toBe(0); + expect(v.proximity(new Point(1, 0))).toBe(1); + expect(v.proximity(new Point(0, 1))).toBe(1); + }); + + test('vectorTo', () => { + const from = new Point(0, 0); + const to = new Point(0, 0); + + let v = from.set(6, 3).vectorTo(to.set(8, 1)); + expect(v.x).toBe(2); + expect(v.y).toBe(-2); + + v = from.set(8, 1).vectorTo(to.set(6, 3)); + expect(v.x).toBe(-2); + expect(v.y).toBe(2); + + v = from.set(4, 4).vectorTo(to.set(2, 2)); + expect(v.x).toBe(-2); + expect(v.y).toBe(-2); + + v = from.set(2, 2).vectorTo(to.set(4, 4)); + expect(v.x).toBe(2); + expect(v.y).toBe(2); + }); + + test('rotate', () => { + const p = new Point(2, 2); + const base = new Point(4, 4); + + p.rotate(PId2, base); + expect(p.x).toBe(2); + expect(p.y).toBe(6); + }); +}); + +describe('Vector', () => { + test('rotate', () => { + const p = new Point(2, 2); + const base = new Point(4, 4); + const v = base.vectorTo(p); + + expect(v.x).toBe(-2); + expect(v.y).toBe(-2); + + v.rotate(PId2).round(); + expect(v.x).toBe(-2); + expect(v.y).toBe(2); + + v.rotate(-PI).round(); + expect(v.x).toBe(2); + expect(v.y).toBe(-2); + }); + + test('atBase', () => { + const v = new Vector(2, -2); + const pBase = new Point(4, 4); + const p = v.atBase(pBase); + + expect(p.hasSameXY(6, 2)).toBe(true); + }); + + test('length / setLength', () => { + const size = 2; + const v = new Vector(size, size); + const vAngleWithX = v.angleWithX; + const v2 = v.clone().setLength(5); + const v2AngleWithX = v2.angleWithX; + + expect(v.length).toBe(Math.sqrt(2 * size * size)); + expect(fround(v2.length)).toBe(5); + expect(vAngleWithX).toEqual(v2AngleWithX); + }); + + test('half', () => { + const v = new Vector(2, 4); + + expect(v.clone().half().length).toBe(v.length / 2); + }); + + test('xAxisAngle', () => { + const v = new Vector(1, -1); + + expect(rad2deg(v.set(1, -1).angleWithX)).toBe(45); + expect(rad2deg(v.set(-1, -1).angleWithX)).toBe(135); + expect(rad2deg(v.set(-1, 1).angleWithX)).toBe(225); + expect(rad2deg(v.set(1, 1).angleWithX)).toBe(315); + }); + + test('angle', () => { + const v = new Vector(0, 0); + const ox = new Vector(1, 0); // 0-right + const oy = new Vector(0, 1); // 0-down + + expect(Math.round(rad2deg(new Vector(1, 1).angle(new Vector(-1, -1))))) + .toBe( + 180, + ); + expect(Math.round(rad2deg(v.set(1, -1).angle(ox)))).toBe(45); + expect(Math.round(rad2deg(v.set(-1, -1).angle(ox)))).toBe(135); + expect(Math.round(rad2deg(v.set(-1, 1).angle(ox)))).toBe(135); + expect(Math.round(rad2deg(v.set(1, 1).angle(ox)))).toBe(45); + expect(Math.round(rad2deg(ox.angle(v.set(1, 1))))).toBe(45); + + expect(Math.round(rad2deg(oy.angle(v.set(1, -1))))).toBe(135); + expect(Math.round(rad2deg(oy.angle(v.set(-1, -1))))).toBe(135); + expect(Math.round(rad2deg(oy.angle(v.set(-1, 0))))).toBe(90); + expect(Math.round(rad2deg(oy.angle(v.set(0, 1))))).toBe(0); + expect(Math.round(rad2deg(oy.angle(v.set(1, 1))))).toBe(45); + }); + + test('mirror', () => { + const v1 = new Vector(-5, -1); + const vAxis = new Vector(-4, 4); + const v2 = v1.mirrorOver(vAxis).round(); + + expect(rad2deg(vAxis.angleWithX)).toBe(225); + expect(v2.x).toBe(1); + expect(v2.y).toBe(5); + + const v3 = v2.mirrorOver(vAxis).round(); + expect(v3.x).toBe(v1.x); + expect(v3.y).toBe(v1.y); + }); +}); + +describe('Box', () => { + const box = new Box(new Point(0, 0), 10, 10); + + test('proximity', () => { + expect(box.c.proximity(new Point(20, 5))).toBe(15); + }); + + test('contains', () => { + expect(box.contains(new Point(0, 0))).toBe(true); + expect(box.contains(new Point(5, 5))).toBe(true); + expect(box.contains(new Point(-1, 0))).toBe(false); + }); +}); diff --git a/tests/time_test.ts b/tests/time_test.ts index 19f039e..26cd4ce 100644 --- a/tests/time_test.ts +++ b/tests/time_test.ts @@ -164,4 +164,4 @@ describe('callingOnce', () => { }); }); -await wait(2e3); +await wait(1e3);
- {caption} -
DelayHandlerCallstack + {caption} Callstack [] + HandlerDelay
{metric.delay} + + {metric.handler} @@ -45,10 +50,7 @@ onclick={() => void onRemoveHandler(metric)} > - - - {metric.delay}
+ + + + {metric.handler}{metric.delay}