@scalebun/react-native 1.2.0 → 1.2.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/dist/scalebun.full.js +103 -17
- package/dist/scalebun.full.js.map +1 -1
- package/dist/scalebun.slim.js +103 -17
- package/dist/scalebun.slim.js.map +1 -1
- package/lib/commonjs/features/performance/PerformanceFeature.js +121 -23
- package/lib/commonjs/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/module/features/performance/PerformanceFeature.js +122 -23
- package/lib/module/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/typescript/features/performance/PerformanceFeature.d.ts +31 -0
- package/lib/typescript/features/performance/PerformanceFeature.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/performance/PerformanceFeature.ts +130 -20
|
@@ -25,6 +25,15 @@ import { PerformanceTransport, type PerfTransportSink } from './transport/Perfor
|
|
|
25
25
|
import { MetricKeys } from './metrics/MetricsEngine';
|
|
26
26
|
import { SessionManager } from '../session/SessionManager';
|
|
27
27
|
|
|
28
|
+
/** Shape accepted by BackendSessionAdapter.queuePerformanceMetric. */
|
|
29
|
+
type BackendPerfMetric = {
|
|
30
|
+
type: string;
|
|
31
|
+
screenName?: string;
|
|
32
|
+
duration?: number;
|
|
33
|
+
value?: number;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
|
|
28
37
|
export class PerformanceFeature implements IFeature {
|
|
29
38
|
readonly name = 'performance';
|
|
30
39
|
private active = false;
|
|
@@ -70,7 +79,31 @@ export class PerformanceFeature implements IFeature {
|
|
|
70
79
|
/** Delay schedule for each attempt (ms) — escalating to let views settle */
|
|
71
80
|
private static readonly CAPTURE_DELAYS = [300, 600, 1200];
|
|
72
81
|
|
|
73
|
-
//
|
|
82
|
+
// ─── Early-metric buffering (backend) ───────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// Performance metrics reach the cloud via the SessionManager's backend
|
|
85
|
+
// transport. But the first metrics — most importantly app_launch — fire
|
|
86
|
+
// during PerformanceFeature.initialize() (appLaunchCollector.collect()),
|
|
87
|
+
// which runs from featureRegistry.initializeAll() BEFORE the bootstrap
|
|
88
|
+
// attaches the backend transport (SDKBootstrapper). In that window
|
|
89
|
+
// getBackendTransport() is null, so without buffering the metric is sent
|
|
90
|
+
// only to the desktop debugger and silently dropped from the cloud
|
|
91
|
+
// ("App start = 0" in Insights). We buffer such metrics locally and drain
|
|
92
|
+
// them the moment the transport appears (via subsequent events or a
|
|
93
|
+
// bounded retry). The backend adapter itself buffers again until the
|
|
94
|
+
// session row is ready, so ordering across the session boundary is safe.
|
|
95
|
+
/** Metrics captured before the backend transport was available. */
|
|
96
|
+
private _pendingBackendMetrics: BackendPerfMetric[] = [];
|
|
97
|
+
/** Retry timer that drains the buffer once the transport attaches. */
|
|
98
|
+
private _backendDrainTimer: ReturnType<typeof setTimeout> | null = null;
|
|
99
|
+
/** Number of drain attempts made for the current buffered batch. */
|
|
100
|
+
private _backendDrainAttempts = 0;
|
|
101
|
+
/** Hard cap on buffered metrics — bounds memory if a transport never attaches (non-SaaS). */
|
|
102
|
+
private static readonly BACKEND_BUFFER_MAX = 64;
|
|
103
|
+
/** Max drain retries before giving up (non-SaaS mode never attaches a transport). */
|
|
104
|
+
private static readonly BACKEND_DRAIN_MAX_ATTEMPTS = 6;
|
|
105
|
+
/** Escalating retry delays (ms); last value repeats. Covers the startup attach race. */
|
|
106
|
+
private static readonly BACKEND_DRAIN_DELAYS = [250, 500, 1000, 2000, 4000];
|
|
74
107
|
|
|
75
108
|
constructor(config?: Partial<PerformanceConfig>) {
|
|
76
109
|
this.config = mergePerformanceConfig(config);
|
|
@@ -297,6 +330,11 @@ export class PerformanceFeature implements IFeature {
|
|
|
297
330
|
clearTimeout(this._captureTimer);
|
|
298
331
|
this._captureTimer = null;
|
|
299
332
|
}
|
|
333
|
+
// Stop the backend-drain retry loop (buffered metrics are dropped —
|
|
334
|
+
// the desktop transport already received them via sendEvent/sendMetric).
|
|
335
|
+
this._clearBackendDrainTimer();
|
|
336
|
+
this._backendDrainAttempts = 0;
|
|
337
|
+
this._pendingBackendMetrics = [];
|
|
300
338
|
}
|
|
301
339
|
|
|
302
340
|
get isActive(): boolean {
|
|
@@ -570,28 +608,100 @@ export class PerformanceFeature implements IFeature {
|
|
|
570
608
|
* and it costs a duplicate serialize + upload per sample on every session.
|
|
571
609
|
*/
|
|
572
610
|
|
|
573
|
-
// Queue to SaaS backend dedicated performance endpoint
|
|
611
|
+
// Queue to SaaS backend dedicated performance endpoint. When the backend
|
|
612
|
+
// transport isn't attached yet (startup race — app_launch fires before
|
|
613
|
+
// SDKBootstrapper wires it), buffer locally and drain once it appears so
|
|
614
|
+
// the cloud gets the metric instead of only the desktop debugger.
|
|
615
|
+
this._forwardMetricToBackend(this._toBackendMetric(event));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Map a raw perf event onto the backend metric envelope. */
|
|
619
|
+
private _toBackendMetric(event: AnyPerfEvent): BackendPerfMetric {
|
|
620
|
+
const e = event as any;
|
|
621
|
+
// Forward structured launch fields so the backend can group by launch
|
|
622
|
+
// type and reconstruct the startup waterfall. Without this, only the
|
|
623
|
+
// raw duration reaches the analytics table and cold/warm/hot + phase
|
|
624
|
+
// timings are lost. Additive + forward-only: pre-existing rows stay as
|
|
625
|
+
// { id } and historical launch-type windows remain empty.
|
|
626
|
+
const metadata: Record<string, unknown> = { id: event.id };
|
|
627
|
+
if (event.type === 'app_launch') {
|
|
628
|
+
if (e.launchType) metadata.launchType = e.launchType;
|
|
629
|
+
if (e.breakdown) metadata.breakdown = e.breakdown;
|
|
630
|
+
if (e.phaseLabels) metadata.phaseLabels = e.phaseLabels;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
type: event.type,
|
|
634
|
+
screenName: e.screenName ?? e.context?.screenKey,
|
|
635
|
+
duration: e.durationMs,
|
|
636
|
+
value: e.estimatedFps ?? e.value,
|
|
637
|
+
metadata,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Hand a metric to the backend transport, or buffer it until one attaches.
|
|
643
|
+
* Every call first drains anything buffered, so a late-arriving transport
|
|
644
|
+
* flushes the backlog in the order the metrics were produced.
|
|
645
|
+
*/
|
|
646
|
+
private _forwardMetricToBackend(metric: BackendPerfMetric): void {
|
|
574
647
|
const backendTransport = SessionManager.getExistingInstance()?.getBackendTransport();
|
|
575
648
|
if (backendTransport) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
649
|
+
this._drainPendingBackendMetrics(backendTransport);
|
|
650
|
+
backendTransport.queuePerformanceMetric(metric);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// No transport yet — buffer (bounded, drop-oldest) and schedule a drain.
|
|
655
|
+
this._pendingBackendMetrics.push(metric);
|
|
656
|
+
if (this._pendingBackendMetrics.length > PerformanceFeature.BACKEND_BUFFER_MAX) {
|
|
657
|
+
this._pendingBackendMetrics.shift();
|
|
658
|
+
}
|
|
659
|
+
this._scheduleBackendDrain();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Flush all buffered metrics into the transport, preserving order. */
|
|
663
|
+
private _drainPendingBackendMetrics(
|
|
664
|
+
backendTransport: NonNullable<ReturnType<SessionManager['getBackendTransport']>>,
|
|
665
|
+
): void {
|
|
666
|
+
if (this._pendingBackendMetrics.length === 0) return;
|
|
667
|
+
const buffered = this._pendingBackendMetrics;
|
|
668
|
+
this._pendingBackendMetrics = [];
|
|
669
|
+
for (const metric of buffered) {
|
|
670
|
+
backendTransport.queuePerformanceMetric(metric);
|
|
671
|
+
}
|
|
672
|
+
this._clearBackendDrainTimer();
|
|
673
|
+
this._backendDrainAttempts = 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Poll for the backend transport a bounded number of times so the sole
|
|
678
|
+
* early metric (app_launch) still reaches the cloud even when no later
|
|
679
|
+
* event arrives to trigger a drain. Gives up after BACKEND_DRAIN_MAX_ATTEMPTS
|
|
680
|
+
* (non-SaaS mode never attaches a transport — the desktop path already has
|
|
681
|
+
* the data), leaving the bounded buffer to be GC'd on teardown.
|
|
682
|
+
*/
|
|
683
|
+
private _scheduleBackendDrain(): void {
|
|
684
|
+
if (this._backendDrainTimer) return;
|
|
685
|
+
if (this._backendDrainAttempts >= PerformanceFeature.BACKEND_DRAIN_MAX_ATTEMPTS) return;
|
|
686
|
+
|
|
687
|
+
const delays = PerformanceFeature.BACKEND_DRAIN_DELAYS;
|
|
688
|
+
const delay = delays[Math.min(this._backendDrainAttempts, delays.length - 1)];
|
|
689
|
+
this._backendDrainAttempts++;
|
|
690
|
+
this._backendDrainTimer = setTimeout(() => {
|
|
691
|
+
this._backendDrainTimer = null;
|
|
692
|
+
const backendTransport = SessionManager.getExistingInstance()?.getBackendTransport();
|
|
693
|
+
if (backendTransport) {
|
|
694
|
+
this._drainPendingBackendMetrics(backendTransport);
|
|
695
|
+
} else if (this._pendingBackendMetrics.length > 0) {
|
|
696
|
+
this._scheduleBackendDrain();
|
|
587
697
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
698
|
+
}, delay);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private _clearBackendDrainTimer(): void {
|
|
702
|
+
if (this._backendDrainTimer) {
|
|
703
|
+
clearTimeout(this._backendDrainTimer);
|
|
704
|
+
this._backendDrainTimer = null;
|
|
595
705
|
}
|
|
596
706
|
}
|
|
597
707
|
}
|