zeitzeuge 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1297 -178
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -602,28 +602,31 @@ var VERIFICATION_RULES = `## Verification rules (mandatory for every finding)
|
|
|
602
602
|
2. **Copy code verbatim** — beforeCode must be copied exactly from the file you
|
|
603
603
|
read, not paraphrased. Line numbers must match what you observed.
|
|
604
604
|
3. **Provide a working fix** — afterCode must be a complete drop-in replacement
|
|
605
|
-
that compiles, preserves the function signature, and only fixes the perf issue.
|
|
606
|
-
4. **Never omit beforeCode/afterCode** — every finding MUST have both fields set
|
|
605
|
+
that compiles, preserves the EXACT function signature, and only fixes the perf issue.
|
|
606
|
+
4. **Never omit beforeCode/afterCode** — every finding MUST have both fields set.
|
|
607
|
+
5. **Do NOT change sync to async** — if a function is synchronous, afterCode must
|
|
608
|
+
also be synchronous. Replace the inefficient implementation with a faster sync
|
|
609
|
+
alternative (e.g., use crypto.createHash instead of a manual loop). Mention the
|
|
610
|
+
async alternative in the description, but keep afterCode as a sync drop-in.`;
|
|
607
611
|
var OUTPUT_FORMAT = `## Output requirements
|
|
608
612
|
|
|
609
|
-
- Report ALL findings
|
|
610
|
-
|
|
613
|
+
- Report ALL findings within YOUR scope — typically 3–5 per subagent.
|
|
614
|
+
Exhaustively analyze every function but stay within your assigned categories.
|
|
611
615
|
- Each finding MUST have sourceFile, beforeCode, and afterCode
|
|
612
616
|
- Be specific — name exact files, functions, and line numbers
|
|
613
617
|
- Provide concrete code-level fixes, not generic advice
|
|
618
|
+
- Do NOT report findings about test files — only about application source files
|
|
614
619
|
|
|
615
620
|
### CRITICAL: Multiple findings per function and per file
|
|
616
621
|
|
|
617
|
-
- A single function CAN have multiple distinct issues
|
|
618
|
-
|
|
619
|
-
loop AND allocate a TextEncoder on every call — these are TWO findings
|
|
620
|
-
with different categories.
|
|
622
|
+
- A single function CAN have multiple distinct issues WITHIN YOUR SCOPE —
|
|
623
|
+
report each as a SEPARATE finding with a different category.
|
|
621
624
|
- A single file often has MANY issues across different functions. Read the
|
|
622
|
-
ENTIRE file top-to-bottom and report EVERY issue you find
|
|
623
|
-
first one.
|
|
625
|
+
ENTIRE file top-to-bottom and report EVERY issue you find within your scope.
|
|
624
626
|
- If function A calls function B and both have issues, report findings for
|
|
625
627
|
BOTH functions separately.
|
|
626
|
-
- Do NOT skip issues you consider "minor" — report them with severity: info
|
|
628
|
+
- Do NOT skip issues you consider "minor" — report them with severity: info.
|
|
629
|
+
- Do NOT report issues that belong to another subagent's scope.`;
|
|
627
630
|
var FINDING_CATEGORIES = `## Finding categories
|
|
628
631
|
|
|
629
632
|
Each finding MUST use one of these EXACT category values — do NOT invent new categories:
|
|
@@ -671,7 +674,7 @@ var STRUCTURED_OUTPUT_FIELDS = `## Structured output fields — REQUIRED for eve
|
|
|
671
674
|
|
|
672
675
|
Every finding MUST include ALL of these fields:
|
|
673
676
|
|
|
674
|
-
- \`sourceFile\` — (REQUIRED) the workspace path (e.g.
|
|
677
|
+
- \`sourceFile\` — (REQUIRED) the workspace path (e.g. src/utils/parser.ts or scripts/app.js)
|
|
675
678
|
- \`lineNumber\` — (REQUIRED) the 1-based line number, verified by reading the file
|
|
676
679
|
- \`confidence\` — \`high\` if you read the source, \`medium\` if strongly suggested,
|
|
677
680
|
\`low\` if inferred
|
|
@@ -695,17 +698,46 @@ Every finding MUST include ALL of these fields:
|
|
|
695
698
|
Copy the COMPLETE function (or the complete relevant section of 5-30 lines).
|
|
696
699
|
Do NOT use "..." or "// ..." to skip lines. Include the full code block.
|
|
697
700
|
- afterCode must be a COMPLETE, WORKING replacement for the beforeCode block:
|
|
698
|
-
-
|
|
701
|
+
- SAME function signature — same name, same parameters, same return type
|
|
702
|
+
- SAME sync/async — if the original is sync, afterCode MUST be sync. Do NOT
|
|
703
|
+
add async/await, Promises, or callbacks. Replace the slow implementation
|
|
704
|
+
with a faster synchronous alternative instead.
|
|
705
|
+
- SAME exports — if the function is exported, afterCode must also export it
|
|
699
706
|
- Must compile and produce identical behavior except for the performance fix
|
|
700
707
|
- Include ALL the code from beforeCode, not just the changed lines
|
|
701
708
|
- If the fix requires adding a module-level constant (e.g., hoisting a RegExp or
|
|
702
|
-
TextEncoder), include that declaration in afterCode
|
|
703
|
-
- For blocking
|
|
704
|
-
(
|
|
705
|
-
|
|
709
|
+
TextEncoder), include that declaration ABOVE the function in afterCode
|
|
710
|
+
- For blocking CPU loops: replace with a faster sync algorithm (e.g., use
|
|
711
|
+
crypto.createHash() instead of a manual loop). Mention async alternatives
|
|
712
|
+
in the finding description, not in afterCode.
|
|
713
|
+
- For excessive instantiation: hoist the construction to module level and reuse it.
|
|
714
|
+
Show the module-level const AND the modified function in afterCode.
|
|
715
|
+
- For listener leaks: show the fix (e.g., .once() instead of .on(), or return
|
|
716
|
+
an unsubscribe function). The beforeCode/afterCode should show the same function
|
|
717
|
+
with only the listener fix changed.
|
|
706
718
|
- afterCode must NOT be a diff, pseudocode, or description of changes
|
|
707
719
|
- If you cannot provide a concrete fix, still include beforeCode and describe
|
|
708
|
-
the fix approach in afterCode as a code comment within the actual code
|
|
720
|
+
the fix approach in afterCode as a code comment within the actual code
|
|
721
|
+
|
|
722
|
+
### Code fix quality rules
|
|
723
|
+
|
|
724
|
+
1. **Named functions for event handlers**: NEVER use anonymous functions with
|
|
725
|
+
.on() or .addEventListener(). Always define a named function or const so
|
|
726
|
+
it can be removed with .off(event, handler). Example:
|
|
727
|
+
- BAD: emitter.on('change', () => { cache = null; })
|
|
728
|
+
- GOOD: const invalidateCache = () => { cache = null; };
|
|
729
|
+
emitter.on('change', invalidateCache);
|
|
730
|
+
2. **Surgical listener removal**: Use .off(event, specificHandler) instead of
|
|
731
|
+
.removeAllListeners(). The cleanup/reset function must remove the EXACT
|
|
732
|
+
handler that was added.
|
|
733
|
+
3. **Complete guard logic**: If you add a guard flag (e.g., listenerRegistered),
|
|
734
|
+
the cleanup function MUST reset the flag AND remove the specific listener.
|
|
735
|
+
4. **Include surrounding context**: If the fix adds module-level variables
|
|
736
|
+
(guard flags, hoisted constants, named handlers), include ALL of them in
|
|
737
|
+
afterCode so it is self-contained.
|
|
738
|
+
5. **Preserve existing functions**: If the original file has a cleanup/reset
|
|
739
|
+
function, update it in afterCode to properly undo whatever your fix added.
|
|
740
|
+
Do NOT ignore existing cleanup functions.`;
|
|
709
741
|
// ../utils/src/prompts/file-list.ts
|
|
710
742
|
function buildFileListPromptSection(config) {
|
|
711
743
|
const { dataFiles, sourceFiles, testFiles, additionalSections } = config;
|
|
@@ -762,29 +794,1158 @@ function insertFileListIntoPrompt(prompt, fileSection) {
|
|
|
762
794
|
` + prompt.slice(firstHeadingIdx);
|
|
763
795
|
}
|
|
764
796
|
// ../utils/src/workspace/builder.ts
|
|
765
|
-
import {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
|
|
797
|
+
import { VfsSandbox } from "@langchain/node-vfs";
|
|
798
|
+
|
|
799
|
+
class PerfAgentSandbox extends VfsSandbox {
|
|
800
|
+
static #toRelative(p) {
|
|
801
|
+
const stripped = p.startsWith("/") ? p.slice(1) : p;
|
|
802
|
+
return stripped || ".";
|
|
803
|
+
}
|
|
804
|
+
async read(filePath, offset = 0, limit = 500) {
|
|
805
|
+
return super.read(PerfAgentSandbox.#toRelative(filePath), offset, limit);
|
|
806
|
+
}
|
|
807
|
+
async lsInfo(dirPath) {
|
|
808
|
+
return super.lsInfo(PerfAgentSandbox.#toRelative(dirPath));
|
|
809
|
+
}
|
|
810
|
+
async grepRaw(pattern, searchPath = "/", glob = null) {
|
|
811
|
+
return super.grepRaw(pattern, PerfAgentSandbox.#toRelative(searchPath), glob);
|
|
812
|
+
}
|
|
813
|
+
async globInfo(pattern, searchPath = "/") {
|
|
814
|
+
return super.globInfo(pattern, PerfAgentSandbox.#toRelative(searchPath));
|
|
815
|
+
}
|
|
816
|
+
static async create(options) {
|
|
817
|
+
const sandbox = new PerfAgentSandbox(options);
|
|
818
|
+
await sandbox.initialize();
|
|
819
|
+
return sandbox;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function createWorkspaceFromFiles(files) {
|
|
823
|
+
const sandbox = await PerfAgentSandbox.create({ initialFiles: files });
|
|
824
|
+
const cleanup = async () => {
|
|
782
825
|
try {
|
|
783
|
-
|
|
826
|
+
await sandbox.stop();
|
|
784
827
|
} catch {}
|
|
785
828
|
};
|
|
786
|
-
return { backend, cleanup
|
|
829
|
+
return { backend: sandbox, cleanup };
|
|
830
|
+
}
|
|
831
|
+
// ../utils/src/skills/data-scripting.ts
|
|
832
|
+
var SKILL_MD = `---
|
|
833
|
+
name: data-scripting
|
|
834
|
+
description: Use this skill when you need to analyze JSON data files in the workspace. Provides instructions for writing Node.js scripts to query, filter, aggregate, and cross-reference data files instead of reading them raw. Includes helper scripts and data file schemas.
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
# Data Scripting
|
|
838
|
+
|
|
839
|
+
## Overview
|
|
840
|
+
|
|
841
|
+
You have a full Node.js runtime available via execute_command. Instead of
|
|
842
|
+
reading large JSON data files with read_file (which consumes many tokens),
|
|
843
|
+
write short scripts that extract exactly what you need.
|
|
844
|
+
|
|
845
|
+
## When to use scripts vs. read_file
|
|
846
|
+
|
|
847
|
+
- **read_file**: Source code files you need to see verbatim for
|
|
848
|
+
beforeCode/afterCode, or small files (<50 lines)
|
|
849
|
+
- **Scripts**: JSON data files, any file >100 lines, cross-referencing
|
|
850
|
+
multiple files, computing aggregations, filtering by thresholds
|
|
851
|
+
|
|
852
|
+
## How to run a script
|
|
853
|
+
|
|
854
|
+
IMPORTANT: All file paths in scripts must use relative paths (no leading
|
|
855
|
+
\`/\`). The execute_command tool runs with the workspace as the current
|
|
856
|
+
directory, so \`fs.readFileSync('hot-functions/application.json')\` resolves
|
|
857
|
+
to \`<workspace>/hot-functions/application.json\`.
|
|
858
|
+
|
|
859
|
+
Option 1 — inline:
|
|
860
|
+
execute_command: node -e "
|
|
861
|
+
const data = JSON.parse(require('fs').readFileSync('path/to/file', 'utf8'));
|
|
862
|
+
const results = data.filter(x => x.duration > 100);
|
|
863
|
+
console.log(JSON.stringify(results, null, 2));
|
|
864
|
+
"
|
|
865
|
+
|
|
866
|
+
Option 2 — use a pre-built helper:
|
|
867
|
+
execute_command: node skills/data-scripting/helpers/top-items.js hot-functions/application.json selfTime 10
|
|
868
|
+
|
|
869
|
+
Option 3 — write a custom script:
|
|
870
|
+
write_file: tmp/my-analysis.js
|
|
871
|
+
execute_command: node tmp/my-analysis.js
|
|
872
|
+
|
|
873
|
+
## Pre-built helper scripts
|
|
874
|
+
|
|
875
|
+
### skills/data-scripting/helpers/top-items.js
|
|
876
|
+
Usage: \`node top-items.js <file> <sortField> [limit]\`
|
|
877
|
+
Reads a JSON array, sorts by the given field descending, prints top N items.
|
|
878
|
+
|
|
879
|
+
### skills/data-scripting/helpers/cross-reference.js
|
|
880
|
+
Usage: \`node cross-reference.js <file1> <field1> <file2> <field2>\`
|
|
881
|
+
Finds items in file1 whose field1 value also appears as field2 in file2.
|
|
882
|
+
|
|
883
|
+
## Data file schemas
|
|
884
|
+
|
|
885
|
+
See skills/data-scripting/schemas.md for the JSON structure of every
|
|
886
|
+
data file in this workspace. Read it before writing custom scripts.
|
|
887
|
+
`;
|
|
888
|
+
var SCHEMAS_MD = `# Workspace Data File Schemas
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
# Workspace files
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
## summary.json
|
|
895
|
+
{
|
|
896
|
+
totalTests: number,
|
|
897
|
+
totalDuration: number, // ms
|
|
898
|
+
passCount: number,
|
|
899
|
+
failCount: number,
|
|
900
|
+
profileCount: number,
|
|
901
|
+
slowestFile: string | null, // file:// URL
|
|
902
|
+
slowestFileDuration: number,
|
|
903
|
+
totalGcTime: number,
|
|
904
|
+
gcPercentage: number
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
## hot-functions/application.json
|
|
908
|
+
Array of application-code hot functions (filtered to sourceCategory "application"):
|
|
909
|
+
{
|
|
910
|
+
functionName: string,
|
|
911
|
+
workspacePath: string, // e.g. "/src/services/crypto.ts" (has leading /)
|
|
912
|
+
lineNumber: number,
|
|
913
|
+
columnNumber: number,
|
|
914
|
+
selfTime: number, // ms of CPU self-time
|
|
915
|
+
totalTime: number, // ms including callees
|
|
916
|
+
hitCount: number,
|
|
917
|
+
selfPercent: number, // % of total profile duration
|
|
918
|
+
sourceCategory: "application",
|
|
919
|
+
sourceSnippet?: string, // source code context around the hot line
|
|
920
|
+
callerChain?: [{ functionName, workspacePath, lineNumber }]
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
## hot-functions/dependencies.json
|
|
924
|
+
Same structure as hot-functions/application.json but sourceCategory "dependency".
|
|
925
|
+
|
|
926
|
+
## hot-functions/global.json
|
|
927
|
+
All hot functions across all categories (application, dependency, framework, unknown).
|
|
928
|
+
Same item structure. Can be very large (2000+ lines).
|
|
929
|
+
|
|
930
|
+
## scripts/application.json
|
|
931
|
+
Per-script summary for application code:
|
|
932
|
+
[{ workspacePath: string, selfTime: number, selfPercent: number, functionCount: number }]
|
|
933
|
+
|
|
934
|
+
## scripts/dependencies.json
|
|
935
|
+
Same structure as scripts/application.json but for dependency scripts.
|
|
936
|
+
|
|
937
|
+
## src/index.json
|
|
938
|
+
Maps source file paths to their hot functions. Key = workspacePath, value = array:
|
|
939
|
+
{
|
|
940
|
+
"/src/services/notification-service.ts": [
|
|
941
|
+
{ functionName: string, lineNumber: number, selfTime: number, selfPercent: number }
|
|
942
|
+
],
|
|
943
|
+
"/src/utils/crypto.ts": [...]
|
|
944
|
+
}
|
|
945
|
+
Use this to know WHICH source files have CPU-hot functions.
|
|
946
|
+
|
|
947
|
+
## listener-tracking.json
|
|
948
|
+
{
|
|
949
|
+
eventTargetCounts: {}, // browser EventTarget counts (usually empty in Node)
|
|
950
|
+
emitterCounts: { // keyed by event name
|
|
951
|
+
"<eventName>": { addCount: number, removeCount: number },
|
|
952
|
+
...
|
|
953
|
+
},
|
|
954
|
+
exceedances: [{ // maxListeners threshold exceeded
|
|
955
|
+
targetType: string, // e.g. "EventEmitter"
|
|
956
|
+
eventType: string, // e.g. "task:changed"
|
|
957
|
+
listenerCount: number, // current count that exceeded threshold
|
|
958
|
+
threshold: number, // the maxListeners value (default 10)
|
|
959
|
+
stack: string // stack trace showing where listener was added
|
|
960
|
+
}]
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
## metrics/current.json
|
|
964
|
+
Comprehensive pre-computed metrics (large file):
|
|
965
|
+
{
|
|
966
|
+
version: number,
|
|
967
|
+
timestamp: string,
|
|
968
|
+
suite: { totalDuration, totalTests, passCount, failCount, averageTestDuration,
|
|
969
|
+
medianTestDuration, p95TestDuration, slowestTestDuration, slowestTestName },
|
|
970
|
+
cpu: { gcPercentage, gcTime, idlePercentage, idleTime, applicationTime,
|
|
971
|
+
applicationPercent, dependencyTime, dependencyPercent, testFrameworkTime,
|
|
972
|
+
testFrameworkPercent },
|
|
973
|
+
files: { "<file:// URL>": { duration, testCount, setupTime, gcPercentage } },
|
|
974
|
+
tests: { "<file::testName>": { duration, status } },
|
|
975
|
+
hotFunctions: [{ key, functionName, scriptUrl, lineNumber, selfTime, selfPercent,
|
|
976
|
+
sourceCategory }],
|
|
977
|
+
listenerTracking: { eventTargetCounts, emitterCounts, exceedances }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
## timing/overview.json
|
|
981
|
+
Array of per-file timing data:
|
|
982
|
+
[{
|
|
983
|
+
file: string, // file:// URL
|
|
984
|
+
duration: number,
|
|
985
|
+
testCount: number,
|
|
986
|
+
passCount: number,
|
|
987
|
+
failCount: number,
|
|
988
|
+
setupTime: number,
|
|
989
|
+
tests: [{ name: string, duration: number, status: string }]
|
|
990
|
+
}]
|
|
991
|
+
|
|
992
|
+
## timing/slow-tests.json
|
|
993
|
+
Array of slow tests sorted by duration descending:
|
|
994
|
+
[{ file: string, name: string, duration: number }]
|
|
995
|
+
|
|
996
|
+
## profiles/index.json
|
|
997
|
+
Manifest mapping test files to their CPU profile paths:
|
|
998
|
+
[{ testFile: string, profilePath: string }]
|
|
999
|
+
|
|
1000
|
+
## profiles/<file>.json
|
|
1001
|
+
Per-test-file profile summary:
|
|
1002
|
+
{
|
|
1003
|
+
profilePath: string,
|
|
1004
|
+
duration: number,
|
|
1005
|
+
sampleCount: number,
|
|
1006
|
+
hotFunctions: [{
|
|
1007
|
+
functionName, lineNumber, columnNumber, selfTime, totalTime,
|
|
1008
|
+
hitCount, selfPercent, callerChain, sourceCategory, workspacePath
|
|
1009
|
+
}]
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
---
|
|
1013
|
+
# Browser workspace files (CLI agent only — not present in Vitest workspaces)
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
## heap/summary.json
|
|
1017
|
+
{
|
|
1018
|
+
metadata: { url, capturedAt, totalSize, nodeCount, edgeCount },
|
|
1019
|
+
largestObjects: [{ name, type, selfSize, retainedSize, retainerPath: string[] }],
|
|
1020
|
+
typeStats: [{ type, count, totalSize, avgSize }],
|
|
1021
|
+
constructorStats: [{ constructor, count, totalSize, avgSize }],
|
|
1022
|
+
detachedNodes: { count, totalSize, examples: [{ name, retainerPath }] },
|
|
1023
|
+
closureStats: { count, totalSize, topClosures: [{ name, contextSize, retainerPath }] }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
## trace/summary.json
|
|
1027
|
+
{
|
|
1028
|
+
url: string,
|
|
1029
|
+
timing: { loadComplete, firstContentfulPaint, largestContentfulPaint, totalBlockingTime, longTasks: [...] },
|
|
1030
|
+
requestCount: number,
|
|
1031
|
+
totalTransferSize: number,
|
|
1032
|
+
totalDecodedSize: number,
|
|
1033
|
+
renderBlockingResources: [{ url, type, size, duration, path }],
|
|
1034
|
+
resourceBreakdown: { scripts: { count, totalSize }, stylesheets: {...}, fonts: {...}, images: {...}, other: {...} }
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
## trace/runtime/blocking-functions.json
|
|
1038
|
+
Array of (up to 50 entries, sorted by duration descending):
|
|
1039
|
+
{
|
|
1040
|
+
functionName: string,
|
|
1041
|
+
scriptUrl: string, // URL of the script
|
|
1042
|
+
lineNumber: number,
|
|
1043
|
+
columnNumber: number,
|
|
1044
|
+
duration: number, // ms blocked on main thread
|
|
1045
|
+
startTime: number, // ms relative to navigation start
|
|
1046
|
+
callStack: [{ // caller chain (array of objects, NOT strings)
|
|
1047
|
+
functionName: string,
|
|
1048
|
+
scriptUrl: string,
|
|
1049
|
+
lineNumber: number
|
|
1050
|
+
}],
|
|
1051
|
+
category: string // "scripting" | "layout" | "paint" | etc.
|
|
1052
|
+
}
|
|
1053
|
+
To get the workspace file path for a scriptUrl, extract the filename:
|
|
1054
|
+
e.g. "https://example.com/static/abc123.js" -> "scripts/abc123.js"
|
|
1055
|
+
|
|
1056
|
+
## trace/runtime/event-listeners.json
|
|
1057
|
+
Array of (only listeners with addCount > 0):
|
|
1058
|
+
{
|
|
1059
|
+
eventType: string,
|
|
1060
|
+
targetType: string,
|
|
1061
|
+
addCount: number,
|
|
1062
|
+
removeCount: number,
|
|
1063
|
+
activeCount: number,
|
|
1064
|
+
stackSnippets: string[]
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
## trace/runtime/summary.json
|
|
1068
|
+
{
|
|
1069
|
+
totalEvents: number,
|
|
1070
|
+
traceDuration: number, // ms
|
|
1071
|
+
mainThreadId: number,
|
|
1072
|
+
frameBreakdown: { scripting, layout, paint, gc, other }, // all in ms
|
|
1073
|
+
blockingFunctionCount: number,
|
|
1074
|
+
listenerImbalances: number,
|
|
1075
|
+
gcPauseCount: number,
|
|
1076
|
+
gcTotalDuration: number, // ms
|
|
1077
|
+
frequentEventTypes: string[] // event types dispatched >10 times
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
## trace/runtime/frame-breakdown.json
|
|
1081
|
+
{
|
|
1082
|
+
scripting: number, // ms spent in script execution
|
|
1083
|
+
layout: number, // ms spent in layout calculations
|
|
1084
|
+
paint: number, // ms spent painting
|
|
1085
|
+
gc: number, // ms spent in garbage collection
|
|
1086
|
+
other: number // ms spent in other tasks
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
## trace/network-waterfall.json
|
|
1090
|
+
Array of (sorted by startTime):
|
|
1091
|
+
{
|
|
1092
|
+
url: string,
|
|
1093
|
+
type: string, // "Script" | "Stylesheet" | "Font" | "Document" | "Image"
|
|
1094
|
+
status: number,
|
|
1095
|
+
size: number, // decoded size in bytes
|
|
1096
|
+
startTime: number, // ms from navigation start
|
|
1097
|
+
endTime: number,
|
|
1098
|
+
duration: number,
|
|
1099
|
+
isRenderBlocking: boolean,
|
|
1100
|
+
priority: string,
|
|
1101
|
+
path: string | null // workspace path to stored content (e.g. "/scripts/abc.js")
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
## trace/asset-manifest.json
|
|
1105
|
+
Array of all network assets:
|
|
1106
|
+
{
|
|
1107
|
+
url: string,
|
|
1108
|
+
type: string,
|
|
1109
|
+
size: number, // decoded size in bytes
|
|
1110
|
+
duration: number,
|
|
1111
|
+
isRenderBlocking: boolean,
|
|
1112
|
+
stored: boolean, // true if content was captured and stored
|
|
1113
|
+
path: string | null // workspace path if stored
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
## trace/runtime/raw-events.json
|
|
1117
|
+
Array of raw Chrome trace events (can be very large). Each entry has:
|
|
1118
|
+
{
|
|
1119
|
+
name: string, // event name (e.g. "FunctionCall", "Layout", "GCEvent")
|
|
1120
|
+
cat: string, // category
|
|
1121
|
+
ph: string, // phase ("X" = complete, "B"/"E" = begin/end)
|
|
1122
|
+
ts: number, // timestamp in microseconds
|
|
1123
|
+
dur: number, // duration in microseconds
|
|
1124
|
+
tid: number, // thread ID
|
|
1125
|
+
pid: number, // process ID
|
|
1126
|
+
args: object // event-specific arguments
|
|
1127
|
+
}
|
|
1128
|
+
Only use for deep investigation — prefer the summary files first.
|
|
1129
|
+
`;
|
|
1130
|
+
var TOP_ITEMS_JS = `'use strict';
|
|
1131
|
+
|
|
1132
|
+
const fs = require('fs');
|
|
1133
|
+
|
|
1134
|
+
const [filePath, sortField, limitArg] = process.argv.slice(2);
|
|
1135
|
+
if (!filePath || !sortField) {
|
|
1136
|
+
console.error('Usage: node top-items.js <file> <sortField> [limit]');
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const limit = parseInt(limitArg, 10) || 10;
|
|
1141
|
+
|
|
1142
|
+
let raw;
|
|
1143
|
+
try {
|
|
1144
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
console.error(\`Error reading \${filePath}: \${err.message}\`);
|
|
1147
|
+
process.exit(1);
|
|
787
1148
|
}
|
|
1149
|
+
|
|
1150
|
+
let data;
|
|
1151
|
+
try {
|
|
1152
|
+
data = JSON.parse(raw);
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
console.error(\`Error parsing JSON from \${filePath}: \${err.message}\`);
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (!Array.isArray(data)) {
|
|
1159
|
+
console.error(\`Expected a JSON array in \${filePath}, got \${typeof data}\`);
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (data.length > 0 && !(sortField in data[0])) {
|
|
1164
|
+
console.error(\`Field "\${sortField}" not found in items. Available fields: \${Object.keys(data[0]).join(', ')}\`);
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const sorted = data
|
|
1169
|
+
.slice()
|
|
1170
|
+
.sort((a, b) => (Number(b[sortField]) || 0) - (Number(a[sortField]) || 0))
|
|
1171
|
+
.slice(0, limit);
|
|
1172
|
+
|
|
1173
|
+
console.log(JSON.stringify(sorted, null, 2));
|
|
1174
|
+
`;
|
|
1175
|
+
var CROSS_REFERENCE_JS = `'use strict';
|
|
1176
|
+
|
|
1177
|
+
const fs = require('fs');
|
|
1178
|
+
|
|
1179
|
+
const [file1Path, field1, file2Path, field2] = process.argv.slice(2);
|
|
1180
|
+
if (!file1Path || !field1 || !file2Path || !field2) {
|
|
1181
|
+
console.error('Usage: node cross-reference.js <file1> <field1> <file2> <field2>');
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function readJsonArray(filePath) {
|
|
1186
|
+
let raw;
|
|
1187
|
+
try {
|
|
1188
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
console.error(\`Error reading \${filePath}: \${err.message}\`);
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
}
|
|
1193
|
+
let data;
|
|
1194
|
+
try {
|
|
1195
|
+
data = JSON.parse(raw);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
console.error(\`Error parsing JSON from \${filePath}: \${err.message}\`);
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
1200
|
+
if (!Array.isArray(data)) {
|
|
1201
|
+
console.error(\`Expected a JSON array in \${filePath}, got \${typeof data}\`);
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
return data;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const data1 = readJsonArray(file1Path);
|
|
1208
|
+
const data2 = readJsonArray(file2Path);
|
|
1209
|
+
|
|
1210
|
+
const lookupValues = new Set(data2.map(item => item[field2]));
|
|
1211
|
+
const matches = data1.filter(item => lookupValues.has(item[field1]));
|
|
1212
|
+
|
|
1213
|
+
if (matches.length === 0) {
|
|
1214
|
+
console.log(\`No items in \${file1Path} have \${field1} matching \${field2} values from \${file2Path}.\`);
|
|
1215
|
+
process.exit(0);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
console.log(\`Found \${matches.length} item(s) in \${file1Path} where \${field1} matches \${field2} in \${file2Path}:\\n\`);
|
|
1219
|
+
for (const item of matches) {
|
|
1220
|
+
console.log(\` [\${field1}=\${JSON.stringify(item[field1])}]\`);
|
|
1221
|
+
console.log(JSON.stringify(item, null, 2));
|
|
1222
|
+
console.log();
|
|
1223
|
+
}
|
|
1224
|
+
`;
|
|
1225
|
+
var DATA_SCRIPTING_SKILL_FILES = {
|
|
1226
|
+
"skills/data-scripting/SKILL.md": SKILL_MD,
|
|
1227
|
+
"skills/data-scripting/schemas.md": SCHEMAS_MD,
|
|
1228
|
+
"skills/data-scripting/helpers/top-items.js": TOP_ITEMS_JS,
|
|
1229
|
+
"skills/data-scripting/helpers/cross-reference.js": CROSS_REFERENCE_JS
|
|
1230
|
+
};
|
|
1231
|
+
// ../utils/src/skills/browser-analysis.ts
|
|
1232
|
+
var SKILL_MD2 = `---
|
|
1233
|
+
name: browser-analysis
|
|
1234
|
+
description: Use this skill when analyzing browser page-load performance data including Chrome traces, heap snapshots, network waterfalls, and runtime blocking functions. Provides pre-built analysis scripts for common browser performance patterns.
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
# Browser Analysis Scripts
|
|
1238
|
+
|
|
1239
|
+
## Overview
|
|
1240
|
+
|
|
1241
|
+
Pre-built scripts for analyzing browser performance data. Run these
|
|
1242
|
+
directly or use them as templates for custom analysis.
|
|
1243
|
+
|
|
1244
|
+
## START HERE — Workspace overview
|
|
1245
|
+
|
|
1246
|
+
Run this FIRST to get a prioritized summary of the entire workspace:
|
|
1247
|
+
|
|
1248
|
+
execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
|
|
1249
|
+
|
|
1250
|
+
Reads trace/summary.json, heap/summary.json, trace/runtime/summary.json,
|
|
1251
|
+
blocking-functions.json, event-listeners.json, and network-waterfall.json.
|
|
1252
|
+
Outputs:
|
|
1253
|
+
- Page load metrics (FCP, LCP, TBT, load time)
|
|
1254
|
+
- Heap snapshot overview (total size, detached DOM nodes, closures)
|
|
1255
|
+
- Runtime trace summary (blocking function count, GC stats, frame breakdown)
|
|
1256
|
+
- Top blocking functions with script locations
|
|
1257
|
+
- Listener imbalances
|
|
1258
|
+
- Render-blocking resources
|
|
1259
|
+
- Large resources (>100KB)
|
|
1260
|
+
- Full list of available source files with sizes
|
|
1261
|
+
|
|
1262
|
+
Use this output to decide which source files to read and which scripts
|
|
1263
|
+
to run for deeper analysis.
|
|
1264
|
+
|
|
1265
|
+
## Additional scripts
|
|
1266
|
+
|
|
1267
|
+
### Analyze blocking functions (detailed)
|
|
1268
|
+
execute_command: node skills/browser-analysis/helpers/analyze-blockers.js [--threshold 50]
|
|
1269
|
+
|
|
1270
|
+
Reads trace/runtime/blocking-functions.json, filters by duration threshold
|
|
1271
|
+
(default 50ms), and outputs a ranked summary with call stacks and script
|
|
1272
|
+
locations. Also identifies compound blockers.
|
|
1273
|
+
|
|
1274
|
+
### Analyze network waterfall (detailed)
|
|
1275
|
+
execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js
|
|
1276
|
+
|
|
1277
|
+
Reads trace/network-waterfall.json and trace/summary.json, outputs:
|
|
1278
|
+
- Render-blocking resources with sizes and durations
|
|
1279
|
+
- Large scripts (>100KB) with workspace paths
|
|
1280
|
+
- Sequential chains that could be parallelized
|
|
1281
|
+
- Uncompressed resources
|
|
1282
|
+
|
|
1283
|
+
### Analyze heap snapshot (detailed)
|
|
1284
|
+
execute_command: node skills/browser-analysis/helpers/analyze-heap.js
|
|
1285
|
+
|
|
1286
|
+
Reads heap/summary.json, outputs:
|
|
1287
|
+
- Detached DOM nodes with retainer paths
|
|
1288
|
+
- Top 10 largest retained objects
|
|
1289
|
+
- Constructor hotspots
|
|
1290
|
+
- Closures with large retained sizes
|
|
1291
|
+
|
|
1292
|
+
### Find source code patterns
|
|
1293
|
+
execute_command: node skills/browser-analysis/helpers/find-patterns.js [--pattern addEventListener]
|
|
1294
|
+
|
|
1295
|
+
Searches source files for performance anti-patterns.
|
|
1296
|
+
|
|
1297
|
+
## Key data files
|
|
1298
|
+
|
|
1299
|
+
- heap/summary.json — parsed V8 heap snapshot (detached nodes, retained objects, closures)
|
|
1300
|
+
- trace/summary.json — page load metrics, render-blocking resources, resource breakdown
|
|
1301
|
+
- trace/network-waterfall.json — every network request with timing, size, priority
|
|
1302
|
+
- trace/asset-manifest.json — index of all assets with stored file paths
|
|
1303
|
+
- trace/runtime/summary.json — runtime trace overview (frame breakdown, GC stats)
|
|
1304
|
+
- trace/runtime/blocking-functions.json — main-thread blocking functions with call stacks
|
|
1305
|
+
- trace/runtime/event-listeners.json — event listener add/remove counts
|
|
1306
|
+
- trace/runtime/frame-breakdown.json — time in scripting vs layout vs paint vs GC
|
|
1307
|
+
- scripts/*.js — actual JavaScript source files captured during page load
|
|
1308
|
+
- styles/*.css — actual CSS source files
|
|
1309
|
+
- html/document.html — the HTML document
|
|
1310
|
+
|
|
1311
|
+
## Writing custom scripts
|
|
1312
|
+
|
|
1313
|
+
Read skills/data-scripting/schemas.md for JSON structures, then write
|
|
1314
|
+
scripts that load and query the data.
|
|
1315
|
+
`;
|
|
1316
|
+
var ANALYZE_BROWSER_WORKSPACE_JS = `'use strict';
|
|
1317
|
+
|
|
1318
|
+
var fs = require('fs');
|
|
1319
|
+
|
|
1320
|
+
function tryRead(path) {
|
|
1321
|
+
try { return JSON.parse(fs.readFileSync(path, 'utf8')); }
|
|
1322
|
+
catch (_) { return null; }
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
var traceSummary = tryRead('trace/summary.json');
|
|
1326
|
+
var heapSummary = tryRead('heap/summary.json');
|
|
1327
|
+
var rtSummary = tryRead('trace/runtime/summary.json');
|
|
1328
|
+
var waterfall = tryRead('trace/network-waterfall.json');
|
|
1329
|
+
var assetManifest = tryRead('trace/asset-manifest.json');
|
|
1330
|
+
var blockingFunctions = tryRead('trace/runtime/blocking-functions.json');
|
|
1331
|
+
var eventListeners = tryRead('trace/runtime/event-listeners.json');
|
|
1332
|
+
|
|
1333
|
+
console.log('=== Browser Workspace Overview ===\\n');
|
|
1334
|
+
|
|
1335
|
+
if (traceSummary) {
|
|
1336
|
+
var t = traceSummary.timing || traceSummary;
|
|
1337
|
+
console.log('URL: ' + (traceSummary.url || '(unknown)'));
|
|
1338
|
+
console.log('Page load: ' + Math.round(t.loadComplete || 0) + 'ms | FCP: ' + Math.round(t.firstContentfulPaint || 0) + 'ms | LCP: ' + Math.round(t.largestContentfulPaint || 0) + 'ms | TBT: ' + Math.round(t.totalBlockingTime || 0) + 'ms');
|
|
1339
|
+
console.log('Requests: ' + (traceSummary.requestCount || 0) + ' | Transfer: ' + ((traceSummary.totalTransferSize || 0) / 1024).toFixed(1) + ' KB | Decoded: ' + ((traceSummary.totalDecodedSize || 0) / 1024).toFixed(1) + ' KB');
|
|
1340
|
+
if (traceSummary.renderBlockingResources && traceSummary.renderBlockingResources.length > 0) {
|
|
1341
|
+
console.log('Render-blocking: ' + traceSummary.renderBlockingResources.length + ' resources');
|
|
1342
|
+
}
|
|
1343
|
+
console.log();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (heapSummary) {
|
|
1347
|
+
var meta = heapSummary.metadata || {};
|
|
1348
|
+
console.log('=== Heap Summary ===\\n');
|
|
1349
|
+
console.log('Total heap: ' + ((meta.totalSize || 0) / 1024 / 1024).toFixed(2) + ' MB | Nodes: ' + (meta.nodeCount || 0) + ' | Edges: ' + (meta.edgeCount || 0));
|
|
1350
|
+
var detached = heapSummary.detachedNodes || heapSummary.detachedDomNodes;
|
|
1351
|
+
if (detached) {
|
|
1352
|
+
var dCount = detached.count || (Array.isArray(detached) ? detached.length : 0);
|
|
1353
|
+
var dSize = detached.totalSize || 0;
|
|
1354
|
+
console.log('Detached DOM nodes: ' + dCount + (dSize ? ' (' + (dSize / 1024).toFixed(1) + ' KB)' : ''));
|
|
1355
|
+
}
|
|
1356
|
+
var closures = heapSummary.closureStats;
|
|
1357
|
+
if (closures && closures.count) {
|
|
1358
|
+
console.log('Closures: ' + closures.count + ' (' + ((closures.totalSize || 0) / 1024).toFixed(1) + ' KB)');
|
|
1359
|
+
}
|
|
1360
|
+
console.log();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
if (rtSummary) {
|
|
1364
|
+
console.log('=== Runtime Trace Summary ===\\n');
|
|
1365
|
+
console.log('Trace duration: ' + Math.round(rtSummary.traceDuration || 0) + 'ms');
|
|
1366
|
+
console.log('Blocking functions: ' + (rtSummary.blockingFunctionCount || 0));
|
|
1367
|
+
console.log('Listener imbalances: ' + (rtSummary.listenerImbalances || 0));
|
|
1368
|
+
console.log('GC pauses: ' + (rtSummary.gcPauseCount || 0) + ' (' + Math.round(rtSummary.gcTotalDuration || 0) + 'ms total)');
|
|
1369
|
+
if (rtSummary.frameBreakdown) {
|
|
1370
|
+
var fb = rtSummary.frameBreakdown;
|
|
1371
|
+
console.log('Frame breakdown: scripting=' + Math.round(fb.scripting || 0) + 'ms layout=' + Math.round(fb.layout || 0) + 'ms paint=' + Math.round(fb.paint || 0) + 'ms gc=' + Math.round(fb.gc || 0) + 'ms');
|
|
1372
|
+
}
|
|
1373
|
+
if (rtSummary.frequentEventTypes && rtSummary.frequentEventTypes.length > 0) {
|
|
1374
|
+
console.log('Frequent events: ' + rtSummary.frequentEventTypes.join(', '));
|
|
1375
|
+
}
|
|
1376
|
+
console.log();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (Array.isArray(blockingFunctions) && blockingFunctions.length > 0) {
|
|
1380
|
+
console.log('=== Top Blocking Functions ===\\n');
|
|
1381
|
+
var top = blockingFunctions.slice(0, 10);
|
|
1382
|
+
for (var i = 0; i < top.length; i++) {
|
|
1383
|
+
var fn = top[i];
|
|
1384
|
+
console.log(' [' + (fn.duration || 0) + 'ms] ' + (fn.functionName || '(anonymous)') + ' @ ' + (fn.scriptUrl || 'unknown') + ':' + (fn.lineNumber || '?'));
|
|
1385
|
+
}
|
|
1386
|
+
if (blockingFunctions.length > 10) {
|
|
1387
|
+
console.log(' ... and ' + (blockingFunctions.length - 10) + ' more');
|
|
1388
|
+
}
|
|
1389
|
+
console.log();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (Array.isArray(eventListeners) && eventListeners.length > 0) {
|
|
1393
|
+
var imbalanced = eventListeners.filter(function (l) {
|
|
1394
|
+
return (l.addCount || 0) > (l.removeCount || 0) + 2;
|
|
1395
|
+
});
|
|
1396
|
+
if (imbalanced.length > 0) {
|
|
1397
|
+
console.log('=== Listener Imbalances (adds >> removes) ===\\n');
|
|
1398
|
+
for (var i = 0; i < imbalanced.length; i++) {
|
|
1399
|
+
var l = imbalanced[i];
|
|
1400
|
+
console.log(' ' + l.eventType + ' on ' + (l.targetType || '?') + ' adds=' + l.addCount + ' removes=' + (l.removeCount || 0) + ' active=' + (l.activeCount || 0));
|
|
1401
|
+
}
|
|
1402
|
+
console.log();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (traceSummary && traceSummary.renderBlockingResources && traceSummary.renderBlockingResources.length > 0) {
|
|
1407
|
+
console.log('=== Render-Blocking Resources ===\\n');
|
|
1408
|
+
var rbs = traceSummary.renderBlockingResources;
|
|
1409
|
+
for (var i = 0; i < rbs.length; i++) {
|
|
1410
|
+
var r = rbs[i];
|
|
1411
|
+
console.log(' [' + (r.type || '?') + '] ' + (r.url || '?'));
|
|
1412
|
+
console.log(' Size: ' + ((r.size || 0) / 1024).toFixed(1) + ' KB | Duration: ' + Math.round(r.duration || 0) + 'ms' + (r.path ? ' | File: ' + r.path : ''));
|
|
1413
|
+
}
|
|
1414
|
+
console.log();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (Array.isArray(waterfall)) {
|
|
1418
|
+
var large = waterfall.filter(function (r) {
|
|
1419
|
+
return (r.size || 0) > 100000;
|
|
1420
|
+
});
|
|
1421
|
+
if (large.length > 0) {
|
|
1422
|
+
large.sort(function (a, b) { return (b.size || 0) - (a.size || 0); });
|
|
1423
|
+
console.log('=== Large Resources (>100KB) ===\\n');
|
|
1424
|
+
for (var i = 0; i < Math.min(10, large.length); i++) {
|
|
1425
|
+
var r = large[i];
|
|
1426
|
+
console.log(' [' + (r.type || '?') + '] ' + ((r.size || 0) / 1024).toFixed(1) + 'KB ' + (r.url || '?'));
|
|
1427
|
+
if (r.path) console.log(' File: ' + r.path);
|
|
1428
|
+
}
|
|
1429
|
+
console.log();
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
console.log('=== Available Files ===\\n');
|
|
1434
|
+
var dirs = ['scripts', 'styles', 'html', 'other'];
|
|
1435
|
+
for (var d = 0; d < dirs.length; d++) {
|
|
1436
|
+
try {
|
|
1437
|
+
var files = fs.readdirSync(dirs[d]);
|
|
1438
|
+
if (files.length > 0) {
|
|
1439
|
+
for (var i = 0; i < files.length; i++) {
|
|
1440
|
+
var full = dirs[d] + '/' + files[i];
|
|
1441
|
+
try {
|
|
1442
|
+
var stat = fs.statSync(full);
|
|
1443
|
+
if (stat.isFile()) console.log(' ' + full + ' (' + (stat.size / 1024).toFixed(1) + ' KB)');
|
|
1444
|
+
} catch (_) {}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
} catch (_) {}
|
|
1448
|
+
}
|
|
1449
|
+
console.log();
|
|
1450
|
+
`;
|
|
1451
|
+
var ANALYZE_BLOCKERS_JS = `'use strict';
|
|
1452
|
+
var fs = require('fs');
|
|
1453
|
+
|
|
1454
|
+
var threshold = 50;
|
|
1455
|
+
var args = process.argv.slice(2);
|
|
1456
|
+
for (var i = 0; i < args.length; i++) {
|
|
1457
|
+
if (args[i] === '--threshold' && args[i + 1]) {
|
|
1458
|
+
threshold = parseInt(args[i + 1], 10);
|
|
1459
|
+
if (isNaN(threshold)) threshold = 50;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
var data;
|
|
1464
|
+
try {
|
|
1465
|
+
data = JSON.parse(fs.readFileSync('trace/runtime/blocking-functions.json', 'utf8'));
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
console.log('Could not read trace/runtime/blocking-functions.json:', e.message);
|
|
1468
|
+
process.exit(0);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (!Array.isArray(data)) {
|
|
1472
|
+
console.log('Expected an array in blocking-functions.json');
|
|
1473
|
+
process.exit(0);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
var blockers = data.filter(function (fn) { return fn.duration >= threshold; });
|
|
1477
|
+
blockers.sort(function (a, b) { return b.duration - a.duration; });
|
|
1478
|
+
|
|
1479
|
+
if (blockers.length === 0) {
|
|
1480
|
+
console.log('No blocking functions found above ' + threshold + 'ms threshold.');
|
|
1481
|
+
process.exit(0);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function workspacePath(url) {
|
|
1485
|
+
if (!url) return '';
|
|
1486
|
+
var parts = url.split('/');
|
|
1487
|
+
var filename = parts[parts.length - 1];
|
|
1488
|
+
if (!filename) return url;
|
|
1489
|
+
return 'scripts/' + filename;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
var blockerNames = {};
|
|
1493
|
+
blockers.forEach(function (fn) {
|
|
1494
|
+
blockerNames[fn.functionName] = true;
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
console.log('=== Blocking Functions (>=' + threshold + 'ms) ===');
|
|
1498
|
+
console.log('Found ' + blockers.length + ' blocking function(s)\\n');
|
|
1499
|
+
|
|
1500
|
+
blockers.forEach(function (fn, idx) {
|
|
1501
|
+
var wsPath = workspacePath(fn.scriptUrl);
|
|
1502
|
+
console.log((idx + 1) + '. [' + fn.duration + 'ms] ' + (fn.functionName || '(anonymous)') +
|
|
1503
|
+
' @ ' + (fn.scriptUrl || 'unknown') + ':' + (fn.lineNumber || '?'));
|
|
1504
|
+
if (wsPath) {
|
|
1505
|
+
console.log(' Workspace: ' + wsPath);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (fn.callStack && fn.callStack.length > 0) {
|
|
1509
|
+
console.log(' Call stack:');
|
|
1510
|
+
fn.callStack.forEach(function (caller) {
|
|
1511
|
+
console.log(' <- ' + (caller.functionName || '(anonymous)') +
|
|
1512
|
+
' @ ' + (caller.scriptUrl || 'unknown') + ':' + (caller.lineNumber || '?'));
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (fn.callStack && fn.callStack.length > 0) {
|
|
1517
|
+
var compounds = fn.callStack.filter(function (caller) {
|
|
1518
|
+
return blockerNames[caller.functionName];
|
|
1519
|
+
});
|
|
1520
|
+
if (compounds.length > 0) {
|
|
1521
|
+
console.log(' ⚠ Compound blocker — calls into:');
|
|
1522
|
+
compounds.forEach(function (c) {
|
|
1523
|
+
console.log(' → ' + c.functionName);
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
console.log('');
|
|
1529
|
+
});
|
|
1530
|
+
`;
|
|
1531
|
+
var ANALYZE_WATERFALL_JS = `'use strict';
|
|
1532
|
+
var fs = require('fs');
|
|
1533
|
+
|
|
1534
|
+
var waterfall;
|
|
1535
|
+
try {
|
|
1536
|
+
waterfall = JSON.parse(fs.readFileSync('trace/network-waterfall.json', 'utf8'));
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
console.log('Could not read trace/network-waterfall.json:', e.message);
|
|
1539
|
+
process.exit(0);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
var summary = null;
|
|
1543
|
+
try {
|
|
1544
|
+
summary = JSON.parse(fs.readFileSync('trace/summary.json', 'utf8'));
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
// summary is optional
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (!Array.isArray(waterfall)) {
|
|
1550
|
+
console.log('Expected an array in network-waterfall.json');
|
|
1551
|
+
process.exit(0);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function workspacePath(url) {
|
|
1555
|
+
if (!url) return '';
|
|
1556
|
+
var parts = url.split('/');
|
|
1557
|
+
var filename = parts[parts.length - 1];
|
|
1558
|
+
if (!filename) return url;
|
|
1559
|
+
var qIdx = filename.indexOf('?');
|
|
1560
|
+
if (qIdx > -1) filename = filename.substring(0, qIdx);
|
|
1561
|
+
return 'scripts/' + filename;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function toKB(bytes) {
|
|
1565
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// 1. Render-blocking resources
|
|
1569
|
+
var renderBlocking = waterfall.filter(function (r) { return r.isRenderBlocking; });
|
|
1570
|
+
console.log('=== Render-Blocking Resources ===');
|
|
1571
|
+
if (renderBlocking.length === 0) {
|
|
1572
|
+
console.log('None found.\\n');
|
|
1573
|
+
} else {
|
|
1574
|
+
console.log('Found ' + renderBlocking.length + ' render-blocking resource(s)\\n');
|
|
1575
|
+
renderBlocking.forEach(function (r) {
|
|
1576
|
+
console.log(' ' + (r.type || 'unknown') + ': ' + (r.url || 'unknown'));
|
|
1577
|
+
console.log(' Size: ' + toKB(r.size || r.encodedSize || 0) +
|
|
1578
|
+
' | Duration: ' + ((r.duration || 0).toFixed(1)) + 'ms');
|
|
1579
|
+
console.log(' Workspace: ' + workspacePath(r.url));
|
|
1580
|
+
console.log('');
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 2. Large scripts (>100KB)
|
|
1585
|
+
var largeScripts = waterfall.filter(function (r) {
|
|
1586
|
+
return r.type === 'Script' && (r.size || r.decodedSize || 0) > 100000;
|
|
1587
|
+
});
|
|
1588
|
+
console.log('=== Large Scripts (>100KB) ===');
|
|
1589
|
+
if (largeScripts.length === 0) {
|
|
1590
|
+
console.log('None found.\\n');
|
|
1591
|
+
} else {
|
|
1592
|
+
largeScripts.sort(function (a, b) {
|
|
1593
|
+
return (b.size || b.decodedSize || 0) - (a.size || a.decodedSize || 0);
|
|
1594
|
+
});
|
|
1595
|
+
console.log('Found ' + largeScripts.length + ' large script(s)\\n');
|
|
1596
|
+
largeScripts.forEach(function (r) {
|
|
1597
|
+
console.log(' ' + (r.url || 'unknown'));
|
|
1598
|
+
console.log(' Size: ' + toKB(r.size || r.decodedSize || 0) +
|
|
1599
|
+
' | Duration: ' + ((r.duration || 0).toFixed(1)) + 'ms');
|
|
1600
|
+
console.log(' Workspace: ' + workspacePath(r.url));
|
|
1601
|
+
console.log('');
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// 3. Sequential chains
|
|
1606
|
+
var scriptAndStyle = waterfall.filter(function (r) {
|
|
1607
|
+
return r.type === 'Script' || r.type === 'Stylesheet';
|
|
1608
|
+
});
|
|
1609
|
+
scriptAndStyle.sort(function (a, b) {
|
|
1610
|
+
return (a.startTime || 0) - (b.startTime || 0);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
var chains = [];
|
|
1614
|
+
for (var i = 0; i < scriptAndStyle.length - 1; i++) {
|
|
1615
|
+
var a = scriptAndStyle[i];
|
|
1616
|
+
var aEnd = (a.startTime || 0) + (a.duration || 0);
|
|
1617
|
+
for (var j = i + 1; j < scriptAndStyle.length; j++) {
|
|
1618
|
+
var b = scriptAndStyle[j];
|
|
1619
|
+
if ((b.startTime || 0) >= aEnd) {
|
|
1620
|
+
var chainDuration = (b.startTime || 0) + (b.duration || 0) - (a.startTime || 0);
|
|
1621
|
+
if (chainDuration > 200) {
|
|
1622
|
+
chains.push({ a: a, b: b, duration: chainDuration });
|
|
1623
|
+
}
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
console.log('=== Sequential Chains (could be parallelized) ===');
|
|
1630
|
+
if (chains.length === 0) {
|
|
1631
|
+
console.log('None found.\\n');
|
|
1632
|
+
} else {
|
|
1633
|
+
console.log('Found ' + chains.length + ' sequential chain(s)\\n');
|
|
1634
|
+
chains.forEach(function (c) {
|
|
1635
|
+
console.log(' ' + (c.a.url || 'unknown') + ' → ' + (c.b.url || 'unknown'));
|
|
1636
|
+
console.log(' Chain duration: ' + c.duration.toFixed(1) + 'ms');
|
|
1637
|
+
console.log('');
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// 4. Summary render-blocking info
|
|
1642
|
+
if (summary && summary.renderBlockingResources) {
|
|
1643
|
+
console.log('=== Summary: Render-Blocking Resources ===');
|
|
1644
|
+
var rbs = summary.renderBlockingResources;
|
|
1645
|
+
if (Array.isArray(rbs)) {
|
|
1646
|
+
rbs.forEach(function (r) {
|
|
1647
|
+
console.log(' ' + (r.url || JSON.stringify(r)));
|
|
1648
|
+
});
|
|
1649
|
+
} else {
|
|
1650
|
+
console.log(' ' + JSON.stringify(rbs, null, 2));
|
|
1651
|
+
}
|
|
1652
|
+
console.log('');
|
|
1653
|
+
}
|
|
1654
|
+
`;
|
|
1655
|
+
var ANALYZE_HEAP_JS = `'use strict';
|
|
1656
|
+
var fs = require('fs');
|
|
1657
|
+
|
|
1658
|
+
var data;
|
|
1659
|
+
try {
|
|
1660
|
+
data = JSON.parse(fs.readFileSync('heap/summary.json', 'utf8'));
|
|
1661
|
+
} catch (e) {
|
|
1662
|
+
console.log('Could not read heap/summary.json:', e.message);
|
|
1663
|
+
process.exit(0);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function retainerChain(retainerPath) {
|
|
1667
|
+
if (!Array.isArray(retainerPath)) return '';
|
|
1668
|
+
return retainerPath.map(function (r) {
|
|
1669
|
+
return r.name || r.className || '(unknown)';
|
|
1670
|
+
}).join(' ← ');
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function toKB(bytes) {
|
|
1674
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// 1. Detached DOM nodes
|
|
1678
|
+
console.log('=== Detached DOM Nodes ===');
|
|
1679
|
+
var detached = data.detachedDomNodes || data.detachedNodes || [];
|
|
1680
|
+
if (!Array.isArray(detached) || detached.length === 0) {
|
|
1681
|
+
console.log('None found.\\n');
|
|
1682
|
+
} else {
|
|
1683
|
+
var totalSize = detached.reduce(function (sum, n) {
|
|
1684
|
+
return sum + (n.retainedSize || n.selfSize || 0);
|
|
1685
|
+
}, 0);
|
|
1686
|
+
console.log('Count: ' + detached.length + ' | Total retained size: ' + toKB(totalSize) + '\\n');
|
|
1687
|
+
detached.forEach(function (node) {
|
|
1688
|
+
console.log(' ' + (node.name || node.className || '(node)'));
|
|
1689
|
+
console.log(' Retained: ' + toKB(node.retainedSize || 0) +
|
|
1690
|
+
' | Self: ' + toKB(node.selfSize || 0));
|
|
1691
|
+
if (node.retainerPath) {
|
|
1692
|
+
console.log(' Retainer path: ' + retainerChain(node.retainerPath));
|
|
1693
|
+
}
|
|
1694
|
+
if (node.scriptUrl) {
|
|
1695
|
+
console.log(' ➜ Read: ' + node.scriptUrl);
|
|
1696
|
+
}
|
|
1697
|
+
console.log('');
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// 2. Top 10 largest retained objects
|
|
1702
|
+
console.log('=== Top 10 Largest Retained Objects ===');
|
|
1703
|
+
var objects = data.topRetainedObjects || data.largestObjects || [];
|
|
1704
|
+
if (!Array.isArray(objects) || objects.length === 0) {
|
|
1705
|
+
console.log('None found.\\n');
|
|
1706
|
+
} else {
|
|
1707
|
+
var top10 = objects.slice(0, 10);
|
|
1708
|
+
top10.forEach(function (obj, idx) {
|
|
1709
|
+
console.log((idx + 1) + '. ' + (obj.name || '(unknown)') + ' [' + (obj.type || obj.className || '?') + ']');
|
|
1710
|
+
console.log(' Self: ' + toKB(obj.selfSize || 0) + ' | Retained: ' + toKB(obj.retainedSize || 0));
|
|
1711
|
+
if (obj.retainerPath) {
|
|
1712
|
+
console.log(' Retainer path: ' + retainerChain(obj.retainerPath));
|
|
1713
|
+
}
|
|
1714
|
+
if (obj.scriptUrl) {
|
|
1715
|
+
console.log(' ➜ Read: ' + obj.scriptUrl);
|
|
1716
|
+
}
|
|
1717
|
+
console.log('');
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// 3. Constructor hotspots
|
|
1722
|
+
console.log('=== Constructor Hotspots (Top 10 by instance count) ===');
|
|
1723
|
+
var constructors = data.constructorStats || [];
|
|
1724
|
+
if (!Array.isArray(constructors) || constructors.length === 0) {
|
|
1725
|
+
console.log('None found.\\n');
|
|
1726
|
+
} else {
|
|
1727
|
+
var sorted = constructors.slice().sort(function (a, b) {
|
|
1728
|
+
return (b.instanceCount || b.count || 0) - (a.instanceCount || a.count || 0);
|
|
1729
|
+
});
|
|
1730
|
+
sorted.slice(0, 10).forEach(function (c, idx) {
|
|
1731
|
+
console.log((idx + 1) + '. ' + (c.name || c.constructor || '(unknown)') +
|
|
1732
|
+
' — ' + (c.instanceCount || c.count || 0) + ' instances' +
|
|
1733
|
+
' | Total size: ' + toKB(c.totalSize || c.retainedSize || 0));
|
|
1734
|
+
});
|
|
1735
|
+
console.log('');
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// 4. Top closures
|
|
1739
|
+
console.log('=== Top Closures by Context Size ===');
|
|
1740
|
+
var closureStats = data.closureStats || {};
|
|
1741
|
+
var topClosures = closureStats.topClosures || [];
|
|
1742
|
+
if (!Array.isArray(topClosures) || topClosures.length === 0) {
|
|
1743
|
+
console.log('None found.\\n');
|
|
1744
|
+
} else {
|
|
1745
|
+
topClosures.slice(0, 10).forEach(function (cl, idx) {
|
|
1746
|
+
console.log((idx + 1) + '. ' + (cl.name || cl.functionName || '(anonymous)') +
|
|
1747
|
+
' — context: ' + toKB(cl.contextSize || 0) +
|
|
1748
|
+
' | retained: ' + toKB(cl.retainedSize || 0));
|
|
1749
|
+
if (cl.scriptUrl) {
|
|
1750
|
+
console.log(' ➜ Read: ' + cl.scriptUrl);
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
console.log('');
|
|
1754
|
+
}
|
|
1755
|
+
`;
|
|
1756
|
+
var FIND_PATTERNS_JS = `'use strict';
|
|
1757
|
+
var fs = require('fs');
|
|
1758
|
+
|
|
1759
|
+
var onlyPattern = null;
|
|
1760
|
+
var args = process.argv.slice(2);
|
|
1761
|
+
for (var i = 0; i < args.length; i++) {
|
|
1762
|
+
if (args[i] === '--pattern' && args[i + 1]) {
|
|
1763
|
+
onlyPattern = args[i + 1];
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function normalizePath(p) {
|
|
1768
|
+
return p.startsWith('/') ? p.slice(1) : p;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
var filePaths = [];
|
|
1772
|
+
|
|
1773
|
+
try {
|
|
1774
|
+
var manifest = JSON.parse(fs.readFileSync('trace/asset-manifest.json', 'utf8'));
|
|
1775
|
+
if (Array.isArray(manifest)) {
|
|
1776
|
+
manifest.forEach(function (entry) {
|
|
1777
|
+
var p = entry.storedPath || entry.path;
|
|
1778
|
+
if (p) filePaths.push(normalizePath(p));
|
|
1779
|
+
});
|
|
1780
|
+
} else if (manifest && typeof manifest === 'object') {
|
|
1781
|
+
Object.keys(manifest).forEach(function (key) {
|
|
1782
|
+
var entry = manifest[key];
|
|
1783
|
+
var p = (typeof entry === 'string') ? entry : (entry && (entry.storedPath || entry.path));
|
|
1784
|
+
if (p) filePaths.push(normalizePath(p));
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
} catch (e) {
|
|
1788
|
+
// manifest not available
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
var extraPaths = ['html/document.html'];
|
|
1792
|
+
extraPaths.forEach(function (p) {
|
|
1793
|
+
if (filePaths.indexOf(p) === -1) {
|
|
1794
|
+
try {
|
|
1795
|
+
fs.accessSync(p);
|
|
1796
|
+
filePaths.push(p);
|
|
1797
|
+
} catch (e) {
|
|
1798
|
+
// not available
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
function readFile(p) {
|
|
1804
|
+
try {
|
|
1805
|
+
return fs.readFileSync(p, 'utf8');
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
var patterns = {
|
|
1812
|
+
addEventListener: {
|
|
1813
|
+
test: function (line) {
|
|
1814
|
+
if (!/addEventListener/.test(line)) return false;
|
|
1815
|
+
if (/\\b(scroll|touchstart|touchmove|wheel)\\b/.test(line) && !/passive\\s*:\\s*true/.test(line)) {
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
return false;
|
|
1819
|
+
},
|
|
1820
|
+
label: 'addEventListener without passive:true for scroll/touch/wheel'
|
|
1821
|
+
},
|
|
1822
|
+
missingDelegation: {
|
|
1823
|
+
test: function (line, allLines, idx) {
|
|
1824
|
+
if (!/querySelectorAll/.test(line)) return false;
|
|
1825
|
+
var window = allLines.slice(idx, Math.min(idx + 5, allLines.length)).join(' ');
|
|
1826
|
+
return /querySelectorAll/.test(window) && /forEach/.test(window) && /addEventListener/.test(window);
|
|
1827
|
+
},
|
|
1828
|
+
label: 'querySelectorAll+forEach+addEventListener (consider event delegation)'
|
|
1829
|
+
},
|
|
1830
|
+
syncXHR: {
|
|
1831
|
+
test: function (line) {
|
|
1832
|
+
return /XMLHttpRequest/.test(line) && /\\.open\\(/.test(line) && /,\\s*false\\s*[),]/.test(line);
|
|
1833
|
+
},
|
|
1834
|
+
label: 'Synchronous XMLHttpRequest'
|
|
1835
|
+
},
|
|
1836
|
+
documentWrite: {
|
|
1837
|
+
test: function (line) {
|
|
1838
|
+
return /document\\.write\\s*\\(/.test(line);
|
|
1839
|
+
},
|
|
1840
|
+
label: 'document.write() blocks parsing'
|
|
1841
|
+
},
|
|
1842
|
+
cssImport: {
|
|
1843
|
+
test: function (line) {
|
|
1844
|
+
return /@import\\b/.test(line);
|
|
1845
|
+
},
|
|
1846
|
+
label: 'CSS @import creates sequential loading'
|
|
1847
|
+
},
|
|
1848
|
+
imgDimensions: {
|
|
1849
|
+
test: function (line) {
|
|
1850
|
+
if (!/<img\\b/i.test(line)) return false;
|
|
1851
|
+
var hasWidth = /\\bwidth\\s*=/.test(line);
|
|
1852
|
+
var hasHeight = /\\bheight\\s*=/.test(line);
|
|
1853
|
+
return !hasWidth || !hasHeight;
|
|
1854
|
+
},
|
|
1855
|
+
label: '<img> without width/height causes layout shift'
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
var patternKeys = Object.keys(patterns);
|
|
1860
|
+
if (onlyPattern) {
|
|
1861
|
+
if (!patterns[onlyPattern]) {
|
|
1862
|
+
console.log('Unknown pattern: ' + onlyPattern);
|
|
1863
|
+
console.log('Available: ' + patternKeys.join(', '));
|
|
1864
|
+
process.exit(0);
|
|
1865
|
+
}
|
|
1866
|
+
patternKeys = [onlyPattern];
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
var results = [];
|
|
1870
|
+
|
|
1871
|
+
filePaths.forEach(function (filePath) {
|
|
1872
|
+
var content = readFile(filePath);
|
|
1873
|
+
if (!content) return;
|
|
1874
|
+
var lines = content.split('\\n');
|
|
1875
|
+
lines.forEach(function (line, idx) {
|
|
1876
|
+
patternKeys.forEach(function (key) {
|
|
1877
|
+
if (patterns[key].test(line, lines, idx)) {
|
|
1878
|
+
results.push({
|
|
1879
|
+
path: filePath,
|
|
1880
|
+
lineNumber: idx + 1,
|
|
1881
|
+
pattern: key,
|
|
1882
|
+
label: patterns[key].label,
|
|
1883
|
+
line: line.trim()
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
if (results.length === 0) {
|
|
1891
|
+
console.log('No performance anti-patterns found.');
|
|
1892
|
+
process.exit(0);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
console.log('=== Performance Anti-Patterns ===');
|
|
1896
|
+
console.log('Found ' + results.length + ' issue(s)\\n');
|
|
1897
|
+
results.forEach(function (r) {
|
|
1898
|
+
console.log(r.path + ':' + r.lineNumber + ': [' + r.pattern + '] ' + r.line);
|
|
1899
|
+
});
|
|
1900
|
+
`;
|
|
1901
|
+
var BROWSER_ANALYSIS_SKILL_FILES = {
|
|
1902
|
+
"skills/browser-analysis/SKILL.md": SKILL_MD2,
|
|
1903
|
+
"skills/browser-analysis/helpers/analyze-browser-workspace.js": ANALYZE_BROWSER_WORKSPACE_JS,
|
|
1904
|
+
"skills/browser-analysis/helpers/analyze-blockers.js": ANALYZE_BLOCKERS_JS,
|
|
1905
|
+
"skills/browser-analysis/helpers/analyze-waterfall.js": ANALYZE_WATERFALL_JS,
|
|
1906
|
+
"skills/browser-analysis/helpers/analyze-heap.js": ANALYZE_HEAP_JS,
|
|
1907
|
+
"skills/browser-analysis/helpers/find-patterns.js": FIND_PATTERNS_JS
|
|
1908
|
+
};
|
|
1909
|
+
// ../utils/src/skills/result-writer.ts
|
|
1910
|
+
var MERGE_RESULTS_JS = `'use strict';
|
|
1911
|
+
|
|
1912
|
+
var fs = require('fs');
|
|
1913
|
+
|
|
1914
|
+
fs.mkdirSync('results', { recursive: true });
|
|
1915
|
+
|
|
1916
|
+
var resultFiles;
|
|
1917
|
+
try {
|
|
1918
|
+
resultFiles = fs.readdirSync('results').filter(function (f) {
|
|
1919
|
+
return f.endsWith('.json') && f !== 'merged.json';
|
|
1920
|
+
});
|
|
1921
|
+
} catch (e) {
|
|
1922
|
+
console.log('[]');
|
|
1923
|
+
process.exit(0);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
if (resultFiles.length === 0) {
|
|
1927
|
+
console.log('[]');
|
|
1928
|
+
process.exit(0);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
var allFindings = [];
|
|
1932
|
+
for (var i = 0; i < resultFiles.length; i++) {
|
|
1933
|
+
try {
|
|
1934
|
+
var data = JSON.parse(fs.readFileSync('results/' + resultFiles[i], 'utf8'));
|
|
1935
|
+
if (Array.isArray(data)) {
|
|
1936
|
+
allFindings = allFindings.concat(data);
|
|
1937
|
+
}
|
|
1938
|
+
} catch (e) {
|
|
1939
|
+
// skip malformed files
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
fs.writeFileSync('results/merged.json', JSON.stringify(allFindings, null, 2));
|
|
1944
|
+
console.log(JSON.stringify(allFindings));
|
|
1945
|
+
`;
|
|
1946
|
+
var RESULT_WRITER_SKILL_FILES = {
|
|
1947
|
+
"skills/result-writer/merge-results.js": MERGE_RESULTS_JS
|
|
1948
|
+
};
|
|
788
1949
|
// ../utils/src/output/terminal.ts
|
|
789
1950
|
import pc2 from "picocolors";
|
|
790
1951
|
import ora from "ora";
|
|
@@ -971,7 +2132,7 @@ function formatBytes(bytes) {
|
|
|
971
2132
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
972
2133
|
}
|
|
973
2134
|
// ../utils/src/output/report.ts
|
|
974
|
-
import { writeFileSync
|
|
2135
|
+
import { writeFileSync } from "node:fs";
|
|
975
2136
|
var SEVERITY_EMOJI = {
|
|
976
2137
|
critical: "\uD83D\uDD34",
|
|
977
2138
|
warning: "\uD83D\uDFE1",
|
|
@@ -1004,7 +2165,7 @@ var CATEGORY_LABELS2 = {
|
|
|
1004
2165
|
};
|
|
1005
2166
|
function writeReport(outputPath, options) {
|
|
1006
2167
|
const md = generateMarkdown(options);
|
|
1007
|
-
|
|
2168
|
+
writeFileSync(outputPath, md, "utf-8");
|
|
1008
2169
|
return outputPath;
|
|
1009
2170
|
}
|
|
1010
2171
|
function generateMarkdown(options) {
|
|
@@ -1113,27 +2274,30 @@ import { createDeepAgent } from "deepagents";
|
|
|
1113
2274
|
import { toolStrategy } from "langchain";
|
|
1114
2275
|
|
|
1115
2276
|
// src/analysis/prompts/shared.ts
|
|
1116
|
-
var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy —
|
|
2277
|
+
var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — scripts first, source selectively
|
|
2278
|
+
|
|
2279
|
+
Your FIRST turn MUST run analysis scripts against the data files (JSON) to
|
|
2280
|
+
extract a concise summary of issues. Use the pre-built helper scripts in
|
|
2281
|
+
skills/browser-analysis/helpers/ or write your own using the data-scripting
|
|
2282
|
+
skill. Do NOT read data JSON files directly with read_file.
|
|
1117
2283
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2284
|
+
After your analysis scripts identify specific issues, read at most 1-3
|
|
2285
|
+
source files that are directly implicated. Derive paths from script URLs
|
|
2286
|
+
in the data (e.g. a URL ending in "abc123.js" → scripts/abc123.js).
|
|
1121
2287
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
is implicated, skip reading source files entirely and report findings from
|
|
1126
|
-
the data alone.
|
|
2288
|
+
PREFERRED actions:
|
|
2289
|
+
- execute_command with pre-built helper scripts or custom Node.js scripts
|
|
2290
|
+
- read_file for source code files you need to see verbatim
|
|
1127
2291
|
|
|
1128
2292
|
FORBIDDEN actions:
|
|
1129
2293
|
- ls — NEVER call ls.
|
|
1130
2294
|
- glob — NEVER call glob.
|
|
1131
|
-
-
|
|
1132
|
-
- Reading source files
|
|
2295
|
+
- read_file on data JSON files — use scripts to extract what you need.
|
|
2296
|
+
- Reading ALL source files — only read the specific ones your scripts point to.
|
|
1133
2297
|
- Reading more than 3 source files — focus on the most impactful issues.`;
|
|
1134
2298
|
var MINIFIED_SOURCE_HANDLING = `## Handling minified / compiled source files
|
|
1135
2299
|
|
|
1136
|
-
The JavaScript files in
|
|
2300
|
+
The JavaScript files in scripts/ are captured from a PRODUCTION page. They are
|
|
1137
2301
|
almost always minified, bundled, or compiled (e.g. by webpack, Vite, Turbopack).
|
|
1138
2302
|
Signs of compiled code: single very long lines, mangled 1-2 character variable
|
|
1139
2303
|
names, no whitespace or comments.
|
|
@@ -1181,11 +2345,11 @@ critical. Even shorter blocking calls prevent the main thread from processing
|
|
|
1181
2345
|
user interactions and paint updates.`;
|
|
1182
2346
|
var CROSS_REFERENCING = `## Cross-referencing data
|
|
1183
2347
|
|
|
1184
|
-
- When a script appears in BOTH
|
|
1185
|
-
AND
|
|
1186
|
-
- Check
|
|
1187
|
-
cross-reference with the actual addEventListener calls in
|
|
1188
|
-
- Use
|
|
2348
|
+
- When a script appears in BOTH trace/runtime/blocking-functions.json (CPU)
|
|
2349
|
+
AND heap/summary.json (memory), mention both dimensions in the finding.
|
|
2350
|
+
- Check trace/runtime/event-listeners.json for listener imbalances and
|
|
2351
|
+
cross-reference with the actual addEventListener calls in scripts/.
|
|
2352
|
+
- Use trace/network-waterfall.json to identify sequential chains, then read
|
|
1189
2353
|
the initiating script to confirm the dependency.
|
|
1190
2354
|
- When GC pauses are significant, cross-reference with heap data to identify
|
|
1191
2355
|
which constructors or allocation patterns are responsible.`;
|
|
@@ -1222,9 +2386,7 @@ document.body.removeChild(el);
|
|
|
1222
2386
|
// 'el' still holds a reference → detached DOM node
|
|
1223
2387
|
\`\`\`
|
|
1224
2388
|
|
|
1225
|
-
**How to detect:**
|
|
1226
|
-
For each detached node, search the source files for references to that node type
|
|
1227
|
-
or constructor name.
|
|
2389
|
+
**How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-heap.js\` to get a summary of heap issues including detached nodes with retainer paths. Then read ONLY the source files referenced in the retainer paths to verify root causes.
|
|
1228
2390
|
|
|
1229
2391
|
### 2. Large Retained Objects
|
|
1230
2392
|
|
|
@@ -1240,10 +2402,7 @@ class DataStore {
|
|
|
1240
2402
|
}
|
|
1241
2403
|
\`\`\`
|
|
1242
2404
|
|
|
1243
|
-
**How to detect:**
|
|
1244
|
-
objects where \`retainedSize\` is significantly larger than \`selfSize\` — they are
|
|
1245
|
-
roots of large object trees. Cross-reference with \`retainerPath\` to understand
|
|
1246
|
-
what keeps them alive.
|
|
2405
|
+
**How to detect:** The analyze-heap.js script outputs the top 10 largest retained objects. Review the retainer paths and read the implicated source files.
|
|
1247
2406
|
|
|
1248
2407
|
### 3. Constructor Hotspots
|
|
1249
2408
|
|
|
@@ -1260,8 +2419,7 @@ function processItems(items) {
|
|
|
1260
2419
|
}
|
|
1261
2420
|
\`\`\`
|
|
1262
2421
|
|
|
1263
|
-
**How to detect:**
|
|
1264
|
-
with unusually high instance counts or total size.
|
|
2422
|
+
**How to detect:** The analyze-heap.js script outputs constructor hotspots. Focus on types with unusually high instance counts.
|
|
1265
2423
|
|
|
1266
2424
|
### 4. Closure Leaks
|
|
1267
2425
|
|
|
@@ -1278,32 +2436,27 @@ function setupHandler(response) {
|
|
|
1278
2436
|
}
|
|
1279
2437
|
\`\`\`
|
|
1280
2438
|
|
|
1281
|
-
**How to detect:**
|
|
1282
|
-
large retained sizes, search the source files for the function patterns and
|
|
1283
|
-
check what variables they capture.
|
|
2439
|
+
**How to detect:** The analyze-heap.js script outputs top closures by retained size. Read the implicated source files to check what variables they capture.
|
|
1284
2440
|
|
|
1285
2441
|
### 5. Unbounded Caches/Maps
|
|
1286
2442
|
|
|
1287
2443
|
Data structures that grow monotonically without eviction, TTL, or size limits.
|
|
1288
2444
|
|
|
1289
|
-
**How to detect:**
|
|
1290
|
-
objects used as stores where items are added but never removed.
|
|
2445
|
+
**How to detect:** After identifying suspicious objects from the heap analysis script output, read the relevant source files and look for Maps, Sets, arrays used as stores where items are added but never removed.
|
|
1291
2446
|
|
|
1292
2447
|
## Your workflow
|
|
1293
2448
|
|
|
1294
|
-
1. In your FIRST turn,
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
3.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
4. Cross-reference with source code to find the root cause and provide
|
|
1306
|
-
before/after code with a concrete fix.
|
|
2449
|
+
1. In your FIRST turn, run BOTH of these:
|
|
2450
|
+
a. Run the workspace overview:
|
|
2451
|
+
execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
|
|
2452
|
+
b. Run the detailed heap analysis:
|
|
2453
|
+
execute_command: node skills/browser-analysis/helpers/analyze-heap.js
|
|
2454
|
+
Do NOT use ls, glob, or read_file on heap/summary.json directly.
|
|
2455
|
+
2. From the script outputs, identify issues and the script URLs that need verification.
|
|
2456
|
+
3. Derive workspace paths from script URLs (e.g. URL ending in "abc123.js" → scripts/abc123.js).
|
|
2457
|
+
Read ONLY the 1-3 source files directly implicated — do NOT read all scripts.
|
|
2458
|
+
4. Cross-reference with source code to find the root cause and provide before/after code.
|
|
2459
|
+
5. For custom queries, use the data-scripting skill to write targeted scripts.
|
|
1307
2460
|
|
|
1308
2461
|
### CRITICAL: Report EVERY distinct issue
|
|
1309
2462
|
|
|
@@ -1349,10 +2502,7 @@ Scripts in \`<head>\` without \`async\` or \`defer\` that block first paint.
|
|
|
1349
2502
|
</head>
|
|
1350
2503
|
\`\`\`
|
|
1351
2504
|
|
|
1352
|
-
**How to detect:**
|
|
1353
|
-
render-blocking script, read the actual source file (from the \`path\` field) and judge
|
|
1354
|
-
whether it MUST be synchronous (e.g., it modifies the DOM before paint) or can safely
|
|
1355
|
-
be deferred.
|
|
2505
|
+
**How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js\` to get a summary of render-blocking resources, large bundles, and sequential chains. For each render-blocking script, read the actual source file and judge whether it MUST be synchronous or can safely be deferred.
|
|
1356
2506
|
|
|
1357
2507
|
### 2. Render-Blocking CSS
|
|
1358
2508
|
|
|
@@ -1367,8 +2517,7 @@ Large stylesheets that block first contentful paint.
|
|
|
1367
2517
|
<link rel="stylesheet" href="/styles/all.css" media="print" onload="this.media='all'">
|
|
1368
2518
|
\`\`\`
|
|
1369
2519
|
|
|
1370
|
-
**How to detect:**
|
|
1371
|
-
summary. Large stylesheets (>50KB) that block FCP are prime candidates for splitting.
|
|
2520
|
+
**How to detect:** The analyze-waterfall.js script flags render-blocking stylesheets. Large stylesheets (>50KB) that block FCP are prime candidates for splitting.
|
|
1372
2521
|
|
|
1373
2522
|
### 3. Large Bundles (>100KB)
|
|
1374
2523
|
|
|
@@ -1383,8 +2532,7 @@ const Chart = lazy(() => import('./components/Chart'));
|
|
|
1383
2532
|
const DataGrid = lazy(() => import('./components/DataGrid'));
|
|
1384
2533
|
\`\`\`
|
|
1385
2534
|
|
|
1386
|
-
**How to detect:**
|
|
1387
|
-
Read their source to find imports or code that could be deferred.
|
|
2535
|
+
**How to detect:** The analyze-waterfall.js script identifies bundles >100KB. Read their source to find imports or code that could be deferred.
|
|
1388
2536
|
|
|
1389
2537
|
### 4. Sequential Waterfalls
|
|
1390
2538
|
|
|
@@ -1398,34 +2546,27 @@ Resources loaded sequentially that could be parallelised or preloaded.
|
|
|
1398
2546
|
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>
|
|
1399
2547
|
\`\`\`
|
|
1400
2548
|
|
|
1401
|
-
**How to detect:**
|
|
1402
|
-
chains where Resource B starts AFTER Resource A finishes, and both are needed for
|
|
1403
|
-
initial render. Calculate the potential savings from parallelisation.
|
|
2549
|
+
**How to detect:** The analyze-waterfall.js script detects sequential chains where Resource B starts AFTER Resource A finishes. Review the chains and calculate the potential savings from parallelisation.
|
|
1404
2550
|
|
|
1405
2551
|
### 5. Uncompressed or Poorly Cached Resources
|
|
1406
2552
|
|
|
1407
2553
|
Assets served without compression or with missing cache headers.
|
|
1408
2554
|
|
|
1409
|
-
**How to detect:**
|
|
1410
|
-
close to \`decodedSize\` (no compression), or where large assets have no caching.
|
|
2555
|
+
**How to detect:** The analyze-waterfall.js script flags uncompressed resources where \`encodedSize\` is close to \`decodedSize\`, and large assets with no caching.
|
|
1411
2556
|
|
|
1412
2557
|
## Your workflow
|
|
1413
2558
|
|
|
1414
|
-
1. In your FIRST turn,
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
- Resources with high load times
|
|
1426
|
-
4. Read ONLY the source files flagged as problematic (render-blocking,
|
|
1427
|
-
oversized, etc.) from "Available source files" above. Batch these reads.
|
|
1428
|
-
5. For EACH issue, verify with the source and provide before/after code
|
|
2559
|
+
1. In your FIRST turn, run BOTH of these:
|
|
2560
|
+
a. Run the workspace overview:
|
|
2561
|
+
execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
|
|
2562
|
+
b. Run the detailed waterfall analysis:
|
|
2563
|
+
execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js
|
|
2564
|
+
Do NOT use ls, glob, or read_file on trace JSON files directly.
|
|
2565
|
+
2. From the script outputs, identify render-blocking resources, large bundles,
|
|
2566
|
+
and sequential chains along with their workspace paths.
|
|
2567
|
+
3. Read ONLY the source files flagged as problematic. Batch these reads.
|
|
2568
|
+
4. For EACH issue, verify with the source and provide before/after code.
|
|
2569
|
+
5. For deeper analysis, use the data-scripting skill to write custom scripts.
|
|
1429
2570
|
|
|
1430
2571
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
1431
2572
|
${VERIFICATION_RULES}
|
|
@@ -1466,9 +2607,7 @@ async function loadData() {
|
|
|
1466
2607
|
}
|
|
1467
2608
|
\`\`\`
|
|
1468
2609
|
|
|
1469
|
-
**How to detect:**
|
|
1470
|
-
duration >50ms. For each one, read the source file at the reported line number
|
|
1471
|
-
to understand what the function does.
|
|
2610
|
+
**How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-blockers.js\` to get a summary of blocking functions with durations, script locations, and compound blockers. For each one, read the source file at the reported line number to understand what the function does.
|
|
1472
2611
|
|
|
1473
2612
|
**IMPORTANT — Compound blockers are SEPARATE findings:**
|
|
1474
2613
|
If function A calls function B and B blocks the main thread, report TWO findings:
|
|
@@ -1499,9 +2638,7 @@ function onRouteChange(route) {
|
|
|
1499
2638
|
}
|
|
1500
2639
|
\`\`\`
|
|
1501
2640
|
|
|
1502
|
-
**How to detect:**
|
|
1503
|
-
addCount >> removeCount. Then search the source files for addEventListener calls
|
|
1504
|
-
for those event types.
|
|
2641
|
+
**How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/event-listeners.json for event types where addCount >> removeCount. Then search the source files for addEventListener calls for those event types.
|
|
1505
2642
|
|
|
1506
2643
|
### 3. GC Pressure
|
|
1507
2644
|
|
|
@@ -1527,9 +2664,7 @@ function animate() {
|
|
|
1527
2664
|
}
|
|
1528
2665
|
\`\`\`
|
|
1529
2666
|
|
|
1530
|
-
**How to detect:**
|
|
1531
|
-
duration. If significant, cross-reference with /heap/summary.json to find which
|
|
1532
|
-
constructors are responsible. Check source files for hot loops creating objects.
|
|
2667
|
+
**How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/summary.json for GC pause count and total duration. If significant, cross-reference with heap data to find which constructors are responsible. Check source files for hot loops creating objects.
|
|
1533
2668
|
|
|
1534
2669
|
### 4. Layout Thrashing
|
|
1535
2670
|
|
|
@@ -1549,7 +2684,7 @@ elements.forEach((el, i) => {
|
|
|
1549
2684
|
});
|
|
1550
2685
|
\`\`\`
|
|
1551
2686
|
|
|
1552
|
-
**How to detect:** Read
|
|
2687
|
+
**How to detect:** Read trace/runtime/raw-events.json and look for rapid
|
|
1553
2688
|
alternation of Layout and scripting events. Also search source files for
|
|
1554
2689
|
patterns that read offsetHeight/offsetWidth/getBoundingClientRect inside
|
|
1555
2690
|
loops that also modify DOM styles.
|
|
@@ -1578,28 +2713,26 @@ window.addEventListener('scroll', () => {
|
|
|
1578
2713
|
});
|
|
1579
2714
|
\`\`\`
|
|
1580
2715
|
|
|
1581
|
-
**How to detect:**
|
|
1582
|
-
These are event types dispatched >10 times during the trace period. Search
|
|
1583
|
-
source files for their handlers and check if they use throttle/debounce/rAF.
|
|
2716
|
+
**How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/summary.json for \`frequentEventTypes\`. These are event types dispatched >10 times during the trace period. Search source files for their handlers and check if they use throttle/debounce/rAF.
|
|
1584
2717
|
|
|
1585
2718
|
## Your workflow
|
|
1586
2719
|
|
|
1587
|
-
1. In your FIRST turn,
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
Do NOT use ls
|
|
1593
|
-
2.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
imbalances — do NOT read all scripts.
|
|
2720
|
+
1. In your FIRST turn, run BOTH of these:
|
|
2721
|
+
a. Run the workspace overview:
|
|
2722
|
+
execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
|
|
2723
|
+
b. Run the detailed blocking functions analysis:
|
|
2724
|
+
execute_command: node skills/browser-analysis/helpers/analyze-blockers.js
|
|
2725
|
+
Do NOT use ls, glob, or read_file on trace data files directly.
|
|
2726
|
+
2. From the script outputs, note blocking functions, their durations, script
|
|
2727
|
+
locations, and compound blockers. Also note listener imbalances and GC stats
|
|
2728
|
+
from the workspace overview.
|
|
2729
|
+
3. Derive workspace paths from scriptUrl (e.g. URL ending in "abc123.js" →
|
|
2730
|
+
scripts/abc123.js). Read ONLY the 1-3 source files directly implicated.
|
|
1599
2731
|
4. Check for compound blockers: if function A calls blocking function B,
|
|
1600
|
-
report BOTH as separate findings
|
|
1601
|
-
5.
|
|
1602
|
-
|
|
2732
|
+
report BOTH as separate findings.
|
|
2733
|
+
5. For deeper listener imbalance or GC analysis, write a custom script using
|
|
2734
|
+
the data-scripting skill to query trace/runtime/event-listeners.json and
|
|
2735
|
+
trace/runtime/summary.json.
|
|
1603
2736
|
|
|
1604
2737
|
### CRITICAL: Report EACH pattern as a SEPARATE finding
|
|
1605
2738
|
|
|
@@ -1646,8 +2779,7 @@ Large \`<script>\` blocks in the HTML document that block rendering.
|
|
|
1646
2779
|
</head>
|
|
1647
2780
|
\`\`\`
|
|
1648
2781
|
|
|
1649
|
-
**How to detect:**
|
|
1650
|
-
that are large (>20 lines or >1KB) and don't need to execute before first paint.
|
|
2782
|
+
**How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/find-patterns.js\` to scan HTML, CSS, and JS files for known anti-patterns. Also read html/document.html to check for inline \`<script>\` blocks that are large (>20 lines or >1KB) and don't need to execute before first paint.
|
|
1651
2783
|
|
|
1652
2784
|
### 2. DOM Manipulation in Loops (Layout Thrashing)
|
|
1653
2785
|
|
|
@@ -1671,9 +2803,7 @@ function resizeAll(elements) {
|
|
|
1671
2803
|
}
|
|
1672
2804
|
\`\`\`
|
|
1673
2805
|
|
|
1674
|
-
**How to detect:**
|
|
1675
|
-
(offsetWidth, offsetHeight, getBoundingClientRect, getComputedStyle) and
|
|
1676
|
-
DOM writes (style.*, setAttribute, classList.*).
|
|
2806
|
+
**How to detect:** The find-patterns.js script detects DOM read+write patterns in loops. Review the flagged files for loops containing both DOM reads (offsetWidth, offsetHeight, getBoundingClientRect, getComputedStyle) and DOM writes (style.*, setAttribute, classList.*).
|
|
1677
2807
|
|
|
1678
2808
|
### 3. Missing Event Delegation
|
|
1679
2809
|
|
|
@@ -1693,9 +2823,7 @@ document.querySelector('.list').addEventListener('click', (e) => {
|
|
|
1693
2823
|
});
|
|
1694
2824
|
\`\`\`
|
|
1695
2825
|
|
|
1696
|
-
**How to detect:**
|
|
1697
|
-
addEventListener...) patterns or similar loops that add the same listener type
|
|
1698
|
-
to many elements.
|
|
2826
|
+
**How to detect:** The find-patterns.js script flags querySelectorAll+forEach+addEventListener patterns. Review the flagged files for similar loops that add the same listener type to many elements.
|
|
1699
2827
|
|
|
1700
2828
|
### 4. Synchronous XMLHttpRequest or Blocking APIs
|
|
1701
2829
|
|
|
@@ -1712,9 +2840,7 @@ const response = await fetch('/api/data');
|
|
|
1712
2840
|
const data = await response.json();
|
|
1713
2841
|
\`\`\`
|
|
1714
2842
|
|
|
1715
|
-
**How to detect:**
|
|
1716
|
-
argument set to \`false\`, or \`document.write()\`, or other deprecated
|
|
1717
|
-
synchronous APIs.
|
|
2843
|
+
**How to detect:** The find-patterns.js script flags synchronous XHR, \`document.write()\`, and other deprecated synchronous APIs.
|
|
1718
2844
|
|
|
1719
2845
|
### 5. Non-Passive Scroll/Touch Event Listeners
|
|
1720
2846
|
|
|
@@ -1729,9 +2855,7 @@ document.addEventListener('touchstart', handler);
|
|
|
1729
2855
|
document.addEventListener('touchstart', handler, { passive: true });
|
|
1730
2856
|
\`\`\`
|
|
1731
2857
|
|
|
1732
|
-
**How to detect:**
|
|
1733
|
-
'scroll', 'touchstart', 'touchmove', or 'wheel' that don't specify
|
|
1734
|
-
\`{ passive: true }\` as the options argument.
|
|
2858
|
+
**How to detect:** The find-patterns.js script flags non-passive listeners for scroll, touchstart, touchmove, and wheel events.
|
|
1735
2859
|
|
|
1736
2860
|
### 6. CSS Issues
|
|
1737
2861
|
|
|
@@ -1745,11 +2869,7 @@ document.addEventListener('touchstart', handler, { passive: true });
|
|
|
1745
2869
|
/* GOOD: use <link> with preconnect for external fonts */
|
|
1746
2870
|
\`\`\`
|
|
1747
2871
|
|
|
1748
|
-
**How to detect:**
|
|
1749
|
-
- \`@import\` statements (add network round-trips vs \`<link>\`)
|
|
1750
|
-
- Complex selectors with many combinators
|
|
1751
|
-
- Large unused rule blocks
|
|
1752
|
-
- Missing \`will-change\` or \`contain\` for animated elements
|
|
2872
|
+
**How to detect:** The find-patterns.js script flags CSS \`@import\` statements and complex selectors. Also read CSS files directly to check for large unused rule blocks and missing \`will-change\` or \`contain\` for animated elements.
|
|
1753
2873
|
|
|
1754
2874
|
### 7. Missing Image Dimensions Causing Layout Shifts
|
|
1755
2875
|
|
|
@@ -1763,26 +2883,20 @@ Images without explicit width/height that cause Cumulative Layout Shift (CLS).
|
|
|
1763
2883
|
<img src="/hero.jpg" width="1200" height="600" loading="lazy">
|
|
1764
2884
|
\`\`\`
|
|
1765
2885
|
|
|
1766
|
-
**How to detect:**
|
|
1767
|
-
\`width\` and \`height\` attributes.
|
|
2886
|
+
**How to detect:** The find-patterns.js script flags \`<img>\` tags without \`width\` and \`height\` attributes.
|
|
1768
2887
|
|
|
1769
2888
|
## Your workflow
|
|
1770
2889
|
|
|
1771
|
-
1. In your FIRST turn,
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
Batch these reads.
|
|
1782
|
-
5. Check each script for: DOM reads+writes in loops, querySelectorAll+forEach
|
|
1783
|
-
+addEventListener (missing delegation), non-passive scroll/touch listeners,
|
|
1784
|
-
synchronous XHR.
|
|
1785
|
-
6. Report EACH pattern as a separate finding with before/after code.
|
|
2890
|
+
1. In your FIRST turn, run BOTH of these:
|
|
2891
|
+
a. Run the workspace overview:
|
|
2892
|
+
execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
|
|
2893
|
+
b. Run the pattern finder:
|
|
2894
|
+
execute_command: node skills/browser-analysis/helpers/find-patterns.js
|
|
2895
|
+
This searches HTML, CSS, and JS files for known anti-patterns.
|
|
2896
|
+
2. Also read HTML and CSS files directly (they're typically small) to check
|
|
2897
|
+
for inline scripts, img without dimensions, and CSS issues.
|
|
2898
|
+
3. Based on issues found, read ONLY the script files that need inspection.
|
|
2899
|
+
4. Report EACH pattern as a separate finding with before/after code.
|
|
1786
2900
|
|
|
1787
2901
|
### CRITICAL: Be thorough but selective
|
|
1788
2902
|
|
|
@@ -1964,7 +3078,8 @@ function buildSubagents(ctx) {
|
|
|
1964
3078
|
return {
|
|
1965
3079
|
name,
|
|
1966
3080
|
description,
|
|
1967
|
-
systemPrompt: insertFileListIntoPrompt(prompt, fileSection)
|
|
3081
|
+
systemPrompt: insertFileListIntoPrompt(prompt, fileSection),
|
|
3082
|
+
skills: ["skills/data-scripting/", "skills/browser-analysis/"]
|
|
1968
3083
|
};
|
|
1969
3084
|
});
|
|
1970
3085
|
}
|
|
@@ -1975,12 +3090,13 @@ async function analyze(model, backend, spinner, context, { animateProgress = tru
|
|
|
1975
3090
|
systemPrompt: BROWSER_ORCHESTRATOR_PROMPT,
|
|
1976
3091
|
backend,
|
|
1977
3092
|
subagents,
|
|
3093
|
+
skills: ["skills/"],
|
|
1978
3094
|
responseFormat: toolStrategy(FindingsSchema)
|
|
1979
3095
|
});
|
|
1980
3096
|
const userMessage = context ? buildBrowserUserMessage(context) : [
|
|
1981
3097
|
"Analyze the frontend performance data in this workspace.",
|
|
1982
3098
|
"",
|
|
1983
|
-
"Start by reading
|
|
3099
|
+
"Start by reading heap/summary.json, trace/summary.json, and trace/runtime/summary.json",
|
|
1984
3100
|
"to understand the overall picture, then explore source files to verify root causes."
|
|
1985
3101
|
].join(`
|
|
1986
3102
|
`);
|
|
@@ -2822,7 +3938,10 @@ async function createWorkspace(options) {
|
|
|
2822
3938
|
}
|
|
2823
3939
|
}
|
|
2824
3940
|
}
|
|
2825
|
-
|
|
3941
|
+
Object.assign(files, DATA_SCRIPTING_SKILL_FILES);
|
|
3942
|
+
Object.assign(files, BROWSER_ANALYSIS_SKILL_FILES);
|
|
3943
|
+
Object.assign(files, RESULT_WRITER_SKILL_FILES);
|
|
3944
|
+
const result = await createWorkspaceFromFiles(files);
|
|
2826
3945
|
return {
|
|
2827
3946
|
backend: result.backend,
|
|
2828
3947
|
cleanup: result.cleanup,
|