@praxisjs/devtools 0.2.34 → 0.3.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/CHANGELOG.md +32 -0
- package/assets/uno.generated.css +5 -0
- package/dist/assets/logo.svg.js +4 -0
- package/dist/assets/tailwind-v4.css.js +4 -0
- package/dist/assets/uno.generated.css.js +4 -0
- package/dist/core/event-bus.js +21 -0
- package/dist/core/profiler.js +79 -0
- package/dist/core/registry.js +147 -0
- package/dist/decorators/debug.js +126 -0
- package/dist/decorators/trace.js +45 -0
- package/dist/icons/ellipsis-vertical.js +38 -0
- package/dist/icons/panel-bottom.js +28 -0
- package/dist/icons/panel-left.js +28 -0
- package/dist/icons/panel-right.js +28 -0
- package/dist/icons/panel-top.js +28 -0
- package/dist/icons/x.js +22 -0
- package/dist/index.d.ts +96 -3
- package/dist/index.js +10 -1157
- package/dist/plugins/components/components/component-detail.js +53 -0
- package/dist/plugins/components/components/component-row.js +22 -0
- package/dist/plugins/components/components/detail-row.js +15 -0
- package/dist/plugins/components/components/detail-section.js +14 -0
- package/dist/plugins/components/components/status-dot.js +15 -0
- package/dist/plugins/components/components-tab.js +54 -0
- package/dist/plugins/components/index.js +9 -0
- package/dist/plugins/performance/index.js +9 -0
- package/dist/plugins/performance/performance-tab.js +62 -0
- package/dist/plugins/signals/components/signal-detail.js +29 -0
- package/dist/plugins/signals/components/signal-row.js +17 -0
- package/dist/plugins/signals/index.js +9 -0
- package/dist/plugins/signals/signals-tab.js +53 -0
- package/dist/plugins/timeline/components/badge.js +16 -0
- package/dist/plugins/timeline/components/timeline-row.js +33 -0
- package/dist/plugins/timeline/constants.js +54 -0
- package/dist/plugins/timeline/index.js +9 -0
- package/dist/plugins/timeline/timeline-tab.js +46 -0
- package/dist/ui/dev-tools.js +74 -0
- package/dist/ui/panel.js +106 -0
- package/dist/ui/shared/empty-state.js +14 -0
- package/dist/ui/shared/icon-button.js +17 -0
- package/dist/ui/shared/panel-section.js +14 -0
- package/dist/ui/shared/search-input.js +17 -0
- package/dist/ui/shared/side-panel.js +15 -0
- package/dist/utils/format-time.js +7 -0
- package/dist/utils/format-value.js +14 -0
- package/package.json +14 -6
- package/src/core/profiler.ts +162 -0
- package/src/core/registry.ts +23 -21
- package/src/decorators/debug.ts +96 -32
- package/src/decorators/index.ts +1 -0
- package/src/decorators/trace.ts +27 -4
- package/src/index.ts +5 -0
- package/src/plugins/performance/index.ts +9 -0
- package/src/plugins/performance/performance-tab.tsx +111 -0
- package/src/ui/dev-tools.tsx +5 -0
- package/src/__tests__/debug.test.ts +0 -333
- package/src/__tests__/event-bus.test.ts +0 -123
- package/src/__tests__/format-time.test.ts +0 -52
- package/src/__tests__/format-value.test.ts +0 -46
- package/src/__tests__/plugins.test.ts +0 -101
- package/src/__tests__/registry.test.ts +0 -386
- package/src/__tests__/trace.test.ts +0 -200
- package/tsconfig.json +0 -21
- package/uno.config.ts +0 -57
- package/vite.config.ts +0 -38
package/dist/ui/panel.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import e from "../assets/logo.svg.js";
|
|
2
|
+
import { EllipsisVerticalIcon as t } from "../icons/ellipsis-vertical.js";
|
|
3
|
+
import { PanelBottomIcon as n } from "../icons/panel-bottom.js";
|
|
4
|
+
import { PanelLeftIcon as r } from "../icons/panel-left.js";
|
|
5
|
+
import { PanelRightIcon as i } from "../icons/panel-right.js";
|
|
6
|
+
import { PanelTopIcon as a } from "../icons/panel-top.js";
|
|
7
|
+
import { XIcon as o } from "../icons/x.js";
|
|
8
|
+
import { IconButton as s } from "./shared/icon-button.js";
|
|
9
|
+
import { For as c, _$className as l, _$event as u, _$html as d, _$insert as f, _$insertOnly as p, _$onEvent as m, _$prop as h, _$reactive as g, _$reactiveOnly as _, _$scope as v } from "@praxisjs/runtime";
|
|
10
|
+
import { jsx as y } from "@praxisjs/jsx/jsx-runtime";
|
|
11
|
+
import { StatefulComponent as b } from "@praxisjs/core";
|
|
12
|
+
import { Component as x, Emit as S, Prop as C, State as w } from "@praxisjs/decorators";
|
|
13
|
+
//#region src/ui/panel.tsx
|
|
14
|
+
var T = /* @__PURE__ */ u("onMouseDown"), E = /* @__PURE__ */ u("onClick"), D = /* @__PURE__ */ d("<div><div></div><div><div class=\"flex flex-col flex-1 w-full h-full overflow-hidden\"><div class=\"flex h-10 items-stretch border-b border-border bg-header shrink-0 px-2 gap-[2px]\"><div class=\"flex items-center gap-[6px] pr-3 mr-[2px] border-r border-border shrink-0\"><img class=\"h-[13px] w-[13px]\"><span class=\"text-[10px] font-bold text-accent tracking-widest uppercase\">praxisjs</span></div><div class=\"flex items-stretch flex-1\"></div><div class=\"flex items-center gap-[2px] shrink-0\"><div class=relative><!----></div><div class=\"w-px h-3 bg-border mx-1\"></div></div></div><div class=\"flex-1 overflow-hidden flex flex-col\">"), O = /* @__PURE__ */ d("<button><!---->"), k = /* @__PURE__ */ d("<span class=\"absolute bottom-0 left-0 right-0 h-[2px] bg-accent\">"), A = /* @__PURE__ */ d("<div class=\"absolute right-0 top-full mt-1 flex gap-[2px] bg-bg border border-border rounded-md shadow-lg p-1 z-10\">"), j = 180, M = 300, N = 240, P = 380, F = {
|
|
15
|
+
bottom: n,
|
|
16
|
+
top: a,
|
|
17
|
+
left: r,
|
|
18
|
+
right: i
|
|
19
|
+
}, I = @x() class extends b {
|
|
20
|
+
@w() activeTab = "";
|
|
21
|
+
@w() dock = "bottom";
|
|
22
|
+
@w() dockMenuOpen = !1;
|
|
23
|
+
@w() bottomSize = M;
|
|
24
|
+
@w() vertSize = P;
|
|
25
|
+
@C() plugins = [];
|
|
26
|
+
@C() registry;
|
|
27
|
+
@S("onClose") handleClose() {}
|
|
28
|
+
onMount() {
|
|
29
|
+
this.plugins.length > 0 && (this.activeTab = this.plugins[0].id);
|
|
30
|
+
}
|
|
31
|
+
handleMouseDown(e) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
let t = e.clientX, n = e.clientY, r = this.dock, i = r === "bottom" || r === "top" ? this.bottomSize : this.vertSize, a = (e) => {
|
|
34
|
+
let r = this.dock;
|
|
35
|
+
r === "bottom" ? this.bottomSize = Math.max(j, Math.min(window.innerHeight * .85, i + (n - e.clientY))) : r === "top" ? this.bottomSize = Math.max(j, Math.min(window.innerHeight * .85, i + (e.clientY - n))) : r === "left" ? this.vertSize = Math.max(N, Math.min(window.innerWidth * .6, i + (e.clientX - t))) : this.vertSize = Math.max(N, Math.min(window.innerWidth * .6, i + (t - e.clientX)));
|
|
36
|
+
}, o = () => {
|
|
37
|
+
document.removeEventListener("mousemove", a), document.removeEventListener("mouseup", o);
|
|
38
|
+
};
|
|
39
|
+
document.addEventListener("mousemove", a), document.addEventListener("mouseup", o);
|
|
40
|
+
}
|
|
41
|
+
render() {
|
|
42
|
+
{
|
|
43
|
+
let n = v(), r = D(), i = r.firstChild, a = i.nextSibling, u = a.firstChild.firstChild, d = u.nextSibling, b = u.firstChild, x = b.nextSibling, S = x.nextSibling, C = b.firstChild, w = S.firstChild, j = w.firstChild;
|
|
44
|
+
return l(i, () => {
|
|
45
|
+
let e = this.dock;
|
|
46
|
+
return e === "bottom" || e === "top" ? "h-1 cursor-[ns-resize] bg-transparent hover:bg-accent transition-colors duration-200" : "w-1 cursor-[ew-resize] bg-transparent hover:bg-accent transition-colors duration-200";
|
|
47
|
+
}, n), m(i, T, (e) => {
|
|
48
|
+
this.handleMouseDown(e);
|
|
49
|
+
}, n), h(C, "src", e, n), p(x, /* @__PURE__ */ y(c, {
|
|
50
|
+
each: () => this.plugins,
|
|
51
|
+
by: (e) => e.id,
|
|
52
|
+
children: (e) => () => {
|
|
53
|
+
let t = e();
|
|
54
|
+
{
|
|
55
|
+
let e = v(), n = O(), r = n.firstChild;
|
|
56
|
+
return f(n, t.label, r, e), g(r, () => this.activeTab === t.id ? k() : null, e), l(n, () => this.activeTab === t.id ? "relative flex items-center px-3 text-[11px] font-semibold text-accent cursor-pointer" : "flex items-center px-3 text-[11px] text-muted cursor-pointer hover:text-text transition-colors duration-150", e), m(n, E, () => {
|
|
57
|
+
this.activeTab = t.id;
|
|
58
|
+
}, e), n;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}), n), f(w, /* @__PURE__ */ y(s, {
|
|
62
|
+
title: "Dock position",
|
|
63
|
+
icon: t,
|
|
64
|
+
active: () => this.dockMenuOpen,
|
|
65
|
+
onClick: () => {
|
|
66
|
+
this.dockMenuOpen = !this.dockMenuOpen;
|
|
67
|
+
}
|
|
68
|
+
}), j, n), g(j, () => this.dockMenuOpen ? ((e) => {
|
|
69
|
+
let t = A();
|
|
70
|
+
return p(t, [
|
|
71
|
+
"bottom",
|
|
72
|
+
"top",
|
|
73
|
+
"left",
|
|
74
|
+
"right"
|
|
75
|
+
].map((e) => /* @__PURE__ */ y(s, {
|
|
76
|
+
title: `Dock ${e}`,
|
|
77
|
+
active: () => this.dock === e,
|
|
78
|
+
icon: F[e],
|
|
79
|
+
onClick: () => {
|
|
80
|
+
this.dock = e, this.dockMenuOpen = !1;
|
|
81
|
+
}
|
|
82
|
+
}, e)), e), t;
|
|
83
|
+
})(v()) : null, n), f(S, /* @__PURE__ */ y(s, {
|
|
84
|
+
title: "Close",
|
|
85
|
+
icon: o,
|
|
86
|
+
onClick: () => {
|
|
87
|
+
this.handleClose();
|
|
88
|
+
}
|
|
89
|
+
}), null, n), _(d, () => {
|
|
90
|
+
let e = this.plugins.find((e) => e.id === this.activeTab)?.component;
|
|
91
|
+
return e ? /* @__PURE__ */ y(e, { registry: this.registry }) : null;
|
|
92
|
+
}, n), l(a, () => {
|
|
93
|
+
let e = this.dock;
|
|
94
|
+
return e === "top" ? "w-full h-full bg-bg border-b border-border" : e === "left" ? "w-full h-full bg-bg border-r border-border" : e === "right" ? "w-full h-full bg-bg border-l border-border" : "w-full h-full bg-bg border-t border-border";
|
|
95
|
+
}, n), h(a, "style", () => {
|
|
96
|
+
let e = this.dock;
|
|
97
|
+
return e === "bottom" || e === "top" ? { height: `${this.bottomSize.toString()}px` } : { width: `${this.vertSize.toString()}px` };
|
|
98
|
+
}, n), l(r, () => {
|
|
99
|
+
let e = this.dock;
|
|
100
|
+
return e === "top" ? "flex flex-col-reverse fixed top-0 left-0 w-full z-[2147483647]" : e === "left" ? "flex flex-row-reverse fixed left-0 top-0 h-screen z-[2147483647]" : e === "right" ? "flex fixed right-0 top-0 h-screen z-[2147483647]" : "flex flex-col fixed bottom-0 left-0 w-full z-[2147483647]";
|
|
101
|
+
}, n), r;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
export { I as Panel };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { _$html as e, _$insertOnly as t, _$scope as n } from "@praxisjs/runtime";
|
|
2
|
+
import { StatelessComponent as r } from "@praxisjs/core";
|
|
3
|
+
import { Component as i } from "@praxisjs/decorators";
|
|
4
|
+
//#region src/ui/shared/empty-state.tsx
|
|
5
|
+
var a = /* @__PURE__ */ e("<div class=\"flex flex-col items-center justify-center gap-2 py-12\"><span class=\"text-subtle text-[20px] leading-none select-none font-mono\">—</span><p class=\"text-subtle text-[11px] text-center leading-relaxed max-w-[200px]\">"), o = @i() class extends r {
|
|
6
|
+
render() {
|
|
7
|
+
{
|
|
8
|
+
let e = n(), r = a(), i = r.firstChild.nextSibling;
|
|
9
|
+
return t(i, this.props.message, e), r;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { o as EmptyState };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as e } from "@praxisjs/jsx/jsx-runtime";
|
|
2
|
+
import { StatelessComponent as t } from "@praxisjs/core";
|
|
3
|
+
import { Component as n } from "@praxisjs/decorators";
|
|
4
|
+
//#region src/ui/shared/icon-button.tsx
|
|
5
|
+
var r = @n() class extends t {
|
|
6
|
+
render() {
|
|
7
|
+
let { title: t, active: n, icon: r, onClick: i } = this.props;
|
|
8
|
+
return /* @__PURE__ */ e("button", {
|
|
9
|
+
title: t,
|
|
10
|
+
onClick: i,
|
|
11
|
+
class: () => `w-[22px] h-[22px] flex items-center justify-center rounded-md text-[11px] cursor-pointer transition-colors duration-150 ${(typeof n == "function" ? n() : n) ? "text-accent bg-soft color-white" : "text-subtle hover:color-muted hover:bg-section"}`,
|
|
12
|
+
children: /* @__PURE__ */ e(r, {})
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { r as IconButton };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { _$html as e, _$insert as t, _$insertOnly as n, _$scope as r } from "@praxisjs/runtime";
|
|
2
|
+
import { StatelessComponent as i } from "@praxisjs/core";
|
|
3
|
+
import { Component as a } from "@praxisjs/decorators";
|
|
4
|
+
//#region src/ui/shared/panel-section.tsx
|
|
5
|
+
var o = /* @__PURE__ */ e("<div class=\"border-b border-border\"><div class=\"px-3 py-[4px] text-[9px] text-subtle font-bold tracking-[0.12em] uppercase bg-section border-b border-border\">"), s = @a() class extends i {
|
|
6
|
+
render() {
|
|
7
|
+
{
|
|
8
|
+
let e = r(), i = o(), a = i.firstChild;
|
|
9
|
+
return n(a, this.props.label, e), t(i, this.props.children, null, e), i;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { s as PanelSection };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { _$event as e, _$html as t, _$onEvent as n, _$prop as r, _$scope as i } from "@praxisjs/runtime";
|
|
2
|
+
import { StatelessComponent as a } from "@praxisjs/core";
|
|
3
|
+
import { Component as o } from "@praxisjs/decorators";
|
|
4
|
+
//#region src/ui/shared/search-input.tsx
|
|
5
|
+
var s = /* @__PURE__ */ e("onInput"), c = /* @__PURE__ */ t("<input type=text class=\"w-full bg-input border border-border rounded-lg text-text text-[11px] px-3 py-[5px] outline-none box-border transition-shadow duration-150\">"), l = @o() class extends a {
|
|
6
|
+
render() {
|
|
7
|
+
let { placeholder: e, onInput: t } = this.props;
|
|
8
|
+
{
|
|
9
|
+
let a = i(), o = c();
|
|
10
|
+
return r(o, "placeholder", e, a), n(o, s, (e) => {
|
|
11
|
+
t(e.target.value);
|
|
12
|
+
}, a), o;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { l as SearchInput };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { _$html as e, _$insertOnly as t, _$prop as n, _$scope as r } from "@praxisjs/runtime";
|
|
2
|
+
import { StatelessComponent as i } from "@praxisjs/core";
|
|
3
|
+
import { Component as a } from "@praxisjs/decorators";
|
|
4
|
+
//#region src/ui/shared/side-panel.tsx
|
|
5
|
+
var o = /* @__PURE__ */ e("<div class=\"shrink-0 border-l border-border flex flex-col overflow-hidden\">"), s = @a() class extends i {
|
|
6
|
+
render() {
|
|
7
|
+
let { children: e, width: i = "280px" } = this.props;
|
|
8
|
+
{
|
|
9
|
+
let a = r(), s = o();
|
|
10
|
+
return t(s, e, a), n(s, "style", { width: i }, a), s;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { s as SidePanel };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/utils/format-time.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
let n = t ? ` ${t}` : "", r = Date.now() - e;
|
|
4
|
+
return r < 1e3 ? `${r.toString()}ms${n}` : r < 6e4 ? `${(r / 1e3).toFixed(1)}s${n}` : `${Math.floor(r / 6e4).toString()}m${n}`;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { e as time };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/utils/format-value.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
if (e === null) return "null";
|
|
4
|
+
if (e === void 0) return "undefined";
|
|
5
|
+
if (typeof e == "string") return `"${e}"`;
|
|
6
|
+
if (typeof e == "object") try {
|
|
7
|
+
return JSON.stringify(e);
|
|
8
|
+
} catch {
|
|
9
|
+
return "[object]";
|
|
10
|
+
}
|
|
11
|
+
return String(e);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { e as formatValue };
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
@@ -10,13 +11,20 @@
|
|
|
10
11
|
"types": "./dist/index.d.ts"
|
|
11
12
|
}
|
|
12
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"assets",
|
|
17
|
+
"src",
|
|
18
|
+
"!src/**/__tests__",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@unocss/reset": "^66.6.7",
|
|
15
|
-
"@praxisjs/core": "2.
|
|
16
|
-
"@praxisjs/decorators": "
|
|
17
|
-
"@praxisjs/jsx": "0.
|
|
18
|
-
"@praxisjs/runtime": "0.
|
|
19
|
-
"@praxisjs/shared": "0.3.
|
|
23
|
+
"@praxisjs/core": "2.3.0",
|
|
24
|
+
"@praxisjs/decorators": "2.0.0",
|
|
25
|
+
"@praxisjs/jsx": "0.8.0",
|
|
26
|
+
"@praxisjs/runtime": "0.8.0",
|
|
27
|
+
"@praxisjs/shared": "0.3.2"
|
|
20
28
|
},
|
|
21
29
|
"devDependencies": {
|
|
22
30
|
"@types/node": "^25.9.1",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setReactiveObserver,
|
|
3
|
+
type ComputedNode,
|
|
4
|
+
type EffectNode,
|
|
5
|
+
type ReactiveNode,
|
|
6
|
+
type ReactiveObserver,
|
|
7
|
+
} from "@praxisjs/core/internal";
|
|
8
|
+
|
|
9
|
+
import { EventBus } from "./event-bus";
|
|
10
|
+
|
|
11
|
+
export interface OwnerStats {
|
|
12
|
+
/** Constructor name of the component the effects belong to, or "(no component)". */
|
|
13
|
+
name: string;
|
|
14
|
+
effectRuns: number;
|
|
15
|
+
effectMs: number;
|
|
16
|
+
/** Slowest single effect run, in ms. */
|
|
17
|
+
maxMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProfileSnapshot {
|
|
21
|
+
recording: boolean;
|
|
22
|
+
/** Milliseconds covered by the recording so far. */
|
|
23
|
+
duration: number;
|
|
24
|
+
writes: number;
|
|
25
|
+
effectRuns: number;
|
|
26
|
+
effectMs: number;
|
|
27
|
+
computeRuns: number;
|
|
28
|
+
computeMs: number;
|
|
29
|
+
/** Per component, busiest first. */
|
|
30
|
+
owners: OwnerStats[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const UNOWNED = "(no component)";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Records what the reactive graph does while recording: signal writes, computed recomputations
|
|
37
|
+
* and effect runs, with their cost, grouped by the component that created each effect. It installs
|
|
38
|
+
* itself as the graph's observer only while recording, so it costs nothing otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export class Profiler implements ReactiveObserver {
|
|
41
|
+
readonly bus = new EventBus();
|
|
42
|
+
|
|
43
|
+
private recording = false;
|
|
44
|
+
private startedAt = 0;
|
|
45
|
+
private stoppedAt = 0;
|
|
46
|
+
private writes = 0;
|
|
47
|
+
private effectRuns = 0;
|
|
48
|
+
private effectMs = 0;
|
|
49
|
+
private computeRuns = 0;
|
|
50
|
+
private computeMs = 0;
|
|
51
|
+
private readonly byOwner = new Map<object | undefined, OwnerStats>();
|
|
52
|
+
private previous: ReactiveObserver | null = null;
|
|
53
|
+
private notifyScheduled = false;
|
|
54
|
+
private mutedDepth = 0;
|
|
55
|
+
|
|
56
|
+
get isRecording(): boolean {
|
|
57
|
+
return this.recording;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Runs `fn` without recording what it causes: the devtools panel updating itself. */
|
|
61
|
+
muted<T>(fn: () => T): T {
|
|
62
|
+
this.mutedDepth++;
|
|
63
|
+
try {
|
|
64
|
+
return fn();
|
|
65
|
+
} finally {
|
|
66
|
+
this.mutedDepth--;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
start(): void {
|
|
71
|
+
if (this.recording) return;
|
|
72
|
+
this.reset();
|
|
73
|
+
this.recording = true;
|
|
74
|
+
this.startedAt = performance.now();
|
|
75
|
+
this.previous = setReactiveObserver(this);
|
|
76
|
+
this.changed();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop(): void {
|
|
80
|
+
if (!this.recording) return;
|
|
81
|
+
this.recording = false;
|
|
82
|
+
this.stoppedAt = performance.now();
|
|
83
|
+
setReactiveObserver(this.previous);
|
|
84
|
+
this.previous = null;
|
|
85
|
+
this.changed();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reset(): void {
|
|
89
|
+
this.writes = 0;
|
|
90
|
+
this.effectRuns = 0;
|
|
91
|
+
this.effectMs = 0;
|
|
92
|
+
this.computeRuns = 0;
|
|
93
|
+
this.computeMs = 0;
|
|
94
|
+
this.byOwner.clear();
|
|
95
|
+
this.startedAt = this.stoppedAt = performance.now();
|
|
96
|
+
this.changed();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
write(_node: ReactiveNode): void {
|
|
100
|
+
if (this.mutedDepth > 0) return;
|
|
101
|
+
this.writes++;
|
|
102
|
+
this.changed();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
compute(_node: ComputedNode, ms: number): void {
|
|
106
|
+
if (this.mutedDepth > 0) return;
|
|
107
|
+
this.computeRuns++;
|
|
108
|
+
this.computeMs += ms;
|
|
109
|
+
this.changed();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
effect(_node: EffectNode, ms: number, owner: object | undefined): void {
|
|
113
|
+
if (this.mutedDepth > 0) return;
|
|
114
|
+
this.effectRuns++;
|
|
115
|
+
this.effectMs += ms;
|
|
116
|
+
let stats = this.byOwner.get(owner);
|
|
117
|
+
if (stats === undefined) {
|
|
118
|
+
stats = { name: owner === undefined ? UNOWNED : owner.constructor.name, effectRuns: 0, effectMs: 0, maxMs: 0 };
|
|
119
|
+
this.byOwner.set(owner, stats);
|
|
120
|
+
}
|
|
121
|
+
stats.effectRuns++;
|
|
122
|
+
stats.effectMs += ms;
|
|
123
|
+
if (ms > stats.maxMs) stats.maxMs = ms;
|
|
124
|
+
this.changed();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Totals plus per-component stats merged by component name, busiest first. */
|
|
128
|
+
snapshot(): ProfileSnapshot {
|
|
129
|
+
const merged = new Map<string, OwnerStats>();
|
|
130
|
+
for (const stats of this.byOwner.values()) {
|
|
131
|
+
const entry = merged.get(stats.name);
|
|
132
|
+
if (entry === undefined) {
|
|
133
|
+
merged.set(stats.name, { ...stats });
|
|
134
|
+
} else {
|
|
135
|
+
entry.effectRuns += stats.effectRuns;
|
|
136
|
+
entry.effectMs += stats.effectMs;
|
|
137
|
+
entry.maxMs = Math.max(entry.maxMs, stats.maxMs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
recording: this.recording,
|
|
142
|
+
duration: (this.recording ? performance.now() : this.stoppedAt) - this.startedAt,
|
|
143
|
+
writes: this.writes,
|
|
144
|
+
effectRuns: this.effectRuns,
|
|
145
|
+
effectMs: this.effectMs,
|
|
146
|
+
computeRuns: this.computeRuns,
|
|
147
|
+
computeMs: this.computeMs,
|
|
148
|
+
owners: [...merged.values()].sort((a, b) => b.effectMs - a.effectMs || b.effectRuns - a.effectRuns),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Coalesced to one notification per task, so a burst of thousands of effect runs does not
|
|
153
|
+
// make the panel re-render thousands of times.
|
|
154
|
+
private changed(): void {
|
|
155
|
+
if (this.notifyScheduled) return;
|
|
156
|
+
this.notifyScheduled = true;
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
this.notifyScheduled = false;
|
|
159
|
+
this.bus.emit("profile:changed", null);
|
|
160
|
+
}, 0);
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/core/registry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventBus } from "./event-bus";
|
|
2
|
+
import { Profiler } from "./profiler";
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
5
|
ComponentEntry,
|
|
@@ -25,9 +26,12 @@ export class Registry {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
readonly bus = new EventBus();
|
|
29
|
+
readonly profiler = new Profiler();
|
|
28
30
|
|
|
29
31
|
private readonly instanceIds = new WeakMap<object, string>();
|
|
30
32
|
private readonly signals = new Map<string, SignalEntry>();
|
|
33
|
+
// Signal ids per component, so unmounting one component drops its signals without scanning all.
|
|
34
|
+
private readonly signalsByComponent = new Map<string, Set<string>>();
|
|
31
35
|
private readonly components = new Map<string, ComponentEntry>();
|
|
32
36
|
private readonly timeline: TimelineEntry[] = [];
|
|
33
37
|
|
|
@@ -58,6 +62,12 @@ export class Registry {
|
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
this.signals.set(id, entry);
|
|
65
|
+
let ids = this.signalsByComponent.get(componentId);
|
|
66
|
+
if (ids === undefined) {
|
|
67
|
+
ids = new Set();
|
|
68
|
+
this.signalsByComponent.set(componentId, ids);
|
|
69
|
+
}
|
|
70
|
+
ids.add(id);
|
|
61
71
|
this.bus.emit("signal:registered", entry);
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -73,20 +83,13 @@ export class Registry {
|
|
|
73
83
|
const entry = this.signals.get(id);
|
|
74
84
|
if (!entry) return;
|
|
75
85
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const updated: SignalEntry = {
|
|
82
|
-
...entry,
|
|
83
|
-
value: newValue,
|
|
84
|
-
history,
|
|
85
|
-
changedAt: Date.now(),
|
|
86
|
-
};
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
entry.history.push({ value: newValue, timestamp: now });
|
|
88
|
+
if (entry.history.length > MAX_HISTORY) entry.history.shift();
|
|
89
|
+
entry.value = newValue;
|
|
90
|
+
entry.changedAt = now;
|
|
87
91
|
|
|
88
|
-
this.
|
|
89
|
-
this.bus.emit("signal:changed", { entry: updated, oldValue });
|
|
92
|
+
this.bus.emit("signal:changed", { entry, oldValue });
|
|
90
93
|
this.pushTimeline({
|
|
91
94
|
type: "signal:change",
|
|
92
95
|
label: `${entry.componentName}.${key}`,
|
|
@@ -158,9 +161,8 @@ export class Registry {
|
|
|
158
161
|
entry.status = "unmounted";
|
|
159
162
|
this.bus.emit("component:unmount", { ...entry });
|
|
160
163
|
this.components.delete(id);
|
|
161
|
-
for (const
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
+
for (const sid of this.signalsByComponent.get(id) ?? []) this.signals.delete(sid);
|
|
165
|
+
this.signalsByComponent.delete(id);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -198,7 +200,10 @@ export class Registry {
|
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
getSignalsByComponent(componentId: string): SignalEntry[] {
|
|
201
|
-
|
|
203
|
+
const out: SignalEntry[] = [];
|
|
204
|
+
// The index and the signal map are updated together, so every id here has an entry.
|
|
205
|
+
for (const id of this.signalsByComponent.get(componentId) ?? []) out.push(this.signals.get(id) as SignalEntry);
|
|
206
|
+
return out;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
private pushTimeline(data: Omit<TimelineEntry, "id" | "timestamp">): void {
|
|
@@ -209,10 +214,7 @@ export class Registry {
|
|
|
209
214
|
};
|
|
210
215
|
|
|
211
216
|
this.timeline.push(entry);
|
|
212
|
-
|
|
213
|
-
if (this.timeline.length > MAX_TIMELINE) {
|
|
214
|
-
this.timeline.shift();
|
|
215
|
-
}
|
|
217
|
+
if (this.timeline.length > MAX_TIMELINE) this.timeline.shift();
|
|
216
218
|
|
|
217
219
|
this.bus.emit("timeline:push", entry);
|
|
218
220
|
}
|