nuxt-devtools-observatory 0.1.10 → 0.1.12
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/client/dist/assets/index-BugqFK5S.css +1 -0
- package/client/dist/assets/index-DILjvs09.js +17 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +67 -13
- package/client/src/views/ComposableTracker.vue +386 -116
- package/client/src/views/ProvideInjectGraph.vue +232 -61
- package/client/src/views/RenderHeatmap.vue +239 -69
- package/client/src/views/TransitionTimeline.vue +2 -2
- package/dist/module.json +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +20 -0
- package/dist/runtime/composables/composable-registry.js +178 -53
- package/dist/runtime/composables/provide-inject-registry.d.ts +13 -1
- package/dist/runtime/composables/provide-inject-registry.js +34 -6
- package/dist/runtime/composables/render-registry.d.ts +19 -6
- package/dist/runtime/composables/render-registry.js +70 -35
- package/dist/runtime/composables/transition-registry.js +13 -7
- package/dist/runtime/plugin.js +43 -12
- package/package.json +7 -3
- package/client/dist/assets/index-BBp7Dvrp.css +0 -1
- package/client/dist/assets/index-mEAMrJUv.js +0 -17
|
@@ -1,10 +1,89 @@
|
|
|
1
|
-
import { ref, isRef, isReadonly, unref, getCurrentInstance, onUnmounted } from "vue";
|
|
1
|
+
import { ref, isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
|
|
2
2
|
export function setupComposableRegistry() {
|
|
3
3
|
const entries = ref(/* @__PURE__ */ new Map());
|
|
4
|
+
const liveRefs = /* @__PURE__ */ new Map();
|
|
5
|
+
const liveRefWatchers = /* @__PURE__ */ new Map();
|
|
6
|
+
const MAX_HISTORY = 50;
|
|
7
|
+
const entryHistory = /* @__PURE__ */ new Map();
|
|
8
|
+
const prevValues = /* @__PURE__ */ new Map();
|
|
9
|
+
const rawRefs = /* @__PURE__ */ new Map();
|
|
10
|
+
function computeSharedKeys(id, name) {
|
|
11
|
+
const ownRaw = rawRefs.get(id);
|
|
12
|
+
if (!ownRaw) return [];
|
|
13
|
+
const shared = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const [otherId, entry] of entries.value.entries()) {
|
|
15
|
+
if (otherId === id || entry.name !== name) continue;
|
|
16
|
+
const otherRaw = rawRefs.get(otherId);
|
|
17
|
+
if (!otherRaw) continue;
|
|
18
|
+
for (const [key, obj] of Object.entries(ownRaw)) {
|
|
19
|
+
if (key in otherRaw && otherRaw[key] === obj) {
|
|
20
|
+
shared.add(key);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...shared];
|
|
25
|
+
}
|
|
26
|
+
let currentRoute = "/";
|
|
27
|
+
function setRoute(path) {
|
|
28
|
+
currentRoute = path;
|
|
29
|
+
}
|
|
30
|
+
function getRoute() {
|
|
31
|
+
return currentRoute;
|
|
32
|
+
}
|
|
4
33
|
function register(entry) {
|
|
5
34
|
entries.value.set(entry.id, entry);
|
|
6
35
|
emit("composable:register", entry);
|
|
7
36
|
}
|
|
37
|
+
function registerLiveRefs(id, refs) {
|
|
38
|
+
const prevStop = liveRefWatchers.get(id);
|
|
39
|
+
if (prevStop) prevStop();
|
|
40
|
+
liveRefWatchers.delete(id);
|
|
41
|
+
if (Object.keys(refs).length === 0) {
|
|
42
|
+
liveRefs.delete(id);
|
|
43
|
+
rawRefs.delete(id);
|
|
44
|
+
prevValues.delete(id);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
liveRefs.set(id, refs);
|
|
48
|
+
prevValues.set(
|
|
49
|
+
id,
|
|
50
|
+
Object.fromEntries(
|
|
51
|
+
Object.entries(refs).map(([k, r]) => {
|
|
52
|
+
try {
|
|
53
|
+
return [k, JSON.stringify(unref(r)) ?? ""];
|
|
54
|
+
} catch {
|
|
55
|
+
return [k, ""];
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
const stop = watchEffect(() => {
|
|
61
|
+
const prev = prevValues.get(id) ?? {};
|
|
62
|
+
const now = {};
|
|
63
|
+
const t = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
64
|
+
for (const [k, r] of Object.entries(refs)) {
|
|
65
|
+
const val = unref(r);
|
|
66
|
+
const serialised = JSON.stringify(val) ?? "";
|
|
67
|
+
now[k] = serialised;
|
|
68
|
+
if (serialised !== prev[k]) {
|
|
69
|
+
const history = entryHistory.get(id) ?? [];
|
|
70
|
+
history.push({ t, key: k, value: safeValue(val) });
|
|
71
|
+
if (history.length > MAX_HISTORY) history.shift();
|
|
72
|
+
entryHistory.set(id, history);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
prevValues.set(id, now);
|
|
76
|
+
_onChange?.();
|
|
77
|
+
});
|
|
78
|
+
liveRefWatchers.set(id, stop);
|
|
79
|
+
}
|
|
80
|
+
function registerRawRefs(id, refs) {
|
|
81
|
+
rawRefs.set(id, refs);
|
|
82
|
+
}
|
|
83
|
+
let _onChange = null;
|
|
84
|
+
function onComposableChange(cb) {
|
|
85
|
+
_onChange = cb;
|
|
86
|
+
}
|
|
8
87
|
function update(id, patch) {
|
|
9
88
|
const existing = entries.value.get(id);
|
|
10
89
|
if (!existing) {
|
|
@@ -31,6 +110,25 @@ export function setupComposableRegistry() {
|
|
|
31
110
|
return val;
|
|
32
111
|
}
|
|
33
112
|
function sanitize(entry) {
|
|
113
|
+
const live = liveRefs.get(entry.id);
|
|
114
|
+
const hasLive = live != null && Object.keys(live).length > 0;
|
|
115
|
+
const freshRefs = hasLive ? Object.fromEntries(
|
|
116
|
+
Object.entries(live).map(([k, r]) => [
|
|
117
|
+
k,
|
|
118
|
+
{
|
|
119
|
+
type: entry.refs[k]?.type ?? "ref",
|
|
120
|
+
value: safeValue(unref(r))
|
|
121
|
+
}
|
|
122
|
+
])
|
|
123
|
+
) : Object.fromEntries(
|
|
124
|
+
Object.entries(entry.refs).map(([k, v]) => [
|
|
125
|
+
k,
|
|
126
|
+
{
|
|
127
|
+
type: v.type,
|
|
128
|
+
value: safeValue(typeof v.value === "object" && v.value !== null && "value" in v.value ? v.value.value : v.value)
|
|
129
|
+
}
|
|
130
|
+
])
|
|
131
|
+
);
|
|
34
132
|
return {
|
|
35
133
|
id: entry.id,
|
|
36
134
|
name: entry.name,
|
|
@@ -39,20 +137,15 @@ export function setupComposableRegistry() {
|
|
|
39
137
|
status: entry.status,
|
|
40
138
|
leak: entry.leak,
|
|
41
139
|
leakReason: entry.leakReason,
|
|
42
|
-
refs:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
type: v.type,
|
|
47
|
-
value: safeValue(typeof v.value === "object" && v.value !== null && "value" in v.value ? v.value.value : v.value)
|
|
48
|
-
}
|
|
49
|
-
])
|
|
50
|
-
),
|
|
140
|
+
refs: freshRefs,
|
|
141
|
+
history: entryHistory.get(entry.id) ?? [],
|
|
142
|
+
sharedKeys: computeSharedKeys(entry.id, entry.name),
|
|
51
143
|
watcherCount: entry.watcherCount,
|
|
52
144
|
intervalCount: entry.intervalCount,
|
|
53
145
|
lifecycle: entry.lifecycle,
|
|
54
146
|
file: entry.file,
|
|
55
|
-
line: entry.line
|
|
147
|
+
line: entry.line,
|
|
148
|
+
route: entry.route
|
|
56
149
|
};
|
|
57
150
|
}
|
|
58
151
|
function getAll() {
|
|
@@ -65,7 +158,17 @@ export function setupComposableRegistry() {
|
|
|
65
158
|
const channel = window.__nuxt_devtools__?.channel;
|
|
66
159
|
channel?.send(event, data);
|
|
67
160
|
}
|
|
68
|
-
|
|
161
|
+
function clear() {
|
|
162
|
+
for (const stop of liveRefWatchers.values()) stop();
|
|
163
|
+
liveRefWatchers.clear();
|
|
164
|
+
liveRefs.clear();
|
|
165
|
+
rawRefs.clear();
|
|
166
|
+
prevValues.clear();
|
|
167
|
+
entryHistory.clear();
|
|
168
|
+
entries.value.clear();
|
|
169
|
+
emit("composable:clear", {});
|
|
170
|
+
}
|
|
171
|
+
return { register, registerLiveRefs, registerRawRefs, onComposableChange, clear, setRoute, getRoute, update, getAll };
|
|
69
172
|
}
|
|
70
173
|
export function __trackComposable(name, callFn, meta) {
|
|
71
174
|
if (!import.meta.dev) {
|
|
@@ -79,40 +182,53 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
79
182
|
return callFn();
|
|
80
183
|
}
|
|
81
184
|
const instance = getCurrentInstance();
|
|
82
|
-
const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}`;
|
|
185
|
+
const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
|
|
83
186
|
const trackedIntervals = [];
|
|
84
|
-
const originalSetInterval = window.setInterval;
|
|
85
|
-
const originalClearInterval = window.clearInterval;
|
|
86
187
|
const clearedIntervals = /* @__PURE__ */ new Set();
|
|
87
|
-
window.setInterval
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
188
|
+
const alreadyPatched = !!window.setInterval.__obs;
|
|
189
|
+
let originalSetInterval = null;
|
|
190
|
+
let originalClearInterval = null;
|
|
191
|
+
if (!alreadyPatched) {
|
|
192
|
+
originalSetInterval = window.setInterval;
|
|
193
|
+
originalClearInterval = window.clearInterval;
|
|
194
|
+
const patchedSetInterval = ((fn, ms, ...rest) => {
|
|
195
|
+
const timerId = originalSetInterval(fn, ms, ...rest);
|
|
196
|
+
trackedIntervals.push(timerId);
|
|
197
|
+
return timerId;
|
|
198
|
+
});
|
|
199
|
+
patchedSetInterval.__obs = true;
|
|
200
|
+
window.setInterval = patchedSetInterval;
|
|
201
|
+
window.clearInterval = ((timerId) => {
|
|
202
|
+
if (timerId !== void 0) clearedIntervals.add(timerId);
|
|
203
|
+
originalClearInterval(timerId);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
98
206
|
const effectsBefore = new Set(instance?.scope?.effects ?? []);
|
|
99
207
|
const mountedHooksBefore = instance?.bm?.length ?? 0;
|
|
100
208
|
const unmountedHooksBefore = instance?.um?.length ?? 0;
|
|
101
209
|
const result = callFn();
|
|
102
|
-
|
|
103
|
-
|
|
210
|
+
if (!alreadyPatched && originalSetInterval) {
|
|
211
|
+
window.setInterval = originalSetInterval;
|
|
212
|
+
window.clearInterval = originalClearInterval;
|
|
213
|
+
}
|
|
104
214
|
const trackedWatchers = (instance?.scope?.effects ?? []).filter((effect) => !effectsBefore.has(effect)).map((effect) => ({
|
|
105
215
|
effect,
|
|
106
216
|
stop: () => effect.stop?.()
|
|
107
217
|
}));
|
|
108
218
|
const refs = {};
|
|
219
|
+
const liveRefMap = {};
|
|
220
|
+
const rawRefMap = {};
|
|
109
221
|
if (result && typeof result === "object") {
|
|
110
222
|
for (const [key, val] of Object.entries(result)) {
|
|
111
223
|
if (isRef(val)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
224
|
+
const type = isReadonly(val) ? "computed" : "ref";
|
|
225
|
+
refs[key] = { type, value: safeSnapshot(unref(val)) };
|
|
226
|
+
liveRefMap[key] = val;
|
|
227
|
+
rawRefMap[key] = val;
|
|
228
|
+
} else if (isReactive(val)) {
|
|
229
|
+
refs[key] = { type: "reactive", value: safeSnapshot(val) };
|
|
230
|
+
liveRefMap[key] = computed(() => val);
|
|
231
|
+
rawRefMap[key] = val;
|
|
116
232
|
}
|
|
117
233
|
}
|
|
118
234
|
}
|
|
@@ -124,6 +240,9 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
124
240
|
status: "mounted",
|
|
125
241
|
leak: false,
|
|
126
242
|
refs,
|
|
243
|
+
history: [],
|
|
244
|
+
sharedKeys: [],
|
|
245
|
+
// computed lazily in sanitize() after all instances are registered
|
|
127
246
|
watcherCount: trackedWatchers.length,
|
|
128
247
|
intervalCount: trackedIntervals.length,
|
|
129
248
|
lifecycle: {
|
|
@@ -133,31 +252,37 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
133
252
|
intervalsCleaned: true
|
|
134
253
|
},
|
|
135
254
|
file: meta.file,
|
|
136
|
-
line: meta.line
|
|
255
|
+
line: meta.line,
|
|
256
|
+
route: registry.getRoute()
|
|
137
257
|
};
|
|
138
258
|
registry.register(entry);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
leak,
|
|
153
|
-
leakReason: reasons.join(" \xB7 ") || void 0,
|
|
154
|
-
lifecycle: {
|
|
155
|
-
...entry.lifecycle,
|
|
156
|
-
watchersCleaned: leakedWatchers.length === 0,
|
|
157
|
-
intervalsCleaned: leakedIntervals.length === 0
|
|
259
|
+
registry.registerLiveRefs(id, liveRefMap);
|
|
260
|
+
registry.registerRawRefs(id, rawRefMap);
|
|
261
|
+
if (instance) {
|
|
262
|
+
onUnmounted(() => {
|
|
263
|
+
const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);
|
|
264
|
+
const leakedIntervals = trackedIntervals.filter((timerId) => !clearedIntervals.has(timerId));
|
|
265
|
+
const leak = leakedWatchers.length > 0 || leakedIntervals.length > 0;
|
|
266
|
+
const reasons = [];
|
|
267
|
+
if (leakedWatchers.length > 0) {
|
|
268
|
+
reasons.push(`${leakedWatchers.length} watcher${leakedWatchers.length > 1 ? "s" : ""} still active after unmount`);
|
|
269
|
+
}
|
|
270
|
+
if (leakedIntervals.length > 0) {
|
|
271
|
+
reasons.push(`setInterval #${leakedIntervals.join(", #")} never cleared`);
|
|
158
272
|
}
|
|
273
|
+
registry.update(id, {
|
|
274
|
+
status: "unmounted",
|
|
275
|
+
leak,
|
|
276
|
+
leakReason: reasons.join(" \xB7 ") || void 0,
|
|
277
|
+
lifecycle: {
|
|
278
|
+
...entry.lifecycle,
|
|
279
|
+
watchersCleaned: leakedWatchers.length === 0,
|
|
280
|
+
intervalsCleaned: leakedIntervals.length === 0
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
registry.registerLiveRefs(id, {});
|
|
159
284
|
});
|
|
160
|
-
}
|
|
285
|
+
}
|
|
161
286
|
return result;
|
|
162
287
|
}
|
|
163
288
|
function safeSnapshot(value) {
|
|
@@ -8,6 +8,17 @@ export interface ProvideEntry {
|
|
|
8
8
|
isReactive: boolean;
|
|
9
9
|
valueSnapshot: unknown;
|
|
10
10
|
line: number;
|
|
11
|
+
/**
|
|
12
|
+
* Scope of the provide:
|
|
13
|
+
* - 'global' — provided via app.provide() or at the app root (no parent component)
|
|
14
|
+
* - 'layout' — provided by a layout component (file path contains 'layout')
|
|
15
|
+
* - 'component' — provided by a regular component
|
|
16
|
+
*/
|
|
17
|
+
scope: 'global' | 'layout' | 'component';
|
|
18
|
+
/**
|
|
19
|
+
* True when a parent component already provides the same key — this provide shadows it.
|
|
20
|
+
*/
|
|
21
|
+
isShadowing: boolean;
|
|
11
22
|
}
|
|
12
23
|
export interface InjectEntry {
|
|
13
24
|
key: string;
|
|
@@ -24,7 +35,7 @@ export interface InjectEntry {
|
|
|
24
35
|
/**
|
|
25
36
|
* Sets up the provide/inject registry, which tracks all provide/inject calls
|
|
26
37
|
* and their associated metadata (e.g. component name, file, line).
|
|
27
|
-
* @returns {object} The provide/inject registry with `registerProvide`, `registerInject`, and `
|
|
38
|
+
* @returns {object} The provide/inject registry with `registerProvide`, `registerInject`, `getAll`, and `clear` members.
|
|
28
39
|
*/
|
|
29
40
|
export declare function setupProvideInjectRegistry(): {
|
|
30
41
|
registerProvide: (entry: ProvideEntry) => void;
|
|
@@ -33,6 +44,7 @@ export declare function setupProvideInjectRegistry(): {
|
|
|
33
44
|
provides: ProvideEntry[];
|
|
34
45
|
injects: InjectEntry[];
|
|
35
46
|
};
|
|
47
|
+
clear: () => void;
|
|
36
48
|
};
|
|
37
49
|
export declare function __devProvide(key: string | symbol, value: unknown, meta: {
|
|
38
50
|
file: string;
|
|
@@ -3,13 +3,28 @@ export function setupProvideInjectRegistry() {
|
|
|
3
3
|
const provides = ref([]);
|
|
4
4
|
const injects = ref([]);
|
|
5
5
|
function registerProvide(entry) {
|
|
6
|
-
provides.value.
|
|
6
|
+
const idx = provides.value.findIndex((e) => e.key === entry.key && e.componentUid === entry.componentUid);
|
|
7
|
+
if (idx !== -1) {
|
|
8
|
+
provides.value[idx] = entry;
|
|
9
|
+
} else {
|
|
10
|
+
provides.value.push(entry);
|
|
11
|
+
}
|
|
7
12
|
emit("provide:register", entry);
|
|
8
13
|
}
|
|
9
14
|
function registerInject(entry) {
|
|
10
|
-
injects.value.
|
|
15
|
+
const idx = injects.value.findIndex((e) => e.key === entry.key && e.componentUid === entry.componentUid);
|
|
16
|
+
if (idx !== -1) {
|
|
17
|
+
injects.value[idx] = entry;
|
|
18
|
+
} else {
|
|
19
|
+
injects.value.push(entry);
|
|
20
|
+
}
|
|
11
21
|
emit("inject:register", entry);
|
|
12
22
|
}
|
|
23
|
+
function clear() {
|
|
24
|
+
provides.value = [];
|
|
25
|
+
injects.value = [];
|
|
26
|
+
emit("provide:clear", {});
|
|
27
|
+
}
|
|
13
28
|
function safeValue(val) {
|
|
14
29
|
if (val === void 0 || val === null) {
|
|
15
30
|
return val;
|
|
@@ -36,7 +51,9 @@ export function setupProvideInjectRegistry() {
|
|
|
36
51
|
parentFile: entry.parentFile,
|
|
37
52
|
isReactive: entry.isReactive,
|
|
38
53
|
valueSnapshot: safeValue(entry.valueSnapshot),
|
|
39
|
-
line: entry.line
|
|
54
|
+
line: entry.line,
|
|
55
|
+
scope: entry.scope,
|
|
56
|
+
isShadowing: entry.isShadowing
|
|
40
57
|
};
|
|
41
58
|
}
|
|
42
59
|
function sanitizeInject(entry) {
|
|
@@ -66,7 +83,7 @@ export function setupProvideInjectRegistry() {
|
|
|
66
83
|
const channel = window.__nuxt_devtools__?.channel;
|
|
67
84
|
channel?.send(event, data);
|
|
68
85
|
}
|
|
69
|
-
return { registerProvide, registerInject, getAll };
|
|
86
|
+
return { registerProvide, registerInject, getAll, clear };
|
|
70
87
|
}
|
|
71
88
|
export function __devProvide(key, value, meta) {
|
|
72
89
|
provide(key, value);
|
|
@@ -78,8 +95,17 @@ export function __devProvide(key, value, meta) {
|
|
|
78
95
|
return;
|
|
79
96
|
}
|
|
80
97
|
const instance = getCurrentInstance();
|
|
98
|
+
const keyStr = String(key);
|
|
99
|
+
const file = meta.file.toLowerCase();
|
|
100
|
+
let scope = "component";
|
|
101
|
+
if (!instance?.parent || instance?.parent?.type === instance?.appContext?.app?._component) {
|
|
102
|
+
scope = "global";
|
|
103
|
+
} else if (file.includes("layout") || file.includes("layouts")) {
|
|
104
|
+
scope = "layout";
|
|
105
|
+
}
|
|
106
|
+
const isShadowing = findProvider(keyStr, instance) !== null;
|
|
81
107
|
registry.registerProvide({
|
|
82
|
-
key:
|
|
108
|
+
key: keyStr,
|
|
83
109
|
componentName: instance?.type?.__name ?? "unknown",
|
|
84
110
|
componentFile: meta.file,
|
|
85
111
|
componentUid: instance?.uid ?? -1,
|
|
@@ -87,7 +113,9 @@ export function __devProvide(key, value, meta) {
|
|
|
87
113
|
parentFile: instance?.parent?.type?.__file,
|
|
88
114
|
isReactive: isRef(value) || isReactive(value),
|
|
89
115
|
valueSnapshot: safeSnapshot(unref(value)),
|
|
90
|
-
line: meta.line
|
|
116
|
+
line: meta.line,
|
|
117
|
+
scope,
|
|
118
|
+
isShadowing
|
|
91
119
|
});
|
|
92
120
|
}
|
|
93
121
|
export function __devInject(key, defaultValue, meta) {
|
|
@@ -3,7 +3,11 @@ export interface RenderEntry {
|
|
|
3
3
|
name: string;
|
|
4
4
|
file: string;
|
|
5
5
|
element?: string;
|
|
6
|
-
|
|
6
|
+
/** Total times this instance mounted (usually 1, >1 means it was unmounted+remounted) */
|
|
7
|
+
mountCount: number;
|
|
8
|
+
/** Re-renders triggered by reactive state changes (excludes initial mount) */
|
|
9
|
+
rerenders: number;
|
|
10
|
+
/** Subset of rerenders that happened within 800ms of a navigation */
|
|
7
11
|
totalMs: number;
|
|
8
12
|
avgMs: number;
|
|
9
13
|
triggers: Array<{
|
|
@@ -19,18 +23,27 @@ export interface RenderEntry {
|
|
|
19
23
|
top: number;
|
|
20
24
|
left: number;
|
|
21
25
|
};
|
|
22
|
-
children: number[];
|
|
23
26
|
parentUid?: number;
|
|
27
|
+
/** True if this component survived at least one reset() — indicates a layout/persistent component */
|
|
28
|
+
isPersistent: boolean;
|
|
29
|
+
/** True if the first mount of this component happened during SSR hydration */
|
|
30
|
+
isHydrationMount: boolean;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
|
-
* Sets up a render registry for
|
|
27
|
-
*
|
|
28
|
-
* @param {object} nuxtApp
|
|
29
|
-
* @
|
|
33
|
+
* Sets up a render registry for tracking render-related metrics (e.g. rerenders, render time, etc.)
|
|
34
|
+
* The registry is exposed over the WebSocket channel, and can be accessed from the browser's devtools.
|
|
35
|
+
* @param {object} nuxtApp - The Nuxt app instance.
|
|
36
|
+
* @param {import('vue').App} nuxtApp.vueApp - The Vue app instance used to register lifecycle hooks.
|
|
37
|
+
* @param {object} [options] - Optional configuration object.
|
|
38
|
+
* @param {function(): boolean} [options.isHydrating] - Function to determine if the current render is during SSR hydration.
|
|
39
|
+
* @returns {object} An object containing the render registry's API methods: `getAll()`, `snapshot()`, and `reset()`.
|
|
30
40
|
*/
|
|
31
41
|
export declare function setupRenderRegistry(nuxtApp: {
|
|
32
42
|
vueApp: import('vue').App;
|
|
43
|
+
}, options?: {
|
|
44
|
+
isHydrating?: () => boolean;
|
|
33
45
|
}): {
|
|
34
46
|
getAll: () => RenderEntry[];
|
|
35
47
|
snapshot: () => RenderEntry[];
|
|
48
|
+
reset: () => void;
|
|
36
49
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
|
-
export function setupRenderRegistry(nuxtApp) {
|
|
2
|
+
export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
3
3
|
const entries = ref(/* @__PURE__ */ new Map());
|
|
4
|
+
const pendingTriggeredRenders = /* @__PURE__ */ new Set();
|
|
5
|
+
const renderStartTimes = /* @__PURE__ */ new Map();
|
|
6
|
+
let preResetUids = /* @__PURE__ */ new Set();
|
|
4
7
|
function ensureEntry(instance) {
|
|
5
8
|
const uid = instance.$.uid;
|
|
6
9
|
if (!entries.value.has(uid)) {
|
|
@@ -23,61 +26,90 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
23
26
|
const uid = instance.$.uid;
|
|
24
27
|
entries.value.delete(uid);
|
|
25
28
|
}
|
|
29
|
+
function startRenderTimer(uid) {
|
|
30
|
+
if (typeof performance === "undefined") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
renderStartTimes.set(uid, performance.now());
|
|
34
|
+
}
|
|
35
|
+
function recordRenderDuration(entry) {
|
|
36
|
+
if (typeof performance === "undefined") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const startedAt = renderStartTimes.get(entry.uid);
|
|
40
|
+
if (startedAt === void 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
renderStartTimes.delete(entry.uid);
|
|
44
|
+
entry.totalMs += Math.max(performance.now() - startedAt, 0);
|
|
45
|
+
const totalEvents = (entry.rerenders ?? 0) + Math.max(entry.mountCount, 1);
|
|
46
|
+
entry.avgMs = Math.round(entry.totalMs / totalEvents * 10) / 10;
|
|
47
|
+
}
|
|
48
|
+
function reset() {
|
|
49
|
+
preResetUids = new Set(entries.value.keys());
|
|
50
|
+
pendingTriggeredRenders.clear();
|
|
51
|
+
renderStartTimes.clear();
|
|
52
|
+
for (const entry of entries.value.values()) {
|
|
53
|
+
entry.rerenders = 0;
|
|
54
|
+
entry.totalMs = 0;
|
|
55
|
+
entry.avgMs = 0;
|
|
56
|
+
entry.triggers = [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
26
59
|
nuxtApp.vueApp.mixin({
|
|
60
|
+
beforeMount() {
|
|
61
|
+
startRenderTimer(this.$.uid);
|
|
62
|
+
},
|
|
27
63
|
mounted() {
|
|
28
64
|
const entry = ensureEntry(this);
|
|
29
|
-
entry.
|
|
65
|
+
entry.mountCount++;
|
|
66
|
+
if (preResetUids.has(entry.uid)) {
|
|
67
|
+
entry.isPersistent = true;
|
|
68
|
+
}
|
|
69
|
+
const isHydration = options.isHydrating?.() ?? false;
|
|
70
|
+
if (isHydration && entry.mountCount === 1) {
|
|
71
|
+
entry.isHydrationMount = true;
|
|
72
|
+
} else if (entry.mountCount > 1) {
|
|
73
|
+
entry.rerenders++;
|
|
74
|
+
}
|
|
30
75
|
syncRect(entry, this);
|
|
31
|
-
|
|
76
|
+
recordRenderDuration(entry);
|
|
77
|
+
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
78
|
+
},
|
|
79
|
+
beforeUpdate() {
|
|
80
|
+
startRenderTimer(this.$.uid);
|
|
32
81
|
},
|
|
33
82
|
renderTriggered({ key, type }) {
|
|
34
83
|
const entry = ensureEntry(this);
|
|
35
84
|
entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
|
|
85
|
+
pendingTriggeredRenders.add(entry.uid);
|
|
36
86
|
if (entry.triggers.length > 50) {
|
|
37
87
|
entry.triggers.shift();
|
|
38
88
|
}
|
|
39
89
|
},
|
|
40
90
|
updated() {
|
|
41
91
|
const entry = ensureEntry(this);
|
|
42
|
-
entry.
|
|
92
|
+
pendingTriggeredRenders.delete(entry.uid);
|
|
93
|
+
entry.rerenders++;
|
|
43
94
|
syncRect(entry, this);
|
|
44
|
-
|
|
95
|
+
recordRenderDuration(entry);
|
|
96
|
+
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
45
97
|
},
|
|
46
98
|
unmounted() {
|
|
99
|
+
pendingTriggeredRenders.delete(this.$.uid);
|
|
100
|
+
renderStartTimes.delete(this.$.uid);
|
|
47
101
|
removeEntry(this);
|
|
48
102
|
emit("render:remove", { uid: this.$.uid });
|
|
49
103
|
}
|
|
50
104
|
});
|
|
51
|
-
if (import.meta.client && typeof PerformanceObserver !== "undefined") {
|
|
52
|
-
const observer = new PerformanceObserver((list) => {
|
|
53
|
-
for (const perf of list.getEntries()) {
|
|
54
|
-
if (!perf.name.includes("vue-component-render")) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const uidMatch = perf.name.match(/uid:(\d+)/);
|
|
58
|
-
if (!uidMatch) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const uid = Number(uidMatch[1]);
|
|
62
|
-
const entry = entries.value.get(uid);
|
|
63
|
-
if (entry) {
|
|
64
|
-
entry.totalMs += perf.duration;
|
|
65
|
-
entry.avgMs = Math.round(entry.totalMs / entry.renders * 10) / 10;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
try {
|
|
70
|
-
observer.observe({ entryTypes: ["measure"] });
|
|
71
|
-
} catch {
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
105
|
function sanitize(entry) {
|
|
75
106
|
return {
|
|
76
107
|
uid: entry.uid,
|
|
77
108
|
name: entry.name,
|
|
78
109
|
file: entry.file,
|
|
79
110
|
element: entry.element,
|
|
80
|
-
|
|
111
|
+
mountCount: entry.mountCount,
|
|
112
|
+
rerenders: entry.rerenders,
|
|
81
113
|
totalMs: entry.totalMs,
|
|
82
114
|
avgMs: entry.avgMs,
|
|
83
115
|
triggers: entry.triggers,
|
|
@@ -89,8 +121,9 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
89
121
|
top: entry.rect.top,
|
|
90
122
|
left: entry.rect.left
|
|
91
123
|
} : void 0,
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
parentUid: entry.parentUid,
|
|
125
|
+
isPersistent: entry.isPersistent,
|
|
126
|
+
isHydrationMount: entry.isHydrationMount
|
|
94
127
|
};
|
|
95
128
|
}
|
|
96
129
|
function getAll() {
|
|
@@ -106,7 +139,7 @@ export function setupRenderRegistry(nuxtApp) {
|
|
|
106
139
|
const channel = window.__nuxt_devtools__?.channel;
|
|
107
140
|
channel?.send(event, data);
|
|
108
141
|
}
|
|
109
|
-
return { getAll, snapshot };
|
|
142
|
+
return { getAll, snapshot, reset };
|
|
110
143
|
}
|
|
111
144
|
function makeEntry(uid, instance) {
|
|
112
145
|
const type = instance.$.type;
|
|
@@ -120,12 +153,14 @@ function makeEntry(uid, instance) {
|
|
|
120
153
|
name: ownLabel ?? inferAnonymousLabel(parentLabel, element) ?? `Component#${uid}`,
|
|
121
154
|
file,
|
|
122
155
|
element,
|
|
123
|
-
|
|
156
|
+
mountCount: 0,
|
|
157
|
+
rerenders: 0,
|
|
124
158
|
totalMs: 0,
|
|
125
159
|
avgMs: 0,
|
|
126
160
|
triggers: [],
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
parentUid: instance.$parent?.$.uid,
|
|
162
|
+
isPersistent: false,
|
|
163
|
+
isHydrationMount: false
|
|
129
164
|
};
|
|
130
165
|
}
|
|
131
166
|
function describeElement(el) {
|
|
@@ -18,13 +18,19 @@ export function setupTransitionRegistry() {
|
|
|
18
18
|
emit("transition:update", updated);
|
|
19
19
|
}
|
|
20
20
|
function sanitize(entry) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
return {
|
|
22
|
+
id: entry.id,
|
|
23
|
+
transitionName: entry.transitionName,
|
|
24
|
+
parentComponent: entry.parentComponent,
|
|
25
|
+
direction: entry.direction,
|
|
26
|
+
phase: entry.phase,
|
|
27
|
+
startTime: entry.startTime,
|
|
28
|
+
endTime: entry.endTime,
|
|
29
|
+
durationMs: entry.durationMs,
|
|
30
|
+
cancelled: entry.cancelled,
|
|
31
|
+
appear: entry.appear,
|
|
32
|
+
mode: entry.mode
|
|
33
|
+
};
|
|
28
34
|
}
|
|
29
35
|
function getAll() {
|
|
30
36
|
return [...entries.value.values()].map(sanitize);
|