bee-ladybug 0.5.4 → 0.6.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/README.MD +17 -1
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +52 -1
- package/src/adapters/CSSAdapter.js +131 -3
- package/src/adapters/CanvasAdapter.js +87 -1
- package/src/adapters/SpiderAdapter.js +53 -2
- package/src/adapters/WebDOMAdapter.js +209 -1
- package/src/adapters/python/PythonBridge.js +74 -2
- package/src/core/BeeLadybugCore.js +48 -4
- package/src/core/ReportExporter.js +695 -0
- package/src/core/UIOverlay.js +3 -1
- package/src/index.d.ts +89 -0
- package/src/index.js +1 -0
package/README.MD
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# BeeLadybug
|
|
3
3
|
|
|
4
4
|
<p align="center">
|
|
5
|
-
<img src="https://
|
|
5
|
+
<img src="https://beeenginejs.com/wp-content/uploads/2026/10/beeladybug-logo-animato-ezgif.com-effects.gif" alt="BeeEngine Logo" width="900" />
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
Universal telemetry and visual inspection overlay.
|
|
@@ -133,12 +133,26 @@ bee.subscribe((packet) => {
|
|
|
133
133
|
| `F2` | mostra / nascondi overlay (e hitbox canvas) |
|
|
134
134
|
| `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
|
|
135
135
|
| `F4` | freeze / run del tempo fittizio |
|
|
136
|
+
| `F8` | scarica il report HTML |
|
|
136
137
|
| `F6` | CSS hunt, se `CSSAdapter` è attaccato |
|
|
137
138
|
| `P` | blocca il pannello CSS sull'elemento corrente, solo con la hunt accesa |
|
|
138
139
|
| `S` | scansione CSS, solo con la hunt accesa |
|
|
139
140
|
|
|
140
141
|
Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
|
|
141
142
|
|
|
143
|
+
## Export
|
|
144
|
+
|
|
145
|
+
`F8`, il pulsante **F8 EXPORT** nel piede dell'overlay, o `exportReport()` scaricano `report-AAAAMMGG-HHMM.html`. Il file raccoglie log, serie e una sezione per ogni adapter attaccato. `json` e `csv` usano gli stessi dati.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { exportReport } from './src/index.js';
|
|
149
|
+
|
|
150
|
+
exportReport(bee, { format: 'html' });
|
|
151
|
+
bee.exportReport('json');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Con `ui: false`, o senza `document`, la chiamata restituisce la stringa o l'oggetto e non scarica nulla.
|
|
155
|
+
|
|
142
156
|
## CanvasAdapter
|
|
143
157
|
|
|
144
158
|
```js
|
|
@@ -350,6 +364,7 @@ const bee = new BeeLadybugCore({
|
|
|
350
364
|
toggleKey: 'F2',
|
|
351
365
|
slowKey: 'F3',
|
|
352
366
|
freezeKey: 'F4',
|
|
367
|
+
exportKey: 'F8',
|
|
353
368
|
slowScale: 0.25,
|
|
354
369
|
maxLogs: 250,
|
|
355
370
|
historySize: 120,
|
|
@@ -368,6 +383,7 @@ bee.getHud();
|
|
|
368
383
|
bee.show(); bee.hide(); bee.toggle();
|
|
369
384
|
bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
|
|
370
385
|
bee.applySlowMo(); bee.restoreRealtime();
|
|
386
|
+
bee.exportReport('html');
|
|
371
387
|
bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
|
|
372
388
|
bee.setActiveAssistant('mock');
|
|
373
389
|
await bee.ask('why fps drop?');
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* AntAdapter — DOM mutation rate tracker.
|
|
3
3
|
*
|
|
4
4
|
* Independent MutationObserver. Can sit on the same root as WebDOMAdapter.
|
|
5
|
+
* `snapshot()` lists nodes still over the mutation threshold.
|
|
5
6
|
* The core must never import this file.
|
|
6
7
|
*
|
|
7
8
|
* @example
|
|
@@ -9,6 +10,10 @@
|
|
|
9
10
|
* ant.attach();
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { isReportNoise } from '../core/ReportExporter.js';
|
|
14
|
+
|
|
15
|
+
const MAX_HOT = 40;
|
|
16
|
+
|
|
12
17
|
export const ANT_ADAPTER_DEFAULTS = Object.freeze({
|
|
13
18
|
threshold: 30,
|
|
14
19
|
windowMs: 1000,
|
|
@@ -35,12 +40,14 @@ export class AntAdapter {
|
|
|
35
40
|
this.#attached = false;
|
|
36
41
|
this.#observer = null;
|
|
37
42
|
this.#counts = new WeakMap();
|
|
43
|
+
this.#hot = [];
|
|
38
44
|
this.#onMutate = (records) => this.#ingest(records);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
#attached;
|
|
42
48
|
#observer;
|
|
43
49
|
#counts;
|
|
50
|
+
#hot;
|
|
44
51
|
#onMutate;
|
|
45
52
|
|
|
46
53
|
attach() {
|
|
@@ -50,7 +57,8 @@ export class AntAdapter {
|
|
|
50
57
|
this.core?.registerAdapter('ant', {
|
|
51
58
|
enable: () => this.#enable(),
|
|
52
59
|
disable: () => this.#disable(),
|
|
53
|
-
label: 'ANT'
|
|
60
|
+
label: 'ANT',
|
|
61
|
+
snapshot: () => this.snapshot()
|
|
54
62
|
});
|
|
55
63
|
this.core?.sendData('log', {
|
|
56
64
|
source: this.source,
|
|
@@ -67,6 +75,30 @@ export class AntAdapter {
|
|
|
67
75
|
return this;
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Nodes whose current window is still at or above the threshold.
|
|
80
|
+
* Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
|
|
81
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
82
|
+
*/
|
|
83
|
+
snapshot() {
|
|
84
|
+
if (!this.#attached) return null;
|
|
85
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
86
|
+
const items = [];
|
|
87
|
+
for (let i = 0; i < this.#hot.length && items.length < MAX_HOT; i++) {
|
|
88
|
+
const row = this.#hot[i];
|
|
89
|
+
if (!row || now - row.windowStart >= this.windowMs) continue;
|
|
90
|
+
if (isBeeChrome(row.node) || isReportNoise(row.node)) continue;
|
|
91
|
+
items.push({
|
|
92
|
+
severity: 'warn',
|
|
93
|
+
selector: row.selector,
|
|
94
|
+
rule: 'mutations',
|
|
95
|
+
value: `${row.count}/${this.windowMs}ms`,
|
|
96
|
+
source: this.source
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { name: 'ant', items };
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
#enable() {
|
|
71
103
|
if (!this.#attached || this.#observer) return;
|
|
72
104
|
if (typeof MutationObserver === 'undefined' || !this.root) return;
|
|
@@ -89,6 +121,7 @@ export class AntAdapter {
|
|
|
89
121
|
if (this.#observer) this.#observer.disconnect();
|
|
90
122
|
this.#observer = null;
|
|
91
123
|
this.#counts = new WeakMap();
|
|
124
|
+
this.#hot = [];
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
#ingest(records) {
|
|
@@ -104,6 +137,7 @@ export class AntAdapter {
|
|
|
104
137
|
this.#counts.set(node, slot);
|
|
105
138
|
}
|
|
106
139
|
slot.count += 1;
|
|
140
|
+
this.#track(node, slot);
|
|
107
141
|
if (slot.count < this.threshold || slot.warned) continue;
|
|
108
142
|
|
|
109
143
|
slot.warned = true;
|
|
@@ -115,6 +149,23 @@ export class AntAdapter {
|
|
|
115
149
|
});
|
|
116
150
|
}
|
|
117
151
|
}
|
|
152
|
+
|
|
153
|
+
#track(node, slot) {
|
|
154
|
+
const index = this.#hot.findIndex((row) => row.node === node);
|
|
155
|
+
if (slot.count < this.threshold) {
|
|
156
|
+
if (index >= 0) this.#hot.splice(index, 1);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const row = {
|
|
160
|
+
node,
|
|
161
|
+
selector: nodeSelector(node),
|
|
162
|
+
count: slot.count,
|
|
163
|
+
windowStart: slot.windowStart
|
|
164
|
+
};
|
|
165
|
+
if (index >= 0) this.#hot[index] = row;
|
|
166
|
+
else this.#hot.push(row);
|
|
167
|
+
if (this.#hot.length > MAX_HOT) this.#hot.splice(0, this.#hot.length - MAX_HOT);
|
|
168
|
+
}
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
function positive(value, fallback) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Hover an element to list !important hits, viewport overflow, negative
|
|
5
5
|
* margins, invisible text, click blockers and stretched images. The core
|
|
6
6
|
* only receives packets and detail rows; it does not parse CSS.
|
|
7
|
+
* `snapshot()` repeats that scan for the report while the hunt is off.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* const css = new CSSAdapter(core);
|
|
@@ -11,6 +12,10 @@
|
|
|
11
12
|
* // F6 toggles hunt. S scans the page while hunt is on.
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
import { isReportNoise } from '../core/ReportExporter.js';
|
|
16
|
+
|
|
17
|
+
const MAX_SNAPSHOT = 80;
|
|
18
|
+
|
|
14
19
|
export const CSS_ADAPTER_DEFAULTS = Object.freeze({
|
|
15
20
|
source: 'css',
|
|
16
21
|
hotkey: 'F6',
|
|
@@ -104,7 +109,8 @@ export class CSSAdapter {
|
|
|
104
109
|
hotkey: this.hotkey,
|
|
105
110
|
enabled: false,
|
|
106
111
|
enable: () => this.#enable(),
|
|
107
|
-
disable: () => this.#disable()
|
|
112
|
+
disable: () => this.#disable(),
|
|
113
|
+
snapshot: () => this.snapshot()
|
|
108
114
|
});
|
|
109
115
|
this.core?.sendData('log', {
|
|
110
116
|
source: this.source,
|
|
@@ -191,6 +197,98 @@ export class CSSAdapter {
|
|
|
191
197
|
return this;
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Full hunt scan for the report. Hunt can stay off: nothing is logged,
|
|
202
|
+
* pinned, or published. Hidden boxes, WordPress admin, and BeeLadybug
|
|
203
|
+
* chrome are skipped.
|
|
204
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
205
|
+
*/
|
|
206
|
+
snapshot() {
|
|
207
|
+
if (!this.#attached) return null;
|
|
208
|
+
if (typeof document === 'undefined' || !document.body) return { name: 'css', items: [] };
|
|
209
|
+
|
|
210
|
+
const built = collectImportantRules();
|
|
211
|
+
const nodes = document.body.querySelectorAll('*');
|
|
212
|
+
const overflow = [];
|
|
213
|
+
const rows = [];
|
|
214
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
215
|
+
const el = nodes[i];
|
|
216
|
+
if (!el || isBeeChrome(el) || isSkippedTag(el) || isReportNoise(el)) continue;
|
|
217
|
+
const measured = measureViewportOverflow(el);
|
|
218
|
+
if (measured) overflow.push(measured);
|
|
219
|
+
const findings = [];
|
|
220
|
+
pushNegativeMargins(el, findings);
|
|
221
|
+
pushInvisible(el, findings);
|
|
222
|
+
pushBlockers(el, findings, true);
|
|
223
|
+
pushImage(el, findings);
|
|
224
|
+
pushStacking(el, findings, true);
|
|
225
|
+
pushImportant(el, built.rules, findings);
|
|
226
|
+
for (let j = 0; j < findings.length; j++) {
|
|
227
|
+
rows.push({ el, selector: cssPath(el), ...findings[j] });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const kept = keepOverflowCauses(overflow);
|
|
231
|
+
for (let i = 0; i < kept.length; i++) {
|
|
232
|
+
const cause = kept[i];
|
|
233
|
+
rows.push({
|
|
234
|
+
el: cause.el,
|
|
235
|
+
selector: cssPath(cause.el),
|
|
236
|
+
kind: 'overflows viewport',
|
|
237
|
+
rank: 0,
|
|
238
|
+
level: 'error',
|
|
239
|
+
score: cause.score
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let causes = [];
|
|
244
|
+
const plain = [];
|
|
245
|
+
for (let i = 0; i < rows.length; i++) {
|
|
246
|
+
const row = rows[i];
|
|
247
|
+
if (describeCause(row.kind, row.selector)) {
|
|
248
|
+
causes = addCause(causes, { kind: row.kind, path: row.selector, el: row.el }, MAX_SNAPSHOT);
|
|
249
|
+
} else {
|
|
250
|
+
plain.push(row);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const items = [];
|
|
255
|
+
for (let i = 0; i < causes.length; i++) {
|
|
256
|
+
const entry = causes[i];
|
|
257
|
+
items.push({
|
|
258
|
+
severity: reportSeverity(entry.level),
|
|
259
|
+
selector: entry.cause,
|
|
260
|
+
rule: causeRule(entry.type),
|
|
261
|
+
value: formatCause(entry),
|
|
262
|
+
source: this.source
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const groups = groupIssues(plain);
|
|
266
|
+
for (let i = 0; i < groups.length; i++) {
|
|
267
|
+
const group = groups[i];
|
|
268
|
+
const count = group.count > 1 ? ` ×${group.count}` : '';
|
|
269
|
+
items.push({
|
|
270
|
+
severity: reportSeverity(group.level),
|
|
271
|
+
selector: group.selector,
|
|
272
|
+
rule: plainRule(group.kind),
|
|
273
|
+
value: `${group.kind}${count}`,
|
|
274
|
+
source: this.source
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
for (let i = 0; i < built.unreadable.length; i++) {
|
|
278
|
+
const label = built.unreadable[i];
|
|
279
|
+
items.push({
|
|
280
|
+
severity: 'ok',
|
|
281
|
+
selector: label,
|
|
282
|
+
rule: 'unreadable sheet',
|
|
283
|
+
value: label,
|
|
284
|
+
source: this.source
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
items.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
288
|
+
if (items.length > MAX_SNAPSHOT) items.length = MAX_SNAPSHOT;
|
|
289
|
+
return { name: 'css', items };
|
|
290
|
+
}
|
|
291
|
+
|
|
194
292
|
#enable() {
|
|
195
293
|
if (this.#active || typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
196
294
|
this.#active = true;
|
|
@@ -657,7 +755,7 @@ function pushInvisible(el, findings) {
|
|
|
657
755
|
}
|
|
658
756
|
}
|
|
659
757
|
|
|
660
|
-
function pushBlockers(el, findings) {
|
|
758
|
+
function pushBlockers(el, findings, skipNoise = false) {
|
|
661
759
|
if (!el.getBoundingClientRect || typeof document.elementFromPoint !== 'function') return;
|
|
662
760
|
const rect = el.getBoundingClientRect();
|
|
663
761
|
const x = rect.left + rect.width / 2;
|
|
@@ -670,6 +768,7 @@ function pushBlockers(el, findings) {
|
|
|
670
768
|
const hit = document.elementFromPoint(x, y);
|
|
671
769
|
if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
|
|
672
770
|
if (pseudo && (hit === document.documentElement || hit === document.body)) return;
|
|
771
|
+
if (skipNoise && isReportNoise(hit)) return;
|
|
673
772
|
findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
|
|
674
773
|
}
|
|
675
774
|
|
|
@@ -852,7 +951,7 @@ function willChangeHint(value) {
|
|
|
852
951
|
return hit.join(', ');
|
|
853
952
|
}
|
|
854
953
|
|
|
855
|
-
function pushStacking(el, findings) {
|
|
954
|
+
function pushStacking(el, findings, skipNoise = false) {
|
|
856
955
|
if (!el || typeof getComputedStyle !== 'function' || !el.parentElement) return;
|
|
857
956
|
const own = getComputedStyle(el);
|
|
858
957
|
const ownZ = own.zIndex || own.getPropertyValue?.('z-index') || 'auto';
|
|
@@ -860,6 +959,7 @@ function pushStacking(el, findings) {
|
|
|
860
959
|
let node = el.parentElement;
|
|
861
960
|
while (node && node.nodeType === 1) {
|
|
862
961
|
if (isBeeChrome(node)) return;
|
|
962
|
+
if (skipNoise && isReportNoise(node)) return;
|
|
863
963
|
const found = stackingReasons(getComputedStyle(node));
|
|
864
964
|
const ancestor = cssPath(node);
|
|
865
965
|
if (found.bugs.length) {
|
|
@@ -919,6 +1019,34 @@ function nowMs() {
|
|
|
919
1019
|
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
920
1020
|
}
|
|
921
1021
|
|
|
1022
|
+
function reportSeverity(level) {
|
|
1023
|
+
if (level === 'error') return 'error';
|
|
1024
|
+
if (level === 'warn' || level === 'warning') return 'warn';
|
|
1025
|
+
return 'ok';
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function severityRank(level) {
|
|
1029
|
+
if (level === 'error') return 0;
|
|
1030
|
+
if (level === 'warn') return 1;
|
|
1031
|
+
return 2;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function causeRule(type) {
|
|
1035
|
+
if (type === 'block') return 'click blocked';
|
|
1036
|
+
if (type === 'stack') return 'stacking context';
|
|
1037
|
+
if (type === 'stack-hint') return 'will-change';
|
|
1038
|
+
return 'overflow';
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function plainRule(kind) {
|
|
1042
|
+
const text = String(kind || '');
|
|
1043
|
+
if (text.startsWith('!important')) return '!important';
|
|
1044
|
+
if (text.startsWith('invisible text')) return 'invisible text';
|
|
1045
|
+
if (text.startsWith('negative margin')) return 'negative margin';
|
|
1046
|
+
if (text === 'distorted image') return 'distorted image';
|
|
1047
|
+
return text;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
922
1050
|
function issue(kind, rank, score) {
|
|
923
1051
|
return { level: 'error', rank, score, kind, text: kind };
|
|
924
1052
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Reads generic AABB entities, measures host FPS, computes collisions,
|
|
5
5
|
* and pushes packets into the core. Hitboxes are drawn on a stacked
|
|
6
6
|
* overlay canvas so the game pixels stay untouched.
|
|
7
|
+
* `snapshot()` hands the report hitboxes, collisions, and FPS min/avg/max.
|
|
7
8
|
*
|
|
8
9
|
* The core must never import this file.
|
|
9
10
|
*
|
|
@@ -59,6 +60,7 @@ export class CanvasAdapter {
|
|
|
59
60
|
this.timeScale = 1;
|
|
60
61
|
|
|
61
62
|
this.#attached = false;
|
|
63
|
+
this.#reporting = true;
|
|
62
64
|
this.#overlay = null;
|
|
63
65
|
this.#unsubscribe = null;
|
|
64
66
|
this.#rafId = 0;
|
|
@@ -66,6 +68,7 @@ export class CanvasAdapter {
|
|
|
66
68
|
this.#manualPumpWarned = false;
|
|
67
69
|
this.#lastPump = 0;
|
|
68
70
|
this.#fps = 0;
|
|
71
|
+
this.#fpsSamples = [];
|
|
69
72
|
this.#wasColliding = new Set();
|
|
70
73
|
this.#onResize = () => this.#syncOverlay();
|
|
71
74
|
this.#onTick = () => {
|
|
@@ -80,6 +83,7 @@ export class CanvasAdapter {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
#attached;
|
|
86
|
+
#reporting;
|
|
83
87
|
#overlay;
|
|
84
88
|
#unsubscribe;
|
|
85
89
|
#rafId;
|
|
@@ -87,6 +91,7 @@ export class CanvasAdapter {
|
|
|
87
91
|
#manualPumpWarned;
|
|
88
92
|
#lastPump;
|
|
89
93
|
#fps;
|
|
94
|
+
#fpsSamples;
|
|
90
95
|
#wasColliding;
|
|
91
96
|
#onResize;
|
|
92
97
|
#onTick;
|
|
@@ -108,6 +113,15 @@ export class CanvasAdapter {
|
|
|
108
113
|
}
|
|
109
114
|
if (this.autoPump) this.startAutoPump();
|
|
110
115
|
|
|
116
|
+
this.#reporting = true;
|
|
117
|
+
this.core?.registerAdapter('canvas', {
|
|
118
|
+
label: 'CANVAS',
|
|
119
|
+
enabled: true,
|
|
120
|
+
enable: () => this.#enable(),
|
|
121
|
+
disable: () => this.#disable(),
|
|
122
|
+
snapshot: () => this.snapshot()
|
|
123
|
+
});
|
|
124
|
+
|
|
111
125
|
this.core?.sendData('log', {
|
|
112
126
|
source: this.source,
|
|
113
127
|
message: `CanvasAdapter attached (${this.entities.length} entities).`
|
|
@@ -126,6 +140,7 @@ export class CanvasAdapter {
|
|
|
126
140
|
this.#stopAutoPumpLoop();
|
|
127
141
|
if (this.#unsubscribe) this.#unsubscribe();
|
|
128
142
|
this.#unsubscribe = null;
|
|
143
|
+
this.core?.unregisterAdapter('canvas');
|
|
129
144
|
this.#unmountOverlay();
|
|
130
145
|
if (typeof window !== 'undefined') {
|
|
131
146
|
window.removeEventListener('resize', this.#onResize);
|
|
@@ -161,6 +176,28 @@ export class CanvasAdapter {
|
|
|
161
176
|
return this;
|
|
162
177
|
}
|
|
163
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Hitboxes currently on the entity list, plus FPS min/avg/max.
|
|
181
|
+
* Returns null when the adapter is not attached. The core stores this
|
|
182
|
+
* function and does not read the rows.
|
|
183
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
184
|
+
*/
|
|
185
|
+
snapshot() {
|
|
186
|
+
if (!this.#attached) return null;
|
|
187
|
+
const boxes = this.#collectBoxes();
|
|
188
|
+
this.#markCollisions(boxes);
|
|
189
|
+
const items = boxes.map((box) => ({
|
|
190
|
+
severity: box.hit ? 'warn' : 'ok',
|
|
191
|
+
selector: box.name,
|
|
192
|
+
rule: 'hitbox',
|
|
193
|
+
value: `${round1(box.x)},${round1(box.y)} ${round1(box.w)}x${round1(box.h)}${box.hit ? ' colliding' : ''}`,
|
|
194
|
+
source: this.source
|
|
195
|
+
}));
|
|
196
|
+
const fps = this.#fpsItem();
|
|
197
|
+
if (fps) items.push(fps);
|
|
198
|
+
return { name: 'canvas', items };
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
/**
|
|
165
202
|
* Call once per host frame, after entity positions are updated.
|
|
166
203
|
* Pass a 2D context only if overlay is disabled and you want hitboxes
|
|
@@ -174,6 +211,7 @@ export class CanvasAdapter {
|
|
|
174
211
|
this.#warnManualPump();
|
|
175
212
|
return this;
|
|
176
213
|
}
|
|
214
|
+
if (!this.#reporting) return this;
|
|
177
215
|
|
|
178
216
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
179
217
|
if (this.#lastPump) {
|
|
@@ -257,6 +295,8 @@ export class CanvasAdapter {
|
|
|
257
295
|
if (!tracked && boxes[0]) tracked = boxes[0];
|
|
258
296
|
|
|
259
297
|
if (this.#fps > 0) {
|
|
298
|
+
this.#fpsSamples.push(this.#fps);
|
|
299
|
+
if (this.#fpsSamples.length > 120) this.#fpsSamples.shift();
|
|
260
300
|
this.core.sendData('fps', { value: this.#fps, source: this.source });
|
|
261
301
|
}
|
|
262
302
|
this.core.sendData('metric', {
|
|
@@ -374,7 +414,49 @@ export class CanvasAdapter {
|
|
|
374
414
|
overlay.style.top = `${rect.top}px`;
|
|
375
415
|
overlay.style.width = `${rect.width}px`;
|
|
376
416
|
overlay.style.height = `${rect.height}px`;
|
|
377
|
-
|
|
417
|
+
const hidden = !this.#reporting || (this.core && this.core.visible === false);
|
|
418
|
+
overlay.style.display = hidden ? 'none' : 'block';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#enable() {
|
|
422
|
+
this.#reporting = true;
|
|
423
|
+
this.#syncOverlay();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#disable() {
|
|
427
|
+
this.#reporting = false;
|
|
428
|
+
this.#syncOverlay();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#fpsItem() {
|
|
432
|
+
const stats = this.#fpsStats();
|
|
433
|
+
if (!stats) return null;
|
|
434
|
+
return {
|
|
435
|
+
severity: 'ok',
|
|
436
|
+
selector: 'host',
|
|
437
|
+
rule: 'fps',
|
|
438
|
+
value: `min ${round1(stats.min)} avg ${round1(stats.avg)} max ${round1(stats.max)}`,
|
|
439
|
+
source: this.source
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#fpsStats() {
|
|
444
|
+
const history = typeof this.core?.getHistory === 'function' ? this.core.getHistory('fps') : null;
|
|
445
|
+
if (history && Array.isArray(history.values) && history.values.length) {
|
|
446
|
+
return { min: history.min, avg: history.avg, max: history.max };
|
|
447
|
+
}
|
|
448
|
+
const samples = this.#fpsSamples;
|
|
449
|
+
if (!samples.length) return null;
|
|
450
|
+
let min = samples[0];
|
|
451
|
+
let max = samples[0];
|
|
452
|
+
let sum = 0;
|
|
453
|
+
for (let i = 0; i < samples.length; i++) {
|
|
454
|
+
const value = samples[i];
|
|
455
|
+
if (value < min) min = value;
|
|
456
|
+
if (value > max) max = value;
|
|
457
|
+
sum += value;
|
|
458
|
+
}
|
|
459
|
+
return { min, avg: sum / samples.length, max };
|
|
378
460
|
}
|
|
379
461
|
}
|
|
380
462
|
|
|
@@ -409,3 +491,7 @@ function idFromKey(id) {
|
|
|
409
491
|
const parts = String(id).split(':');
|
|
410
492
|
return parts[parts.length - 1] || id;
|
|
411
493
|
}
|
|
494
|
+
|
|
495
|
+
function round1(value) {
|
|
496
|
+
return Math.round(Number(value) * 10) / 10;
|
|
497
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SpiderAdapter — long-task and slow-resource tracker.
|
|
3
3
|
*
|
|
4
|
-
* Native PerformanceObserver only.
|
|
4
|
+
* Native PerformanceObserver only. `snapshot()` keeps the long tasks and
|
|
5
|
+
* slow resources already seen. The core must never import this file.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
|
|
8
9
|
* spider.attach();
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
const MAX_SPIDER = 40;
|
|
13
|
+
|
|
11
14
|
export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
|
|
12
15
|
longTaskThreshold: 50,
|
|
13
16
|
resourceThreshold: 500,
|
|
@@ -30,6 +33,8 @@ export class SpiderAdapter {
|
|
|
30
33
|
this.#longObserver = null;
|
|
31
34
|
this.#resourceObserver = null;
|
|
32
35
|
this.#longTaskMissingLogged = false;
|
|
36
|
+
this.#longTasks = [];
|
|
37
|
+
this.#resources = [];
|
|
33
38
|
this.#onLongTask = (list) => this.#ingestLongTasks(list);
|
|
34
39
|
this.#onResource = (list) => this.#ingestResources(list);
|
|
35
40
|
}
|
|
@@ -38,6 +43,8 @@ export class SpiderAdapter {
|
|
|
38
43
|
#longObserver;
|
|
39
44
|
#resourceObserver;
|
|
40
45
|
#longTaskMissingLogged;
|
|
46
|
+
#longTasks;
|
|
47
|
+
#resources;
|
|
41
48
|
#onLongTask;
|
|
42
49
|
#onResource;
|
|
43
50
|
|
|
@@ -48,7 +55,8 @@ export class SpiderAdapter {
|
|
|
48
55
|
this.core?.registerAdapter('spider', {
|
|
49
56
|
enable: () => this.#enable(),
|
|
50
57
|
disable: () => this.#disable(),
|
|
51
|
-
label: 'SPIDER'
|
|
58
|
+
label: 'SPIDER',
|
|
59
|
+
snapshot: () => this.snapshot()
|
|
52
60
|
});
|
|
53
61
|
this.core?.sendData('log', {
|
|
54
62
|
source: this.source,
|
|
@@ -65,6 +73,36 @@ export class SpiderAdapter {
|
|
|
65
73
|
return this;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Long tasks and slow resources kept since attach. Returns null when detached.
|
|
78
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
79
|
+
*/
|
|
80
|
+
snapshot() {
|
|
81
|
+
if (!this.#attached) return null;
|
|
82
|
+
const items = [];
|
|
83
|
+
for (let i = 0; i < this.#longTasks.length; i++) {
|
|
84
|
+
const entry = this.#longTasks[i];
|
|
85
|
+
items.push({
|
|
86
|
+
severity: 'warn',
|
|
87
|
+
selector: entry.attribution,
|
|
88
|
+
rule: 'longtask',
|
|
89
|
+
value: `${entry.duration}ms`,
|
|
90
|
+
source: this.source
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < this.#resources.length; i++) {
|
|
94
|
+
const entry = this.#resources[i];
|
|
95
|
+
items.push({
|
|
96
|
+
severity: 'warn',
|
|
97
|
+
selector: entry.name,
|
|
98
|
+
rule: 'resource',
|
|
99
|
+
value: `${entry.duration}ms`,
|
|
100
|
+
source: this.source
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { name: 'spider', items };
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
#enable() {
|
|
69
107
|
if (!this.#attached) return;
|
|
70
108
|
if (typeof PerformanceObserver !== 'function') {
|
|
@@ -111,6 +149,10 @@ export class SpiderAdapter {
|
|
|
111
149
|
for (let i = 0; i < entries.length; i++) {
|
|
112
150
|
const entry = entries[i];
|
|
113
151
|
if (!entry || entry.duration <= this.longTaskThreshold) continue;
|
|
152
|
+
remember(this.#longTasks, {
|
|
153
|
+
attribution: entry.attribution?.[0]?.containerType ?? 'unknown',
|
|
154
|
+
duration: Math.round(entry.duration)
|
|
155
|
+
});
|
|
114
156
|
this.core.sendData('telemetry', {
|
|
115
157
|
source: this.source,
|
|
116
158
|
kind: 'longtask',
|
|
@@ -126,6 +168,10 @@ export class SpiderAdapter {
|
|
|
126
168
|
for (let i = 0; i < entries.length; i++) {
|
|
127
169
|
const entry = entries[i];
|
|
128
170
|
if (!entry || entry.duration <= this.resourceThreshold) continue;
|
|
171
|
+
remember(this.#resources, {
|
|
172
|
+
name: String(entry.name || 'resource'),
|
|
173
|
+
duration: Math.round(entry.duration)
|
|
174
|
+
});
|
|
129
175
|
this.core.sendData('warn', {
|
|
130
176
|
source: this.source,
|
|
131
177
|
message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
|
|
@@ -143,6 +189,11 @@ export class SpiderAdapter {
|
|
|
143
189
|
}
|
|
144
190
|
}
|
|
145
191
|
|
|
192
|
+
function remember(list, entry) {
|
|
193
|
+
list.unshift(entry);
|
|
194
|
+
if (list.length > MAX_SPIDER) list.pop();
|
|
195
|
+
}
|
|
196
|
+
|
|
146
197
|
function positive(value, fallback) {
|
|
147
198
|
const n = Number(value);
|
|
148
199
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|