@scalebun/react-native 1.2.1 → 1.2.2-beta.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/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 +39 -4
- package/dist/scalebun.full.js.map +1 -1
- package/dist/scalebun.slim.js +39 -4
- package/dist/scalebun.slim.js.map +1 -1
- package/lib/commonjs/features/session/BackendSessionAdapter.js +39 -4
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/features/session/BackendSessionAdapter.js +39 -4
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/session/BackendSessionAdapter.ts +30 -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
|
@@ -15749,12 +15749,23 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
|
|
|
15749
15749
|
this._triggerNativeFlush();
|
|
15750
15750
|
}
|
|
15751
15751
|
queuePerformanceMetric(data) {
|
|
15752
|
-
if (this.destroyed || !this.config.clientKey)
|
|
15752
|
+
if (this.destroyed || !this.config.clientKey) {
|
|
15753
|
+
logger.warn("[perf.queue] dropped \u2014 no clientKey or adapter destroyed", {
|
|
15754
|
+
type: data.type,
|
|
15755
|
+
destroyed: this.destroyed,
|
|
15756
|
+
hasClientKey: !!this.config.clientKey
|
|
15757
|
+
});
|
|
15758
|
+
return;
|
|
15759
|
+
}
|
|
15753
15760
|
this.pendingPerformanceMetrics.push({
|
|
15754
15761
|
...data,
|
|
15755
15762
|
clientId: this._clientId(),
|
|
15756
15763
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15757
15764
|
});
|
|
15765
|
+
logger.debug("[perf.queue] enqueued performance metric", {
|
|
15766
|
+
type: data.type,
|
|
15767
|
+
pending: this.pendingPerformanceMetrics.length
|
|
15768
|
+
});
|
|
15758
15769
|
if (this.pendingPerformanceMetrics.length >= 20) {
|
|
15759
15770
|
this._flushPerformance().catch(() => {
|
|
15760
15771
|
});
|
|
@@ -16321,19 +16332,43 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
|
|
|
16321
16332
|
}
|
|
16322
16333
|
}
|
|
16323
16334
|
async _flushPerformance() {
|
|
16324
|
-
if (this.pendingPerformanceMetrics.length === 0
|
|
16335
|
+
if (this.pendingPerformanceMetrics.length === 0) return;
|
|
16336
|
+
if (!this.currentSessionId || !this.config.clientKey) {
|
|
16337
|
+
logger.debug("[perf.flush] waiting \u2014 not deliverable yet", {
|
|
16338
|
+
pending: this.pendingPerformanceMetrics.length,
|
|
16339
|
+
hasSession: !!this.currentSessionId,
|
|
16340
|
+
hasClientKey: !!this.config.clientKey
|
|
16341
|
+
});
|
|
16342
|
+
return;
|
|
16343
|
+
}
|
|
16325
16344
|
const batch = this.pendingPerformanceMetrics.splice(0, 50);
|
|
16326
16345
|
try {
|
|
16327
|
-
if (await this._enqueueNative("performance", batch))
|
|
16346
|
+
if (await this._enqueueNative("performance", batch)) {
|
|
16347
|
+
logger.debug("[perf.flush] handed to native outbox", {
|
|
16348
|
+
count: batch.length
|
|
16349
|
+
});
|
|
16350
|
+
return;
|
|
16351
|
+
}
|
|
16328
16352
|
if (!this._sessionRowReady()) {
|
|
16353
|
+
logger.debug("[perf.flush] session row not ready \u2014 re-buffering", {
|
|
16354
|
+
count: batch.length
|
|
16355
|
+
});
|
|
16329
16356
|
this.pendingPerformanceMetrics.unshift(...batch);
|
|
16330
16357
|
return;
|
|
16331
16358
|
}
|
|
16332
16359
|
await this._post(ENDPOINTS.INGESTION_PERFORMANCE(this.currentSessionId), {
|
|
16333
16360
|
metrics: batch
|
|
16334
16361
|
});
|
|
16362
|
+
logger.info("[perf.flush] POST performance metrics ok", {
|
|
16363
|
+
count: batch.length
|
|
16364
|
+
});
|
|
16335
16365
|
} catch (err) {
|
|
16336
|
-
|
|
16366
|
+
const permanent = _BackendSessionAdapter._isPermanentHttpError(err);
|
|
16367
|
+
logger.warn("[perf.flush] POST performance metrics failed", {
|
|
16368
|
+
count: batch.length,
|
|
16369
|
+
permanent
|
|
16370
|
+
});
|
|
16371
|
+
if (!permanent) this.pendingPerformanceMetrics.unshift(...batch);
|
|
16337
16372
|
}
|
|
16338
16373
|
}
|
|
16339
16374
|
async _flushLogs() {
|