react-dev-profiler 1.0.0
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 +111 -0
- package/dist/index.cjs +628 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +179 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +601 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
// src/DevProfiler.tsx
|
|
2
|
+
import { Profiler, useRef as useRef4, useState as useState4, useEffect as useEffect4, useCallback as useCallback3 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/DevProfiler.module.css
|
|
5
|
+
var DevProfiler_default = {};
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var HISTORY_SIZE = 60;
|
|
9
|
+
var INITIAL_PROFILER = {
|
|
10
|
+
phase: "mount",
|
|
11
|
+
actualDuration: 0,
|
|
12
|
+
baseDuration: 0,
|
|
13
|
+
commitCount: 0
|
|
14
|
+
};
|
|
15
|
+
var INITIAL_STATS = {
|
|
16
|
+
domMutations: 0,
|
|
17
|
+
domNodes: 0,
|
|
18
|
+
frameTime: 0,
|
|
19
|
+
frameTimeMin: 0,
|
|
20
|
+
frameTimeMax: 0,
|
|
21
|
+
frameTimeP99: 0,
|
|
22
|
+
frameTimeHistory: [],
|
|
23
|
+
longTasks: 0,
|
|
24
|
+
rendersPerSecond: 0,
|
|
25
|
+
memory: 0,
|
|
26
|
+
dimensions: "\u2013",
|
|
27
|
+
profiler: INITIAL_PROFILER
|
|
28
|
+
};
|
|
29
|
+
function percentile(sorted, p) {
|
|
30
|
+
if (sorted.length === 0) return 0;
|
|
31
|
+
const idx = Math.ceil(p / 100 * sorted.length) - 1;
|
|
32
|
+
return sorted[Math.max(0, idx)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/env.ts
|
|
36
|
+
var __DEV__ = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
|
|
37
|
+
|
|
38
|
+
// src/hooks.ts
|
|
39
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
40
|
+
function useAnchorPosition(ref, position = "bottom-left") {
|
|
41
|
+
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!ref.current) return;
|
|
44
|
+
const update = () => {
|
|
45
|
+
if (!ref.current) return;
|
|
46
|
+
const rect = ref.current.getBoundingClientRect();
|
|
47
|
+
const margin = 8;
|
|
48
|
+
switch (position) {
|
|
49
|
+
case "top-left":
|
|
50
|
+
setPos({ top: rect.top + margin, left: rect.left + margin });
|
|
51
|
+
break;
|
|
52
|
+
case "top-right":
|
|
53
|
+
setPos({ top: rect.top + margin, left: rect.right - margin });
|
|
54
|
+
break;
|
|
55
|
+
case "bottom-right":
|
|
56
|
+
setPos({ top: rect.bottom - margin, left: rect.right - margin });
|
|
57
|
+
break;
|
|
58
|
+
case "bottom-left":
|
|
59
|
+
default:
|
|
60
|
+
setPos({ top: rect.bottom - margin, left: rect.left + margin });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const observer = new ResizeObserver(update);
|
|
65
|
+
observer.observe(ref.current);
|
|
66
|
+
observer.observe(document.documentElement);
|
|
67
|
+
return () => observer.disconnect();
|
|
68
|
+
}, [ref, position]);
|
|
69
|
+
return pos;
|
|
70
|
+
}
|
|
71
|
+
function useDraggable() {
|
|
72
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
73
|
+
const dragging = useRef(false);
|
|
74
|
+
const start = useRef({ x: 0, y: 0 });
|
|
75
|
+
const onPointerDown = useCallback((e) => {
|
|
76
|
+
dragging.current = true;
|
|
77
|
+
start.current = { x: e.clientX - offset.x, y: e.clientY - offset.y };
|
|
78
|
+
e.target.setPointerCapture(e.pointerId);
|
|
79
|
+
}, [offset]);
|
|
80
|
+
const onPointerMove = useCallback((e) => {
|
|
81
|
+
if (!dragging.current) return;
|
|
82
|
+
setOffset({
|
|
83
|
+
x: e.clientX - start.current.x,
|
|
84
|
+
y: e.clientY - start.current.y
|
|
85
|
+
});
|
|
86
|
+
}, []);
|
|
87
|
+
const onPointerUp = useCallback(() => {
|
|
88
|
+
dragging.current = false;
|
|
89
|
+
}, []);
|
|
90
|
+
return { offset, handlers: { onPointerDown, onPointerMove, onPointerUp } };
|
|
91
|
+
}
|
|
92
|
+
function useDomTracker(wrapperRef, enabled) {
|
|
93
|
+
const mutations = useRef(0);
|
|
94
|
+
const nodeCount = useRef(0);
|
|
95
|
+
const dirty = useRef(true);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!enabled || !wrapperRef.current) return;
|
|
98
|
+
const observer = new MutationObserver((records) => {
|
|
99
|
+
let realMutation = false;
|
|
100
|
+
for (const record of records) {
|
|
101
|
+
if (record.type === "attributes" && record.attributeName === "class") continue;
|
|
102
|
+
realMutation = true;
|
|
103
|
+
}
|
|
104
|
+
if (realMutation) {
|
|
105
|
+
mutations.current++;
|
|
106
|
+
dirty.current = true;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
observer.observe(wrapperRef.current, {
|
|
110
|
+
childList: true,
|
|
111
|
+
subtree: true,
|
|
112
|
+
attributes: true,
|
|
113
|
+
attributeFilter: ["style"]
|
|
114
|
+
});
|
|
115
|
+
return () => observer.disconnect();
|
|
116
|
+
}, [wrapperRef, enabled]);
|
|
117
|
+
const getNodeCount = useCallback(() => {
|
|
118
|
+
if (dirty.current && wrapperRef.current) {
|
|
119
|
+
nodeCount.current = wrapperRef.current.querySelectorAll("*").length;
|
|
120
|
+
dirty.current = false;
|
|
121
|
+
}
|
|
122
|
+
return nodeCount.current;
|
|
123
|
+
}, [wrapperRef]);
|
|
124
|
+
const reset = useCallback(() => {
|
|
125
|
+
mutations.current = 0;
|
|
126
|
+
dirty.current = true;
|
|
127
|
+
}, []);
|
|
128
|
+
return { mutations, getNodeCount, reset };
|
|
129
|
+
}
|
|
130
|
+
function useRenderRate() {
|
|
131
|
+
const renderCount = useRef(0);
|
|
132
|
+
const rendersPerSecond = useRef(0);
|
|
133
|
+
const tick = useCallback(() => {
|
|
134
|
+
renderCount.current++;
|
|
135
|
+
}, []);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const id = setInterval(() => {
|
|
138
|
+
rendersPerSecond.current = renderCount.current;
|
|
139
|
+
renderCount.current = 0;
|
|
140
|
+
}, 1e3);
|
|
141
|
+
return () => clearInterval(id);
|
|
142
|
+
}, []);
|
|
143
|
+
const reset = useCallback(() => {
|
|
144
|
+
renderCount.current = 0;
|
|
145
|
+
rendersPerSecond.current = 0;
|
|
146
|
+
}, []);
|
|
147
|
+
return { rendersPerSecond, tick, reset };
|
|
148
|
+
}
|
|
149
|
+
function useRenderFlash(wrapperRef, open) {
|
|
150
|
+
const mutationCount = useRef(0);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!open || !wrapperRef.current) return;
|
|
153
|
+
const observer = new MutationObserver(() => {
|
|
154
|
+
mutationCount.current++;
|
|
155
|
+
if (!wrapperRef.current || mutationCount.current <= 1) return;
|
|
156
|
+
const el = wrapperRef.current;
|
|
157
|
+
el.classList.add(DevProfiler_default.flash);
|
|
158
|
+
setTimeout(() => el.classList.remove(DevProfiler_default.flash), 150);
|
|
159
|
+
});
|
|
160
|
+
observer.observe(wrapperRef.current, { childList: true, subtree: true });
|
|
161
|
+
return () => observer.disconnect();
|
|
162
|
+
}, [wrapperRef, open]);
|
|
163
|
+
}
|
|
164
|
+
function useLongTasks(enabled) {
|
|
165
|
+
const count = useRef(0);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!enabled || typeof PerformanceObserver === "undefined") return;
|
|
168
|
+
try {
|
|
169
|
+
const observer = new PerformanceObserver((list) => {
|
|
170
|
+
count.current += list.getEntries().length;
|
|
171
|
+
});
|
|
172
|
+
observer.observe({ entryTypes: ["longtask"] });
|
|
173
|
+
return () => observer.disconnect();
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}, [enabled]);
|
|
177
|
+
const reset = useCallback(() => {
|
|
178
|
+
count.current = 0;
|
|
179
|
+
}, []);
|
|
180
|
+
return { count, reset };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/DevStatsPanel.tsx
|
|
184
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
185
|
+
import { createPortal } from "react-dom";
|
|
186
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
187
|
+
function FrameTimeGraph({ history }) {
|
|
188
|
+
const max = Math.max(33, ...history);
|
|
189
|
+
const w = 140;
|
|
190
|
+
const h = 32;
|
|
191
|
+
const barW = Math.max(1, w / HISTORY_SIZE - 0.5);
|
|
192
|
+
return /* @__PURE__ */ jsx("div", { className: DevProfiler_default.graphWrap, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, style: { display: "block" }, children: [
|
|
193
|
+
/* @__PURE__ */ jsx("rect", { width: w, height: h, rx: 3, fill: "#111" }),
|
|
194
|
+
/* @__PURE__ */ jsx(
|
|
195
|
+
"line",
|
|
196
|
+
{
|
|
197
|
+
x1: 0,
|
|
198
|
+
y1: h - 16.67 / max * h,
|
|
199
|
+
x2: w,
|
|
200
|
+
y2: h - 16.67 / max * h,
|
|
201
|
+
stroke: "#1a3a1a",
|
|
202
|
+
strokeWidth: 1
|
|
203
|
+
}
|
|
204
|
+
),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"line",
|
|
207
|
+
{
|
|
208
|
+
x1: 0,
|
|
209
|
+
y1: h - 33 / max * h,
|
|
210
|
+
x2: w,
|
|
211
|
+
y2: h - 33 / max * h,
|
|
212
|
+
stroke: "#3a1a1a",
|
|
213
|
+
strokeWidth: 1
|
|
214
|
+
}
|
|
215
|
+
),
|
|
216
|
+
history.map((ms, i) => {
|
|
217
|
+
const x = i / HISTORY_SIZE * w;
|
|
218
|
+
const barH = Math.min(ms / max * h, h);
|
|
219
|
+
const color = ms > 33 ? "#ef4444" : ms > 16.67 ? "#f59e0b" : "#4ade80";
|
|
220
|
+
return /* @__PURE__ */ jsx("rect", { x, y: h - barH, width: barW, height: barH, fill: color, opacity: 0.8, rx: 0.5 }, i);
|
|
221
|
+
})
|
|
222
|
+
] }) });
|
|
223
|
+
}
|
|
224
|
+
function StatRow({ label, value, sub, color = "#4ade80" }) {
|
|
225
|
+
return /* @__PURE__ */ jsxs("div", { className: DevProfiler_default.row, children: [
|
|
226
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.rowLabel, children: label }),
|
|
227
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
228
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.rowValue, style: { color }, children: value }),
|
|
229
|
+
sub && /* @__PURE__ */ jsx("span", { style: { color: "#444", fontSize: 9, marginLeft: 4 }, children: sub })
|
|
230
|
+
] })
|
|
231
|
+
] });
|
|
232
|
+
}
|
|
233
|
+
function getPanelStyle(pos, offset, position) {
|
|
234
|
+
const style = {
|
|
235
|
+
transform: `translate(${offset.x}px, ${offset.y}px)`
|
|
236
|
+
};
|
|
237
|
+
if (position.startsWith("bottom")) {
|
|
238
|
+
style.bottom = window.innerHeight - pos.top;
|
|
239
|
+
} else {
|
|
240
|
+
style.top = pos.top;
|
|
241
|
+
}
|
|
242
|
+
if (position.endsWith("right")) {
|
|
243
|
+
style.right = window.innerWidth - pos.left;
|
|
244
|
+
} else {
|
|
245
|
+
style.left = pos.left;
|
|
246
|
+
}
|
|
247
|
+
return style;
|
|
248
|
+
}
|
|
249
|
+
function DevStatsPanel({
|
|
250
|
+
targetRef,
|
|
251
|
+
domTracker,
|
|
252
|
+
renderRate,
|
|
253
|
+
longTasks,
|
|
254
|
+
profilerData,
|
|
255
|
+
onClose,
|
|
256
|
+
onReset,
|
|
257
|
+
position = "bottom-left",
|
|
258
|
+
instanceId,
|
|
259
|
+
instanceCount = 1
|
|
260
|
+
}) {
|
|
261
|
+
const [stats, setStats] = useState2(INITIAL_STATS);
|
|
262
|
+
const [exported, setExported] = useState2(false);
|
|
263
|
+
const lastFrame = useRef2(performance.now());
|
|
264
|
+
const frameTimeHistory = useRef2([]);
|
|
265
|
+
const allFrameTimes = useRef2([]);
|
|
266
|
+
const pos = useAnchorPosition(targetRef, position);
|
|
267
|
+
const { offset, handlers: dragHandlers } = useDraggable();
|
|
268
|
+
useEffect2(() => {
|
|
269
|
+
let animId;
|
|
270
|
+
let frameCount = 0;
|
|
271
|
+
let frameTotalMs = 0;
|
|
272
|
+
let lastSecond = performance.now();
|
|
273
|
+
const tick = () => {
|
|
274
|
+
const now = performance.now();
|
|
275
|
+
const delta = now - lastFrame.current;
|
|
276
|
+
lastFrame.current = now;
|
|
277
|
+
frameCount++;
|
|
278
|
+
frameTotalMs += delta;
|
|
279
|
+
if (now - lastSecond >= 1e3) {
|
|
280
|
+
const avgFrameTime = frameTotalMs / frameCount;
|
|
281
|
+
const hist = frameTimeHistory.current;
|
|
282
|
+
if (hist.length >= HISTORY_SIZE) hist.shift();
|
|
283
|
+
hist.push(avgFrameTime);
|
|
284
|
+
allFrameTimes.current.push(avgFrameTime);
|
|
285
|
+
const sorted = [...allFrameTimes.current].sort((a, b) => a - b);
|
|
286
|
+
const el = targetRef.current;
|
|
287
|
+
const dims = el ? `${el.offsetWidth} x ${el.offsetHeight}` : "\u2013";
|
|
288
|
+
const perf = performance;
|
|
289
|
+
const mem = perf.memory ? Math.round(perf.memory.usedJSHeapSize / 1024 / 1024) : 0;
|
|
290
|
+
setStats({
|
|
291
|
+
domMutations: domTracker.mutations.current,
|
|
292
|
+
domNodes: domTracker.getNodeCount(),
|
|
293
|
+
frameTime: avgFrameTime,
|
|
294
|
+
frameTimeMin: sorted[0] ?? 0,
|
|
295
|
+
frameTimeMax: sorted[sorted.length - 1] ?? 0,
|
|
296
|
+
frameTimeP99: percentile(sorted, 99),
|
|
297
|
+
frameTimeHistory: [...hist],
|
|
298
|
+
longTasks: longTasks.count.current,
|
|
299
|
+
rendersPerSecond: renderRate.rendersPerSecond.current,
|
|
300
|
+
memory: mem,
|
|
301
|
+
dimensions: dims,
|
|
302
|
+
profiler: { ...profilerData.current }
|
|
303
|
+
});
|
|
304
|
+
frameCount = 0;
|
|
305
|
+
frameTotalMs = 0;
|
|
306
|
+
lastSecond = now;
|
|
307
|
+
}
|
|
308
|
+
animId = requestAnimationFrame(tick);
|
|
309
|
+
};
|
|
310
|
+
animId = requestAnimationFrame(tick);
|
|
311
|
+
return () => cancelAnimationFrame(animId);
|
|
312
|
+
}, [targetRef, domTracker, renderRate, longTasks]);
|
|
313
|
+
const handleReset = useCallback2(() => {
|
|
314
|
+
frameTimeHistory.current = [];
|
|
315
|
+
allFrameTimes.current = [];
|
|
316
|
+
setStats(INITIAL_STATS);
|
|
317
|
+
onReset();
|
|
318
|
+
}, [onReset]);
|
|
319
|
+
const handleExport = useCallback2(() => {
|
|
320
|
+
const payload = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...stats };
|
|
321
|
+
const json = JSON.stringify(payload, null, 2);
|
|
322
|
+
try {
|
|
323
|
+
navigator.clipboard.writeText(json).then(() => {
|
|
324
|
+
setExported(true);
|
|
325
|
+
setTimeout(() => setExported(false), 1200);
|
|
326
|
+
});
|
|
327
|
+
} catch {
|
|
328
|
+
const ta = document.createElement("textarea");
|
|
329
|
+
ta.value = json;
|
|
330
|
+
ta.style.position = "fixed";
|
|
331
|
+
ta.style.opacity = "0";
|
|
332
|
+
document.body.appendChild(ta);
|
|
333
|
+
ta.select();
|
|
334
|
+
document.execCommand("copy");
|
|
335
|
+
document.body.removeChild(ta);
|
|
336
|
+
setExported(true);
|
|
337
|
+
setTimeout(() => setExported(false), 1200);
|
|
338
|
+
}
|
|
339
|
+
}, [stats]);
|
|
340
|
+
const ftColor = stats.frameTime > 33 ? "#ef4444" : stats.frameTime > 16.67 ? "#f59e0b" : "#4ade80";
|
|
341
|
+
const rpsColor = stats.rendersPerSecond > 30 ? "#ef4444" : stats.rendersPerSecond > 10 ? "#f59e0b" : "#4ade80";
|
|
342
|
+
const actualColor = stats.profiler.actualDuration > 16 ? "#ef4444" : stats.profiler.actualDuration > 8 ? "#f59e0b" : "#4ade80";
|
|
343
|
+
const fps = stats.frameTime > 0 ? Math.round(1e3 / stats.frameTime) : 0;
|
|
344
|
+
const memoGain = stats.profiler.baseDuration > 0 ? Math.round((1 - stats.profiler.actualDuration / stats.profiler.baseDuration) * 100) : 0;
|
|
345
|
+
const p99Color = stats.frameTimeP99 > 33 ? "#ef4444" : stats.frameTimeP99 > 16.67 ? "#f59e0b" : "#4ade80";
|
|
346
|
+
return createPortal(
|
|
347
|
+
/* @__PURE__ */ jsxs("div", { className: DevProfiler_default.panel, style: getPanelStyle(pos, offset, position), children: [
|
|
348
|
+
/* @__PURE__ */ jsxs(
|
|
349
|
+
"div",
|
|
350
|
+
{
|
|
351
|
+
className: DevProfiler_default.panelHeader,
|
|
352
|
+
...dragHandlers,
|
|
353
|
+
children: [
|
|
354
|
+
/* @__PURE__ */ jsxs("span", { className: DevProfiler_default.panelTitle, children: [
|
|
355
|
+
"Dev Profiler",
|
|
356
|
+
instanceCount > 1 && instanceId && /* @__PURE__ */ jsx("span", { className: DevProfiler_default.instanceBadge, children: instanceId })
|
|
357
|
+
] }),
|
|
358
|
+
/* @__PURE__ */ jsxs("div", { className: DevProfiler_default.headerActions, children: [
|
|
359
|
+
/* @__PURE__ */ jsx(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
className: `${DevProfiler_default.exportBtn} ${exported ? DevProfiler_default.exportBtnActive : ""}`,
|
|
363
|
+
onClick: handleExport,
|
|
364
|
+
title: exported ? "Copied!" : "Copy stats to clipboard",
|
|
365
|
+
children: exported ? /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z" }) }) : /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: [
|
|
366
|
+
/* @__PURE__ */ jsx("path", { d: "M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25z" }),
|
|
367
|
+
/* @__PURE__ */ jsx("path", { d: "M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25z" })
|
|
368
|
+
] })
|
|
369
|
+
}
|
|
370
|
+
),
|
|
371
|
+
/* @__PURE__ */ jsx("button", { className: DevProfiler_default.resetBtn, onClick: handleReset, title: "Reset counters", children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M14 1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1 0-2h1.6A6 6 0 0 0 2.07 7.5a1 1 0 1 1-1.97-.36A8 8 0 0 1 13 3.35V2a1 1 0 0 1 1-1zM2 15a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h4a1 1 0 0 1 0 2H4.4A6 6 0 0 0 13.93 8.5a1 1 0 1 1 1.97.36A8 8 0 0 1 3 12.65V14a1 1 0 0 1-1 1z" }) }) }),
|
|
372
|
+
/* @__PURE__ */ jsx("button", { className: DevProfiler_default.closeBtn, onClick: onClose, children: "\u2715" })
|
|
373
|
+
] })
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
),
|
|
377
|
+
/* @__PURE__ */ jsxs("div", { className: DevProfiler_default.body, children: [
|
|
378
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.section, children: "Rendering" }),
|
|
379
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Frame time", value: `${stats.frameTime.toFixed(1)}ms`, sub: `${fps} fps`, color: ftColor }),
|
|
380
|
+
/* @__PURE__ */ jsx(FrameTimeGraph, { history: stats.frameTimeHistory }),
|
|
381
|
+
/* @__PURE__ */ jsxs("div", { className: DevProfiler_default.miniRow, children: [
|
|
382
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
383
|
+
"min ",
|
|
384
|
+
stats.frameTimeMin.toFixed(1)
|
|
385
|
+
] }),
|
|
386
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
387
|
+
"max ",
|
|
388
|
+
stats.frameTimeMax.toFixed(1)
|
|
389
|
+
] }),
|
|
390
|
+
/* @__PURE__ */ jsxs("span", { style: { color: p99Color }, children: [
|
|
391
|
+
"p99 ",
|
|
392
|
+
stats.frameTimeP99.toFixed(1)
|
|
393
|
+
] })
|
|
394
|
+
] }),
|
|
395
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Renders/s", value: String(stats.rendersPerSecond), color: rpsColor }),
|
|
396
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Long tasks", value: String(stats.longTasks), color: stats.longTasks > 0 ? "#f59e0b" : "#4ade80" }),
|
|
397
|
+
/* @__PURE__ */ jsx("div", { className: DevProfiler_default.separator }),
|
|
398
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.section, children: "React Profiler" }),
|
|
399
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Phase", value: stats.profiler.phase, color: "#888" }),
|
|
400
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Render", value: `${stats.profiler.actualDuration.toFixed(2)}ms`, color: actualColor }),
|
|
401
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Base (no memo)", value: `${stats.profiler.baseDuration.toFixed(2)}ms`, color: "#888" }),
|
|
402
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Memo gain", value: `${memoGain}%`, color: memoGain > 50 ? "#4ade80" : memoGain > 20 ? "#f59e0b" : "#ef4444" }),
|
|
403
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Commits", value: String(stats.profiler.commitCount) }),
|
|
404
|
+
/* @__PURE__ */ jsx("div", { className: DevProfiler_default.separator }),
|
|
405
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.section, children: "DOM" }),
|
|
406
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Nodes", value: stats.domNodes.toLocaleString() }),
|
|
407
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Mutations", value: String(stats.domMutations) }),
|
|
408
|
+
/* @__PURE__ */ jsx(StatRow, { label: "Size", value: stats.dimensions, color: "#888" }),
|
|
409
|
+
/* @__PURE__ */ jsx("div", { className: DevProfiler_default.separator }),
|
|
410
|
+
/* @__PURE__ */ jsx("span", { className: DevProfiler_default.section, children: "Memory" }),
|
|
411
|
+
/* @__PURE__ */ jsx(StatRow, { label: "JS Heap", value: stats.memory > 0 ? `${stats.memory} MB` : "N/A" })
|
|
412
|
+
] }),
|
|
413
|
+
/* @__PURE__ */ jsx("div", { className: DevProfiler_default.footer, children: "Ctrl+I to toggle" })
|
|
414
|
+
] }),
|
|
415
|
+
document.body
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/ToggleButton.tsx
|
|
420
|
+
import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
421
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
422
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
423
|
+
function getButtonStyle(pos, position) {
|
|
424
|
+
const style = {};
|
|
425
|
+
if (position.startsWith("bottom")) {
|
|
426
|
+
style.bottom = window.innerHeight - pos.top + 8;
|
|
427
|
+
} else {
|
|
428
|
+
style.top = pos.top + 8;
|
|
429
|
+
}
|
|
430
|
+
if (position.endsWith("right")) {
|
|
431
|
+
style.right = window.innerWidth - pos.left;
|
|
432
|
+
} else {
|
|
433
|
+
style.left = pos.left;
|
|
434
|
+
}
|
|
435
|
+
return style;
|
|
436
|
+
}
|
|
437
|
+
function ToggleButton({
|
|
438
|
+
targetRef,
|
|
439
|
+
onClick,
|
|
440
|
+
position = "bottom-left"
|
|
441
|
+
}) {
|
|
442
|
+
const pos = useAnchorPosition(targetRef, position);
|
|
443
|
+
const [fps, setFps] = useState3(0);
|
|
444
|
+
const lastFrame = useRef3(performance.now());
|
|
445
|
+
useEffect3(() => {
|
|
446
|
+
let animId;
|
|
447
|
+
let count = 0;
|
|
448
|
+
let lastSecond = performance.now();
|
|
449
|
+
const tick = () => {
|
|
450
|
+
const now = performance.now();
|
|
451
|
+
lastFrame.current = now;
|
|
452
|
+
count++;
|
|
453
|
+
if (now - lastSecond >= 1e3) {
|
|
454
|
+
setFps(count);
|
|
455
|
+
count = 0;
|
|
456
|
+
lastSecond = now;
|
|
457
|
+
}
|
|
458
|
+
animId = requestAnimationFrame(tick);
|
|
459
|
+
};
|
|
460
|
+
animId = requestAnimationFrame(tick);
|
|
461
|
+
return () => cancelAnimationFrame(animId);
|
|
462
|
+
}, []);
|
|
463
|
+
const fpsColor = fps < 30 ? "#ef4444" : fps < 55 ? "#f59e0b" : "#4ade80";
|
|
464
|
+
return createPortal2(
|
|
465
|
+
/* @__PURE__ */ jsx2(
|
|
466
|
+
"button",
|
|
467
|
+
{
|
|
468
|
+
className: DevProfiler_default.toggleBtn,
|
|
469
|
+
onClick,
|
|
470
|
+
title: "Dev Profiler (Ctrl+I)",
|
|
471
|
+
style: getButtonStyle(pos, position),
|
|
472
|
+
children: /* @__PURE__ */ jsx2("span", { className: DevProfiler_default.toggleFps, style: { color: fpsColor }, children: fps })
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
document.body
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/DevProfiler.tsx
|
|
480
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
481
|
+
var TOGGLE_EVENT = "devprofiler:toggle";
|
|
482
|
+
var BOUND_KEY = "__devprofiler_bound";
|
|
483
|
+
if (typeof window !== "undefined" && __DEV__ && !window[BOUND_KEY]) {
|
|
484
|
+
window[BOUND_KEY] = true;
|
|
485
|
+
window.addEventListener("keydown", (e) => {
|
|
486
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "i") {
|
|
487
|
+
e.preventDefault();
|
|
488
|
+
window.dispatchEvent(new CustomEvent(TOGGLE_EVENT));
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
var instanceCounter = 0;
|
|
493
|
+
var activeInstances = /* @__PURE__ */ new Set();
|
|
494
|
+
function DevProfiler({
|
|
495
|
+
children,
|
|
496
|
+
position = "bottom-left",
|
|
497
|
+
id
|
|
498
|
+
}) {
|
|
499
|
+
const wrapperRef = useRef4(null);
|
|
500
|
+
const [open, setOpen] = useState4(false);
|
|
501
|
+
const toggle = useCallback3(() => setOpen((prev) => !prev), []);
|
|
502
|
+
const instanceId = useRef4(id ?? `profiler-${++instanceCounter}`).current;
|
|
503
|
+
useEffect4(() => {
|
|
504
|
+
activeInstances.add(instanceId);
|
|
505
|
+
return () => {
|
|
506
|
+
activeInstances.delete(instanceId);
|
|
507
|
+
};
|
|
508
|
+
}, [instanceId]);
|
|
509
|
+
const domTracker = useDomTracker(wrapperRef, open);
|
|
510
|
+
const renderRate = useRenderRate();
|
|
511
|
+
const longTasks = useLongTasks(open);
|
|
512
|
+
useRenderFlash(wrapperRef, open);
|
|
513
|
+
const profilerData = useRef4({ ...INITIAL_PROFILER });
|
|
514
|
+
const onRender = useCallback3((_id, phase, actualDuration, baseDuration) => {
|
|
515
|
+
profilerData.current = {
|
|
516
|
+
phase,
|
|
517
|
+
actualDuration,
|
|
518
|
+
baseDuration,
|
|
519
|
+
commitCount: profilerData.current.commitCount + 1
|
|
520
|
+
};
|
|
521
|
+
renderRate.tick();
|
|
522
|
+
}, [renderRate]);
|
|
523
|
+
const handleReset = useCallback3(() => {
|
|
524
|
+
domTracker.reset();
|
|
525
|
+
renderRate.reset();
|
|
526
|
+
longTasks.reset();
|
|
527
|
+
profilerData.current = { ...INITIAL_PROFILER };
|
|
528
|
+
}, [domTracker, renderRate, longTasks]);
|
|
529
|
+
useEffect4(() => {
|
|
530
|
+
const handler = () => setOpen((prev) => !prev);
|
|
531
|
+
window.addEventListener(TOGGLE_EVENT, handler);
|
|
532
|
+
return () => window.removeEventListener(TOGGLE_EVENT, handler);
|
|
533
|
+
}, []);
|
|
534
|
+
if (!__DEV__) return /* @__PURE__ */ jsx3(Fragment, { children });
|
|
535
|
+
return /* @__PURE__ */ jsxs2("div", { ref: wrapperRef, className: DevProfiler_default.wrapper, children: [
|
|
536
|
+
/* @__PURE__ */ jsx3(Profiler, { id: "DevProfiler", onRender, children }),
|
|
537
|
+
!open && /* @__PURE__ */ jsx3(ToggleButton, { targetRef: wrapperRef, onClick: toggle, position }),
|
|
538
|
+
open && /* @__PURE__ */ jsx3(
|
|
539
|
+
DevStatsPanel,
|
|
540
|
+
{
|
|
541
|
+
targetRef: wrapperRef,
|
|
542
|
+
domTracker,
|
|
543
|
+
renderRate,
|
|
544
|
+
longTasks,
|
|
545
|
+
profilerData,
|
|
546
|
+
onClose: toggle,
|
|
547
|
+
onReset: handleReset,
|
|
548
|
+
position,
|
|
549
|
+
instanceId,
|
|
550
|
+
instanceCount: activeInstances.size
|
|
551
|
+
}
|
|
552
|
+
)
|
|
553
|
+
] });
|
|
554
|
+
}
|
|
555
|
+
export {
|
|
556
|
+
DevProfiler
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* @module react-dev-profiler
|
|
560
|
+
* @description Type definitions and constants for the profiler.
|
|
561
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
562
|
+
* @license MIT
|
|
563
|
+
*/
|
|
564
|
+
/**
|
|
565
|
+
* @module react-dev-profiler
|
|
566
|
+
* @description Environment detection — works with Vite, webpack, Next.js, and any bundler.
|
|
567
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
568
|
+
* @license MIT
|
|
569
|
+
*/
|
|
570
|
+
/**
|
|
571
|
+
* @module react-dev-profiler
|
|
572
|
+
* @description Custom hooks that power the profiler's data collection.
|
|
573
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
574
|
+
* @license MIT
|
|
575
|
+
*/
|
|
576
|
+
/**
|
|
577
|
+
* @module react-dev-profiler
|
|
578
|
+
* @description The main stats panel — renders all performance metrics in a floating overlay.
|
|
579
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
580
|
+
* @license MIT
|
|
581
|
+
*/
|
|
582
|
+
/**
|
|
583
|
+
* @module react-dev-profiler
|
|
584
|
+
* @description Minimal floating button that shows the current FPS at a glance.
|
|
585
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
586
|
+
* @license MIT
|
|
587
|
+
*/
|
|
588
|
+
/**
|
|
589
|
+
* @module react-dev-profiler
|
|
590
|
+
* @description Main wrapper component — wraps your app and enables profiling.
|
|
591
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
592
|
+
* @license MIT
|
|
593
|
+
*/
|
|
594
|
+
/**
|
|
595
|
+
* react-dev-profiler
|
|
596
|
+
* Real-time React performance monitoring for development.
|
|
597
|
+
*
|
|
598
|
+
* @author Frederic Denis (billywild87) — https://github.com/billywild87
|
|
599
|
+
* @license MIT
|
|
600
|
+
*/
|
|
601
|
+
//# sourceMappingURL=index.js.map
|