bee-ladybug 0.2.0 → 0.4.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/{README.md → README.MD} +77 -14
- package/package.json +3 -3
- package/src/adapters/AntAdapter.js +144 -0
- package/src/adapters/CanvasAdapter.js +63 -0
- package/src/adapters/SpiderAdapter.js +160 -0
- package/src/core/BeeLadybugCore.js +196 -27
- package/src/core/UIOverlay.js +225 -3
- package/src/index.d.ts +66 -2
- package/src/index.js +2 -0
package/{README.md → README.MD}
RENAMED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
|
|
1
2
|
# BeeLadybug
|
|
2
3
|
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
Universal telemetry and visual inspection overlay.
|
|
4
7
|
|
|
5
8
|
BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
|
|
@@ -34,6 +37,8 @@ bee-ladybug/
|
|
|
34
37
|
│ └── adapters/
|
|
35
38
|
│ ├── CanvasAdapter.js
|
|
36
39
|
│ ├── WebDOMAdapter.js
|
|
40
|
+
│ ├── AntAdapter.js
|
|
41
|
+
│ ├── SpiderAdapter.js
|
|
37
42
|
│ └── python/PythonBridge.js
|
|
38
43
|
├── examples/
|
|
39
44
|
│ ├── core.html
|
|
@@ -93,6 +98,7 @@ Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
|
|
|
93
98
|
| `log` | `{ message, level?, source? }` | console |
|
|
94
99
|
| `warn` | `{ message, source? }` | console ambra |
|
|
95
100
|
| `error` | `{ message, source? }` | console rossa |
|
|
101
|
+
| `telemetry` | `{ kind?, duration?, source? }` | history + lampeggio pulsante adapter |
|
|
96
102
|
| `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
|
|
97
103
|
| `sys` | emesso dal Core | freeze / visibilità / scala |
|
|
98
104
|
|
|
@@ -113,6 +119,8 @@ bee.subscribe((packet) => {
|
|
|
113
119
|
});
|
|
114
120
|
```
|
|
115
121
|
|
|
122
|
+

|
|
123
|
+
|
|
116
124
|
## Tasti
|
|
117
125
|
|
|
118
126
|
| Tasto | Azione |
|
|
@@ -167,6 +175,50 @@ adapter.attach();
|
|
|
167
175
|
|
|
168
176
|
Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
|
|
169
177
|
|
|
178
|
+
## Registro adapter (overlay)
|
|
179
|
+
|
|
180
|
+
Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
bee.registerAdapter('probe', {
|
|
184
|
+
label: 'PROBE',
|
|
185
|
+
enable: () => observer.observe(root, opts),
|
|
186
|
+
disable: () => observer.disconnect()
|
|
187
|
+
});
|
|
188
|
+
adapter.detach(); // chiama bee.unregisterAdapter('probe')
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Click sul pulsante → `enable()` / `disable()`. Un pacchetto `warn` o `telemetry` con `source` uguale al nome registrato fa lampeggiare quel pulsante, al massimo una volta ogni 1.5s.
|
|
192
|
+
|
|
193
|
+
## AntAdapter
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { BeeLadybugCore, AntAdapter } from './src/index.js';
|
|
197
|
+
|
|
198
|
+
const bee = new BeeLadybugCore();
|
|
199
|
+
const ant = new AntAdapter(bee, {
|
|
200
|
+
root: document.body,
|
|
201
|
+
threshold: 30 // mutazioni / secondo per nodo
|
|
202
|
+
});
|
|
203
|
+
ant.attach();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`MutationObserver` indipendente da `WebDOMAdapter` (possono condividere lo stesso `root`). Se un nodo supera la soglia, arriva un `warn` `source: 'ant'`.
|
|
207
|
+
|
|
208
|
+
## SpiderAdapter
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { BeeLadybugCore, SpiderAdapter } from './src/index.js';
|
|
212
|
+
|
|
213
|
+
const bee = new BeeLadybugCore();
|
|
214
|
+
const spider = new SpiderAdapter(bee, {
|
|
215
|
+
resourceThreshold: 500 // ms
|
|
216
|
+
});
|
|
217
|
+
spider.attach();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`PerformanceObserver`: `longtask` > 50ms → `telemetry`; risorse > soglia → `warn`. Se il browser non espone `longtask` (Firefox/Safari), un log una tantum e quella parte si spegne senza errori.
|
|
221
|
+
|
|
170
222
|
## PythonBridge
|
|
171
223
|
|
|
172
224
|
Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
|
|
@@ -197,6 +249,8 @@ python examples/python/sender.py
|
|
|
197
249
|
|
|
198
250
|
Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
|
|
199
251
|
|
|
252
|
+

