@rejourneyco/react-native 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
- package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
- package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
- package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
- package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
- package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
- package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
- package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
- package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
- package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Engine/DeviceRegistrar.swift +288 -0
- package/ios/Engine/DiagnosticLog.swift +387 -0
- package/ios/Engine/RejourneyImpl.swift +719 -0
- package/ios/Recording/AnrSentinel.swift +142 -0
- package/ios/Recording/EventBuffer.swift +326 -0
- package/ios/Recording/InteractionRecorder.swift +428 -0
- package/ios/Recording/ReplayOrchestrator.swift +624 -0
- package/ios/Recording/SegmentDispatcher.swift +492 -0
- package/ios/Recording/StabilityMonitor.swift +223 -0
- package/ios/Recording/TelemetryPipeline.swift +547 -0
- package/ios/Recording/ViewHierarchyScanner.swift +156 -0
- package/ios/Recording/VisualCapture.swift +675 -0
- package/ios/Rejourney.h +38 -0
- package/ios/Rejourney.mm +375 -0
- package/ios/Utility/DataCompression.swift +55 -0
- package/ios/Utility/ImageBlur.swift +89 -0
- package/ios/Utility/RuntimeMethodSwap.swift +41 -0
- package/ios/Utility/ViewIdentifier.swift +37 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +88 -0
- package/lib/commonjs/index.js +1443 -0
- package/lib/commonjs/sdk/autoTracking.js +1087 -0
- package/lib/commonjs/sdk/constants.js +166 -0
- package/lib/commonjs/sdk/errorTracking.js +187 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +205 -0
- package/lib/commonjs/sdk/navigation.js +128 -0
- package/lib/commonjs/sdk/networkInterceptor.js +375 -0
- package/lib/commonjs/sdk/utils.js +433 -0
- package/lib/commonjs/sdk/version.js +13 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +83 -0
- package/lib/module/index.js +1341 -0
- package/lib/module/sdk/autoTracking.js +1059 -0
- package/lib/module/sdk/constants.js +154 -0
- package/lib/module/sdk/errorTracking.js +177 -0
- package/lib/module/sdk/index.js +26 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +120 -0
- package/lib/module/sdk/networkInterceptor.js +364 -0
- package/lib/module/sdk/utils.js +412 -0
- package/lib/module/sdk/version.js +7 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +160 -0
- package/lib/typescript/components/Mask.d.ts +54 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +226 -0
- package/lib/typescript/sdk/constants.d.ts +138 -0
- package/lib/typescript/sdk/errorTracking.d.ts +47 -0
- package/lib/typescript/sdk/index.d.ts +24 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
- package/lib/typescript/sdk/navigation.d.ts +48 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
- package/lib/typescript/sdk/utils.d.ts +193 -0
- package/lib/typescript/sdk/version.d.ts +6 -0
- package/lib/typescript/types/index.d.ts +618 -0
- package/package.json +122 -0
- package/rejourney.podspec +23 -0
- package/src/NativeRejourney.ts +185 -0
- package/src/components/Mask.tsx +93 -0
- package/src/index.ts +1555 -0
- package/src/sdk/autoTracking.ts +1245 -0
- package/src/sdk/constants.ts +155 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +25 -0
- package/src/sdk/metricsTracking.ts +227 -0
- package/src/sdk/navigation.ts +152 -0
- package/src/sdk/networkInterceptor.ts +423 -0
- package/src/sdk/utils.ts +442 -0
- package/src/sdk/version.ts +6 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +709 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Rejourney
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Performance timing utility for profiling SDK operations.
|
|
19
|
+
* All logging is runtime-gated by debug mode via DiagnosticLog.
|
|
20
|
+
*
|
|
21
|
+
* ANDROID-SPECIFIC: Uses Android Debug APIs for memory profiling
|
|
22
|
+
* that have no iOS equivalent (iOS uses different memory APIs).
|
|
23
|
+
*/
|
|
24
|
+
package com.rejourney.platform
|
|
25
|
+
|
|
26
|
+
import android.os.Debug
|
|
27
|
+
import android.os.Looper
|
|
28
|
+
import android.os.SystemClock
|
|
29
|
+
import com.rejourney.engine.DiagnosticLog
|
|
30
|
+
import java.io.File
|
|
31
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
32
|
+
import kotlin.concurrent.withLock
|
|
33
|
+
import kotlin.math.max
|
|
34
|
+
import kotlin.math.min
|
|
35
|
+
|
|
36
|
+
enum class PerfMetric {
|
|
37
|
+
FRAME,
|
|
38
|
+
SCREENSHOT,
|
|
39
|
+
RENDER,
|
|
40
|
+
PRIVACY_MASK,
|
|
41
|
+
VIEW_SCAN,
|
|
42
|
+
VIEW_SERIALIZE,
|
|
43
|
+
ENCODE,
|
|
44
|
+
PIXEL_BUFFER,
|
|
45
|
+
DOWNSCALE,
|
|
46
|
+
BUFFER_ALLOC,
|
|
47
|
+
ENCODE_APPEND,
|
|
48
|
+
UPLOAD
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
data class PerfFrameContext(
|
|
52
|
+
val reason: String = "unknown",
|
|
53
|
+
val shouldRender: Boolean = false,
|
|
54
|
+
val totalViewsScanned: Int = 0,
|
|
55
|
+
val sensitiveRects: Int = 0,
|
|
56
|
+
val didBailOut: Boolean = false,
|
|
57
|
+
val bailOutReason: String = "none",
|
|
58
|
+
val hasBlockedSurface: Boolean = false,
|
|
59
|
+
val performanceLevel: String = "normal",
|
|
60
|
+
val isWarmup: Boolean = false
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
object PerfTiming {
|
|
64
|
+
private const val PERF_ENABLED = true
|
|
65
|
+
private const val DUMP_INTERVAL_MS = 5000.0
|
|
66
|
+
private const val MIN_SAMPLES = 5L
|
|
67
|
+
private const val SAMPLE_CAPACITY = 256
|
|
68
|
+
|
|
69
|
+
private val names = arrayOf(
|
|
70
|
+
"frame_total",
|
|
71
|
+
"screenshot_ui",
|
|
72
|
+
"render_draw",
|
|
73
|
+
"privacy_mask",
|
|
74
|
+
"view_scan",
|
|
75
|
+
"view_serialize",
|
|
76
|
+
"encode_h264",
|
|
77
|
+
"pixel_buffer",
|
|
78
|
+
"downscale",
|
|
79
|
+
"buffer_alloc",
|
|
80
|
+
"encode_append",
|
|
81
|
+
"upload_net"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
private val outlierThresholdMs = doubleArrayOf(
|
|
85
|
+
80.0, // frame_total
|
|
86
|
+
60.0, // screenshot_ui
|
|
87
|
+
50.0, // render_draw
|
|
88
|
+
8.0, // privacy_mask
|
|
89
|
+
12.0, // view_scan
|
|
90
|
+
8.0, // view_serialize
|
|
91
|
+
20.0, // encode_h264
|
|
92
|
+
8.0, // pixel_buffer
|
|
93
|
+
16.0, // downscale
|
|
94
|
+
6.0, // buffer_alloc
|
|
95
|
+
12.0, // encode_append
|
|
96
|
+
120.0 // upload_net
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
private val totals = DoubleArray(PerfMetric.values().size)
|
|
100
|
+
private val maxes = DoubleArray(PerfMetric.values().size)
|
|
101
|
+
private val counts = LongArray(PerfMetric.values().size)
|
|
102
|
+
private val warmupTotals = DoubleArray(PerfMetric.values().size)
|
|
103
|
+
private val warmupMaxes = DoubleArray(PerfMetric.values().size)
|
|
104
|
+
private val warmupCounts = LongArray(PerfMetric.values().size)
|
|
105
|
+
private val peakMemMbByMetric = DoubleArray(PerfMetric.values().size)
|
|
106
|
+
private val samples = Array(PerfMetric.values().size) { DoubleArray(SAMPLE_CAPACITY) }
|
|
107
|
+
private var processPeakRssMb = 0.0
|
|
108
|
+
private var processPeakHeapMb = 0.0
|
|
109
|
+
private var processPeakPssMb = 0.0
|
|
110
|
+
private var processPeakNativeHeapMb = 0.0
|
|
111
|
+
private var lastDumpTimeNs = 0L
|
|
112
|
+
|
|
113
|
+
@Volatile
|
|
114
|
+
private var frameContext = PerfFrameContext()
|
|
115
|
+
|
|
116
|
+
private val lock = ReentrantLock()
|
|
117
|
+
|
|
118
|
+
fun isEnabled(): Boolean = PERF_ENABLED && DiagnosticLog.detailedOutput
|
|
119
|
+
|
|
120
|
+
fun now(): Long = SystemClock.elapsedRealtimeNanos()
|
|
121
|
+
|
|
122
|
+
fun setFrameContext(context: PerfFrameContext) {
|
|
123
|
+
frameContext = context
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun record(metric: PerfMetric, startNs: Long, endNs: Long) {
|
|
127
|
+
if (!isEnabled()) return
|
|
128
|
+
|
|
129
|
+
val durationMs = (endNs - startNs).toDouble() / 1_000_000.0
|
|
130
|
+
val isMain = Looper.getMainLooper().thread == Thread.currentThread()
|
|
131
|
+
val threadName = if (isMain) "MAIN" else "BG"
|
|
132
|
+
val name = names[metric.ordinal]
|
|
133
|
+
val memory = memorySnapshotMb()
|
|
134
|
+
|
|
135
|
+
if (isMain && durationMs > 4.0) {
|
|
136
|
+
DiagnosticLog.caution(
|
|
137
|
+
"[RJ-PERF] ⚠️ [$threadName]${if (frameContext.isWarmup) "[WARMUP]" else ""} $name: ${"%.2f".format(durationMs)}ms"
|
|
138
|
+
)
|
|
139
|
+
} else {
|
|
140
|
+
DiagnosticLog.trace(
|
|
141
|
+
"[RJ-PERF] [$threadName]${if (frameContext.isWarmup) "[WARMUP]" else ""} $name: ${"%.2f".format(durationMs)}ms"
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lock.withLock {
|
|
146
|
+
val idx = metric.ordinal
|
|
147
|
+
val isWarmupSample = frameContext.isWarmup
|
|
148
|
+
if (isWarmupSample) {
|
|
149
|
+
val warmCount = warmupCounts[idx]
|
|
150
|
+
warmupCounts[idx] = warmCount + 1
|
|
151
|
+
warmupTotals[idx] += durationMs
|
|
152
|
+
warmupMaxes[idx] = max(warmupMaxes[idx], durationMs)
|
|
153
|
+
} else {
|
|
154
|
+
val count = counts[idx]
|
|
155
|
+
counts[idx] = count + 1
|
|
156
|
+
totals[idx] += durationMs
|
|
157
|
+
maxes[idx] = max(maxes[idx], durationMs)
|
|
158
|
+
|
|
159
|
+
val sampleSlot = (count % SAMPLE_CAPACITY).toInt()
|
|
160
|
+
samples[idx][sampleSlot] = durationMs
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
peakMemMbByMetric[idx] = max(peakMemMbByMetric[idx], memory.rssMb)
|
|
164
|
+
processPeakRssMb = max(processPeakRssMb, memory.rssMb)
|
|
165
|
+
processPeakHeapMb = max(processPeakHeapMb, memory.heapMb)
|
|
166
|
+
processPeakPssMb = max(processPeakPssMb, memory.pssMb)
|
|
167
|
+
processPeakNativeHeapMb = max(processPeakNativeHeapMb, memory.nativeHeapMb)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (durationMs >= outlierThresholdMs[metric.ordinal]) {
|
|
171
|
+
val ctx = frameContext
|
|
172
|
+
DiagnosticLog.caution(
|
|
173
|
+
"[RJ-PERF-OUTLIER]${if (ctx.isWarmup) "[WARMUP]" else ""} " +
|
|
174
|
+
"$name=${"%.2f".format(durationMs)}ms " +
|
|
175
|
+
"(reason=${ctx.reason}, render=${ctx.shouldRender}, views=${ctx.totalViewsScanned}, " +
|
|
176
|
+
"sensitive=${ctx.sensitiveRects}, bailout=${ctx.didBailOut}/${ctx.bailOutReason}, blocked=${ctx.hasBlockedSurface}, " +
|
|
177
|
+
"perf=${ctx.performanceLevel}, rss=${"%.1f".format(memory.rssMb)}MB, " +
|
|
178
|
+
"heap=${"%.1f".format(memory.heapMb)}MB, pss=${"%.1f".format(memory.pssMb)}MB, " +
|
|
179
|
+
"native=${"%.1f".format(memory.nativeHeapMb)}MB)"
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fun dumpIfNeeded() {
|
|
185
|
+
if (!isEnabled()) return
|
|
186
|
+
|
|
187
|
+
val now = now()
|
|
188
|
+
if (lastDumpTimeNs != 0L && msBetween(lastDumpTimeNs, now) < DUMP_INTERVAL_MS) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lock.withLock {
|
|
193
|
+
if (lastDumpTimeNs != 0L && msBetween(lastDumpTimeNs, now) < DUMP_INTERVAL_MS) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var totalSamples = 0L
|
|
198
|
+
var totalWarmupSamples = 0L
|
|
199
|
+
counts.forEach { totalSamples += it }
|
|
200
|
+
warmupCounts.forEach { totalWarmupSamples += it }
|
|
201
|
+
if (totalSamples < MIN_SAMPLES && totalWarmupSamples < MIN_SAMPLES) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
lastDumpTimeNs = now
|
|
206
|
+
|
|
207
|
+
val log = StringBuilder("[Rejourney PERF SUMMARY]")
|
|
208
|
+
for (i in counts.indices) {
|
|
209
|
+
if (counts[i] > 0) {
|
|
210
|
+
val avg = totals[i] / counts[i].toDouble()
|
|
211
|
+
val p95 = percentileForMetricLocked(i, 95.0)
|
|
212
|
+
log.append(" ${names[i]}=${counts[i]}/${"%.1f".format(avg)}/${"%.1f".format(p95)}/${"%.1f".format(maxes[i])}ms")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (totalWarmupSamples > 0) {
|
|
216
|
+
log.append(" warmup(")
|
|
217
|
+
var hasWarmupMetric = false
|
|
218
|
+
for (i in warmupCounts.indices) {
|
|
219
|
+
if (warmupCounts[i] == 0L) continue
|
|
220
|
+
if (hasWarmupMetric) log.append(" ")
|
|
221
|
+
hasWarmupMetric = true
|
|
222
|
+
val avg = warmupTotals[i] / warmupCounts[i].toDouble()
|
|
223
|
+
log.append("${names[i]}=${warmupCounts[i]}/${"%.1f".format(avg)}/${"%.1f".format(warmupMaxes[i])}ms")
|
|
224
|
+
}
|
|
225
|
+
log.append(")")
|
|
226
|
+
}
|
|
227
|
+
log.append(" mem_peak(rss/heap/pss/native)=${"%.1f/%.1f/%.1f/%.1fMB".format(processPeakRssMb, processPeakHeapMb, processPeakPssMb, processPeakNativeHeapMb)}")
|
|
228
|
+
|
|
229
|
+
val currentMemory = memorySnapshotMb()
|
|
230
|
+
val sys = systemSnapshot()
|
|
231
|
+
log.append(" mem_now(rss/heap/pss/native)=${"%.1f/%.1f/%.1f/%.1fMB".format(currentMemory.rssMb, currentMemory.heapMb, currentMemory.pssMb, currentMemory.nativeHeapMb)}")
|
|
232
|
+
log.append(" sys(threads/fds)=${sys.threadCount}/${sys.openFdCount}")
|
|
233
|
+
|
|
234
|
+
DiagnosticLog.trace(log.toString())
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fun reset() {
|
|
239
|
+
if (!PERF_ENABLED) return
|
|
240
|
+
|
|
241
|
+
lock.withLock {
|
|
242
|
+
for (i in counts.indices) {
|
|
243
|
+
totals[i] = 0.0
|
|
244
|
+
maxes[i] = 0.0
|
|
245
|
+
counts[i] = 0
|
|
246
|
+
warmupTotals[i] = 0.0
|
|
247
|
+
warmupMaxes[i] = 0.0
|
|
248
|
+
warmupCounts[i] = 0
|
|
249
|
+
peakMemMbByMetric[i] = 0.0
|
|
250
|
+
for (j in 0 until SAMPLE_CAPACITY) {
|
|
251
|
+
samples[i][j] = 0.0
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
processPeakRssMb = 0.0
|
|
255
|
+
processPeakHeapMb = 0.0
|
|
256
|
+
processPeakPssMb = 0.0
|
|
257
|
+
processPeakNativeHeapMb = 0.0
|
|
258
|
+
lastDumpTimeNs = 0L
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (DiagnosticLog.detailedOutput) {
|
|
262
|
+
DiagnosticLog.trace("[Rejourney PERF] Metrics reset")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fun snapshot(): Map<String, Map<String, Number>> {
|
|
267
|
+
if (!PERF_ENABLED) return emptyMap()
|
|
268
|
+
|
|
269
|
+
return lock.withLock {
|
|
270
|
+
val result = mutableMapOf<String, Map<String, Number>>()
|
|
271
|
+
for (i in counts.indices) {
|
|
272
|
+
if (counts[i] > 0) {
|
|
273
|
+
val avg = totals[i] / counts[i].toDouble()
|
|
274
|
+
result[names[i]] = mapOf(
|
|
275
|
+
"count" to counts[i],
|
|
276
|
+
"avg_ms" to avg,
|
|
277
|
+
"p95_ms" to percentileForMetricLocked(i, 95.0),
|
|
278
|
+
"p99_ms" to percentileForMetricLocked(i, 99.0),
|
|
279
|
+
"max_ms" to maxes[i],
|
|
280
|
+
"peak_rss_mb" to peakMemMbByMetric[i],
|
|
281
|
+
"peak_native_heap_mb" to processPeakNativeHeapMb
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
result
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fun nameForMetric(metric: PerfMetric): String = names.getOrElse(metric.ordinal) { "unknown" }
|
|
290
|
+
|
|
291
|
+
fun <T> time(metric: PerfMetric, block: () -> T): T {
|
|
292
|
+
if (!isEnabled()) return block()
|
|
293
|
+
val start = now()
|
|
294
|
+
return try {
|
|
295
|
+
block()
|
|
296
|
+
} finally {
|
|
297
|
+
record(metric, start, now())
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fun <T> measure(label: String, block: () -> T): T {
|
|
302
|
+
if (!isEnabled()) return block()
|
|
303
|
+
val start = now()
|
|
304
|
+
return try {
|
|
305
|
+
block()
|
|
306
|
+
} finally {
|
|
307
|
+
val end = now()
|
|
308
|
+
val durationUs = (end - start) / 1000
|
|
309
|
+
val durationMs = durationUs / 1000.0
|
|
310
|
+
DiagnosticLog.trace("[PerfTiming] $label: ${durationUs}us (${"%.3f".format(durationMs)}ms)")
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private fun percentileForMetricLocked(metricIndex: Int, percentile: Double): Double {
|
|
315
|
+
val count = counts[metricIndex]
|
|
316
|
+
if (count <= 0) return 0.0
|
|
317
|
+
|
|
318
|
+
val sampleCount = min(count.toInt(), SAMPLE_CAPACITY)
|
|
319
|
+
val bucket = samples[metricIndex]
|
|
320
|
+
val values = DoubleArray(sampleCount)
|
|
321
|
+
|
|
322
|
+
val latestCount = count
|
|
323
|
+
if (latestCount <= SAMPLE_CAPACITY) {
|
|
324
|
+
for (i in 0 until sampleCount) {
|
|
325
|
+
values[i] = bucket[i]
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
val start = (latestCount % SAMPLE_CAPACITY).toInt()
|
|
329
|
+
for (i in 0 until sampleCount) {
|
|
330
|
+
values[i] = bucket[(start + i) % SAMPLE_CAPACITY]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
values.sort()
|
|
335
|
+
val rank = ((percentile / 100.0) * (sampleCount - 1)).toInt().coerceIn(0, sampleCount - 1)
|
|
336
|
+
return values[rank]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun msBetween(startNs: Long, endNs: Long): Double = (endNs - startNs).toDouble() / 1_000_000.0
|
|
340
|
+
|
|
341
|
+
private data class MemorySnapshot(
|
|
342
|
+
val rssMb: Double,
|
|
343
|
+
val heapMb: Double,
|
|
344
|
+
val pssMb: Double,
|
|
345
|
+
val nativeHeapMb: Double
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
private data class SystemSnapshot(
|
|
349
|
+
val threadCount: Int,
|
|
350
|
+
val openFdCount: Int
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
private fun memorySnapshotMb(): MemorySnapshot {
|
|
354
|
+
val runtime = Runtime.getRuntime()
|
|
355
|
+
val usedHeapMb = (runtime.totalMemory() - runtime.freeMemory()).toDouble() / (1024.0 * 1024.0)
|
|
356
|
+
val nativeHeapMb = Debug.getNativeHeapAllocatedSize().toDouble() / (1024.0 * 1024.0)
|
|
357
|
+
|
|
358
|
+
return try {
|
|
359
|
+
val memoryInfo = Debug.MemoryInfo()
|
|
360
|
+
Debug.getMemoryInfo(memoryInfo)
|
|
361
|
+
val pssMb = memoryInfo.totalPss.toDouble() / 1024.0
|
|
362
|
+
val rssMb = memoryInfo.totalPrivateDirty.toDouble() / 1024.0
|
|
363
|
+
MemorySnapshot(rssMb = rssMb, heapMb = usedHeapMb, pssMb = pssMb, nativeHeapMb = nativeHeapMb)
|
|
364
|
+
} catch (_: Exception) {
|
|
365
|
+
MemorySnapshot(rssMb = usedHeapMb, heapMb = usedHeapMb, pssMb = usedHeapMb, nativeHeapMb = nativeHeapMb)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun systemSnapshot(): SystemSnapshot {
|
|
370
|
+
val threads = try {
|
|
371
|
+
Thread.getAllStackTraces().size
|
|
372
|
+
} catch (_: Exception) {
|
|
373
|
+
-1
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
val openFdCount = try {
|
|
377
|
+
File("/proc/self/fd").list()?.size ?: -1
|
|
378
|
+
} catch (_: Exception) {
|
|
379
|
+
-1
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return SystemSnapshot(threadCount = threads, openFdCount = openFdCount)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Rejourney
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Service to detect when the app is swiped away from recent apps or killed.
|
|
19
|
+
*
|
|
20
|
+
* This service uses onTaskRemoved() callback which is called when the user
|
|
21
|
+
* swipes the app away from the recent apps screen. By setting stopWithTask="false"
|
|
22
|
+
* in the manifest, this callback will fire even when the app is killed.
|
|
23
|
+
*
|
|
24
|
+
* IMPORTANT: This is not 100% reliable across all Android versions and OEMs,
|
|
25
|
+
* but it's the best available mechanism for detecting app termination.
|
|
26
|
+
*
|
|
27
|
+
* ANDROID-SPECIFIC: iOS handles app termination through applicationWillTerminate
|
|
28
|
+
* and doesn't need this service-based approach.
|
|
29
|
+
*/
|
|
30
|
+
package com.rejourney.platform
|
|
31
|
+
|
|
32
|
+
import android.app.Service
|
|
33
|
+
import android.content.Intent
|
|
34
|
+
import android.os.IBinder
|
|
35
|
+
import android.os.SystemClock
|
|
36
|
+
import com.rejourney.engine.DiagnosticLog
|
|
37
|
+
import kotlinx.coroutines.*
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Callback interface for notifying when app is being killed.
|
|
41
|
+
* Defined outside the class for better accessibility.
|
|
42
|
+
*
|
|
43
|
+
* CRITICAL: This callback is invoked SYNCHRONOUSLY on the main thread.
|
|
44
|
+
* The implementation should complete session end as fast as possible
|
|
45
|
+
* since the process may be killed immediately after this returns.
|
|
46
|
+
*/
|
|
47
|
+
interface TaskRemovedListener {
|
|
48
|
+
/**
|
|
49
|
+
* Called when the app is being killed/swiped away.
|
|
50
|
+
* Implementation should be as fast as possible and use runBlocking
|
|
51
|
+
* to ensure critical operations complete before process death.
|
|
52
|
+
*/
|
|
53
|
+
fun onTaskRemoved()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Service that detects app termination via onTaskRemoved() callback.
|
|
58
|
+
*
|
|
59
|
+
* This service must be registered in AndroidManifest.xml with:
|
|
60
|
+
* - android:stopWithTask="false" (critical - allows onTaskRemoved to fire)
|
|
61
|
+
* - android:exported="false" (security - don't allow external apps to start it)
|
|
62
|
+
*/
|
|
63
|
+
class SessionLifecycleService : Service() {
|
|
64
|
+
|
|
65
|
+
companion object {
|
|
66
|
+
private const val TAG = "SessionLifecycleService"
|
|
67
|
+
|
|
68
|
+
@Volatile
|
|
69
|
+
var taskRemovedListener: TaskRemovedListener? = null
|
|
70
|
+
|
|
71
|
+
@Volatile
|
|
72
|
+
var isRunning = false
|
|
73
|
+
private set
|
|
74
|
+
|
|
75
|
+
private const val MIN_TIME_BEFORE_TASK_REMOVED_MS = 2000L
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
79
|
+
|
|
80
|
+
private var serviceStartTime: Long = 0
|
|
81
|
+
|
|
82
|
+
override fun onCreate() {
|
|
83
|
+
super.onCreate()
|
|
84
|
+
isRunning = true
|
|
85
|
+
serviceStartTime = SystemClock.elapsedRealtime()
|
|
86
|
+
DiagnosticLog.trace("[$TAG] Service created (OEM: ${OEMDetector.getOEM()})")
|
|
87
|
+
DiagnosticLog.trace("[$TAG] ${OEMDetector.getRecommendations()}")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
91
|
+
serviceStartTime = SystemClock.elapsedRealtime()
|
|
92
|
+
DiagnosticLog.trace("[$TAG] Service started (startId=$startId, OEM: ${OEMDetector.getOEM()})")
|
|
93
|
+
return START_STICKY
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Called when the user removes a task from the recent apps list.
|
|
98
|
+
*
|
|
99
|
+
* This is the key callback for detecting app swipe-away/kill events.
|
|
100
|
+
* It fires when:
|
|
101
|
+
* - User swipes the app away from recent apps
|
|
102
|
+
* - User taps "Clear all" in recent apps (on some devices)
|
|
103
|
+
*
|
|
104
|
+
* NOTE: This may NOT fire in all cases:
|
|
105
|
+
* - Some OEMs suppress this callback
|
|
106
|
+
* - "Clear all" on some devices may not trigger it
|
|
107
|
+
* - System-initiated kills (low memory) may not trigger it
|
|
108
|
+
* - Samsung devices have a bug where this fires on app launch (we filter this)
|
|
109
|
+
*
|
|
110
|
+
* That's why we also use ApplicationExitInfo (Android 11+) and
|
|
111
|
+
* persistent state checking on next app launch as fallbacks.
|
|
112
|
+
*/
|
|
113
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
114
|
+
val timeSinceStart = SystemClock.elapsedRealtime() - serviceStartTime
|
|
115
|
+
val oem = OEMDetector.getOEM()
|
|
116
|
+
|
|
117
|
+
DiagnosticLog.notice("[$TAG] ⚠️ onTaskRemoved() called (OEM: $oem, timeSinceStart: ${timeSinceStart}ms)")
|
|
118
|
+
|
|
119
|
+
// Samsung bug: onTaskRemoved fires incorrectly on app launch
|
|
120
|
+
if (OEMDetector.isSamsung() && timeSinceStart < MIN_TIME_BEFORE_TASK_REMOVED_MS) {
|
|
121
|
+
DiagnosticLog.caution("[$TAG] ⚠️ Ignoring onTaskRemoved() - likely Samsung false positive (fired ${timeSinceStart}ms after start, expected > ${MIN_TIME_BEFORE_TASK_REMOVED_MS}ms)")
|
|
122
|
+
DiagnosticLog.caution("[$TAG] This is a known Samsung bug where onTaskRemoved fires on app launch")
|
|
123
|
+
super.onTaskRemoved(rootIntent)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (OEMDetector.hasAggressiveTaskKilling()) {
|
|
128
|
+
DiagnosticLog.trace("[$TAG] OEM has aggressive task killing - onTaskRemoved may be unreliable")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
DiagnosticLog.notice("[$TAG] ✅ Valid onTaskRemoved() - app is being killed/swiped away")
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
DiagnosticLog.trace("[$TAG] Calling listener synchronously...")
|
|
135
|
+
taskRemovedListener?.onTaskRemoved()
|
|
136
|
+
DiagnosticLog.trace("[$TAG] Task removed listener completed")
|
|
137
|
+
} catch (e: Exception) {
|
|
138
|
+
DiagnosticLog.fault("[$TAG] Error notifying task removed listener: ${e.message}")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
stopSelf()
|
|
143
|
+
} catch (e: Exception) {
|
|
144
|
+
DiagnosticLog.caution("[$TAG] Error stopping service: ${e.message}")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
super.onTaskRemoved(rootIntent)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun onBind(intent: Intent?): IBinder? {
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
override fun onDestroy() {
|
|
155
|
+
isRunning = false
|
|
156
|
+
serviceScope.cancel()
|
|
157
|
+
DiagnosticLog.trace("[$TAG] Service destroyed")
|
|
158
|
+
super.onDestroy()
|
|
159
|
+
}
|
|
160
|
+
}
|