@scalebun/react-native 1.2.1 → 1.2.2-beta.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/android/src/main/java/com/scalebun/core/performance/AppStartCollector.kt +61 -13
- package/android/src/main/java/com/scalebun/rn/performance/PerformanceModule.kt +11 -0
- package/dist/scalebun.full.js +78 -9
- package/dist/scalebun.full.js.map +1 -1
- package/dist/scalebun.slim.js +78 -9
- package/dist/scalebun.slim.js.map +1 -1
- package/lib/commonjs/features/performance/PerformanceFeature.js +22 -2
- package/lib/commonjs/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/commonjs/features/performance/collectors/AppLaunchCollector.js +21 -3
- package/lib/commonjs/features/performance/collectors/AppLaunchCollector.js.map +1 -1
- package/lib/commonjs/features/session/BackendSessionAdapter.js +41 -4
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/features/performance/PerformanceFeature.js +22 -2
- package/lib/module/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/module/features/performance/collectors/AppLaunchCollector.js +21 -3
- package/lib/module/features/performance/collectors/AppLaunchCollector.js.map +1 -1
- package/lib/module/features/session/BackendSessionAdapter.js +41 -4
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/typescript/features/performance/PerformanceFeature.d.ts.map +1 -1
- package/lib/typescript/features/performance/collectors/AppLaunchCollector.d.ts.map +1 -1
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/performance/PerformanceFeature.ts +19 -2
- package/src/features/performance/collectors/AppLaunchCollector.ts +17 -3
- package/src/features/session/BackendSessionAdapter.ts +32 -5
|
@@ -6,6 +6,7 @@ import android.os.Build
|
|
|
6
6
|
import android.os.Bundle
|
|
7
7
|
import android.os.Process
|
|
8
8
|
import android.os.SystemClock
|
|
9
|
+
import android.util.Log
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Captures app start timing using Android-native hooks.
|
|
@@ -23,10 +24,24 @@ import android.os.SystemClock
|
|
|
23
24
|
*/
|
|
24
25
|
class AppStartCollector(private val app: Application) {
|
|
25
26
|
|
|
27
|
+
companion object {
|
|
28
|
+
private const val TAG = "ScaleBunPerformance"
|
|
29
|
+
/** Ignore an inferred cold-start proxy longer than this (stale/warm process). */
|
|
30
|
+
private const val MAX_INFERRED_DURATION_MS = 60_000L
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
data class AppStartInfo(
|
|
27
34
|
val launchType: String, // "cold" | "warm"
|
|
28
35
|
val durationMs: Long,
|
|
29
|
-
val breakdown: Breakdown
|
|
36
|
+
val breakdown: Breakdown?,
|
|
37
|
+
/**
|
|
38
|
+
* true when the value was NOT captured from Activity lifecycle callbacks
|
|
39
|
+
* but inferred from process-start → query time. Happens on the common RN
|
|
40
|
+
* path where the SDK's native init runs (JS-driven) only after the launch
|
|
41
|
+
* activity has already resumed, so onActivityResumed is never observed.
|
|
42
|
+
* Without this fallback getInfo() returns null and no app_launch is emitted.
|
|
43
|
+
*/
|
|
44
|
+
val inferred: Boolean = false
|
|
30
45
|
)
|
|
31
46
|
|
|
32
47
|
data class Breakdown(
|
|
@@ -86,9 +101,11 @@ class AppStartCollector(private val app: Application) {
|
|
|
86
101
|
processStartMs = null, // Already at process start
|
|
87
102
|
activityCreateMs = createDuration,
|
|
88
103
|
firstFrameMs = resumeDuration
|
|
89
|
-
)
|
|
104
|
+
),
|
|
105
|
+
inferred = false
|
|
90
106
|
)
|
|
91
107
|
|
|
108
|
+
Log.i(TAG, "app start captured from lifecycle: durationMs=$totalDuration")
|
|
92
109
|
callback?.invoke(info)
|
|
93
110
|
stop()
|
|
94
111
|
}
|
|
@@ -113,20 +130,51 @@ class AppStartCollector(private val app: Application) {
|
|
|
113
130
|
/** Check if app start has been captured */
|
|
114
131
|
fun isCaptured(): Boolean = captured
|
|
115
132
|
|
|
116
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Get app start info for on-demand queries.
|
|
135
|
+
*
|
|
136
|
+
* Prefers the precise value captured from Activity lifecycle callbacks. When
|
|
137
|
+
* capture never happened — the common RN case, because native init is driven
|
|
138
|
+
* from JS and runs only AFTER the launch activity resumed — falls back to an
|
|
139
|
+
* inferred duration from process start to now. Returning null here (the old
|
|
140
|
+
* behaviour) is exactly what caused App start = 0: AppLaunchCollector.collect()
|
|
141
|
+
* bails on a null/invalid duration, so no app_launch event was ever emitted.
|
|
142
|
+
*/
|
|
117
143
|
fun getInfo(): AppStartInfo? {
|
|
118
|
-
if (
|
|
119
|
-
|
|
144
|
+
if (captured) {
|
|
145
|
+
val totalDuration = activityResumeUptimeMs - processStartUptimeMs
|
|
146
|
+
return AppStartInfo(
|
|
147
|
+
launchType = "cold",
|
|
148
|
+
durationMs = totalDuration,
|
|
149
|
+
breakdown = Breakdown(
|
|
150
|
+
processStartMs = null,
|
|
151
|
+
activityCreateMs = if (activityCreateUptimeMs > 0)
|
|
152
|
+
activityCreateUptimeMs - processStartUptimeMs else null,
|
|
153
|
+
firstFrameMs = if (activityCreateUptimeMs > 0)
|
|
154
|
+
activityResumeUptimeMs - activityCreateUptimeMs else null
|
|
155
|
+
),
|
|
156
|
+
inferred = false
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback: infer from process start → now. Guards against a bad clock
|
|
161
|
+
// (non-positive) and against a long-lived process where this proxy would
|
|
162
|
+
// be meaningless (e.g. queried minutes after a warm start).
|
|
163
|
+
if (processStartUptimeMs <= 0) {
|
|
164
|
+
Log.w(TAG, "app start not captured and no process-start clock — returning null")
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
val inferredDuration = SystemClock.uptimeMillis() - processStartUptimeMs
|
|
168
|
+
if (inferredDuration <= 0 || inferredDuration > MAX_INFERRED_DURATION_MS) {
|
|
169
|
+
Log.w(TAG, "app start not captured; inferred duration out of range ($inferredDuration ms) — returning null")
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
Log.i(TAG, "app start not captured from lifecycle; inferred durationMs=$inferredDuration")
|
|
120
173
|
return AppStartInfo(
|
|
121
174
|
launchType = "cold",
|
|
122
|
-
durationMs =
|
|
123
|
-
breakdown =
|
|
124
|
-
|
|
125
|
-
activityCreateMs = if (activityCreateUptimeMs > 0)
|
|
126
|
-
activityCreateUptimeMs - processStartUptimeMs else null,
|
|
127
|
-
firstFrameMs = if (activityCreateUptimeMs > 0)
|
|
128
|
-
activityResumeUptimeMs - activityCreateUptimeMs else null
|
|
129
|
-
)
|
|
175
|
+
durationMs = inferredDuration,
|
|
176
|
+
breakdown = null,
|
|
177
|
+
inferred = true
|
|
130
178
|
)
|
|
131
179
|
}
|
|
132
180
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.scalebun.rn.performance
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
|
+
import android.util.Log
|
|
4
5
|
import com.facebook.react.bridge.*
|
|
5
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
6
7
|
import com.scalebun.core.performance.AppStartCollector
|
|
@@ -33,6 +34,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
33
34
|
|
|
34
35
|
companion object {
|
|
35
36
|
const val NAME = "ScaleBunPerformance"
|
|
37
|
+
private const val TAG = "ScaleBunPerformance"
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
override fun getName(): String = NAME
|
|
@@ -65,6 +67,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
65
67
|
putDouble("maxFrameTimeMs", snapshot.maxFrameTimeMs)
|
|
66
68
|
putDouble("windowMs", snapshot.windowMs.toDouble())
|
|
67
69
|
}
|
|
70
|
+
Log.i(TAG, "emit FrameMetrics: totalFrames=${snapshot.totalFrames} dropped=${snapshot.droppedFrames}")
|
|
68
71
|
emit("ScaleBunPerformance_FrameMetrics", params)
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -73,6 +76,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
73
76
|
putDouble("durationMs", info.durationMs.toDouble())
|
|
74
77
|
putDouble("timestamp", info.timestamp.toDouble())
|
|
75
78
|
}
|
|
79
|
+
Log.i(TAG, "emit UiHang: durationMs=${info.durationMs}")
|
|
76
80
|
emit("ScaleBunPerformance_UiHang", params)
|
|
77
81
|
}
|
|
78
82
|
}
|
|
@@ -83,12 +87,14 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
83
87
|
override fun initialize(promise: Promise) {
|
|
84
88
|
try {
|
|
85
89
|
if (core?.isInitialized == true) {
|
|
90
|
+
Log.i(TAG, "initialize() — already initialized, no-op")
|
|
86
91
|
promise.resolve(true)
|
|
87
92
|
return
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
val app = reactApplicationContext.applicationContext as? Application
|
|
91
96
|
if (app == null) {
|
|
97
|
+
Log.w(TAG, "initialize() — Application context not available")
|
|
92
98
|
promise.reject("PERF_INIT_ERROR", "Application context not available")
|
|
93
99
|
return
|
|
94
100
|
}
|
|
@@ -98,8 +104,10 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
98
104
|
performanceCore.initialize()
|
|
99
105
|
core = performanceCore
|
|
100
106
|
|
|
107
|
+
Log.i(TAG, "initialize() — native performance core started")
|
|
101
108
|
promise.resolve(true)
|
|
102
109
|
} catch (e: Exception) {
|
|
110
|
+
Log.w(TAG, "initialize() — failed: ${e.message}")
|
|
103
111
|
promise.reject("PERF_INIT_ERROR", e.message, e)
|
|
104
112
|
}
|
|
105
113
|
}
|
|
@@ -111,9 +119,11 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
111
119
|
try {
|
|
112
120
|
val info = core?.getAppStartInfo()
|
|
113
121
|
if (info != null) {
|
|
122
|
+
Log.i(TAG, "getAppStartInfo() — launchType=${info.launchType} durationMs=${info.durationMs} inferred=${info.inferred}")
|
|
114
123
|
val result = Arguments.createMap().apply {
|
|
115
124
|
putString("launchType", info.launchType)
|
|
116
125
|
putDouble("durationMs", info.durationMs.toDouble())
|
|
126
|
+
putBoolean("inferred", info.inferred)
|
|
117
127
|
info.breakdown?.let { breakdown ->
|
|
118
128
|
val breakdownMap = Arguments.createMap().apply {
|
|
119
129
|
breakdown.processStartMs?.let { putDouble("processStartMs", it.toDouble()) }
|
|
@@ -125,6 +135,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
|
|
|
125
135
|
}
|
|
126
136
|
promise.resolve(result)
|
|
127
137
|
} else {
|
|
138
|
+
Log.w(TAG, "getAppStartInfo() — no info available (core=${if (core == null) "null" else "present"})")
|
|
128
139
|
promise.resolve(null)
|
|
129
140
|
}
|
|
130
141
|
} catch (e: Exception) {
|
package/dist/scalebun.full.js
CHANGED
|
@@ -7480,6 +7480,7 @@ var init_AppLaunchCollector = __esm({
|
|
|
7480
7480
|
"use strict";
|
|
7481
7481
|
init_getNativePerformanceModule();
|
|
7482
7482
|
init_models();
|
|
7483
|
+
init_internalLogger();
|
|
7483
7484
|
JS_IMPORT_TIME = Date.now();
|
|
7484
7485
|
AppLaunchCollector = class {
|
|
7485
7486
|
collected = false;
|
|
@@ -7497,10 +7498,19 @@ var init_AppLaunchCollector = __esm({
|
|
|
7497
7498
|
if (this.collected || !this.config.appLaunch) return;
|
|
7498
7499
|
try {
|
|
7499
7500
|
const perfModule = getNativePerformanceModule();
|
|
7500
|
-
if (!perfModule?.getAppStartInfo)
|
|
7501
|
+
if (!perfModule?.getAppStartInfo) {
|
|
7502
|
+
logger.warn("[perf.trace] AppLaunchCollector: native perf module unavailable \u2014 no app_launch");
|
|
7503
|
+
return;
|
|
7504
|
+
}
|
|
7501
7505
|
const jsReadyTimestamp = Date.now();
|
|
7502
7506
|
const info = await perfModule.getAppStartInfo();
|
|
7503
|
-
if (!info || typeof info.durationMs !== "number")
|
|
7507
|
+
if (!info || typeof info.durationMs !== "number") {
|
|
7508
|
+
logger.warn("[perf.trace] AppLaunchCollector: getAppStartInfo returned no usable duration \u2014 no app_launch", {
|
|
7509
|
+
hasInfo: !!info,
|
|
7510
|
+
durationType: typeof info?.durationMs
|
|
7511
|
+
});
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7504
7514
|
this.collected = true;
|
|
7505
7515
|
const breakdown = {};
|
|
7506
7516
|
const phaseLabels = {};
|
|
@@ -7545,8 +7555,15 @@ var init_AppLaunchCollector = __esm({
|
|
|
7545
7555
|
breakdown: hasBreakdown ? breakdown : void 0,
|
|
7546
7556
|
phaseLabels: hasBreakdown ? phaseLabels : void 0
|
|
7547
7557
|
};
|
|
7558
|
+
logger.warn("[perf.trace] AppLaunchCollector: app_launch built, invoking handler", {
|
|
7559
|
+
durationMs: event.durationMs,
|
|
7560
|
+
launchType: event.launchType
|
|
7561
|
+
});
|
|
7548
7562
|
this.onLaunch(event);
|
|
7549
|
-
} catch {
|
|
7563
|
+
} catch (e) {
|
|
7564
|
+
logger.warn("[perf.trace] AppLaunchCollector: collect() threw", {
|
|
7565
|
+
message: e?.message
|
|
7566
|
+
});
|
|
7550
7567
|
}
|
|
7551
7568
|
}
|
|
7552
7569
|
};
|
|
@@ -8341,6 +8358,7 @@ var init_PerformanceFeature = __esm({
|
|
|
8341
8358
|
init_PerformanceTransport();
|
|
8342
8359
|
init_MetricsEngine();
|
|
8343
8360
|
init_SessionManager();
|
|
8361
|
+
init_internalLogger();
|
|
8344
8362
|
PerformanceFeature = class _PerformanceFeature {
|
|
8345
8363
|
name = "performance";
|
|
8346
8364
|
active = false;
|
|
@@ -8741,7 +8759,14 @@ var init_PerformanceFeature = __esm({
|
|
|
8741
8759
|
* flushes the backlog in the order the metrics were produced.
|
|
8742
8760
|
*/
|
|
8743
8761
|
_forwardMetricToBackend(metric) {
|
|
8744
|
-
const
|
|
8762
|
+
const sm = SessionManager.getExistingInstance();
|
|
8763
|
+
const backendTransport = sm?.getBackendTransport();
|
|
8764
|
+
logger.warn("[perf.trace] forwardMetricToBackend", {
|
|
8765
|
+
type: metric.type,
|
|
8766
|
+
hasSessionManager: !!sm,
|
|
8767
|
+
hasBackendTransport: !!backendTransport,
|
|
8768
|
+
buffered: this._pendingBackendMetrics.length
|
|
8769
|
+
});
|
|
8745
8770
|
if (backendTransport) {
|
|
8746
8771
|
this._drainPendingBackendMetrics(backendTransport);
|
|
8747
8772
|
backendTransport.queuePerformanceMetric(metric);
|
|
@@ -8761,6 +8786,9 @@ var init_PerformanceFeature = __esm({
|
|
|
8761
8786
|
for (const metric of buffered) {
|
|
8762
8787
|
backendTransport.queuePerformanceMetric(metric);
|
|
8763
8788
|
}
|
|
8789
|
+
logger.warn("[perf.trace] drained buffered metrics to backend", {
|
|
8790
|
+
count: buffered.length
|
|
8791
|
+
});
|
|
8764
8792
|
this._clearBackendDrainTimer();
|
|
8765
8793
|
this._backendDrainAttempts = 0;
|
|
8766
8794
|
}
|
|
@@ -8773,7 +8801,13 @@ var init_PerformanceFeature = __esm({
|
|
|
8773
8801
|
*/
|
|
8774
8802
|
_scheduleBackendDrain() {
|
|
8775
8803
|
if (this._backendDrainTimer) return;
|
|
8776
|
-
if (this._backendDrainAttempts >= _PerformanceFeature.BACKEND_DRAIN_MAX_ATTEMPTS)
|
|
8804
|
+
if (this._backendDrainAttempts >= _PerformanceFeature.BACKEND_DRAIN_MAX_ATTEMPTS) {
|
|
8805
|
+
logger.warn("[perf.trace] drain gave up \u2014 buffered metrics never reached the backend", {
|
|
8806
|
+
attempts: this._backendDrainAttempts,
|
|
8807
|
+
dropped: this._pendingBackendMetrics.length
|
|
8808
|
+
});
|
|
8809
|
+
return;
|
|
8810
|
+
}
|
|
8777
8811
|
const delays = _PerformanceFeature.BACKEND_DRAIN_DELAYS;
|
|
8778
8812
|
const delay = delays[Math.min(this._backendDrainAttempts, delays.length - 1)];
|
|
8779
8813
|
this._backendDrainAttempts++;
|
|
@@ -15749,12 +15783,23 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
|
|
|
15749
15783
|
this._triggerNativeFlush();
|
|
15750
15784
|
}
|
|
15751
15785
|
queuePerformanceMetric(data) {
|
|
15752
|
-
if (this.destroyed || !this.config.clientKey)
|
|
15786
|
+
if (this.destroyed || !this.config.clientKey) {
|
|
15787
|
+
logger.warn("[perf.queue] dropped \u2014 no clientKey or adapter destroyed", {
|
|
15788
|
+
type: data.type,
|
|
15789
|
+
destroyed: this.destroyed,
|
|
15790
|
+
hasClientKey: !!this.config.clientKey
|
|
15791
|
+
});
|
|
15792
|
+
return;
|
|
15793
|
+
}
|
|
15753
15794
|
this.pendingPerformanceMetrics.push({
|
|
15754
15795
|
...data,
|
|
15755
15796
|
clientId: this._clientId(),
|
|
15756
15797
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15757
15798
|
});
|
|
15799
|
+
logger.warn("[perf.trace] queue enqueued performance metric", {
|
|
15800
|
+
type: data.type,
|
|
15801
|
+
pending: this.pendingPerformanceMetrics.length
|
|
15802
|
+
});
|
|
15758
15803
|
if (this.pendingPerformanceMetrics.length >= 20) {
|
|
15759
15804
|
this._flushPerformance().catch(() => {
|
|
15760
15805
|
});
|
|
@@ -16321,19 +16366,43 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
|
|
|
16321
16366
|
}
|
|
16322
16367
|
}
|
|
16323
16368
|
async _flushPerformance() {
|
|
16324
|
-
if (this.pendingPerformanceMetrics.length === 0
|
|
16369
|
+
if (this.pendingPerformanceMetrics.length === 0) return;
|
|
16370
|
+
if (!this.currentSessionId || !this.config.clientKey) {
|
|
16371
|
+
logger.warn("[perf.trace] flush waiting \u2014 not deliverable yet", {
|
|
16372
|
+
pending: this.pendingPerformanceMetrics.length,
|
|
16373
|
+
hasSession: !!this.currentSessionId,
|
|
16374
|
+
hasClientKey: !!this.config.clientKey
|
|
16375
|
+
});
|
|
16376
|
+
return;
|
|
16377
|
+
}
|
|
16325
16378
|
const batch = this.pendingPerformanceMetrics.splice(0, 50);
|
|
16326
16379
|
try {
|
|
16327
|
-
if (await this._enqueueNative("performance", batch))
|
|
16380
|
+
if (await this._enqueueNative("performance", batch)) {
|
|
16381
|
+
logger.warn("[perf.trace] flush handed to native outbox", {
|
|
16382
|
+
count: batch.length
|
|
16383
|
+
});
|
|
16384
|
+
return;
|
|
16385
|
+
}
|
|
16328
16386
|
if (!this._sessionRowReady()) {
|
|
16387
|
+
logger.warn("[perf.trace] flush session row not ready \u2014 re-buffering", {
|
|
16388
|
+
count: batch.length
|
|
16389
|
+
});
|
|
16329
16390
|
this.pendingPerformanceMetrics.unshift(...batch);
|
|
16330
16391
|
return;
|
|
16331
16392
|
}
|
|
16332
16393
|
await this._post(ENDPOINTS.INGESTION_PERFORMANCE(this.currentSessionId), {
|
|
16333
16394
|
metrics: batch
|
|
16334
16395
|
});
|
|
16396
|
+
logger.warn("[perf.trace] flush POST performance metrics ok", {
|
|
16397
|
+
count: batch.length
|
|
16398
|
+
});
|
|
16335
16399
|
} catch (err) {
|
|
16336
|
-
|
|
16400
|
+
const permanent = _BackendSessionAdapter._isPermanentHttpError(err);
|
|
16401
|
+
logger.warn("[perf.flush] POST performance metrics failed", {
|
|
16402
|
+
count: batch.length,
|
|
16403
|
+
permanent
|
|
16404
|
+
});
|
|
16405
|
+
if (!permanent) this.pendingPerformanceMetrics.unshift(...batch);
|
|
16337
16406
|
}
|
|
16338
16407
|
}
|
|
16339
16408
|
async _flushLogs() {
|