|
|
253
|
+
|
|
200
254
|
## npm / TypeScript
|
|
201
255
|
|
|
202
256
|
```bash
|
|
@@ -216,14 +270,19 @@ npm run typecheck
|
|
|
216
270
|
npm run lint
|
|
217
271
|
```
|
|
218
272
|
|
|
219
|
-
|
|
273
|
+
```bash
|
|
274
|
+
npm login
|
|
275
|
+
npm publish --access public
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Versione runtime e pacchetto: **0.4.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
|
|
220
279
|
|
|
221
280
|
## Assistente AI (gancio, nessun vendor)
|
|
222
281
|
|
|
223
|
-
BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta
|
|
282
|
+
BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta i propri modelli. `registerAssistant` **aggiunge** (non sostituisce gli altri). `setActiveAssistant(name)` sceglie chi risponde a **ASK AI**.
|
|
224
283
|
|
|
225
284
|
```js
|
|
226
|
-
bee.
|
|
285
|
+
bee.registerAssistant({
|
|
227
286
|
name: 'ollama',
|
|
228
287
|
async complete({ question, snapshot }) {
|
|
229
288
|
const res = await fetch('http://127.0.0.1:11434/api/generate', {
|
|
@@ -240,17 +299,18 @@ bee.setAssistant({
|
|
|
240
299
|
}
|
|
241
300
|
});
|
|
242
301
|
|
|
302
|
+
bee.registerAssistant({
|
|
303
|
+
name: 'mock',
|
|
304
|
+
complete({ snapshot }) {
|
|
305
|
+
return `logs=${snapshot.logs.length}`;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
bee.setActiveAssistant('ollama');
|
|
243
310
|
await bee.ask('perché il loss sale?');
|
|
244
311
|
```
|
|
245
312
|
|
|
246
|
-
Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI
|
|
247
|
-
|
|
248
|
-
## API Core
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
npm login
|
|
252
|
-
npm publish --access public
|
|
253
|
-
```
|
|
313
|
+
Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI** e un quadratino per ogni provider; quello attivo è evidenziato. Senza provider, il bottone spiega come collegarlo. `setAssistant` resta come scorciatoia: registra e attiva, senza cancellare gli altri.
|
|
254
314
|
|
|
255
315
|
## API Core
|
|
256
316
|
|
|
@@ -277,15 +337,18 @@ bee.getHud();
|
|
|
277
337
|
bee.show(); bee.hide(); bee.toggle();
|
|
278
338
|
bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
|
|
279
339
|
bee.applySlowMo(); bee.restoreRealtime();
|
|
280
|
-
bee.
|
|
340
|
+
bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
|
|
341
|
+
bee.setActiveAssistant('mock');
|
|
281
342
|
await bee.ask('why fps drop?');
|
|
343
|
+
bee.registerAdapter('probe', { label: 'PROBE', enable() {}, disable() {} });
|
|
344
|
+
bee.toggleAdapter('probe');
|
|
282
345
|
bee.destroy();
|
|
283
346
|
```
|
|
284
347
|
|
|
285
348
|
## Roadmap
|
|
286
349
|
|
|
287
|
-
1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI. Fatto.
|
|
288
|
-
2. **npm 0.
|
|
350
|
+
1. **Ora** — Core, adapter, registro toggle overlay, Ant/Spider, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
|
|
351
|
+
2. **npm 0.4.0** — `npm publish`.
|
|
289
352
|
|
|
290
353
|
## Come contribuire
|
|
291
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bee-ladybug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
21
|
"lint": "eslint src examples",
|
|
22
|
-
"test": "node --test tests
|
|
22
|
+
"test": "node --test tests/*.test.js"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"debug",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=20"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.36.0",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AntAdapter — DOM mutation rate tracker.
|
|
3
|
+
*
|
|
4
|
+
* Independent MutationObserver. Can sit on the same root as WebDOMAdapter.
|
|
5
|
+
* The core must never import this file.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const ant = new AntAdapter(core, { root: document.body, threshold: 30 });
|
|
9
|
+
* ant.attach();
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const ANT_ADAPTER_DEFAULTS = Object.freeze({
|
|
13
|
+
threshold: 30,
|
|
14
|
+
windowMs: 1000,
|
|
15
|
+
source: 'ant'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export class AntAdapter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
21
|
+
* @param {Partial<typeof ANT_ADAPTER_DEFAULTS> & {
|
|
22
|
+
* root?: ParentNode | null
|
|
23
|
+
* }} [options]
|
|
24
|
+
*/
|
|
25
|
+
constructor(core, options = {}) {
|
|
26
|
+
const cfg = { ...ANT_ADAPTER_DEFAULTS, ...options };
|
|
27
|
+
this.core = core;
|
|
28
|
+
this.root = options.root !== undefined
|
|
29
|
+
? options.root
|
|
30
|
+
: (typeof document !== 'undefined' ? document.body : null);
|
|
31
|
+
this.threshold = positive(cfg.threshold, ANT_ADAPTER_DEFAULTS.threshold);
|
|
32
|
+
this.windowMs = positive(cfg.windowMs, ANT_ADAPTER_DEFAULTS.windowMs);
|
|
33
|
+
this.source = cfg.source;
|
|
34
|
+
|
|
35
|
+
this.#attached = false;
|
|
36
|
+
this.#observer = null;
|
|
37
|
+
this.#counts = new WeakMap();
|
|
38
|
+
this.#onMutate = (records) => this.#ingest(records);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#attached;
|
|
42
|
+
#observer;
|
|
43
|
+
#counts;
|
|
44
|
+
#onMutate;
|
|
45
|
+
|
|
46
|
+
attach() {
|
|
47
|
+
if (this.#attached) return this;
|
|
48
|
+
this.#attached = true;
|
|
49
|
+
this.#enable();
|
|
50
|
+
this.core?.registerAdapter('ant', {
|
|
51
|
+
enable: () => this.#enable(),
|
|
52
|
+
disable: () => this.#disable(),
|
|
53
|
+
label: 'ANT'
|
|
54
|
+
});
|
|
55
|
+
this.core?.sendData('log', {
|
|
56
|
+
source: this.source,
|
|
57
|
+
message: 'AntAdapter attached. Watching DOM mutation rate.'
|
|
58
|
+
});
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
detach() {
|
|
63
|
+
if (!this.#attached) return this;
|
|
64
|
+
this.#attached = false;
|
|
65
|
+
this.#disable();
|
|
66
|
+
this.core?.unregisterAdapter('ant');
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#enable() {
|
|
71
|
+
if (!this.#attached || this.#observer) return;
|
|
72
|
+
if (typeof MutationObserver === 'undefined' || !this.root) return;
|
|
73
|
+
this.#counts = new WeakMap();
|
|
74
|
+
this.#observer = new MutationObserver(this.#onMutate);
|
|
75
|
+
try {
|
|
76
|
+
this.#observer.observe(this.root, {
|
|
77
|
+
childList: true,
|
|
78
|
+
subtree: true,
|
|
79
|
+
attributes: true,
|
|
80
|
+
characterData: true
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
this.#observer.disconnect();
|
|
84
|
+
this.#observer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#disable() {
|
|
89
|
+
if (this.#observer) this.#observer.disconnect();
|
|
90
|
+
this.#observer = null;
|
|
91
|
+
this.#counts = new WeakMap();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#ingest(records) {
|
|
95
|
+
if (!this.#attached || !this.core || !records?.length) return;
|
|
96
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
97
|
+
for (let i = 0; i < records.length; i++) {
|
|
98
|
+
const node = records[i]?.target;
|
|
99
|
+
if (!node || isBeeChrome(node)) continue;
|
|
100
|
+
|
|
101
|
+
let slot = this.#counts.get(node);
|
|
102
|
+
if (!slot || now - slot.windowStart >= this.windowMs) {
|
|
103
|
+
slot = { count: 0, windowStart: now, warned: false };
|
|
104
|
+
this.#counts.set(node, slot);
|
|
105
|
+
}
|
|
106
|
+
slot.count += 1;
|
|
107
|
+
if (slot.count < this.threshold || slot.warned) continue;
|
|
108
|
+
|
|
109
|
+
slot.warned = true;
|
|
110
|
+
const selector = nodeSelector(node);
|
|
111
|
+
this.core.sendData('warn', {
|
|
112
|
+
source: this.source,
|
|
113
|
+
message: `High Mutation Rate on ${selector}`,
|
|
114
|
+
node: selector
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function positive(value, fallback) {
|
|
121
|
+
const n = Number(value);
|
|
122
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function nodeSelector(node) {
|
|
126
|
+
const el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
127
|
+
if (!el || !el.tagName) return 'node';
|
|
128
|
+
if (el.id) return `#${el.id}`;
|
|
129
|
+
const cls = el.classList && el.classList[0];
|
|
130
|
+
if (cls) return `.${cls}`;
|
|
131
|
+
return el.tagName.toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isBeeChrome(node) {
|
|
135
|
+
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
136
|
+
while (el) {
|
|
137
|
+
const tag = el.tagName;
|
|
138
|
+
if (tag === 'BEE-LADYBUG-OVERLAY') return true;
|
|
139
|
+
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
140
|
+
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
141
|
+
el = el.parentElement;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
* adapter.attach();
|
|
16
16
|
* // after updating entity positions each host frame:
|
|
17
17
|
* adapter.pump();
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // third-party / bookmarklet: no access to the host loop
|
|
21
|
+
* const remote = new CanvasAdapter(core, { canvas, entities, autoPump: true });
|
|
22
|
+
* remote.attach();
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
|
|
21
26
|
overlay: true,
|
|
22
27
|
computeCollisions: true,
|
|
28
|
+
autoPump: false,
|
|
23
29
|
colorActive: '#3dff6a',
|
|
24
30
|
colorColliding: '#ff3b3b',
|
|
25
31
|
colorInactive: '#8a8a8a',
|
|
@@ -41,6 +47,7 @@ export class CanvasAdapter {
|
|
|
41
47
|
this.entities = options.entities ?? [];
|
|
42
48
|
this.overlayEnabled = cfg.overlay;
|
|
43
49
|
this.computeCollisions = cfg.computeCollisions;
|
|
50
|
+
this.autoPump = Boolean(cfg.autoPump);
|
|
44
51
|
this.colorActive = cfg.colorActive;
|
|
45
52
|
this.colorColliding = cfg.colorColliding;
|
|
46
53
|
this.colorInactive = cfg.colorInactive;
|
|
@@ -54,19 +61,35 @@ export class CanvasAdapter {
|
|
|
54
61
|
this.#attached = false;
|
|
55
62
|
this.#overlay = null;
|
|
56
63
|
this.#unsubscribe = null;
|
|
64
|
+
this.#rafId = 0;
|
|
65
|
+
this.#pumpFromAuto = false;
|
|
66
|
+
this.#manualPumpWarned = false;
|
|
57
67
|
this.#lastPump = 0;
|
|
58
68
|
this.#fps = 0;
|
|
59
69
|
this.#wasColliding = new Set();
|
|
60
70
|
this.#onResize = () => this.#syncOverlay();
|
|
71
|
+
this.#onTick = () => {
|
|
72
|
+
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
73
|
+
this.#pumpFromAuto = true;
|
|
74
|
+
try {
|
|
75
|
+
this.pump();
|
|
76
|
+
} finally {
|
|
77
|
+
this.#pumpFromAuto = false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
#attached;
|
|
64
83
|
#overlay;
|
|
65
84
|
#unsubscribe;
|
|
85
|
+
#rafId;
|
|
86
|
+
#pumpFromAuto;
|
|
87
|
+
#manualPumpWarned;
|
|
66
88
|
#lastPump;
|
|
67
89
|
#fps;
|
|
68
90
|
#wasColliding;
|
|
69
91
|
#onResize;
|
|
92
|
+
#onTick;
|
|
70
93
|
|
|
71
94
|
attach() {
|
|
72
95
|
if (this.#attached) return this;
|
|
@@ -83,6 +106,7 @@ export class CanvasAdapter {
|
|
|
83
106
|
window.addEventListener('resize', this.#onResize);
|
|
84
107
|
window.addEventListener('scroll', this.#onResize, true);
|
|
85
108
|
}
|
|
109
|
+
if (this.autoPump) this.startAutoPump();
|
|
86
110
|
|
|
87
111
|
this.core?.sendData('log', {
|
|
88
112
|
source: this.source,
|
|
@@ -99,6 +123,7 @@ export class CanvasAdapter {
|
|
|
99
123
|
detach() {
|
|
100
124
|
if (!this.#attached) return this;
|
|
101
125
|
this.#attached = false;
|
|
126
|
+
this.#stopAutoPumpLoop();
|
|
102
127
|
if (this.#unsubscribe) this.#unsubscribe();
|
|
103
128
|
this.#unsubscribe = null;
|
|
104
129
|
this.#unmountOverlay();
|
|
@@ -109,6 +134,20 @@ export class CanvasAdapter {
|
|
|
109
134
|
return this;
|
|
110
135
|
}
|
|
111
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Drive `pump()` from an internal `requestAnimationFrame` loop.
|
|
139
|
+
* Use this when the host game loop cannot call `pump()` (bookmarklet / console).
|
|
140
|
+
*/
|
|
141
|
+
startAutoPump() {
|
|
142
|
+
this.autoPump = true;
|
|
143
|
+
if (!this.#attached) return this;
|
|
144
|
+
if (this.#rafId) return this;
|
|
145
|
+
if (typeof requestAnimationFrame !== 'function') return this;
|
|
146
|
+
this.#manualPumpWarned = false;
|
|
147
|
+
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
/** @param {HTMLCanvasElement | null} canvas */
|
|
113
152
|
setCanvas(canvas) {
|
|
114
153
|
this.canvas = canvas;
|
|
@@ -126,10 +165,15 @@ export class CanvasAdapter {
|
|
|
126
165
|
* Call once per host frame, after entity positions are updated.
|
|
127
166
|
* Pass a 2D context only if overlay is disabled and you want hitboxes
|
|
128
167
|
* drawn onto the game canvas itself.
|
|
168
|
+
* Ignored (with a warning) while `autoPump` owns the frame loop.
|
|
129
169
|
* @param {CanvasRenderingContext2D} [ctx]
|
|
130
170
|
*/
|
|
131
171
|
pump(ctx) {
|
|
132
172
|
if (!this.#attached || !this.core) return this;
|
|
173
|
+
if (this.#rafId && !this.#pumpFromAuto) {
|
|
174
|
+
this.#warnManualPump();
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
133
177
|
|
|
134
178
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
135
179
|
if (this.#lastPump) {
|
|
@@ -147,6 +191,25 @@ export class CanvasAdapter {
|
|
|
147
191
|
return this;
|
|
148
192
|
}
|
|
149
193
|
|
|
194
|
+
#stopAutoPumpLoop() {
|
|
195
|
+
if (this.#rafId && typeof cancelAnimationFrame === 'function') {
|
|
196
|
+
cancelAnimationFrame(this.#rafId);
|
|
197
|
+
}
|
|
198
|
+
this.#rafId = 0;
|
|
199
|
+
this.#pumpFromAuto = false;
|
|
200
|
+
this.#manualPumpWarned = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#warnManualPump() {
|
|
204
|
+
if (this.#manualPumpWarned) return;
|
|
205
|
+
this.#manualPumpWarned = true;
|
|
206
|
+
const message = 'pump() skipped: autoPump already drives the frame loop.';
|
|
207
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
208
|
+
console.warn(`[BeeLadybug] CanvasAdapter ${message}`);
|
|
209
|
+
}
|
|
210
|
+
this.core.sendData('warn', { source: this.source, message });
|
|
211
|
+
}
|
|
212
|
+
|
|
150
213
|
#onCore(packet) {
|
|
151
214
|
if (packet.type !== 'sys') return;
|
|
152
215
|
const op = packet.payload.op;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpiderAdapter — long-task and slow-resource tracker.
|
|
3
|
+
*
|
|
4
|
+
* Native PerformanceObserver only. The core must never import this file.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
|
|
8
|
+
* spider.attach();
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
|
|
12
|
+
longTaskThreshold: 50,
|
|
13
|
+
resourceThreshold: 500,
|
|
14
|
+
source: 'spider'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export class SpiderAdapter {
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
20
|
+
* @param {Partial<typeof SPIDER_ADAPTER_DEFAULTS>} [options]
|
|
21
|
+
*/
|
|
22
|
+
constructor(core, options = {}) {
|
|
23
|
+
const cfg = { ...SPIDER_ADAPTER_DEFAULTS, ...options };
|
|
24
|
+
this.core = core;
|
|
25
|
+
this.longTaskThreshold = positive(cfg.longTaskThreshold, SPIDER_ADAPTER_DEFAULTS.longTaskThreshold);
|
|
26
|
+
this.resourceThreshold = positive(cfg.resourceThreshold, SPIDER_ADAPTER_DEFAULTS.resourceThreshold);
|
|
27
|
+
this.source = cfg.source;
|
|
28
|
+
|
|
29
|
+
this.#attached = false;
|
|
30
|
+
this.#longObserver = null;
|
|
31
|
+
this.#resourceObserver = null;
|
|
32
|
+
this.#longTaskMissingLogged = false;
|
|
33
|
+
this.#onLongTask = (list) => this.#ingestLongTasks(list);
|
|
34
|
+
this.#onResource = (list) => this.#ingestResources(list);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#attached;
|
|
38
|
+
#longObserver;
|
|
39
|
+
#resourceObserver;
|
|
40
|
+
#longTaskMissingLogged;
|
|
41
|
+
#onLongTask;
|
|
42
|
+
#onResource;
|
|
43
|
+
|
|
44
|
+
attach() {
|
|
45
|
+
if (this.#attached) return this;
|
|
46
|
+
this.#attached = true;
|
|
47
|
+
this.#enable();
|
|
48
|
+
this.core?.registerAdapter('spider', {
|
|
49
|
+
enable: () => this.#enable(),
|
|
50
|
+
disable: () => this.#disable(),
|
|
51
|
+
label: 'SPIDER'
|
|
52
|
+
});
|
|
53
|
+
this.core?.sendData('log', {
|
|
54
|
+
source: this.source,
|
|
55
|
+
message: 'SpiderAdapter attached. Watching long tasks and slow resources.'
|
|
56
|
+
});
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
detach() {
|
|
61
|
+
if (!this.#attached) return this;
|
|
62
|
+
this.#attached = false;
|
|
63
|
+
this.#disable();
|
|
64
|
+
this.core?.unregisterAdapter('spider');
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#enable() {
|
|
69
|
+
if (!this.#attached) return;
|
|
70
|
+
if (typeof PerformanceObserver !== 'function') {
|
|
71
|
+
this.#logLongTaskUnavailable();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const types = supportedEntryTypes();
|
|
75
|
+
if (types.includes('longtask')) this.#watchLongTasks();
|
|
76
|
+
else this.#logLongTaskUnavailable();
|
|
77
|
+
if (types.includes('resource')) this.#watchResources();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#disable() {
|
|
81
|
+
if (this.#longObserver) this.#longObserver.disconnect();
|
|
82
|
+
if (this.#resourceObserver) this.#resourceObserver.disconnect();
|
|
83
|
+
this.#longObserver = null;
|
|
84
|
+
this.#resourceObserver = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#watchLongTasks() {
|
|
88
|
+
if (this.#longObserver) return;
|
|
89
|
+
try {
|
|
90
|
+
this.#longObserver = new PerformanceObserver(this.#onLongTask);
|
|
91
|
+
this.#longObserver.observe({ entryTypes: ['longtask'] });
|
|
92
|
+
} catch {
|
|
93
|
+
this.#longObserver = null;
|
|
94
|
+
this.#logLongTaskUnavailable();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#watchResources() {
|
|
99
|
+
if (this.#resourceObserver) return;
|
|
100
|
+
try {
|
|
101
|
+
this.#resourceObserver = new PerformanceObserver(this.#onResource);
|
|
102
|
+
this.#resourceObserver.observe({ entryTypes: ['resource'] });
|
|
103
|
+
} catch {
|
|
104
|
+
this.#resourceObserver = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#ingestLongTasks(list) {
|
|
109
|
+
if (!this.#attached || !this.core) return;
|
|
110
|
+
const entries = list?.getEntries?.() ?? [];
|
|
111
|
+
for (let i = 0; i < entries.length; i++) {
|
|
112
|
+
const entry = entries[i];
|
|
113
|
+
if (!entry || entry.duration <= this.longTaskThreshold) continue;
|
|
114
|
+
this.core.sendData('telemetry', {
|
|
115
|
+
source: this.source,
|
|
116
|
+
kind: 'longtask',
|
|
117
|
+
duration: entry.duration,
|
|
118
|
+
attribution: entry.attribution?.[0]?.containerType ?? 'unknown'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#ingestResources(list) {
|
|
124
|
+
if (!this.#attached || !this.core) return;
|
|
125
|
+
const entries = list?.getEntries?.() ?? [];
|
|
126
|
+
for (let i = 0; i < entries.length; i++) {
|
|
127
|
+
const entry = entries[i];
|
|
128
|
+
if (!entry || entry.duration <= this.resourceThreshold) continue;
|
|
129
|
+
this.core.sendData('warn', {
|
|
130
|
+
source: this.source,
|
|
131
|
+
message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#logLongTaskUnavailable() {
|
|
137
|
+
if (this.#longTaskMissingLogged) return;
|
|
138
|
+
this.#longTaskMissingLogged = true;
|
|
139
|
+
this.core?.sendData('log', {
|
|
140
|
+
source: this.source,
|
|
141
|
+
message: 'Long task detection non disponibile in questo browser'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function positive(value, fallback) {
|
|
147
|
+
const n = Number(value);
|
|
148
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function supportedEntryTypes() {
|
|
152
|
+
if (typeof PerformanceObserver !== 'function') return [];
|
|
153
|
+
const list = PerformanceObserver.supportedEntryTypes;
|
|
154
|
+
if (!list) return [];
|
|
155
|
+
try {
|
|
156
|
+
return Array.from(list);
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|