@treelocator/runtime 0.4.7 → 0.5.2
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.
|
@@ -47,10 +47,15 @@ export interface DejitterConfig {
|
|
|
47
47
|
outlier: {
|
|
48
48
|
ratioThreshold: number;
|
|
49
49
|
};
|
|
50
|
+
lag: {
|
|
51
|
+
minDelay: number;
|
|
52
|
+
highDelay: number;
|
|
53
|
+
medDelay: number;
|
|
54
|
+
};
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
export interface DejitterFinding {
|
|
53
|
-
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier';
|
|
58
|
+
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier' | 'lag';
|
|
54
59
|
severity: 'high' | 'medium' | 'low' | 'info';
|
|
55
60
|
elem: string;
|
|
56
61
|
elemLabel: {
|
|
@@ -85,6 +90,7 @@ export interface DejitterAPI {
|
|
|
85
90
|
getRaw(): {
|
|
86
91
|
rawFrames: any[];
|
|
87
92
|
mutations: any[];
|
|
93
|
+
interactions: any[];
|
|
88
94
|
};
|
|
89
95
|
toJSON(): string;
|
|
90
96
|
}
|
|
@@ -48,6 +48,11 @@ export function createDejitterRecorder() {
|
|
|
48
48
|
},
|
|
49
49
|
outlier: {
|
|
50
50
|
ratioThreshold: 3
|
|
51
|
+
},
|
|
52
|
+
lag: {
|
|
53
|
+
minDelay: 50,
|
|
54
|
+
highDelay: 200,
|
|
55
|
+
medDelay: 100
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
};
|
|
@@ -59,12 +64,14 @@ export function createDejitterRecorder() {
|
|
|
59
64
|
let rafId = null;
|
|
60
65
|
let stopTimer = null;
|
|
61
66
|
let mutationObserver = null;
|
|
67
|
+
let interactionAbort = null;
|
|
62
68
|
let onStopCallbacks = [];
|
|
63
69
|
let lastSeen = new Map();
|
|
64
70
|
let rawFrames = [];
|
|
65
71
|
let lastChangeTime = 0;
|
|
66
72
|
let hasSeenChange = false;
|
|
67
73
|
let mutations = [];
|
|
74
|
+
let interactions = [];
|
|
68
75
|
let nextElemId = 0;
|
|
69
76
|
function elemId(el) {
|
|
70
77
|
if (!el.__dj_id) {
|
|
@@ -210,6 +217,24 @@ export function createDejitterRecorder() {
|
|
|
210
217
|
characterData: true
|
|
211
218
|
});
|
|
212
219
|
}
|
|
220
|
+
function startInteractionListeners() {
|
|
221
|
+
interactionAbort = new AbortController();
|
|
222
|
+
const opts = {
|
|
223
|
+
capture: true,
|
|
224
|
+
signal: interactionAbort.signal
|
|
225
|
+
};
|
|
226
|
+
const handler = e => {
|
|
227
|
+
if (!recording) return;
|
|
228
|
+
const t = Math.round(performance.now() - startTime);
|
|
229
|
+
interactions.push({
|
|
230
|
+
t,
|
|
231
|
+
type: e.type
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
for (const evt of ['click', 'pointerdown', 'keydown']) {
|
|
235
|
+
document.addEventListener(evt, handler, opts);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
213
238
|
|
|
214
239
|
// --- Downsampling ---
|
|
215
240
|
|
|
@@ -736,6 +761,39 @@ export function createDejitterRecorder() {
|
|
|
736
761
|
}
|
|
737
762
|
return findings;
|
|
738
763
|
}
|
|
764
|
+
function detectLagFindings(elements) {
|
|
765
|
+
const findings = [];
|
|
766
|
+
if (interactions.length === 0 || rawFrames.length === 0) return findings;
|
|
767
|
+
const lt = config.thresholds.lag;
|
|
768
|
+
for (const interaction of interactions) {
|
|
769
|
+
let firstFrame = null;
|
|
770
|
+
for (const frame of rawFrames) {
|
|
771
|
+
if (frame.t > interaction.t) {
|
|
772
|
+
firstFrame = frame;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!firstFrame) continue;
|
|
777
|
+
const delay = firstFrame.t - interaction.t;
|
|
778
|
+
if (delay < lt.minDelay) continue;
|
|
779
|
+
const severity = delay >= lt.highDelay ? 'high' : delay >= lt.medDelay ? 'medium' : 'low';
|
|
780
|
+
const firstChange = firstFrame.changes[0];
|
|
781
|
+
const label = firstChange && elements[firstChange.id] || {
|
|
782
|
+
tag: '?',
|
|
783
|
+
cls: '',
|
|
784
|
+
text: ''
|
|
785
|
+
};
|
|
786
|
+
findings.push(makeFinding('lag', severity, firstChange?.id || '?', label, interaction.type, `${delay}ms between ${interaction.type} at t=${interaction.t}ms and first visual change at t=${firstFrame.t}ms`, {
|
|
787
|
+
lag: {
|
|
788
|
+
interactionType: interaction.type,
|
|
789
|
+
interactionT: interaction.t,
|
|
790
|
+
firstChangeT: firstFrame.t,
|
|
791
|
+
delay
|
|
792
|
+
}
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
return findings;
|
|
796
|
+
}
|
|
739
797
|
function deduplicateShivers(findings) {
|
|
740
798
|
const shiverFindings = findings.filter(f => f.type === 'shiver');
|
|
741
799
|
const otherFindings = findings.filter(f => f.type !== 'shiver');
|
|
@@ -769,6 +827,7 @@ export function createDejitterRecorder() {
|
|
|
769
827
|
findings = findings.concat(detectJumpFindings(propStats, elements));
|
|
770
828
|
findings = findings.concat(detectStutterFindings(propStats, elements));
|
|
771
829
|
findings = findings.concat(detectStuckFindings(propStats, elements));
|
|
830
|
+
findings = findings.concat(detectLagFindings(elements));
|
|
772
831
|
findings = deduplicateShivers(findings);
|
|
773
832
|
const sevOrder = {
|
|
774
833
|
high: 0,
|
|
@@ -810,6 +869,7 @@ export function createDejitterRecorder() {
|
|
|
810
869
|
start() {
|
|
811
870
|
rawFrames = [];
|
|
812
871
|
mutations = [];
|
|
872
|
+
interactions = [];
|
|
813
873
|
lastSeen = new Map();
|
|
814
874
|
nextElemId = 0;
|
|
815
875
|
recording = true;
|
|
@@ -818,6 +878,7 @@ export function createDejitterRecorder() {
|
|
|
818
878
|
hasSeenChange = false;
|
|
819
879
|
onStopCallbacks = [];
|
|
820
880
|
startMutationObserver();
|
|
881
|
+
startInteractionListeners();
|
|
821
882
|
rafId = requestAnimationFrame(loop);
|
|
822
883
|
if (config.maxDuration > 0) {
|
|
823
884
|
stopTimer = setTimeout(() => api.stop(), config.maxDuration);
|
|
@@ -830,6 +891,7 @@ export function createDejitterRecorder() {
|
|
|
830
891
|
if (rafId) cancelAnimationFrame(rafId);
|
|
831
892
|
if (stopTimer) clearTimeout(stopTimer);
|
|
832
893
|
mutationObserver?.disconnect();
|
|
894
|
+
interactionAbort?.abort();
|
|
833
895
|
const msg = `Stopped. ${rawFrames.length} raw frames, ${mutations.length} mutation events.`;
|
|
834
896
|
for (const cb of onStopCallbacks) {
|
|
835
897
|
try {
|
|
@@ -892,7 +954,8 @@ export function createDejitterRecorder() {
|
|
|
892
954
|
getRaw() {
|
|
893
955
|
return {
|
|
894
956
|
rawFrames,
|
|
895
|
-
mutations
|
|
957
|
+
mutations,
|
|
958
|
+
interactions
|
|
896
959
|
};
|
|
897
960
|
},
|
|
898
961
|
toJSON() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelocator/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"locator",
|
package/src/dejitter/recorder.ts
CHANGED
|
@@ -20,11 +20,12 @@ export interface DejitterConfig {
|
|
|
20
20
|
stutter: { velocityRatio: number; maxFrames: number; minVelocity: number };
|
|
21
21
|
stuck: { minStillFrames: number; maxDelta: number; minSurroundingVelocity: number; highDuration: number; medDuration: number };
|
|
22
22
|
outlier: { ratioThreshold: number };
|
|
23
|
+
lag: { minDelay: number; highDelay: number; medDelay: number };
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface DejitterFinding {
|
|
27
|
-
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier';
|
|
28
|
+
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier' | 'lag';
|
|
28
29
|
severity: 'high' | 'medium' | 'low' | 'info';
|
|
29
30
|
elem: string;
|
|
30
31
|
elemLabel: { tag: string; cls: string; text: string };
|
|
@@ -50,7 +51,7 @@ export interface DejitterAPI {
|
|
|
50
51
|
getData(): any;
|
|
51
52
|
summary(json?: boolean): string | DejitterSummary;
|
|
52
53
|
findings(json?: boolean): string | DejitterFinding[];
|
|
53
|
-
getRaw(): { rawFrames: any[]; mutations: any[] };
|
|
54
|
+
getRaw(): { rawFrames: any[]; mutations: any[]; interactions: any[] };
|
|
54
55
|
toJSON(): string;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -70,6 +71,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
70
71
|
stutter: { velocityRatio: 0.3, maxFrames: 3, minVelocity: 0.5 },
|
|
71
72
|
stuck: { minStillFrames: 3, maxDelta: 0.5, minSurroundingVelocity: 1, highDuration: 500, medDuration: 200 },
|
|
72
73
|
outlier: { ratioThreshold: 3 },
|
|
74
|
+
lag: { minDelay: 50, highDelay: 200, medDelay: 100 },
|
|
73
75
|
},
|
|
74
76
|
};
|
|
75
77
|
|
|
@@ -79,6 +81,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
79
81
|
let rafId: number | null = null;
|
|
80
82
|
let stopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
81
83
|
let mutationObserver: MutationObserver | null = null;
|
|
84
|
+
let interactionAbort: AbortController | null = null;
|
|
82
85
|
let onStopCallbacks: Array<() => void> = [];
|
|
83
86
|
|
|
84
87
|
let lastSeen = new Map<string, Record<string, any>>();
|
|
@@ -86,6 +89,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
86
89
|
let lastChangeTime = 0;
|
|
87
90
|
let hasSeenChange = false;
|
|
88
91
|
let mutations: any[] = [];
|
|
92
|
+
let interactions: Array<{ t: number; type: string }> = [];
|
|
89
93
|
let nextElemId = 0;
|
|
90
94
|
|
|
91
95
|
function elemId(el: any): string {
|
|
@@ -212,6 +216,19 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
212
216
|
});
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
function startInteractionListeners(): void {
|
|
220
|
+
interactionAbort = new AbortController();
|
|
221
|
+
const opts: AddEventListenerOptions & { signal: AbortSignal } = { capture: true, signal: interactionAbort.signal };
|
|
222
|
+
const handler = (e: Event): void => {
|
|
223
|
+
if (!recording) return;
|
|
224
|
+
const t = Math.round(performance.now() - startTime);
|
|
225
|
+
interactions.push({ t, type: e.type });
|
|
226
|
+
};
|
|
227
|
+
for (const evt of ['click', 'pointerdown', 'keydown'] as const) {
|
|
228
|
+
document.addEventListener(evt, handler, opts);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
215
232
|
// --- Downsampling ---
|
|
216
233
|
|
|
217
234
|
function downsample(): any[] {
|
|
@@ -778,6 +795,49 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
778
795
|
return findings;
|
|
779
796
|
}
|
|
780
797
|
|
|
798
|
+
function detectLagFindings(elements: Record<string, any>): DejitterFinding[] {
|
|
799
|
+
const findings: DejitterFinding[] = [];
|
|
800
|
+
if (interactions.length === 0 || rawFrames.length === 0) return findings;
|
|
801
|
+
|
|
802
|
+
const lt = config.thresholds.lag;
|
|
803
|
+
|
|
804
|
+
for (const interaction of interactions) {
|
|
805
|
+
let firstFrame: { t: number; changes: any[] } | null = null;
|
|
806
|
+
for (const frame of rawFrames) {
|
|
807
|
+
if (frame.t > interaction.t) {
|
|
808
|
+
firstFrame = frame;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!firstFrame) continue;
|
|
814
|
+
|
|
815
|
+
const delay = firstFrame.t - interaction.t;
|
|
816
|
+
if (delay < lt.minDelay) continue;
|
|
817
|
+
|
|
818
|
+
const severity = delay >= lt.highDelay ? 'high' : delay >= lt.medDelay ? 'medium' : 'low';
|
|
819
|
+
|
|
820
|
+
const firstChange = firstFrame.changes[0];
|
|
821
|
+
const label = (firstChange && elements[firstChange.id]) || { tag: '?', cls: '', text: '' };
|
|
822
|
+
|
|
823
|
+
findings.push(makeFinding(
|
|
824
|
+
'lag', severity,
|
|
825
|
+
firstChange?.id || '?', label, interaction.type,
|
|
826
|
+
`${delay}ms between ${interaction.type} at t=${interaction.t}ms and first visual change at t=${firstFrame.t}ms`,
|
|
827
|
+
{
|
|
828
|
+
lag: {
|
|
829
|
+
interactionType: interaction.type,
|
|
830
|
+
interactionT: interaction.t,
|
|
831
|
+
firstChangeT: firstFrame.t,
|
|
832
|
+
delay,
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return findings;
|
|
839
|
+
}
|
|
840
|
+
|
|
781
841
|
function deduplicateShivers(findings: DejitterFinding[]): DejitterFinding[] {
|
|
782
842
|
const shiverFindings = findings.filter((f) => f.type === 'shiver');
|
|
783
843
|
const otherFindings = findings.filter((f) => f.type !== 'shiver');
|
|
@@ -814,6 +874,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
814
874
|
findings = findings.concat(detectJumpFindings(propStats, elements));
|
|
815
875
|
findings = findings.concat(detectStutterFindings(propStats, elements));
|
|
816
876
|
findings = findings.concat(detectStuckFindings(propStats, elements));
|
|
877
|
+
findings = findings.concat(detectLagFindings(elements));
|
|
817
878
|
findings = deduplicateShivers(findings);
|
|
818
879
|
|
|
819
880
|
const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
|
|
@@ -842,6 +903,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
842
903
|
start() {
|
|
843
904
|
rawFrames = [];
|
|
844
905
|
mutations = [];
|
|
906
|
+
interactions = [];
|
|
845
907
|
lastSeen = new Map();
|
|
846
908
|
nextElemId = 0;
|
|
847
909
|
recording = true;
|
|
@@ -851,6 +913,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
851
913
|
onStopCallbacks = [];
|
|
852
914
|
|
|
853
915
|
startMutationObserver();
|
|
916
|
+
startInteractionListeners();
|
|
854
917
|
rafId = requestAnimationFrame(loop);
|
|
855
918
|
|
|
856
919
|
if (config.maxDuration > 0) {
|
|
@@ -865,6 +928,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
865
928
|
if (rafId) cancelAnimationFrame(rafId);
|
|
866
929
|
if (stopTimer) clearTimeout(stopTimer);
|
|
867
930
|
mutationObserver?.disconnect();
|
|
931
|
+
interactionAbort?.abort();
|
|
868
932
|
|
|
869
933
|
const msg = `Stopped. ${rawFrames.length} raw frames, ${mutations.length} mutation events.`;
|
|
870
934
|
|
|
@@ -921,7 +985,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
921
985
|
},
|
|
922
986
|
|
|
923
987
|
getRaw() {
|
|
924
|
-
return { rawFrames, mutations };
|
|
988
|
+
return { rawFrames, mutations, interactions };
|
|
925
989
|
},
|
|
926
990
|
|
|
927
991
|
toJSON() {
|