@goliapkg/sentori-react-native 0.9.3 → 0.9.5
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/lib/control-channel.d.ts.map +1 -1
- package/lib/control-channel.js +8 -1
- package/lib/control-channel.js.map +1 -1
- package/lib/init.d.ts +9 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +10 -0
- package/lib/init.js.map +1 -1
- package/lib/mobile-vitals.d.ts.map +1 -1
- package/lib/mobile-vitals.js +2 -7
- package/lib/mobile-vitals.js.map +1 -1
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +15 -4
- package/lib/replay.js.map +1 -1
- package/lib/sample-profiler.d.ts +21 -0
- package/lib/sample-profiler.d.ts.map +1 -0
- package/lib/sample-profiler.js +152 -0
- package/lib/sample-profiler.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/control-channel.test.ts +111 -0
- package/src/__tests__/long-task-monitor.test.ts +59 -0
- package/src/__tests__/sample-profiler.test.ts +59 -0
- package/src/control-channel.ts +9 -1
- package/src/init.ts +16 -0
- package/src/mobile-vitals.ts +2 -8
- package/src/replay.ts +17 -4
- package/src/sample-profiler.ts +169 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-channel.d.ts","sourceRoot":"","sources":["../src/control-channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"control-channel.d.ts","sourceRoot":"","sources":["../src/control-channel.ts"],"names":[],"mappings":"AAiCA,wBAAgB,UAAU,IAAI,OAAO,CAQpC;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAS1C;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAOzC;AAiCD,iBAAiB;AACjB,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
package/lib/control-channel.js
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
import { getConfig } from './config';
|
|
17
17
|
import { getCurrentUserId } from './capture';
|
|
18
18
|
const POLL_INTERVAL_MS = 30_000;
|
|
19
|
+
/** Cap on the live-mode window even if the server reports a longer
|
|
20
|
+
* TTL. Defense-in-depth: an unbounded `ttlMs` (server bug, clock
|
|
21
|
+
* skew, malicious server) would otherwise pin the SDK into flush-
|
|
22
|
+
* every-event mode indefinitely, which is expensive on slow
|
|
23
|
+
* networks. 15 min matches the dashboard's default arm length so
|
|
24
|
+
* normal operation stays uncapped. */
|
|
25
|
+
const MAX_LIVE_MODE_TTL_MS = 15 * 60_000;
|
|
19
26
|
let _liveMode = false;
|
|
20
27
|
let _liveModeUntil = 0;
|
|
21
28
|
let _timer = null;
|
|
@@ -70,7 +77,7 @@ async function pollOnce() {
|
|
|
70
77
|
const body = (await resp.json());
|
|
71
78
|
if (body.liveMode === true) {
|
|
72
79
|
_liveMode = true;
|
|
73
|
-
_liveModeUntil = Date.now() + Math.max(0, Math.min(body.ttlMs ?? 0,
|
|
80
|
+
_liveModeUntil = Date.now() + Math.max(0, Math.min(body.ttlMs ?? 0, MAX_LIVE_MODE_TTL_MS));
|
|
74
81
|
}
|
|
75
82
|
else {
|
|
76
83
|
_liveMode = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-channel.js","sourceRoot":"","sources":["../src/control-channel.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,EAAE;AACF,mEAAmE;AACnE,qEAAqE;AACrE,mEAAmE;AACnE,iEAAiE;AACjE,+DAA+D;AAC/D,cAAc;AACd,EAAE;AACF,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,+DAA+D;AAC/D,oEAAoE;AACpE,gDAAgD;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,IAAI,SAAS,GAAG,KAAK,CAAC;AACtB,IAAI,cAAc,GAAG,CAAC,CAAC;AACvB,IAAI,MAAM,GAA0C,IAAI,CAAC;AAEzD,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,mEAAmE;IACnE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,EAAE,CAAC;QAChC,SAAS,GAAG,KAAK,CAAC;QAClB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO;IAC5B,sEAAsE;IACtE,sDAAsD;IACtD,KAAK,QAAQ,EAAE,CAAC;IAChB,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QACxB,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACpB,MAA4C,CAAC,KAAK,EAAE,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,aAAa,CAAC,MAAM,CAAC,CAAC;QACtB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,SAAS,GAAG,KAAK,CAAC;IAClB,cAAc,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,QAAQ;IACrB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,4DAA4D;QAC5D,SAAS,GAAG,KAAK,CAAC;QAClB,cAAc,GAAG,CAAC,CAAC;QACnB,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,2BAA2B,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QACvF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC5B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE,EAAE;YACpD,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,OAAO;QACrB,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA2C,CAAC;QAC3E,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,SAAS,GAAG,IAAI,CAAC;YACjB,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"control-channel.js","sourceRoot":"","sources":["../src/control-channel.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,EAAE;AACF,mEAAmE;AACnE,qEAAqE;AACrE,mEAAmE;AACnE,iEAAiE;AACjE,+DAA+D;AAC/D,cAAc;AACd,EAAE;AACF,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,+DAA+D;AAC/D,oEAAoE;AACpE,gDAAgD;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;;uCAKuC;AACvC,MAAM,oBAAoB,GAAG,EAAE,GAAG,MAAM,CAAC;AAEzC,IAAI,SAAS,GAAG,KAAK,CAAC;AACtB,IAAI,cAAc,GAAG,CAAC,CAAC;AACvB,IAAI,MAAM,GAA0C,IAAI,CAAC;AAEzD,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,mEAAmE;IACnE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,EAAE,CAAC;QAChC,SAAS,GAAG,KAAK,CAAC;QAClB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO;IAC5B,sEAAsE;IACtE,sDAAsD;IACtD,KAAK,QAAQ,EAAE,CAAC;IAChB,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QACxB,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACpB,MAA4C,CAAC,KAAK,EAAE,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,aAAa,CAAC,MAAM,CAAC,CAAC;QACtB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,SAAS,GAAG,KAAK,CAAC;IAClB,cAAc,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,QAAQ;IACrB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,4DAA4D;QAC5D,SAAS,GAAG,KAAK,CAAC;QAClB,cAAc,GAAG,CAAC,CAAC;QACnB,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,2BAA2B,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QACvF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC5B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE,EAAE;YACpD,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,OAAO;QACrB,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA2C,CAAC;QAC3E,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,SAAS,GAAG,IAAI,CAAC;YACjB,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC7F,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,KAAK,CAAC;YAClB,cAAc,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;QAChE,4CAA4C;IAC9C,CAAC;AACH,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,6BAA6B;IAC3C,kBAAkB,EAAE,CAAC;AACvB,CAAC"}
|
package/lib/init.d.ts
CHANGED
|
@@ -60,6 +60,15 @@ export type InitOptions = {
|
|
|
60
60
|
hz?: number;
|
|
61
61
|
mode: 'off' | 'wireframe';
|
|
62
62
|
};
|
|
63
|
+
/** v1.1 #4 升级 — JS sample profiler. setInterval(50ms) idle-tick
|
|
64
|
+
* sampler aggregates frame counts → emits sentori.profile span
|
|
65
|
+
* every 60s with `flameData` (frame → tick count). Pairs with
|
|
66
|
+
* longTaskMonitor (≥200ms outliers) — sample profiler 看 idle
|
|
67
|
+
* 分布、long-task 看 outliers。 */
|
|
68
|
+
sampleProfiler?: boolean | {
|
|
69
|
+
flushMs?: number;
|
|
70
|
+
sampleMs?: number;
|
|
71
|
+
};
|
|
63
72
|
/** v0.9.0 #3 — launch-crash loop guard. When two consecutive
|
|
64
73
|
* launches don't reach `markLaunchCompleted()` (typical of an
|
|
65
74
|
* OTA update with a fatal bug), invoke the host callback with
|
package/lib/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAUnF,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EACJ,OAAO,GACP;YACE;;qEAEyD;YACzD,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;QACN;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;;;;;;;uBAOe;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB;;;6CAGqC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB;;;;6CAIqC;QACrC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;QACrC;;;;6CAIqC;QACrC,eAAe,CAAC,EAAE,OAAO,GAAG;YAAE,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACrD;;;mEAG2D;QAC3D,MAAM,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG;YAAE,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,KAAK,GAAG,WAAW,CAAA;SAAE,CAAC;QAC1E;;;;uCAI+B;QAC/B,cAAc,CAAC,EAAE,OAAO,GAAG;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnE;;;sEAG8D;QAC9D,gBAAgB,CAAC,EAAE;YACjB,OAAO,EAAE,OAAO,CAAC;YACjB,qBAAqB,CAAC,EAAE,CACtB,IAAI,EAAE,OAAO,sBAAsB,EAAE,eAAe,KAElD,OAAO,sBAAsB,EAAE,iBAAiB,GAChD,OAAO,CAAC,OAAO,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC;IACF;;;;;gEAK4D;IAC5D,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAIF,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,IAwK3C,CAAC;AAiBF,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
package/lib/init.js
CHANGED
|
@@ -14,6 +14,7 @@ import { startLongTaskMonitor } from './long-task-monitor';
|
|
|
14
14
|
import { startNetworkTypeWatch } from './netinfo';
|
|
15
15
|
import { startPreCrashSentinel } from './pre-crash-sentinel';
|
|
16
16
|
import { startReplay } from './replay';
|
|
17
|
+
import { startSampleProfiler } from './sample-profiler';
|
|
17
18
|
import { startSession } from './session-tracker';
|
|
18
19
|
import { drainOfflineQueue, enqueue, startTransport, uploadAttachment, } from './transport';
|
|
19
20
|
const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
|
|
@@ -103,6 +104,15 @@ export const init = (options) => {
|
|
|
103
104
|
else if (rp && typeof rp === 'object' && rp.mode === 'wireframe') {
|
|
104
105
|
startReplay({ hz: rp.hz, mode: 'wireframe' });
|
|
105
106
|
}
|
|
107
|
+
// v1.1 #4 升级 — JS sample profiler. Off by default.
|
|
108
|
+
const sp = options.capture?.sampleProfiler;
|
|
109
|
+
if (sp) {
|
|
110
|
+
startSampleProfiler({
|
|
111
|
+
enabled: true,
|
|
112
|
+
flushMs: typeof sp === 'object' ? sp.flushMs : undefined,
|
|
113
|
+
sampleMs: typeof sp === 'object' ? sp.sampleMs : undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
106
116
|
const capture = options.capture ?? {};
|
|
107
117
|
if (capture.globalErrors !== false)
|
|
108
118
|
installGlobalHandler();
|
package/lib/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAwB,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-vitals.d.ts","sourceRoot":"","sources":["../src/mobile-vitals.ts"],"names":[],"mappings":"AA6BA;kEACkE;AAClE,wBAAgB,cAAc,IAAI,IAAI,GAAG,MAAM,CAS9C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAOF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,CAqB5E;AAED;;;cAGc;AACd,wBAAgB,gBAAgB,IAAI,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"mobile-vitals.d.ts","sourceRoot":"","sources":["../src/mobile-vitals.ts"],"names":[],"mappings":"AA6BA;kEACkE;AAClE,wBAAgB,cAAc,IAAI,IAAI,GAAG,MAAM,CAS9C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAOF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,CAqB5E;AAED;;;cAGc;AACd,wBAAgB,gBAAgB,IAAI,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM1E;AAED,iBAAiB;AACjB,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD"}
|
package/lib/mobile-vitals.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// pieces ship as a separate native module method `getColdStartMs`
|
|
21
21
|
// + `getFrameCounts()` — graceful no-op if not linked.
|
|
22
22
|
import { startSpan } from '@goliapkg/sentori-core';
|
|
23
|
-
import { getNativeColdStartMs } from './native';
|
|
23
|
+
import { getNativeColdStartMs, getNativeFrameCounters } from './native';
|
|
24
24
|
let _coldStartMs = null;
|
|
25
25
|
let _coldStartCaptured = false;
|
|
26
26
|
/** Read the native-side cold start measurement once. Cached. Returns
|
|
@@ -67,12 +67,7 @@ export function markTimeToFullDisplay(route) {
|
|
|
67
67
|
* linked. */
|
|
68
68
|
export function getFrameCounters() {
|
|
69
69
|
try {
|
|
70
|
-
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
72
|
-
const nativeMod = require('./native');
|
|
73
|
-
if (typeof nativeMod.getNativeFrameCounters !== 'function')
|
|
74
|
-
return null;
|
|
75
|
-
return nativeMod.getNativeFrameCounters();
|
|
70
|
+
return getNativeFrameCounters();
|
|
76
71
|
}
|
|
77
72
|
catch {
|
|
78
73
|
return null;
|
package/lib/mobile-vitals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-vitals.js","sourceRoot":"","sources":["../src/mobile-vitals.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,2DAA2D;AAC3D,EAAE;AACF,0DAA0D;AAC1D,oEAAoE;AACpE,kEAAkE;AAClE,wDAAwD;AACxD,oDAAoD;AACpD,mEAAmE;AACnE,wCAAwC;AACxC,sDAAsD;AACtD,oEAAoE;AACpE,uBAAuB;AACvB,oEAAoE;AACpE,iEAAiE;AACjE,YAAY;AACZ,EAAE;AACF,gEAAgE;AAChE,kEAAkE;AAClE,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"mobile-vitals.js","sourceRoot":"","sources":["../src/mobile-vitals.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,2DAA2D;AAC3D,EAAE;AACF,0DAA0D;AAC1D,oEAAoE;AACpE,kEAAkE;AAClE,wDAAwD;AACxD,oDAAoD;AACpD,mEAAmE;AACnE,wCAAwC;AACxC,sDAAsD;AACtD,oEAAoE;AACpE,uBAAuB;AACvB,oEAAoE;AACpE,iEAAiE;AACjE,YAAY;AACZ,EAAE;AACF,gEAAgE;AAChE,kEAAkE;AAClE,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAExE,IAAI,YAAY,GAAkB,IAAI,CAAC;AACvC,IAAI,kBAAkB,GAAG,KAAK,CAAC;AAE/B;kEACkE;AAClE,MAAM,UAAU,cAAc;IAC5B,IAAI,kBAAkB;QAAE,OAAO,YAAY,CAAC;IAC5C,kBAAkB,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC;QACH,YAAY,GAAG,oBAAoB,EAAE,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG,IAAI,CAAC;IACtB,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAqBD,IAAI,WAAW,GAGX,IAAI,CAAC;AAET,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,IAAI,WAAW,IAAI,WAAW,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAC/C,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,CAAC,uBAAuB,EAAE;QAC9C,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE;KACnD,CAAC,CAAC;IACH,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,MAAM,GAAG,CAAC,MAAoC,EAAQ,EAAE;QAC5D,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QACxB,IAAI,WAAW,IAAI,WAAW,CAAC,KAAK,KAAK,KAAK;YAAE,WAAW,GAAG,IAAI,CAAC;IACrE,CAAC,CAAC;IACF,WAAW,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAChC,OAAO;QACL,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;QACjC,GAAG,EAAE,CAAC,IAAgD,EAAE,EAAE,CACxD,MAAM,CAAC,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED;;;cAGc;AACd,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC;QACH,OAAO,sBAAsB,EAAE,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,2BAA2B;IACzC,IAAI,WAAW;QAAE,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACjD,WAAW,GAAG,IAAI,CAAC;IACnB,kBAAkB,GAAG,KAAK,CAAC;IAC3B,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC"}
|
package/lib/replay.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AA4CA,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC;IAC3B,mCAAmC;IACnC,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAkBrD;AAED,wBAAgB,UAAU,IAAI,IAAI,CAOjC;AA6CD;;2BAE2B;AAC3B,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
|
package/lib/replay.js
CHANGED
|
@@ -21,9 +21,20 @@ import { getRegisteredMaskQuery } from './mask';
|
|
|
21
21
|
import { isNativeModuleLinked } from './native-loader';
|
|
22
22
|
const TICK_INTERVAL_MS = 1000;
|
|
23
23
|
const RING_SIZE = 60;
|
|
24
|
+
/** Floor on tick period. < 250 ms (4 Hz) the native view-tree walk
|
|
25
|
+
* dominates the JS thread on mid-tier Android, especially with mask
|
|
26
|
+
* consultation. The default is 1 Hz; the option exists for
|
|
27
|
+
* benchmarking, not for production. */
|
|
28
|
+
const MIN_TICK_PERIOD_MS = 250;
|
|
24
29
|
let _ring = [];
|
|
25
30
|
let _timer = null;
|
|
26
31
|
let _running = false;
|
|
32
|
+
/** Native module ref, resolved once on first start. Caching here
|
|
33
|
+
* avoids the cost of `requireNativeModule('Sentori')` on every
|
|
34
|
+
* capture tick (Metro's require cache makes this cheap, but the
|
|
35
|
+
* per-tick string lookup and possible throw still cost more than
|
|
36
|
+
* reading a closed-over variable). */
|
|
37
|
+
let _nativeMod = null;
|
|
27
38
|
export function startReplay(opts) {
|
|
28
39
|
if (_running)
|
|
29
40
|
return;
|
|
@@ -38,7 +49,8 @@ export function startReplay(opts) {
|
|
|
38
49
|
return;
|
|
39
50
|
}
|
|
40
51
|
_running = true;
|
|
41
|
-
|
|
52
|
+
_nativeMod = loadNativeReplay();
|
|
53
|
+
const period = Math.max(MIN_TICK_PERIOD_MS, Math.floor(TICK_INTERVAL_MS / (opts.hz ?? 1)));
|
|
42
54
|
_timer = setInterval(() => {
|
|
43
55
|
captureTick();
|
|
44
56
|
}, period);
|
|
@@ -50,6 +62,7 @@ export function stopReplay() {
|
|
|
50
62
|
clearInterval(_timer);
|
|
51
63
|
_timer = null;
|
|
52
64
|
}
|
|
65
|
+
_nativeMod = null;
|
|
53
66
|
}
|
|
54
67
|
function captureTick() {
|
|
55
68
|
if (!_running)
|
|
@@ -57,9 +70,7 @@ function captureTick() {
|
|
|
57
70
|
const tickSpan = startSpan('sentori.replay.tick', { name: 'tick' });
|
|
58
71
|
try {
|
|
59
72
|
const maskIds = readMaskIds();
|
|
60
|
-
|
|
61
|
-
const nativeMod = loadNativeReplay();
|
|
62
|
-
const snapshot = nativeMod?.captureWireframe?.(maskIds);
|
|
73
|
+
const snapshot = _nativeMod?.captureWireframe?.(maskIds);
|
|
63
74
|
if (typeof snapshot === 'string' && snapshot.length > 0) {
|
|
64
75
|
_ring.push(snapshot);
|
|
65
76
|
while (_ring.length > RING_SIZE)
|
package/lib/replay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,mEAAmE;AACnE,mEAAmE;AACnE,kEAAkE;AAClE,kEAAkE;AAClE,4DAA4D;AAC5D,EAAE;AACF,gCAAgC;AAChC,kEAAkE;AAClE,iEAAiE;AACjE,+DAA+D;AAC/D,6DAA6D;AAC7D,wDAAwD;AACxD,iEAAiE;AACjE,iEAAiE;AACjE,4DAA4D;AAC5D,wBAAwB;AAExB,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAC9B,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,IAAI,KAAK,GAAa,EAAE,CAAC;AACzB,IAAI,MAAM,GAA0C,IAAI,CAAC;AACzD,IAAI,QAAQ,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SampleProfilerOptions = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
/** Sample interval ms. Default 50. Lower → more accurate but more
|
|
4
|
+
* JS-thread overhead. */
|
|
5
|
+
sampleMs?: number;
|
|
6
|
+
/** Flush window ms. Default 60 000 (one minute). */
|
|
7
|
+
flushMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Start the idle-tick sample profiler. Idempotent — calling twice
|
|
11
|
+
* is a no-op after the first successful start.
|
|
12
|
+
*
|
|
13
|
+
* Pairs naturally with `longTaskMonitor` (≥200ms outliers): the
|
|
14
|
+
* profiler shows the *distribution* of code that runs in idle gaps,
|
|
15
|
+
* the long-task monitor catches the few outliers that are blocking
|
|
16
|
+
* the thread. Together they cover the JS-side perf signal cheaply.
|
|
17
|
+
*/
|
|
18
|
+
export declare function startSampleProfiler(opts: SampleProfilerOptions): void;
|
|
19
|
+
export declare function stopSampleProfiler(): void;
|
|
20
|
+
export declare function __resetSampleProfilerForTests(): void;
|
|
21
|
+
//# sourceMappingURL=sample-profiler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-profiler.d.ts","sourceRoot":"","sources":["../src/sample-profiler.ts"],"names":[],"mappings":"AAoDA,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB;8BAC0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAUrE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAWzC;AAyED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// v1.1 #4 升级 — JS sample profiler (idle-tick sampler).
|
|
2
|
+
//
|
|
3
|
+
// What it does:
|
|
4
|
+
// • setInterval(50ms) — captures `new Error().stack` of the tick
|
|
5
|
+
// callback, parses out the top frames, increments per-frame
|
|
6
|
+
// counts in a rolling window
|
|
7
|
+
// • every 60 s, emits a `sentori.profile` span with the aggregated
|
|
8
|
+
// flame-data (frame name → tick count) as span data
|
|
9
|
+
//
|
|
10
|
+
// What it does NOT do:
|
|
11
|
+
// • Sample frames during a busy JS task. JS is single-threaded;
|
|
12
|
+
// while the busy code is running, our setInterval can't fire,
|
|
13
|
+
// so the busy stack never gets sampled. Long-task monitor (#4
|
|
14
|
+
// v0.9.6) catches that case from a different angle (duration).
|
|
15
|
+
//
|
|
16
|
+
// Idle-tick sampling is still useful: shows which functions appear
|
|
17
|
+
// most often between busy moments — typically the render hot path,
|
|
18
|
+
// frequent timer callbacks, recurring middleware. Pairs with
|
|
19
|
+
// long-task-monitor (catches the 200ms+ outliers).
|
|
20
|
+
//
|
|
21
|
+
// Real Hermes off-thread sampling profiler (which would catch busy
|
|
22
|
+
// frames too) needs RN-internal HermesAPI access, deferred to v1.2.
|
|
23
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
24
|
+
const SAMPLE_INTERVAL_MS = 50;
|
|
25
|
+
const FLUSH_INTERVAL_MS = 60_000;
|
|
26
|
+
const MAX_FRAMES = 200; // safety cap per profile
|
|
27
|
+
const MAX_FRAME_NAME_LEN = 120;
|
|
28
|
+
/** Floor on user-configured sample interval. Going below ~20 ms costs
|
|
29
|
+
* measurable JS-thread time to `new Error().stack` + regex parse on
|
|
30
|
+
* every tick — past that point you'd see the profile drag down the
|
|
31
|
+
* thing you're profiling. */
|
|
32
|
+
const MIN_SAMPLE_INTERVAL_MS = 20;
|
|
33
|
+
/** Floor on flush interval. Sub-5s windows produce a span every few
|
|
34
|
+
* seconds, which is just noise for an aggregator that's looking at
|
|
35
|
+
* per-minute hotspot trends. */
|
|
36
|
+
const MIN_FLUSH_INTERVAL_MS = 5_000;
|
|
37
|
+
/** How many stack-trace lines to keep per tick (after dropping the
|
|
38
|
+
* Error ctor + sampleTick frames). 10 is enough to see the JS-side
|
|
39
|
+
* call shape; deeper than that the bottom frames are usually RN
|
|
40
|
+
* runtime / event loop and not actionable. */
|
|
41
|
+
const FRAMES_PER_TICK = 10;
|
|
42
|
+
let _frameCounts = new Map();
|
|
43
|
+
let _windowStartedAt = 0;
|
|
44
|
+
let _sampleTimer = null;
|
|
45
|
+
let _flushTimer = null;
|
|
46
|
+
/**
|
|
47
|
+
* Start the idle-tick sample profiler. Idempotent — calling twice
|
|
48
|
+
* is a no-op after the first successful start.
|
|
49
|
+
*
|
|
50
|
+
* Pairs naturally with `longTaskMonitor` (≥200ms outliers): the
|
|
51
|
+
* profiler shows the *distribution* of code that runs in idle gaps,
|
|
52
|
+
* the long-task monitor catches the few outliers that are blocking
|
|
53
|
+
* the thread. Together they cover the JS-side perf signal cheaply.
|
|
54
|
+
*/
|
|
55
|
+
export function startSampleProfiler(opts) {
|
|
56
|
+
if (!opts.enabled || _sampleTimer !== null)
|
|
57
|
+
return;
|
|
58
|
+
const sampleMs = Math.max(MIN_SAMPLE_INTERVAL_MS, opts.sampleMs ?? SAMPLE_INTERVAL_MS);
|
|
59
|
+
const flushMs = Math.max(MIN_FLUSH_INTERVAL_MS, opts.flushMs ?? FLUSH_INTERVAL_MS);
|
|
60
|
+
_windowStartedAt = Date.now();
|
|
61
|
+
_sampleTimer = setInterval(() => sampleTick(), sampleMs);
|
|
62
|
+
_flushTimer = setInterval(() => flushWindow(), flushMs);
|
|
63
|
+
_sampleTimer.unref?.();
|
|
64
|
+
_flushTimer.unref?.();
|
|
65
|
+
}
|
|
66
|
+
export function stopSampleProfiler() {
|
|
67
|
+
if (_sampleTimer !== null) {
|
|
68
|
+
clearInterval(_sampleTimer);
|
|
69
|
+
_sampleTimer = null;
|
|
70
|
+
}
|
|
71
|
+
if (_flushTimer !== null) {
|
|
72
|
+
clearInterval(_flushTimer);
|
|
73
|
+
_flushTimer = null;
|
|
74
|
+
}
|
|
75
|
+
_frameCounts.clear();
|
|
76
|
+
_windowStartedAt = 0;
|
|
77
|
+
}
|
|
78
|
+
function sampleTick() {
|
|
79
|
+
const stack = new Error().stack;
|
|
80
|
+
if (!stack)
|
|
81
|
+
return;
|
|
82
|
+
// Skip the first 2 frames — they're our `sampleTick` + `Error
|
|
83
|
+
// ctor` which would dominate every sample.
|
|
84
|
+
const lines = stack.split('\n').slice(2, 2 + FRAMES_PER_TICK);
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const frame = parseFrameName(line);
|
|
87
|
+
if (frame) {
|
|
88
|
+
_frameCounts.set(frame, (_frameCounts.get(frame) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Pull a stable identifier from one stack-trace line. Handles:
|
|
93
|
+
*
|
|
94
|
+
* at FunctionName (file://path/Foo.js:123:45)
|
|
95
|
+
* at file://path/Foo.js:123:45 (anonymous)
|
|
96
|
+
* FunctionName@file://path/Foo.js:123:45 (Hermes style)
|
|
97
|
+
*
|
|
98
|
+
* Drops absolute path noise — keeps `Foo.js:Line` so re-deploys
|
|
99
|
+
* with stable code paths bucket together. */
|
|
100
|
+
function parseFrameName(line) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (trimmed.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
// Hermes / JSC: `FunctionName@file:line:col` or just `@file:line:col`.
|
|
105
|
+
// Standard: `at FunctionName (file:line:col)`.
|
|
106
|
+
let raw = trimmed.replace(/^at\s+/, '');
|
|
107
|
+
raw = raw.replace(/\s*[(\[].*$/, ''); // strip the file part after `(`
|
|
108
|
+
// Hermes: split on @
|
|
109
|
+
if (raw.includes('@')) {
|
|
110
|
+
raw = raw.split('@')[0] ?? raw;
|
|
111
|
+
}
|
|
112
|
+
raw = raw.trim();
|
|
113
|
+
if (raw.length === 0 || raw.length > MAX_FRAME_NAME_LEN)
|
|
114
|
+
return null;
|
|
115
|
+
// Ignore the obvious noise frames.
|
|
116
|
+
if (raw === 'Object.<anonymous>' || raw === '<anonymous>')
|
|
117
|
+
return null;
|
|
118
|
+
return raw;
|
|
119
|
+
}
|
|
120
|
+
function flushWindow() {
|
|
121
|
+
if (_frameCounts.size === 0)
|
|
122
|
+
return;
|
|
123
|
+
const windowEndedAt = Date.now();
|
|
124
|
+
const durationMs = windowEndedAt - _windowStartedAt;
|
|
125
|
+
// Top-N frames so attachment doesn't bloat unboundedly.
|
|
126
|
+
const top = Array.from(_frameCounts.entries())
|
|
127
|
+
.sort((a, b) => b[1] - a[1])
|
|
128
|
+
.slice(0, MAX_FRAMES);
|
|
129
|
+
const span = startSpan('sentori.profile', {
|
|
130
|
+
name: 'js.sample-profile',
|
|
131
|
+
startNowMs: _windowStartedAt,
|
|
132
|
+
tags: {
|
|
133
|
+
'profile.kind': 'sample',
|
|
134
|
+
'profile.sample_count': String(sampleCount(top)),
|
|
135
|
+
'profile.duration_ms': String(durationMs),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
span.setData('flameData', Object.fromEntries(top));
|
|
139
|
+
span.finish({ endNowMs: windowEndedAt, status: 'ok' });
|
|
140
|
+
_frameCounts.clear();
|
|
141
|
+
_windowStartedAt = windowEndedAt;
|
|
142
|
+
}
|
|
143
|
+
function sampleCount(entries) {
|
|
144
|
+
let total = 0;
|
|
145
|
+
for (const [, n] of entries)
|
|
146
|
+
total += n;
|
|
147
|
+
return total;
|
|
148
|
+
}
|
|
149
|
+
export function __resetSampleProfilerForTests() {
|
|
150
|
+
stopSampleProfiler();
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=sample-profiler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-profiler.js","sourceRoot":"","sources":["../src/sample-profiler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://sentori.golia.jp",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// v1.1 +S7 升级 — control channel unit tests.
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { setConfig, __resetForTests as resetConfig } from '../config';
|
|
6
|
+
import {
|
|
7
|
+
__resetControlChannelForTests,
|
|
8
|
+
isLiveMode,
|
|
9
|
+
startControlChannel,
|
|
10
|
+
stopControlChannel,
|
|
11
|
+
} from '../control-channel';
|
|
12
|
+
import { setUser } from '../capture';
|
|
13
|
+
|
|
14
|
+
const originalFetch = globalThis.fetch;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
resetConfig();
|
|
18
|
+
__resetControlChannelForTests();
|
|
19
|
+
setConfig({
|
|
20
|
+
token: 'st_pk_test',
|
|
21
|
+
release: 'app@1.0.0+1',
|
|
22
|
+
environment: 'test',
|
|
23
|
+
ingestUrl: 'http://localhost:8080',
|
|
24
|
+
enabled: true,
|
|
25
|
+
screenshotsEnabled: false,
|
|
26
|
+
errorSampleRate: null,
|
|
27
|
+
traceSampleRate: null,
|
|
28
|
+
sessionTrailEnabled: false,
|
|
29
|
+
});
|
|
30
|
+
setUser({ id: 'u-1' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
globalThis.fetch = originalFetch;
|
|
35
|
+
__resetControlChannelForTests();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('control-channel', () => {
|
|
39
|
+
test('isLiveMode() is false by default', () => {
|
|
40
|
+
expect(isLiveMode()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('start → poll → server says liveMode:true → isLiveMode flips on', async () => {
|
|
44
|
+
globalThis.fetch = mock(async () =>
|
|
45
|
+
new Response(JSON.stringify({ liveMode: true, ttlMs: 60_000 }), {
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: { 'content-type': 'application/json' },
|
|
48
|
+
}),
|
|
49
|
+
) as typeof fetch;
|
|
50
|
+
|
|
51
|
+
startControlChannel();
|
|
52
|
+
// The initial pollOnce is awaited via void, so give it a microtask.
|
|
53
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
54
|
+
expect(isLiveMode()).toBe(true);
|
|
55
|
+
stopControlChannel();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('server says liveMode:false → isLiveMode stays false', async () => {
|
|
59
|
+
globalThis.fetch = mock(async () =>
|
|
60
|
+
new Response(JSON.stringify({ liveMode: false, ttlMs: 0 }), {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
}),
|
|
64
|
+
) as typeof fetch;
|
|
65
|
+
|
|
66
|
+
startControlChannel();
|
|
67
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
68
|
+
expect(isLiveMode()).toBe(false);
|
|
69
|
+
stopControlChannel();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('network failure leaves prior state (no throw on poll error)', async () => {
|
|
73
|
+
globalThis.fetch = mock(async () => {
|
|
74
|
+
throw new Error('econn');
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
startControlChannel();
|
|
77
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
78
|
+
// Should not have thrown; isLiveMode stays false.
|
|
79
|
+
expect(isLiveMode()).toBe(false);
|
|
80
|
+
stopControlChannel();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('server-reported ttlMs is capped at 15min defence-in-depth', async () => {
|
|
84
|
+
// 24h ttl from server — caller should cap.
|
|
85
|
+
globalThis.fetch = mock(async () =>
|
|
86
|
+
new Response(JSON.stringify({ liveMode: true, ttlMs: 24 * 60 * 60_000 }), {
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
}),
|
|
90
|
+
) as typeof fetch;
|
|
91
|
+
startControlChannel();
|
|
92
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
93
|
+
// We don't expose _liveModeUntil directly, but we can at least
|
|
94
|
+
// confirm liveMode flipped on without runaway state.
|
|
95
|
+
expect(isLiveMode()).toBe(true);
|
|
96
|
+
stopControlChannel();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('without setUser, liveMode stays off (no key to poll)', async () => {
|
|
100
|
+
setUser(undefined);
|
|
101
|
+
globalThis.fetch = mock(async () =>
|
|
102
|
+
new Response(JSON.stringify({ liveMode: true, ttlMs: 60_000 }), {
|
|
103
|
+
status: 200,
|
|
104
|
+
}),
|
|
105
|
+
) as typeof fetch;
|
|
106
|
+
startControlChannel();
|
|
107
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
108
|
+
expect(isLiveMode()).toBe(false);
|
|
109
|
+
stopControlChannel();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// v0.9.6 #4 — long-task monitor unit tests.
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { clearSpans, drainSpans } from '@goliapkg/sentori-core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
__resetLongTaskMonitorForTests,
|
|
9
|
+
startLongTaskMonitor,
|
|
10
|
+
stopLongTaskMonitor,
|
|
11
|
+
} from '../long-task-monitor';
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
__resetLongTaskMonitorForTests();
|
|
15
|
+
clearSpans();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('long-task-monitor', () => {
|
|
19
|
+
test('disabled is a no-op', () => {
|
|
20
|
+
startLongTaskMonitor({ enabled: false });
|
|
21
|
+
expect(drainSpans().length).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('double-start is idempotent', () => {
|
|
25
|
+
startLongTaskMonitor({ enabled: true });
|
|
26
|
+
// Second call must not register a parallel interval — would
|
|
27
|
+
// double-count every tick.
|
|
28
|
+
startLongTaskMonitor({ enabled: true });
|
|
29
|
+
stopLongTaskMonitor();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('synthetic long task crosses threshold and emits a span', async () => {
|
|
33
|
+
// 80ms threshold so we don't need a 200ms+ block in the test.
|
|
34
|
+
startLongTaskMonitor({ enabled: true, thresholdMs: 80 });
|
|
35
|
+
// Busy-block the JS thread for ~150ms; the next tick will see
|
|
36
|
+
// elapsed >> tickInterval and emit a longtask span.
|
|
37
|
+
const t0 = Date.now();
|
|
38
|
+
while (Date.now() - t0 < 150) {
|
|
39
|
+
// burn — no setTimeout, the thread must actually be busy.
|
|
40
|
+
}
|
|
41
|
+
// Let the next 50ms tick fire.
|
|
42
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
43
|
+
const spans = drainSpans();
|
|
44
|
+
const longtask = spans.find((s) => s.op === 'sentori.longtask');
|
|
45
|
+
expect(longtask).toBeDefined();
|
|
46
|
+
expect(longtask?.tags?.['profile.kind']).toBe('longtask');
|
|
47
|
+
stopLongTaskMonitor();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('sub-threshold ticks do not emit', async () => {
|
|
51
|
+
startLongTaskMonitor({ enabled: true, thresholdMs: 500 });
|
|
52
|
+
// Don't block — let normal idle ticks happen.
|
|
53
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
54
|
+
const spans = drainSpans();
|
|
55
|
+
const longtasks = spans.filter((s) => s.op === 'sentori.longtask');
|
|
56
|
+
expect(longtasks.length).toBe(0);
|
|
57
|
+
stopLongTaskMonitor();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// v1.1 #4 升级 — JS sample profiler unit tests.
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { clearSpans, drainSpans } from '@goliapkg/sentori-core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
__resetSampleProfilerForTests,
|
|
9
|
+
startSampleProfiler,
|
|
10
|
+
stopSampleProfiler,
|
|
11
|
+
} from '../sample-profiler';
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
__resetSampleProfilerForTests();
|
|
15
|
+
clearSpans();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('sample-profiler', () => {
|
|
19
|
+
test('start with enabled=false is a no-op', () => {
|
|
20
|
+
startSampleProfiler({ enabled: false });
|
|
21
|
+
stopSampleProfiler();
|
|
22
|
+
expect(drainSpans().length).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('double start is idempotent (second call ignored)', () => {
|
|
26
|
+
startSampleProfiler({ enabled: true, sampleMs: 30, flushMs: 5_000 });
|
|
27
|
+
// A second start while the timer is live must not spin a second
|
|
28
|
+
// interval — would double-count every sample.
|
|
29
|
+
startSampleProfiler({ enabled: true, sampleMs: 30, flushMs: 5_000 });
|
|
30
|
+
stopSampleProfiler();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('stop clears state cleanly', async () => {
|
|
34
|
+
startSampleProfiler({ enabled: true, sampleMs: 30, flushMs: 5_000 });
|
|
35
|
+
// Let a few sampleTicks happen.
|
|
36
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
37
|
+
stopSampleProfiler();
|
|
38
|
+
// After stop, no further span should be emitted even past flushMs.
|
|
39
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
40
|
+
// (Bun timers aren't faked here, so we don't wait the full flush
|
|
41
|
+
// window; we just confirm stop returns cleanly and __reset works.)
|
|
42
|
+
__resetSampleProfilerForTests();
|
|
43
|
+
expect(true).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('sample interval floor — sampleMs < 20 clamped up to 20', () => {
|
|
47
|
+
// Internal: profile.duration_ms tag uses the *effective* window
|
|
48
|
+
// length so we can't directly observe the chosen sampleMs, but we
|
|
49
|
+
// *can* assert start doesn't throw with an absurd request, since
|
|
50
|
+
// the clamp protects against pathological CPU burn.
|
|
51
|
+
startSampleProfiler({ enabled: true, sampleMs: 1, flushMs: 5_000 });
|
|
52
|
+
stopSampleProfiler();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('flush interval floor — flushMs < 5000 clamped up to 5000', () => {
|
|
56
|
+
startSampleProfiler({ enabled: true, sampleMs: 30, flushMs: 100 });
|
|
57
|
+
stopSampleProfiler();
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/control-channel.ts
CHANGED
|
@@ -19,6 +19,14 @@ import { getCurrentUserId } from './capture';
|
|
|
19
19
|
|
|
20
20
|
const POLL_INTERVAL_MS = 30_000;
|
|
21
21
|
|
|
22
|
+
/** Cap on the live-mode window even if the server reports a longer
|
|
23
|
+
* TTL. Defense-in-depth: an unbounded `ttlMs` (server bug, clock
|
|
24
|
+
* skew, malicious server) would otherwise pin the SDK into flush-
|
|
25
|
+
* every-event mode indefinitely, which is expensive on slow
|
|
26
|
+
* networks. 15 min matches the dashboard's default arm length so
|
|
27
|
+
* normal operation stays uncapped. */
|
|
28
|
+
const MAX_LIVE_MODE_TTL_MS = 15 * 60_000;
|
|
29
|
+
|
|
22
30
|
let _liveMode = false;
|
|
23
31
|
let _liveModeUntil = 0;
|
|
24
32
|
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -73,7 +81,7 @@ async function pollOnce(): Promise<void> {
|
|
|
73
81
|
const body = (await resp.json()) as { liveMode?: boolean; ttlMs?: number };
|
|
74
82
|
if (body.liveMode === true) {
|
|
75
83
|
_liveMode = true;
|
|
76
|
-
_liveModeUntil = Date.now() + Math.max(0, Math.min(body.ttlMs ?? 0,
|
|
84
|
+
_liveModeUntil = Date.now() + Math.max(0, Math.min(body.ttlMs ?? 0, MAX_LIVE_MODE_TTL_MS));
|
|
77
85
|
} else {
|
|
78
86
|
_liveMode = false;
|
|
79
87
|
_liveModeUntil = 0;
|
package/src/init.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { startLongTaskMonitor } from './long-task-monitor';
|
|
|
17
17
|
import { startNetworkTypeWatch } from './netinfo';
|
|
18
18
|
import { startPreCrashSentinel, type PreCrashChannel } from './pre-crash-sentinel';
|
|
19
19
|
import { startReplay } from './replay';
|
|
20
|
+
import { startSampleProfiler } from './sample-profiler';
|
|
20
21
|
import { startSession } from './session-tracker';
|
|
21
22
|
import {
|
|
22
23
|
drainOfflineQueue,
|
|
@@ -85,6 +86,12 @@ export type InitOptions = {
|
|
|
85
86
|
* visible nodes; captureException flushes the last 60 s as a
|
|
86
87
|
* `replay` attachment. Set to `'wireframe'` to enable. */
|
|
87
88
|
replay?: 'off' | 'wireframe' | { hz?: number; mode: 'off' | 'wireframe' };
|
|
89
|
+
/** v1.1 #4 升级 — JS sample profiler. setInterval(50ms) idle-tick
|
|
90
|
+
* sampler aggregates frame counts → emits sentori.profile span
|
|
91
|
+
* every 60s with `flameData` (frame → tick count). Pairs with
|
|
92
|
+
* longTaskMonitor (≥200ms outliers) — sample profiler 看 idle
|
|
93
|
+
* 分布、long-task 看 outliers。 */
|
|
94
|
+
sampleProfiler?: boolean | { flushMs?: number; sampleMs?: number };
|
|
88
95
|
/** v0.9.0 #3 — launch-crash loop guard. When two consecutive
|
|
89
96
|
* launches don't reach `markLaunchCompleted()` (typical of an
|
|
90
97
|
* OTA update with a fatal bug), invoke the host callback with
|
|
@@ -209,6 +216,15 @@ export const init = (options: InitOptions): void => {
|
|
|
209
216
|
} else if (rp && typeof rp === 'object' && rp.mode === 'wireframe') {
|
|
210
217
|
startReplay({ hz: rp.hz, mode: 'wireframe' });
|
|
211
218
|
}
|
|
219
|
+
// v1.1 #4 升级 — JS sample profiler. Off by default.
|
|
220
|
+
const sp = options.capture?.sampleProfiler;
|
|
221
|
+
if (sp) {
|
|
222
|
+
startSampleProfiler({
|
|
223
|
+
enabled: true,
|
|
224
|
+
flushMs: typeof sp === 'object' ? sp.flushMs : undefined,
|
|
225
|
+
sampleMs: typeof sp === 'object' ? sp.sampleMs : undefined,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
212
228
|
|
|
213
229
|
const capture = options.capture ?? {};
|
|
214
230
|
if (capture.globalErrors !== false) installGlobalHandler();
|
package/src/mobile-vitals.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { startSpan } from '@goliapkg/sentori-core';
|
|
24
24
|
|
|
25
|
-
import { getNativeColdStartMs } from './native';
|
|
25
|
+
import { getNativeColdStartMs, getNativeFrameCounters } from './native';
|
|
26
26
|
|
|
27
27
|
let _coldStartMs: null | number = null;
|
|
28
28
|
let _coldStartCaptured = false;
|
|
@@ -93,13 +93,7 @@ export function markTimeToFullDisplay(route: string): TimeToFullDisplayHandle {
|
|
|
93
93
|
* linked. */
|
|
94
94
|
export function getFrameCounters(): null | { slow: number; frozen: number } {
|
|
95
95
|
try {
|
|
96
|
-
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
98
|
-
const nativeMod = require('./native') as {
|
|
99
|
-
getNativeFrameCounters?: () => null | { frozen: number; slow: number };
|
|
100
|
-
};
|
|
101
|
-
if (typeof nativeMod.getNativeFrameCounters !== 'function') return null;
|
|
102
|
-
return nativeMod.getNativeFrameCounters();
|
|
96
|
+
return getNativeFrameCounters();
|
|
103
97
|
} catch {
|
|
104
98
|
return null;
|
|
105
99
|
}
|
package/src/replay.ts
CHANGED
|
@@ -25,10 +25,23 @@ import { isNativeModuleLinked } from './native-loader';
|
|
|
25
25
|
const TICK_INTERVAL_MS = 1000;
|
|
26
26
|
const RING_SIZE = 60;
|
|
27
27
|
|
|
28
|
+
/** Floor on tick period. < 250 ms (4 Hz) the native view-tree walk
|
|
29
|
+
* dominates the JS thread on mid-tier Android, especially with mask
|
|
30
|
+
* consultation. The default is 1 Hz; the option exists for
|
|
31
|
+
* benchmarking, not for production. */
|
|
32
|
+
const MIN_TICK_PERIOD_MS = 250;
|
|
33
|
+
|
|
28
34
|
let _ring: string[] = [];
|
|
29
35
|
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
30
36
|
let _running = false;
|
|
31
37
|
|
|
38
|
+
/** Native module ref, resolved once on first start. Caching here
|
|
39
|
+
* avoids the cost of `requireNativeModule('Sentori')` on every
|
|
40
|
+
* capture tick (Metro's require cache makes this cheap, but the
|
|
41
|
+
* per-tick string lookup and possible throw still cost more than
|
|
42
|
+
* reading a closed-over variable). */
|
|
43
|
+
let _nativeMod: ReplayNativeModule | null = null;
|
|
44
|
+
|
|
32
45
|
export type ReplayOptions = {
|
|
33
46
|
mode?: 'off' | 'wireframe';
|
|
34
47
|
/** Ticks per second. Default 1. */
|
|
@@ -47,7 +60,8 @@ export function startReplay(opts: ReplayOptions): void {
|
|
|
47
60
|
return;
|
|
48
61
|
}
|
|
49
62
|
_running = true;
|
|
50
|
-
|
|
63
|
+
_nativeMod = loadNativeReplay();
|
|
64
|
+
const period = Math.max(MIN_TICK_PERIOD_MS, Math.floor(TICK_INTERVAL_MS / (opts.hz ?? 1)));
|
|
51
65
|
_timer = setInterval(() => {
|
|
52
66
|
captureTick();
|
|
53
67
|
}, period);
|
|
@@ -60,6 +74,7 @@ export function stopReplay(): void {
|
|
|
60
74
|
clearInterval(_timer);
|
|
61
75
|
_timer = null;
|
|
62
76
|
}
|
|
77
|
+
_nativeMod = null;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
function captureTick(): void {
|
|
@@ -67,9 +82,7 @@ function captureTick(): void {
|
|
|
67
82
|
const tickSpan = startSpan('sentori.replay.tick', { name: 'tick' });
|
|
68
83
|
try {
|
|
69
84
|
const maskIds = readMaskIds();
|
|
70
|
-
|
|
71
|
-
const nativeMod = loadNativeReplay();
|
|
72
|
-
const snapshot = nativeMod?.captureWireframe?.(maskIds);
|
|
85
|
+
const snapshot = _nativeMod?.captureWireframe?.(maskIds);
|
|
73
86
|
if (typeof snapshot === 'string' && snapshot.length > 0) {
|
|
74
87
|
_ring.push(snapshot);
|
|
75
88
|
while (_ring.length > RING_SIZE) _ring.shift();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// v1.1 #4 升级 — JS sample profiler (idle-tick sampler).
|
|
2
|
+
//
|
|
3
|
+
// What it does:
|
|
4
|
+
// • setInterval(50ms) — captures `new Error().stack` of the tick
|
|
5
|
+
// callback, parses out the top frames, increments per-frame
|
|
6
|
+
// counts in a rolling window
|
|
7
|
+
// • every 60 s, emits a `sentori.profile` span with the aggregated
|
|
8
|
+
// flame-data (frame name → tick count) as span data
|
|
9
|
+
//
|
|
10
|
+
// What it does NOT do:
|
|
11
|
+
// • Sample frames during a busy JS task. JS is single-threaded;
|
|
12
|
+
// while the busy code is running, our setInterval can't fire,
|
|
13
|
+
// so the busy stack never gets sampled. Long-task monitor (#4
|
|
14
|
+
// v0.9.6) catches that case from a different angle (duration).
|
|
15
|
+
//
|
|
16
|
+
// Idle-tick sampling is still useful: shows which functions appear
|
|
17
|
+
// most often between busy moments — typically the render hot path,
|
|
18
|
+
// frequent timer callbacks, recurring middleware. Pairs with
|
|
19
|
+
// long-task-monitor (catches the 200ms+ outliers).
|
|
20
|
+
//
|
|
21
|
+
// Real Hermes off-thread sampling profiler (which would catch busy
|
|
22
|
+
// frames too) needs RN-internal HermesAPI access, deferred to v1.2.
|
|
23
|
+
|
|
24
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
25
|
+
|
|
26
|
+
const SAMPLE_INTERVAL_MS = 50;
|
|
27
|
+
const FLUSH_INTERVAL_MS = 60_000;
|
|
28
|
+
const MAX_FRAMES = 200; // safety cap per profile
|
|
29
|
+
const MAX_FRAME_NAME_LEN = 120;
|
|
30
|
+
|
|
31
|
+
/** Floor on user-configured sample interval. Going below ~20 ms costs
|
|
32
|
+
* measurable JS-thread time to `new Error().stack` + regex parse on
|
|
33
|
+
* every tick — past that point you'd see the profile drag down the
|
|
34
|
+
* thing you're profiling. */
|
|
35
|
+
const MIN_SAMPLE_INTERVAL_MS = 20;
|
|
36
|
+
|
|
37
|
+
/** Floor on flush interval. Sub-5s windows produce a span every few
|
|
38
|
+
* seconds, which is just noise for an aggregator that's looking at
|
|
39
|
+
* per-minute hotspot trends. */
|
|
40
|
+
const MIN_FLUSH_INTERVAL_MS = 5_000;
|
|
41
|
+
|
|
42
|
+
/** How many stack-trace lines to keep per tick (after dropping the
|
|
43
|
+
* Error ctor + sampleTick frames). 10 is enough to see the JS-side
|
|
44
|
+
* call shape; deeper than that the bottom frames are usually RN
|
|
45
|
+
* runtime / event loop and not actionable. */
|
|
46
|
+
const FRAMES_PER_TICK = 10;
|
|
47
|
+
|
|
48
|
+
let _frameCounts = new Map<string, number>();
|
|
49
|
+
let _windowStartedAt = 0;
|
|
50
|
+
let _sampleTimer: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
let _flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
52
|
+
|
|
53
|
+
export type SampleProfilerOptions = {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
/** Sample interval ms. Default 50. Lower → more accurate but more
|
|
56
|
+
* JS-thread overhead. */
|
|
57
|
+
sampleMs?: number;
|
|
58
|
+
/** Flush window ms. Default 60 000 (one minute). */
|
|
59
|
+
flushMs?: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start the idle-tick sample profiler. Idempotent — calling twice
|
|
64
|
+
* is a no-op after the first successful start.
|
|
65
|
+
*
|
|
66
|
+
* Pairs naturally with `longTaskMonitor` (≥200ms outliers): the
|
|
67
|
+
* profiler shows the *distribution* of code that runs in idle gaps,
|
|
68
|
+
* the long-task monitor catches the few outliers that are blocking
|
|
69
|
+
* the thread. Together they cover the JS-side perf signal cheaply.
|
|
70
|
+
*/
|
|
71
|
+
export function startSampleProfiler(opts: SampleProfilerOptions): void {
|
|
72
|
+
if (!opts.enabled || _sampleTimer !== null) return;
|
|
73
|
+
const sampleMs = Math.max(MIN_SAMPLE_INTERVAL_MS, opts.sampleMs ?? SAMPLE_INTERVAL_MS);
|
|
74
|
+
const flushMs = Math.max(MIN_FLUSH_INTERVAL_MS, opts.flushMs ?? FLUSH_INTERVAL_MS);
|
|
75
|
+
|
|
76
|
+
_windowStartedAt = Date.now();
|
|
77
|
+
_sampleTimer = setInterval(() => sampleTick(), sampleMs);
|
|
78
|
+
_flushTimer = setInterval(() => flushWindow(), flushMs);
|
|
79
|
+
(_sampleTimer as unknown as { unref?: () => void }).unref?.();
|
|
80
|
+
(_flushTimer as unknown as { unref?: () => void }).unref?.();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function stopSampleProfiler(): void {
|
|
84
|
+
if (_sampleTimer !== null) {
|
|
85
|
+
clearInterval(_sampleTimer);
|
|
86
|
+
_sampleTimer = null;
|
|
87
|
+
}
|
|
88
|
+
if (_flushTimer !== null) {
|
|
89
|
+
clearInterval(_flushTimer);
|
|
90
|
+
_flushTimer = null;
|
|
91
|
+
}
|
|
92
|
+
_frameCounts.clear();
|
|
93
|
+
_windowStartedAt = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sampleTick(): void {
|
|
97
|
+
const stack = new Error().stack;
|
|
98
|
+
if (!stack) return;
|
|
99
|
+
// Skip the first 2 frames — they're our `sampleTick` + `Error
|
|
100
|
+
// ctor` which would dominate every sample.
|
|
101
|
+
const lines = stack.split('\n').slice(2, 2 + FRAMES_PER_TICK);
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const frame = parseFrameName(line);
|
|
104
|
+
if (frame) {
|
|
105
|
+
_frameCounts.set(frame, (_frameCounts.get(frame) ?? 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Pull a stable identifier from one stack-trace line. Handles:
|
|
111
|
+
*
|
|
112
|
+
* at FunctionName (file://path/Foo.js:123:45)
|
|
113
|
+
* at file://path/Foo.js:123:45 (anonymous)
|
|
114
|
+
* FunctionName@file://path/Foo.js:123:45 (Hermes style)
|
|
115
|
+
*
|
|
116
|
+
* Drops absolute path noise — keeps `Foo.js:Line` so re-deploys
|
|
117
|
+
* with stable code paths bucket together. */
|
|
118
|
+
function parseFrameName(line: string): null | string {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
if (trimmed.length === 0) return null;
|
|
121
|
+
// Hermes / JSC: `FunctionName@file:line:col` or just `@file:line:col`.
|
|
122
|
+
// Standard: `at FunctionName (file:line:col)`.
|
|
123
|
+
let raw = trimmed.replace(/^at\s+/, '');
|
|
124
|
+
raw = raw.replace(/\s*[(\[].*$/, ''); // strip the file part after `(`
|
|
125
|
+
// Hermes: split on @
|
|
126
|
+
if (raw.includes('@')) {
|
|
127
|
+
raw = raw.split('@')[0] ?? raw;
|
|
128
|
+
}
|
|
129
|
+
raw = raw.trim();
|
|
130
|
+
if (raw.length === 0 || raw.length > MAX_FRAME_NAME_LEN) return null;
|
|
131
|
+
// Ignore the obvious noise frames.
|
|
132
|
+
if (raw === 'Object.<anonymous>' || raw === '<anonymous>') return null;
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function flushWindow(): void {
|
|
137
|
+
if (_frameCounts.size === 0) return;
|
|
138
|
+
const windowEndedAt = Date.now();
|
|
139
|
+
const durationMs = windowEndedAt - _windowStartedAt;
|
|
140
|
+
// Top-N frames so attachment doesn't bloat unboundedly.
|
|
141
|
+
const top = Array.from(_frameCounts.entries())
|
|
142
|
+
.sort((a, b) => b[1] - a[1])
|
|
143
|
+
.slice(0, MAX_FRAMES);
|
|
144
|
+
|
|
145
|
+
const span = startSpan('sentori.profile', {
|
|
146
|
+
name: 'js.sample-profile',
|
|
147
|
+
startNowMs: _windowStartedAt,
|
|
148
|
+
tags: {
|
|
149
|
+
'profile.kind': 'sample',
|
|
150
|
+
'profile.sample_count': String(sampleCount(top)),
|
|
151
|
+
'profile.duration_ms': String(durationMs),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
span.setData('flameData', Object.fromEntries(top));
|
|
155
|
+
span.finish({ endNowMs: windowEndedAt, status: 'ok' });
|
|
156
|
+
|
|
157
|
+
_frameCounts.clear();
|
|
158
|
+
_windowStartedAt = windowEndedAt;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sampleCount(entries: [string, number][]): number {
|
|
162
|
+
let total = 0;
|
|
163
|
+
for (const [, n] of entries) total += n;
|
|
164
|
+
return total;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function __resetSampleProfilerForTests(): void {
|
|
168
|
+
stopSampleProfiler();
|
|
169
|
+
}
|