@praxisjs/devtools 0.2.34 → 0.3.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/CHANGELOG.md +40 -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/src/decorators/debug.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed } from "@praxisjs/core/internal";
|
|
1
|
+
import { computed, effect, untrack } from "@praxisjs/core/internal";
|
|
2
2
|
import {
|
|
3
3
|
createMethodDecorator,
|
|
4
4
|
createGetterObserverDecorator,
|
|
@@ -8,8 +8,23 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { Registry } from "../core/registry";
|
|
10
10
|
|
|
11
|
+
// Read inline (not through a helper): bundlers replace process.env.NODE_ENV at build time, and only a
|
|
12
|
+
// literal check lets them drop the instrumentation, and the registry it uses, from production builds.
|
|
13
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
14
|
+
|
|
11
15
|
export interface DebugOptions {
|
|
16
|
+
/** Name shown in the panel. Defaults to the member name. */
|
|
12
17
|
label?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Also logs every change (fields, getters) or call (methods) to the console. `"trace"` logs with
|
|
20
|
+
* a stack trace, which shows what caused it.
|
|
21
|
+
*/
|
|
22
|
+
log?: boolean | "trace";
|
|
23
|
+
/**
|
|
24
|
+
* Pauses in the debugger on a change or call. A function decides per change: it receives the new
|
|
25
|
+
* and the previous value (for methods, the arguments and `undefined`).
|
|
26
|
+
*/
|
|
27
|
+
break?: boolean | ((next: unknown, prev: unknown) => boolean);
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
// Duck-type check: callable with .subscribe but no .set → Computed
|
|
@@ -37,11 +52,48 @@ interface DebugDecorator {
|
|
|
37
52
|
(value: undefined, context: ClassFieldDecoratorContext): void;
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
function componentNameOf(instance: object): string {
|
|
56
|
+
return (instance.constructor as { name: string }).name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
60
|
+
return (
|
|
61
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Console logging and debugger pauses requested through the options. */
|
|
68
|
+
function makeReporter(options: DebugOptions) {
|
|
69
|
+
const { log, break: pause } = options;
|
|
70
|
+
if (!log && !pause) return null;
|
|
71
|
+
return (what: string, next: unknown, prev: unknown): void => {
|
|
72
|
+
untrack(() => {
|
|
73
|
+
if (log === "trace") console.trace(`[PraxisJS] ${what}`, next, prev);
|
|
74
|
+
else if (log) console.log(`[PraxisJS] ${what}`, next, prev);
|
|
75
|
+
if (pause === true || (typeof pause === "function" && pause(next, prev))) {
|
|
76
|
+
debugger;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
/**
|
|
41
|
-
* Tracks state, computed values
|
|
42
|
-
*
|
|
83
|
+
* Tracks state, props, computed values and methods in the devtools panel.
|
|
84
|
+
*
|
|
85
|
+
* - Fields with an accessor (`@State`, `@Prop`, `@Persisted`, …) are observed reactively, so a
|
|
86
|
+
* change is reported whatever caused it, including a parent passing a new prop.
|
|
87
|
+
* - Fields holding a `computed()` report each new value.
|
|
88
|
+
* - `@Computed()` getters report each new value.
|
|
89
|
+
* - Methods report every call with its arguments, result and duration; for a method that returns a
|
|
90
|
+
* promise, when the promise settles.
|
|
43
91
|
*/
|
|
44
92
|
export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
93
|
+
// Production builds keep the member exactly as declared: no wrapper, no observation.
|
|
94
|
+
if (process.env.NODE_ENV === "production") return (() => undefined) as DebugDecorator;
|
|
95
|
+
const report = makeReporter(options);
|
|
96
|
+
|
|
45
97
|
const impl = function (
|
|
46
98
|
value: unknown,
|
|
47
99
|
context:
|
|
@@ -56,23 +108,28 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
56
108
|
createMethodDecorator({
|
|
57
109
|
wrap(_original, _instance, _name) {
|
|
58
110
|
return function (this: object, ...args: unknown[]) {
|
|
59
|
-
const componentName = (this
|
|
111
|
+
const componentName = componentNameOf(this);
|
|
112
|
+
report?.(`${componentName}.${label}()`, args, undefined);
|
|
60
113
|
const start = performance.now();
|
|
114
|
+
const record = (result: unknown): void => {
|
|
115
|
+
Registry.instance.recordMethodCall(
|
|
116
|
+
this, label, args, result, performance.now() - start, componentName,
|
|
117
|
+
);
|
|
118
|
+
};
|
|
61
119
|
let result: unknown;
|
|
62
|
-
let threw = false;
|
|
63
120
|
try {
|
|
64
121
|
result = original.apply(this, args);
|
|
65
122
|
} catch (err) {
|
|
66
|
-
|
|
67
|
-
result = err;
|
|
123
|
+
record(`throw ${String(err)}`);
|
|
68
124
|
throw err;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
duration, componentName,
|
|
125
|
+
}
|
|
126
|
+
if (isThenable(result)) {
|
|
127
|
+
result.then(
|
|
128
|
+
(resolved) => { record(resolved); },
|
|
129
|
+
(err: unknown) => { record(`reject ${String(err)}`); },
|
|
75
130
|
);
|
|
131
|
+
} else {
|
|
132
|
+
record(result);
|
|
76
133
|
}
|
|
77
134
|
return result;
|
|
78
135
|
};
|
|
@@ -85,7 +142,7 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
85
142
|
const originalGetter = value as (this: object) => unknown;
|
|
86
143
|
createGetterObserverDecorator({
|
|
87
144
|
observe(getter, instance, _name) {
|
|
88
|
-
const componentName = (instance
|
|
145
|
+
const componentName = componentNameOf(instance);
|
|
89
146
|
// Defer so field initializers (e.g. @State()) have run before we read them.
|
|
90
147
|
queueMicrotask(() => {
|
|
91
148
|
const c = computed(() => getter.call(instance));
|
|
@@ -95,6 +152,7 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
95
152
|
c.subscribe((newValue) => {
|
|
96
153
|
if (skipFirst) { skipFirst = false; return; }
|
|
97
154
|
Registry.instance.updateSignal(instance, label, newValue, prevValue);
|
|
155
|
+
report?.(`${componentName}.${label}`, newValue, prevValue);
|
|
98
156
|
prevValue = newValue;
|
|
99
157
|
});
|
|
100
158
|
});
|
|
@@ -105,26 +163,30 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
105
163
|
|
|
106
164
|
createFieldDecorator({
|
|
107
165
|
bind(instance, name, _initialValue): FieldBinding {
|
|
108
|
-
const componentName = (instance
|
|
166
|
+
const componentName = componentNameOf(instance);
|
|
109
167
|
|
|
110
|
-
// @State()
|
|
168
|
+
// @State(), @Prop() and friends run inner-first, so their accessor is already on the
|
|
169
|
+
// instance. Reading it in an effect reports every change, whoever made it.
|
|
111
170
|
const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
|
|
112
|
-
if (existingDesc?.get
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
if (existingDesc?.get !== undefined) {
|
|
172
|
+
const read = (): unknown => (instance as unknown as Record<string, unknown>)[name];
|
|
173
|
+
let prev: unknown;
|
|
174
|
+
let first = true;
|
|
175
|
+
const stop = effect(() => {
|
|
176
|
+
const next = read();
|
|
177
|
+
if (first) {
|
|
178
|
+
first = false;
|
|
179
|
+
prev = next;
|
|
180
|
+
untrack(() => { Registry.instance.registerSignal(instance, label, next, componentName); });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (Object.is(next, prev)) return;
|
|
184
|
+
const old = prev;
|
|
185
|
+
prev = next;
|
|
186
|
+
untrack(() => { Registry.instance.updateSignal(instance, label, next, old); });
|
|
187
|
+
report?.(`${componentName}.${label}`, next, old);
|
|
188
|
+
});
|
|
189
|
+
return { onUnmount: stop };
|
|
128
190
|
}
|
|
129
191
|
|
|
130
192
|
const initialValue = _initialValue;
|
|
@@ -148,6 +210,7 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
148
210
|
const unsub = comp.subscribe((newValue) => {
|
|
149
211
|
if (skipFirst) { skipFirst = false; return; }
|
|
150
212
|
Registry.instance.updateSignal(instance, label, newValue, prevValue);
|
|
213
|
+
report?.(`${componentName}.${label}`, newValue, prevValue);
|
|
151
214
|
prevValue = newValue;
|
|
152
215
|
});
|
|
153
216
|
Registry.instance.registerSignal(instance, label, prevValue, componentName);
|
|
@@ -171,6 +234,7 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
|
|
|
171
234
|
slot = { computed: newValue, unsub: subscribe(newValue) };
|
|
172
235
|
},
|
|
173
236
|
},
|
|
237
|
+
onUnmount() { slot.unsub(); },
|
|
174
238
|
};
|
|
175
239
|
},
|
|
176
240
|
})(undefined, context);
|
package/src/decorators/index.ts
CHANGED
package/src/decorators/trace.ts
CHANGED
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import { Registry } from "../core/registry";
|
|
9
9
|
|
|
10
|
+
// Read inline (not through a helper): bundlers replace process.env.NODE_ENV at build time, and only a
|
|
11
|
+
// literal check lets them drop the instrumentation, and the registry it uses, from production builds.
|
|
12
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
13
|
+
|
|
10
14
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
15
|
type AnyConstructor = new (...args: any[]) => any;
|
|
12
16
|
|
|
@@ -16,13 +20,23 @@ const EXTRA_HOOKS = ["onBeforeUpdate", "onUpdate", "onAfterUpdate"] as const;
|
|
|
16
20
|
* Instruments a component class to report renders and lifecycle events
|
|
17
21
|
* to the devtools panel.
|
|
18
22
|
*
|
|
19
|
-
* @Trace()
|
|
23
|
+
* @Trace({ slow: 4 }) // also warn when a render takes more than 4 ms
|
|
20
24
|
* @Component()
|
|
21
25
|
* class MyComponent extends StatefulComponent { ... }
|
|
22
26
|
*/
|
|
27
|
+
export interface TraceOptions {
|
|
28
|
+
/** Warns in the console when a render of this component takes longer than this many ms. */
|
|
29
|
+
slow?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
class TraceBehavior extends ClassBehavior {
|
|
33
|
+
constructor(private readonly slow: number | undefined) {
|
|
34
|
+
super();
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
create(instance: RootComponent): ClassEnhancement {
|
|
25
38
|
const registry = Registry.instance;
|
|
39
|
+
const slow = this.slow;
|
|
26
40
|
|
|
27
41
|
return {
|
|
28
42
|
onMount() {
|
|
@@ -36,6 +50,11 @@ class TraceBehavior extends ClassBehavior {
|
|
|
36
50
|
const result = originalRender();
|
|
37
51
|
const duration = performance.now() - start;
|
|
38
52
|
registry.recordRender(instance, duration);
|
|
53
|
+
if (slow !== undefined && duration > slow) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[PraxisJS DevTools] <${instance.constructor.name}> rendered in ${duration.toFixed(1)} ms (over ${String(slow)} ms).`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
39
58
|
return result;
|
|
40
59
|
},
|
|
41
60
|
};
|
|
@@ -64,8 +83,12 @@ class TraceBehavior extends ClassBehavior {
|
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
|
|
86
|
+
// Created on first use: a module-level instance would keep this class and the registry in
|
|
87
|
+
// production bundles, where @Trace() does nothing.
|
|
88
|
+
let defaultBehavior: TraceBehavior | undefined;
|
|
68
89
|
|
|
69
|
-
export function Trace() {
|
|
70
|
-
|
|
90
|
+
export function Trace(options: TraceOptions = {}) {
|
|
91
|
+
// Production builds keep the class exactly as declared.
|
|
92
|
+
if (process.env.NODE_ENV === "production") return <T>(constructor: T, _context: ClassDecoratorContext): T => constructor;
|
|
93
|
+
return createClassDecorator(options.slow === undefined ? (defaultBehavior ??= new TraceBehavior(undefined)) : new TraceBehavior(options.slow));
|
|
71
94
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { DevToolsOptions } from "@/ui/dev-tools";
|
|
|
3
3
|
|
|
4
4
|
export { Debug, Trace } from "@/decorators";
|
|
5
5
|
export type { DebugOptions } from "@/decorators/debug";
|
|
6
|
+
export type { TraceOptions } from "@/decorators/trace";
|
|
6
7
|
|
|
7
8
|
export { Registry } from "@/core/registry";
|
|
8
9
|
export type {
|
|
@@ -18,3 +19,7 @@ export type { DevtoolsPlugin } from "@/plugins/types";
|
|
|
18
19
|
export { SignalsPlugin } from "@/plugins/signals";
|
|
19
20
|
export { ComponentsPlugin } from "@/plugins/components";
|
|
20
21
|
export { TimelinePlugin } from "@/plugins/timeline";
|
|
22
|
+
export { PerformancePlugin } from "@/plugins/performance";
|
|
23
|
+
|
|
24
|
+
export { Profiler } from "@/core/profiler";
|
|
25
|
+
export type { ProfileSnapshot, OwnerStats } from "@/core/profiler";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { EmptyState } from "@shared/empty-state";
|
|
2
|
+
|
|
3
|
+
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
+
import { Component, Prop, State } from "@praxisjs/decorators";
|
|
5
|
+
import { For } from "@praxisjs/runtime";
|
|
6
|
+
|
|
7
|
+
import type { ProfileSnapshot } from "@core/profiler";
|
|
8
|
+
import type { Registry } from "@core/registry";
|
|
9
|
+
|
|
10
|
+
const BUTTON =
|
|
11
|
+
"text-[11px] px-2 py-[3px] rounded cursor-pointer font-sans border border-border text-muted hover:text-text transition-colors duration-150";
|
|
12
|
+
|
|
13
|
+
function ms(value: number): string {
|
|
14
|
+
return `${value.toFixed(value < 10 ? 2 : 1)} ms`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Component()
|
|
18
|
+
export class PerformanceTab extends StatefulComponent {
|
|
19
|
+
@State() snapshot: ProfileSnapshot | null = null;
|
|
20
|
+
@Prop() registry!: Registry;
|
|
21
|
+
|
|
22
|
+
private _off: (() => void) | null = null;
|
|
23
|
+
|
|
24
|
+
onMount() {
|
|
25
|
+
const { profiler } = this.registry;
|
|
26
|
+
this.refresh();
|
|
27
|
+
this._off = profiler.bus.on("profile:changed", () => { this.refresh(); });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onUnmount() {
|
|
31
|
+
this._off?.();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The panel's own updates are kept out of the recording.
|
|
35
|
+
refresh() {
|
|
36
|
+
const { profiler } = this.registry;
|
|
37
|
+
profiler.muted(() => { this.snapshot = profiler.snapshot(); });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render() {
|
|
41
|
+
const { profiler } = this.registry;
|
|
42
|
+
return (
|
|
43
|
+
<div class="flex flex-col h-full overflow-hidden">
|
|
44
|
+
<div class="flex items-center gap-[6px] px-2 py-2 border-b border-border bg-bg shrink-0">
|
|
45
|
+
<button
|
|
46
|
+
class={() => `${BUTTON} ${this.snapshot?.recording ? "text-warn border-warn" : ""}`}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
if (profiler.isRecording) profiler.stop();
|
|
49
|
+
else profiler.start();
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{() => (this.snapshot?.recording ? "Stop" : "Record")}
|
|
53
|
+
</button>
|
|
54
|
+
<button class={BUTTON} onClick={() => { profiler.reset(); }}>
|
|
55
|
+
Reset
|
|
56
|
+
</button>
|
|
57
|
+
<div class="flex-1" />
|
|
58
|
+
<span class="text-[10px] text-subtle font-mono">
|
|
59
|
+
{() => (this.snapshot ? `${(this.snapshot.duration / 1000).toFixed(1)} s` : "")}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="grid grid-cols-4 gap-px bg-border border-b border-border shrink-0">
|
|
64
|
+
{() => {
|
|
65
|
+
const s = this.snapshot;
|
|
66
|
+
const cells: Array<[string, string]> = s
|
|
67
|
+
? [
|
|
68
|
+
["Writes", String(s.writes)],
|
|
69
|
+
["Effect runs", String(s.effectRuns)],
|
|
70
|
+
["Effect time", ms(s.effectMs)],
|
|
71
|
+
["Computed runs", `${String(s.computeRuns)} · ${ms(s.computeMs)}`],
|
|
72
|
+
]
|
|
73
|
+
: [];
|
|
74
|
+
return cells.map(([label, value]) => (
|
|
75
|
+
<div class="bg-bg px-3 py-2">
|
|
76
|
+
<div class="text-[9px] text-subtle font-bold tracking-[0.12em] uppercase">{label}</div>
|
|
77
|
+
<div class="text-[13px] text-text font-mono">{value}</div>
|
|
78
|
+
</div>
|
|
79
|
+
));
|
|
80
|
+
}}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="grid grid-cols-[1.4fr_0.6fr_0.8fr_0.8fr] items-center px-3 h-7 text-[9px] text-subtle font-bold tracking-[0.12em] uppercase border-b border-border bg-section gap-2 shrink-0">
|
|
84
|
+
<span>Component</span>
|
|
85
|
+
<span>Runs</span>
|
|
86
|
+
<span>Time</span>
|
|
87
|
+
<span>Slowest</span>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="flex-1 overflow-y-auto">
|
|
91
|
+
<For
|
|
92
|
+
each={() => this.snapshot?.owners ?? []}
|
|
93
|
+
by="name"
|
|
94
|
+
fallback={
|
|
95
|
+
<EmptyState message="Press Record, use your app, then Stop. Bindings and effects are grouped by the component that created them." />
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
{(owner) => (
|
|
99
|
+
<div class="grid grid-cols-[1.4fr_0.6fr_0.8fr_0.8fr] items-center px-3 h-7 text-[11px] border-b border-border gap-2 font-mono">
|
|
100
|
+
<span class="text-accent truncate">{() => owner().name}</span>
|
|
101
|
+
<span class="text-text">{() => String(owner().effectRuns)}</span>
|
|
102
|
+
<span class="text-text">{() => ms(owner().effectMs)}</span>
|
|
103
|
+
<span class="text-muted">{() => ms(owner().maxMs)}</span>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</For>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/ui/dev-tools.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import logo from "@assets/logo.svg";
|
|
|
2
2
|
import unoStyles from "@assets/uno.generated.css?inline";
|
|
3
3
|
import { Registry } from "@core/registry";
|
|
4
4
|
import { ComponentsPlugin } from "@plugins/components";
|
|
5
|
+
import { PerformancePlugin } from "@plugins/performance";
|
|
5
6
|
import { SignalsPlugin } from "@plugins/signals";
|
|
6
7
|
import { TimelinePlugin } from "@plugins/timeline";
|
|
7
8
|
import unoReset from "@unocss/reset/tailwind-v4.css?inline";
|
|
@@ -18,6 +19,7 @@ const DEFAULT_PLUGINS: DevtoolsPlugin[] = [
|
|
|
18
19
|
SignalsPlugin,
|
|
19
20
|
ComponentsPlugin,
|
|
20
21
|
TimelinePlugin,
|
|
22
|
+
PerformancePlugin,
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
export interface DevToolsOptions {
|
|
@@ -75,6 +77,9 @@ export const DevTools = {
|
|
|
75
77
|
|
|
76
78
|
this.plugins.forEach((p) => p.setup?.(Registry.instance));
|
|
77
79
|
|
|
80
|
+
// Reachable from the browser console: __PRAXIS_DEVTOOLS__.profiler.start(), .getSignals(), …
|
|
81
|
+
(globalThis as { __PRAXIS_DEVTOOLS__?: Registry }).__PRAXIS_DEVTOOLS__ = Registry.instance;
|
|
82
|
+
|
|
78
83
|
const host = document.createElement("div");
|
|
79
84
|
host.id = "praxisjs-devtools";
|
|
80
85
|
document.body.appendChild(host);
|