react-render-pulse 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +318 -2
- package/dist/aggregate/aggregator.d.cts +27 -0
- package/dist/aggregate/aggregator.d.ts +28 -0
- package/dist/aggregate/aggregator.d.ts.map +1 -0
- package/dist/aggregate/histogram.d.cts +18 -0
- package/dist/aggregate/histogram.d.ts +19 -0
- package/dist/aggregate/histogram.d.ts.map +1 -0
- package/dist/components/RenderMonitor.d.cts +19 -0
- package/dist/components/RenderMonitor.d.ts +20 -0
- package/dist/components/RenderMonitor.d.ts.map +1 -0
- package/dist/context/device.d.cts +2 -0
- package/dist/context/device.d.ts +3 -0
- package/dist/context/device.d.ts.map +1 -0
- package/dist/context/route.d.cts +7 -0
- package/dist/context/route.d.ts +8 -0
- package/dist/context/route.d.ts.map +1 -0
- package/dist/core.cjs +56 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +2 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +28 -0
- package/dist/core.js.map +1 -0
- package/dist/export/console.d.cts +3 -0
- package/dist/export/console.d.ts +4 -0
- package/dist/export/console.d.ts.map +1 -0
- package/dist/export/otel.d.cts +34 -0
- package/dist/export/otel.d.ts +35 -0
- package/dist/export/otel.d.ts.map +1 -0
- package/dist/hooks/useRenderMonitor.d.cts +9 -0
- package/dist/hooks/useRenderMonitor.d.ts +10 -0
- package/dist/hooks/useRenderMonitor.d.ts.map +1 -0
- package/dist/index.cjs +634 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +602 -0
- package/dist/index.js.map +1 -0
- package/dist/modes/detect.d.cts +16 -0
- package/dist/modes/detect.d.ts +17 -0
- package/dist/modes/detect.d.ts.map +1 -0
- package/dist/observe/longtasks.d.cts +12 -0
- package/dist/observe/longtasks.d.ts +13 -0
- package/dist/observe/longtasks.d.ts.map +1 -0
- package/dist/observe/renderWindows.d.cts +17 -0
- package/dist/observe/renderWindows.d.ts +18 -0
- package/dist/observe/renderWindows.d.ts.map +1 -0
- package/dist/otel.cjs +65 -0
- package/dist/otel.cjs.map +1 -0
- package/dist/otel.d.cts +2 -0
- package/dist/otel.d.ts +3 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +38 -0
- package/dist/otel.js.map +1 -0
- package/dist/runtime/guard.d.cts +7 -0
- package/dist/runtime/guard.d.ts +8 -0
- package/dist/runtime/guard.d.ts.map +1 -0
- package/dist/runtime/initRenderPulse.d.cts +44 -0
- package/dist/runtime/initRenderPulse.d.ts +45 -0
- package/dist/runtime/initRenderPulse.d.ts.map +1 -0
- package/dist/runtime/runtime.d.cts +13 -0
- package/dist/runtime/runtime.d.ts +14 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/useIsomorphicLayoutEffect.d.cts +2 -0
- package/dist/runtime/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/runtime/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/sample/sample.d.cts +7 -0
- package/dist/sample/sample.d.ts +8 -0
- package/dist/sample/sample.d.ts.map +1 -0
- package/dist/transport/payload.d.cts +8 -0
- package/dist/transport/payload.d.ts +9 -0
- package/dist/transport/payload.d.ts.map +1 -0
- package/dist/transport/transport.d.cts +18 -0
- package/dist/transport/transport.d.ts +19 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/types.d.cts +120 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +128 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"use client";// src/aggregate/histogram.ts
|
|
2
|
+
var HISTOGRAM_BOUNDS = [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1e3];
|
|
3
|
+
function createHistogram() {
|
|
4
|
+
return new Array(HISTOGRAM_BOUNDS.length + 1).fill(0);
|
|
5
|
+
}
|
|
6
|
+
function bucketIndex(ms) {
|
|
7
|
+
for (let i = 0; i < HISTOGRAM_BOUNDS.length; i++) {
|
|
8
|
+
if (ms <= (HISTOGRAM_BOUNDS[i] ?? 0)) return i;
|
|
9
|
+
}
|
|
10
|
+
return HISTOGRAM_BOUNDS.length;
|
|
11
|
+
}
|
|
12
|
+
function histogramPercentile(hist, p, max, min = 0) {
|
|
13
|
+
const total = hist.reduce((sum2, n) => sum2 + n, 0);
|
|
14
|
+
if (total === 0) return 0;
|
|
15
|
+
const target = Math.max(p / 100 * total, Number.MIN_VALUE);
|
|
16
|
+
let seen = 0;
|
|
17
|
+
for (let i = 0; i < hist.length; i++) {
|
|
18
|
+
const count = hist[i] ?? 0;
|
|
19
|
+
if (count === 0) continue;
|
|
20
|
+
if (seen + count >= target) {
|
|
21
|
+
const upper = Math.min(HISTOGRAM_BOUNDS[i] ?? max, max);
|
|
22
|
+
const lower = Math.min(Math.max(i === 0 ? 0 : HISTOGRAM_BOUNDS[i - 1] ?? 0, min), upper);
|
|
23
|
+
const fraction = (target - seen) / count;
|
|
24
|
+
return round(Math.min(lower + (upper - lower) * fraction, max));
|
|
25
|
+
}
|
|
26
|
+
seen += count;
|
|
27
|
+
}
|
|
28
|
+
return round(max);
|
|
29
|
+
}
|
|
30
|
+
function round(ms) {
|
|
31
|
+
return Math.round(ms * 10) / 10;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/aggregate/aggregator.ts
|
|
35
|
+
function createAggregator(options) {
|
|
36
|
+
let areas = /* @__PURE__ */ new Map();
|
|
37
|
+
let longTasks = /* @__PURE__ */ new Map();
|
|
38
|
+
let longFrames = /* @__PURE__ */ new Map();
|
|
39
|
+
let droppedAreas = 0;
|
|
40
|
+
function addRender(event) {
|
|
41
|
+
const key = `${event.route}\0${event.area}`;
|
|
42
|
+
let area = areas.get(key);
|
|
43
|
+
if (!area) {
|
|
44
|
+
if (areas.size >= options.maxAreas) {
|
|
45
|
+
droppedAreas++;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
area = {
|
|
49
|
+
id: event.area,
|
|
50
|
+
route: event.route,
|
|
51
|
+
renders: 0,
|
|
52
|
+
mounts: 0,
|
|
53
|
+
slow: 0,
|
|
54
|
+
totalMs: 0,
|
|
55
|
+
min: Infinity,
|
|
56
|
+
max: 0,
|
|
57
|
+
hist: createHistogram(),
|
|
58
|
+
worst: []
|
|
59
|
+
};
|
|
60
|
+
areas.set(key, area);
|
|
61
|
+
}
|
|
62
|
+
const ms = event.duration;
|
|
63
|
+
area.renders++;
|
|
64
|
+
if (event.phase === "mount") area.mounts++;
|
|
65
|
+
if (ms > options.slowRenderMs) area.slow++;
|
|
66
|
+
area.totalMs += ms;
|
|
67
|
+
area.min = Math.min(area.min, ms);
|
|
68
|
+
area.max = Math.max(area.max, ms);
|
|
69
|
+
const bucket = bucketIndex(ms);
|
|
70
|
+
area.hist[bucket] = (area.hist[bucket] ?? 0) + 1;
|
|
71
|
+
addWorstCase(area.worst, { ms, phase: event.phase, at: event.startTime - options.sessionStart });
|
|
72
|
+
}
|
|
73
|
+
function addWorstCase(worst, candidate) {
|
|
74
|
+
if (worst.length < options.worstCases) {
|
|
75
|
+
worst.push(candidate);
|
|
76
|
+
} else {
|
|
77
|
+
const fastest = worst.reduce((min, w, i) => w.ms < (worst[min]?.ms ?? 0) ? i : min, 0);
|
|
78
|
+
if (candidate.ms <= (worst[fastest]?.ms ?? 0)) return;
|
|
79
|
+
worst[fastest] = candidate;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function addLongTask(event) {
|
|
83
|
+
const target = event.kind === "longtask" ? longTasks : longFrames;
|
|
84
|
+
const key = `${event.route}\0${event.area ?? ""}`;
|
|
85
|
+
let entry = target.get(key);
|
|
86
|
+
if (!entry) {
|
|
87
|
+
entry = { area: event.area, route: event.route, count: 0, totalMs: 0, maxMs: 0 };
|
|
88
|
+
target.set(key, entry);
|
|
89
|
+
}
|
|
90
|
+
entry.count++;
|
|
91
|
+
entry.totalMs += event.duration;
|
|
92
|
+
entry.maxMs = Math.max(entry.maxMs, event.duration);
|
|
93
|
+
}
|
|
94
|
+
function drain() {
|
|
95
|
+
const areaReports = Array.from(areas.values(), toAreaReport);
|
|
96
|
+
const taskReports = Array.from(longTasks.values(), toLongTaskReport);
|
|
97
|
+
const frameReports = Array.from(longFrames.values(), toLongTaskReport);
|
|
98
|
+
const batch = {
|
|
99
|
+
summary: {
|
|
100
|
+
renders: sum(areaReports, (a) => a.renders),
|
|
101
|
+
slowRenders: sum(areaReports, (a) => a.slow),
|
|
102
|
+
longTasks: sum(taskReports, (t) => t.count),
|
|
103
|
+
longTaskMs: round(sum(taskReports, (t) => t.totalMs)),
|
|
104
|
+
longFrames: sum(frameReports, (t) => t.count),
|
|
105
|
+
longFrameMs: round(sum(frameReports, (t) => t.totalMs))
|
|
106
|
+
},
|
|
107
|
+
areas: areaReports,
|
|
108
|
+
longTasks: taskReports,
|
|
109
|
+
longFrames: frameReports,
|
|
110
|
+
droppedAreas
|
|
111
|
+
};
|
|
112
|
+
areas = /* @__PURE__ */ new Map();
|
|
113
|
+
longTasks = /* @__PURE__ */ new Map();
|
|
114
|
+
longFrames = /* @__PURE__ */ new Map();
|
|
115
|
+
droppedAreas = 0;
|
|
116
|
+
return batch;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
addRender,
|
|
120
|
+
addLongTask,
|
|
121
|
+
isEmpty: () => areas.size === 0 && longTasks.size === 0 && longFrames.size === 0 && droppedAreas === 0,
|
|
122
|
+
drain
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function toAreaReport(area) {
|
|
126
|
+
return {
|
|
127
|
+
id: area.id,
|
|
128
|
+
route: area.route,
|
|
129
|
+
renders: area.renders,
|
|
130
|
+
mounts: area.mounts,
|
|
131
|
+
slow: area.slow,
|
|
132
|
+
totalMs: round(area.totalMs),
|
|
133
|
+
min: round(area.min),
|
|
134
|
+
max: round(area.max),
|
|
135
|
+
p50: histogramPercentile(area.hist, 50, area.max, area.min),
|
|
136
|
+
p75: histogramPercentile(area.hist, 75, area.max, area.min),
|
|
137
|
+
p95: histogramPercentile(area.hist, 95, area.max, area.min),
|
|
138
|
+
hist: area.hist,
|
|
139
|
+
worst: area.worst.sort((a, b) => b.ms - a.ms).map((w) => ({ ms: round(w.ms), phase: w.phase, at: Math.round(w.at) }))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function toLongTaskReport(entry) {
|
|
143
|
+
return { ...entry, totalMs: round(entry.totalMs), maxMs: round(entry.maxMs) };
|
|
144
|
+
}
|
|
145
|
+
function sum(items, pick) {
|
|
146
|
+
return items.reduce((total, item) => total + pick(item), 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/context/device.ts
|
|
150
|
+
function getDeviceContext() {
|
|
151
|
+
const nav = typeof navigator === "undefined" ? {} : navigator;
|
|
152
|
+
return {
|
|
153
|
+
memoryGb: typeof nav.deviceMemory === "number" ? nav.deviceMemory : null,
|
|
154
|
+
cores: typeof nav.hardwareConcurrency === "number" ? nav.hardwareConcurrency : null,
|
|
155
|
+
connection: nav.connection?.effectiveType ?? null,
|
|
156
|
+
viewport: typeof window === "undefined" ? "0x0" : `${window.innerWidth}x${window.innerHeight}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/context/route.ts
|
|
161
|
+
function defaultGetRoute() {
|
|
162
|
+
if (typeof location === "undefined") return "/";
|
|
163
|
+
return location.pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
164
|
+
}
|
|
165
|
+
function createRouteReader(getRoute) {
|
|
166
|
+
let lastHref = null;
|
|
167
|
+
let lastRoute = "/";
|
|
168
|
+
return () => {
|
|
169
|
+
const href = typeof location === "undefined" ? "" : location.href;
|
|
170
|
+
if (href !== lastHref) {
|
|
171
|
+
lastHref = href;
|
|
172
|
+
lastRoute = getRoute();
|
|
173
|
+
}
|
|
174
|
+
return lastRoute;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/export/console.ts
|
|
179
|
+
function createConsoleExporter() {
|
|
180
|
+
return {
|
|
181
|
+
onFlush(payload) {
|
|
182
|
+
if (typeof console === "undefined") return;
|
|
183
|
+
const rows = [...payload.areas].sort((a, b) => b.p75 - a.p75).map((a) => ({
|
|
184
|
+
area: a.id,
|
|
185
|
+
route: a.route,
|
|
186
|
+
renders: a.renders,
|
|
187
|
+
slow: a.slow,
|
|
188
|
+
p50: a.p50,
|
|
189
|
+
p75: a.p75,
|
|
190
|
+
p95: a.p95,
|
|
191
|
+
max: a.max
|
|
192
|
+
}));
|
|
193
|
+
console.groupCollapsed(
|
|
194
|
+
`[render-pulse] batch ${payload.session.seq} (${payload.session.mode}): ${payload.summary.renders} renders, ${payload.summary.longTasks} long tasks`
|
|
195
|
+
);
|
|
196
|
+
if (rows.length > 0) console.table(rows);
|
|
197
|
+
if (payload.longTasks.length > 0) console.table(payload.longTasks);
|
|
198
|
+
if (payload.longFrames.length > 0) console.table(payload.longFrames);
|
|
199
|
+
console.groupEnd();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/modes/detect.ts
|
|
205
|
+
function createModeDetector(option) {
|
|
206
|
+
let mode = option === "profiling" ? "profiling" : "lightweight";
|
|
207
|
+
return {
|
|
208
|
+
current: () => mode,
|
|
209
|
+
measuresWithMarks: () => mode === "lightweight",
|
|
210
|
+
profilerFired() {
|
|
211
|
+
if (option === "auto") mode = "profiling";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/observe/longtasks.ts
|
|
217
|
+
function observeLongTasks(thresholdMs, onEntry) {
|
|
218
|
+
if (typeof PerformanceObserver === "undefined") return () => {
|
|
219
|
+
};
|
|
220
|
+
const supported = PerformanceObserver.supportedEntryTypes ?? [];
|
|
221
|
+
const observers = [];
|
|
222
|
+
for (const kind of ["longtask", "long-animation-frame"]) {
|
|
223
|
+
if (!supported.includes(kind)) continue;
|
|
224
|
+
try {
|
|
225
|
+
const observer = new PerformanceObserver((list) => {
|
|
226
|
+
for (const entry of list.getEntries()) {
|
|
227
|
+
if (entry.duration < thresholdMs) continue;
|
|
228
|
+
onEntry({ kind, startTime: entry.startTime, duration: entry.duration });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
observer.observe({ type: kind, buffered: true });
|
|
232
|
+
observers.push(observer);
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return () => {
|
|
237
|
+
for (const observer of observers) observer.disconnect();
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/observe/renderWindows.ts
|
|
242
|
+
function createRenderWindows(size = 200) {
|
|
243
|
+
const windows = new Array(size);
|
|
244
|
+
let next = 0;
|
|
245
|
+
return {
|
|
246
|
+
add(area, start2, end, duration) {
|
|
247
|
+
windows[next] = { area, start: start2, end, duration };
|
|
248
|
+
next = (next + 1) % size;
|
|
249
|
+
},
|
|
250
|
+
findArea(start2, end) {
|
|
251
|
+
const overlapping = windows.filter((w) => w !== void 0 && w.end >= start2 && w.start <= end);
|
|
252
|
+
if (overlapping.length === 0) return null;
|
|
253
|
+
const longest = Math.max(...overlapping.map((w) => w.duration));
|
|
254
|
+
let best = null;
|
|
255
|
+
for (const window2 of overlapping) {
|
|
256
|
+
if (window2.duration < longest / 2) continue;
|
|
257
|
+
if (!best || window2.duration < best.duration) best = window2;
|
|
258
|
+
}
|
|
259
|
+
return best?.area ?? null;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/sample/sample.ts
|
|
265
|
+
function shouldSample(sampleRate, random = Math.random) {
|
|
266
|
+
if (!(sampleRate > 0)) return false;
|
|
267
|
+
if (sampleRate >= 1) return true;
|
|
268
|
+
return random() < sampleRate;
|
|
269
|
+
}
|
|
270
|
+
function createSessionId() {
|
|
271
|
+
try {
|
|
272
|
+
const cryptoApi = globalThis.crypto;
|
|
273
|
+
if (cryptoApi && typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/transport/payload.ts
|
|
280
|
+
function byteLength(text) {
|
|
281
|
+
return typeof TextEncoder === "undefined" ? text.length * 3 : new TextEncoder().encode(text).length;
|
|
282
|
+
}
|
|
283
|
+
function serializePayload(payload, maxBytes) {
|
|
284
|
+
let json = JSON.stringify(payload);
|
|
285
|
+
if (byteLength(json) <= maxBytes) return json;
|
|
286
|
+
let trimmed = { ...payload, areas: payload.areas.map((a) => ({ ...a, worst: [] })) };
|
|
287
|
+
json = JSON.stringify(trimmed);
|
|
288
|
+
if (byteLength(json) > maxBytes) {
|
|
289
|
+
const areas = [...trimmed.areas].sort((a, b) => b.renders - a.renders);
|
|
290
|
+
let dropped = trimmed.dropped.areas;
|
|
291
|
+
while (areas.length > 0 && byteLength(json) > maxBytes) {
|
|
292
|
+
const remove = Math.max(1, Math.floor(areas.length / 2));
|
|
293
|
+
areas.splice(areas.length - remove, remove);
|
|
294
|
+
dropped += remove;
|
|
295
|
+
trimmed = { ...trimmed, areas: [...areas], dropped: { areas: dropped } };
|
|
296
|
+
json = JSON.stringify(trimmed);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return byteLength(json) <= maxBytes ? json : null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/transport/transport.ts
|
|
303
|
+
function createTransport(options) {
|
|
304
|
+
const queue = [];
|
|
305
|
+
function enqueue(item) {
|
|
306
|
+
if (item.attempts >= options.maxAttempts) return;
|
|
307
|
+
queue.push(item);
|
|
308
|
+
if (queue.length > options.maxQueued) queue.shift();
|
|
309
|
+
}
|
|
310
|
+
function beacon(body) {
|
|
311
|
+
try {
|
|
312
|
+
return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? navigator.sendBeacon(options.endpoint, body) : false;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function post(item) {
|
|
318
|
+
if (typeof fetch !== "function") return;
|
|
319
|
+
const attempt = { body: item.body, attempts: item.attempts + 1 };
|
|
320
|
+
try {
|
|
321
|
+
fetch(options.endpoint, { method: "POST", body: item.body, keepalive: true }).then((response) => {
|
|
322
|
+
if (response.status >= 500 || response.status === 429) enqueue(attempt);
|
|
323
|
+
}).catch(() => enqueue(attempt));
|
|
324
|
+
} catch {
|
|
325
|
+
enqueue(attempt);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
send(body, { unloading }) {
|
|
330
|
+
const items = [...queue.splice(0), { body, attempts: 0 }];
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
if (unloading && beacon(item.body)) continue;
|
|
333
|
+
post(item);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/runtime/guard.ts
|
|
340
|
+
var debug = false;
|
|
341
|
+
function setDebug(value) {
|
|
342
|
+
debug = value;
|
|
343
|
+
}
|
|
344
|
+
function guard(fn, fallback) {
|
|
345
|
+
try {
|
|
346
|
+
return fn();
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (debug && typeof console !== "undefined") console.warn("[render-pulse]", error);
|
|
349
|
+
return fallback;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/runtime/runtime.ts
|
|
354
|
+
var KEY = /* @__PURE__ */ Symbol.for("react-render-pulse.runtime");
|
|
355
|
+
function getRuntime() {
|
|
356
|
+
return globalThis[KEY] ?? null;
|
|
357
|
+
}
|
|
358
|
+
function setRuntime(runtime) {
|
|
359
|
+
globalThis[KEY] = runtime;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/runtime/initRenderPulse.ts
|
|
363
|
+
var INERT = { sampled: false, mode: "lightweight", flush: () => null, destroy: () => {
|
|
364
|
+
} };
|
|
365
|
+
var active = null;
|
|
366
|
+
function initRenderPulse(options = {}) {
|
|
367
|
+
if (typeof window === "undefined") return INERT;
|
|
368
|
+
return guard(() => start(options), INERT);
|
|
369
|
+
}
|
|
370
|
+
function start(options) {
|
|
371
|
+
active?.destroy();
|
|
372
|
+
const {
|
|
373
|
+
endpoint,
|
|
374
|
+
sampleRate = 1,
|
|
375
|
+
mode: modeOption = "auto",
|
|
376
|
+
slowRenderMs = 16,
|
|
377
|
+
longTaskMs = 50,
|
|
378
|
+
flushIntervalMs = 18e4,
|
|
379
|
+
debug: debug2 = !endpoint
|
|
380
|
+
} = options;
|
|
381
|
+
const maxPayloadBytes = clamp(options.maxPayloadBytes ?? 6e4, 1e3, 65536);
|
|
382
|
+
const maxAreas = clamp(options.maxAreas ?? 200, 1, 1e3);
|
|
383
|
+
const worstCases = clamp(options.worstCases ?? 5, 0, 50);
|
|
384
|
+
const appVersion = options.appVersion?.slice(0, 100);
|
|
385
|
+
setDebug(debug2);
|
|
386
|
+
const sampled = shouldSample(sampleRate);
|
|
387
|
+
const now = () => performance.now();
|
|
388
|
+
const sessionStart = now();
|
|
389
|
+
const startedAt = Date.now();
|
|
390
|
+
const sessionId = createSessionId();
|
|
391
|
+
let seq = 0;
|
|
392
|
+
const detector = createModeDetector(modeOption);
|
|
393
|
+
const readRoute = createRouteReader(options.getRoute ?? defaultGetRoute);
|
|
394
|
+
const route = () => guard(readRoute, "/").slice(0, 500);
|
|
395
|
+
const areaId = (id) => id ? id.slice(0, 200) : "(unnamed)";
|
|
396
|
+
const exporters = [...options.exporters ?? [], ...debug2 ? [createConsoleExporter()] : []];
|
|
397
|
+
const aggregator = createAggregator({ slowRenderMs, maxAreas, worstCases, sessionStart });
|
|
398
|
+
const windows = createRenderWindows();
|
|
399
|
+
const transport = endpoint ? createTransport({ endpoint, maxAttempts: 3, maxQueued: 5 }) : null;
|
|
400
|
+
function record(event) {
|
|
401
|
+
aggregator.addRender(event);
|
|
402
|
+
windows.add(event.area, event.startTime, event.endTime, event.duration);
|
|
403
|
+
for (const exporter of exporters) guard(() => exporter.onRender?.(event), void 0);
|
|
404
|
+
}
|
|
405
|
+
function recordLongTask(event) {
|
|
406
|
+
aggregator.addLongTask(event);
|
|
407
|
+
for (const exporter of exporters) guard(() => exporter.onLongTask?.(event), void 0);
|
|
408
|
+
}
|
|
409
|
+
const runtime = {
|
|
410
|
+
sampled,
|
|
411
|
+
now,
|
|
412
|
+
measuresWithMarks: () => sampled && detector.measuresWithMarks(),
|
|
413
|
+
recordMarks(area, phase, startTime, endTime) {
|
|
414
|
+
if (!sampled) return;
|
|
415
|
+
guard(() => {
|
|
416
|
+
const duration = endTime - startTime;
|
|
417
|
+
record({ area: areaId(area), route: route(), phase, duration, startTime, endTime, mode: "lightweight" });
|
|
418
|
+
}, void 0);
|
|
419
|
+
},
|
|
420
|
+
recordProfiler(area, phase, actualDuration, startTime, commitTime) {
|
|
421
|
+
if (!sampled) return false;
|
|
422
|
+
return guard(() => {
|
|
423
|
+
detector.profilerFired();
|
|
424
|
+
if (detector.current() !== "profiling") return false;
|
|
425
|
+
record({
|
|
426
|
+
area: areaId(area),
|
|
427
|
+
route: route(),
|
|
428
|
+
phase,
|
|
429
|
+
duration: actualDuration,
|
|
430
|
+
startTime,
|
|
431
|
+
endTime: commitTime,
|
|
432
|
+
mode: "profiling"
|
|
433
|
+
});
|
|
434
|
+
return true;
|
|
435
|
+
}, false);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
function flush(unloading) {
|
|
439
|
+
if (!sampled || aggregator.isEmpty()) return null;
|
|
440
|
+
return guard(() => {
|
|
441
|
+
const batch = aggregator.drain();
|
|
442
|
+
const payload = {
|
|
443
|
+
v: 1,
|
|
444
|
+
session: {
|
|
445
|
+
id: sessionId,
|
|
446
|
+
seq: ++seq,
|
|
447
|
+
mode: detector.current(),
|
|
448
|
+
route: route(),
|
|
449
|
+
appVersion: appVersion ?? null,
|
|
450
|
+
startedAt,
|
|
451
|
+
sentAt: Date.now()
|
|
452
|
+
},
|
|
453
|
+
device: getDeviceContext(),
|
|
454
|
+
summary: batch.summary,
|
|
455
|
+
areas: batch.areas,
|
|
456
|
+
longTasks: batch.longTasks,
|
|
457
|
+
longFrames: batch.longFrames,
|
|
458
|
+
dropped: { areas: batch.droppedAreas }
|
|
459
|
+
};
|
|
460
|
+
for (const exporter of exporters) guard(() => exporter.onFlush?.(payload), void 0);
|
|
461
|
+
if (transport) {
|
|
462
|
+
const body = serializePayload(payload, maxPayloadBytes);
|
|
463
|
+
if (body) transport.send(body, { unloading });
|
|
464
|
+
}
|
|
465
|
+
return payload;
|
|
466
|
+
}, null);
|
|
467
|
+
}
|
|
468
|
+
const cleanups = [];
|
|
469
|
+
if (sampled) {
|
|
470
|
+
cleanups.push(
|
|
471
|
+
observeLongTasks(
|
|
472
|
+
longTaskMs,
|
|
473
|
+
(task) => guard(() => {
|
|
474
|
+
const area = windows.findArea(task.startTime, task.startTime + task.duration);
|
|
475
|
+
recordLongTask({ ...task, area, route: route() });
|
|
476
|
+
}, void 0)
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
const onHidden = () => {
|
|
480
|
+
if (document.visibilityState === "hidden") flush(true);
|
|
481
|
+
};
|
|
482
|
+
const onPageHide = () => flush(true);
|
|
483
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
484
|
+
window.addEventListener("pagehide", onPageHide);
|
|
485
|
+
cleanups.push(() => {
|
|
486
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
487
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
488
|
+
});
|
|
489
|
+
if (flushIntervalMs > 0) {
|
|
490
|
+
const timer = setInterval(() => flush(false), flushIntervalMs);
|
|
491
|
+
cleanups.push(() => clearInterval(timer));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
setRuntime(runtime);
|
|
495
|
+
const instance = {
|
|
496
|
+
destroy() {
|
|
497
|
+
for (const cleanup of cleanups.splice(0)) guard(cleanup, void 0);
|
|
498
|
+
if (getRuntime() === runtime) setRuntime(null);
|
|
499
|
+
if (active === instance) active = null;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
active = instance;
|
|
503
|
+
return {
|
|
504
|
+
sampled,
|
|
505
|
+
get mode() {
|
|
506
|
+
return detector.current();
|
|
507
|
+
},
|
|
508
|
+
flush: () => flush(false),
|
|
509
|
+
destroy: instance.destroy
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function clamp(value, min, max) {
|
|
513
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.floor(value))) : min;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/components/RenderMonitor.tsx
|
|
517
|
+
import { Profiler, useCallback, useRef } from "react";
|
|
518
|
+
|
|
519
|
+
// src/runtime/useIsomorphicLayoutEffect.ts
|
|
520
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
521
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
522
|
+
|
|
523
|
+
// src/components/RenderMonitor.tsx
|
|
524
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
525
|
+
function RenderMonitor({ id, children }) {
|
|
526
|
+
const stateRef = useRef(null);
|
|
527
|
+
if (stateRef.current === null) {
|
|
528
|
+
stateRef.current = { id, mounted: false, start: 0, end: 0, recordedStart: -1, pending: null };
|
|
529
|
+
}
|
|
530
|
+
const state = stateRef.current;
|
|
531
|
+
state.id = id;
|
|
532
|
+
const onRender = useCallback(
|
|
533
|
+
(_id, phase, actualDuration, _baseDuration, startTime, commitTime) => {
|
|
534
|
+
if (getRuntime()?.recordProfiler(state.id, phase, actualDuration, startTime, commitTime) && state.pending) {
|
|
535
|
+
state.pending.superseded = true;
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
[state]
|
|
539
|
+
);
|
|
540
|
+
return /* @__PURE__ */ jsxs(Profiler, { id, onRender, children: [
|
|
541
|
+
/* @__PURE__ */ jsx(StartMarker, { state }),
|
|
542
|
+
children,
|
|
543
|
+
/* @__PURE__ */ jsx(EndMarker, { state })
|
|
544
|
+
] });
|
|
545
|
+
}
|
|
546
|
+
function StartMarker({ state }) {
|
|
547
|
+
const runtime = getRuntime();
|
|
548
|
+
if (runtime?.measuresWithMarks()) state.start = runtime.now();
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
function EndMarker({ state }) {
|
|
552
|
+
const runtime = getRuntime();
|
|
553
|
+
if (runtime?.measuresWithMarks()) state.end = runtime.now();
|
|
554
|
+
useIsomorphicLayoutEffect(() => {
|
|
555
|
+
guard(() => {
|
|
556
|
+
const current = getRuntime();
|
|
557
|
+
if (!current?.measuresWithMarks() || state.start === state.recordedStart || state.end < state.start) return;
|
|
558
|
+
state.recordedStart = state.start;
|
|
559
|
+
const measurement = {
|
|
560
|
+
start: state.start,
|
|
561
|
+
end: state.end,
|
|
562
|
+
phase: state.mounted ? "update" : "mount",
|
|
563
|
+
superseded: false
|
|
564
|
+
};
|
|
565
|
+
state.pending = measurement;
|
|
566
|
+
state.mounted = true;
|
|
567
|
+
queueMicrotask(() => {
|
|
568
|
+
if (state.pending === measurement) state.pending = null;
|
|
569
|
+
if (!measurement.superseded) {
|
|
570
|
+
getRuntime()?.recordMarks(state.id, measurement.phase, measurement.start, measurement.end);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}, void 0);
|
|
574
|
+
});
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/hooks/useRenderMonitor.ts
|
|
579
|
+
import { useRef as useRef2 } from "react";
|
|
580
|
+
function useRenderMonitor(id) {
|
|
581
|
+
const state = useRef2({ start: 0, recordedStart: -1, mounted: false }).current;
|
|
582
|
+
const runtime = getRuntime();
|
|
583
|
+
if (runtime?.sampled) state.start = runtime.now();
|
|
584
|
+
useIsomorphicLayoutEffect(() => {
|
|
585
|
+
guard(() => {
|
|
586
|
+
const current = getRuntime();
|
|
587
|
+
if (!current?.sampled || state.start === state.recordedStart) return;
|
|
588
|
+
state.recordedStart = state.start;
|
|
589
|
+
current.recordMarks(id, state.mounted ? "update" : "mount", state.start, current.now());
|
|
590
|
+
state.mounted = true;
|
|
591
|
+
}, void 0);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
export {
|
|
595
|
+
HISTOGRAM_BOUNDS,
|
|
596
|
+
RenderMonitor,
|
|
597
|
+
createConsoleExporter,
|
|
598
|
+
histogramPercentile,
|
|
599
|
+
initRenderPulse,
|
|
600
|
+
useRenderMonitor
|
|
601
|
+
};
|
|
602
|
+
//# sourceMappingURL=index.js.map
|