catalyst-core 0.2.0-beta.2 → 0.2.0-beta.3
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/native/androidProject/app/build.gradle.kts +1 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/bridge/WebBridge.js +2 -2
- package/dist/native/bridge/constants/NativeInterfaces.js +2 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +22 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +43 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +47 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +38 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +53 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +7 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +27 -0
- package/dist/native/bridge/perf/core/constants.js +43 -0
- package/dist/native/bridge/perf/core/measure.js +40 -0
- package/dist/native/bridge/perf/core/session.js +34 -0
- package/dist/native/bridge/perf/core/store.js +1 -0
- package/dist/native/bridge/perf/core/summary.js +1 -0
- package/dist/native/bridge/perf/core/traceExport.js +1 -0
- package/dist/native/bridge/perf/core/waterfalls.js +1 -0
- package/dist/native/bridge/perf/index.js +81 -0
- package/dist/native/buildIos/config.js +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- package/package.json +1 -1
|
@@ -110,6 +110,7 @@ android {
|
|
|
110
110
|
applicationIdSuffix = ".debug"
|
|
111
111
|
versionNameSuffix = "-debug"
|
|
112
112
|
isMinifyEnabled = false
|
|
113
|
+
enableUnitTestCoverage = true
|
|
113
114
|
buildConfigField("Boolean", "ALLOW_MIXED_CONTENT", "true")
|
|
114
115
|
buildConfigField("String", "LOCAL_IP", "\"${getLocalIpAddress()}\"")
|
|
115
116
|
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -7,18 +7,26 @@ import android.graphics.Bitmap
|
|
|
7
7
|
import android.net.Uri
|
|
8
8
|
import android.os.Bundle
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import android.view.GestureDetector
|
|
11
|
+
import android.view.MotionEvent
|
|
10
12
|
import android.view.View
|
|
11
13
|
import android.webkit.*
|
|
12
14
|
import androidx.webkit.ServiceWorkerClientCompat
|
|
13
15
|
import androidx.webkit.ServiceWorkerControllerCompat
|
|
16
|
+
import androidx.webkit.WebViewCompat
|
|
17
|
+
import androidx.webkit.WebViewFeature
|
|
14
18
|
import androidx.webkit.WebResourceRequestCompat
|
|
15
19
|
import android.widget.ProgressBar
|
|
16
20
|
import androidx.webkit.WebViewAssetLoader
|
|
17
21
|
import io.yourname.androidproject.WebCacheManager
|
|
18
22
|
import io.yourname.androidproject.URLWhitelistManager
|
|
19
23
|
import io.yourname.androidproject.matchesCachePattern
|
|
24
|
+
import io.yourname.androidproject.utils.BridgeUtils
|
|
20
25
|
import io.yourname.androidproject.utils.CameraUtils
|
|
21
26
|
import io.yourname.androidproject.utils.NetworkUtils
|
|
27
|
+
import io.yourname.androidproject.utils.PerfEventBuffer
|
|
28
|
+
import io.yourname.androidproject.utils.ProfilerNavigationState
|
|
29
|
+
import org.json.JSONObject
|
|
22
30
|
import kotlinx.coroutines.*
|
|
23
31
|
import java.io.FileNotFoundException
|
|
24
32
|
import java.util.Properties
|
|
@@ -45,11 +53,15 @@ class CustomWebView(
|
|
|
45
53
|
private var isInitialApiCalled: Boolean = false // Flag to track initial page load
|
|
46
54
|
private var isInitialPageLoaded: Boolean = false
|
|
47
55
|
private var buildOptimisation: Boolean = false // Added property for build optimization
|
|
56
|
+
private var profilerEnabled: Boolean = false
|
|
57
|
+
private val profilerNavigationState = ProfilerNavigationState()
|
|
48
58
|
private lateinit var assetLoader: WebViewAssetLoader
|
|
49
59
|
private var allowedUrls: List<String> = emptyList()
|
|
50
60
|
private var accessControlEnabled: Boolean = false
|
|
51
61
|
private val offlineAssetPath = "offline/offline.html"
|
|
52
62
|
private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
|
|
63
|
+
private var scrollSessionOpen = false
|
|
64
|
+
private lateinit var scrollGestureDetector: GestureDetector
|
|
53
65
|
private var offlinePageVisible = false
|
|
54
66
|
private var lastTargetUrl: String? = null
|
|
55
67
|
private var activeOfflineRouteOrigin: String? = null
|
|
@@ -62,8 +74,16 @@ class CustomWebView(
|
|
|
62
74
|
private var assetLoadFailures = 0
|
|
63
75
|
|
|
64
76
|
init {
|
|
77
|
+
if (BuildConfig.DEBUG) {
|
|
78
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
79
|
+
put("type", "boot-webview-constructed")
|
|
80
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
81
|
+
put("thread", Thread.currentThread().name)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
65
84
|
setupFromProperties()
|
|
66
85
|
cacheManager = WebCacheManager(context, properties)
|
|
86
|
+
if (profilerEnabled) metricsMonitor.attachWebView(webView)
|
|
67
87
|
offlineCacheService = OfflineCacheService(context)
|
|
68
88
|
setupWebView()
|
|
69
89
|
}
|
|
@@ -90,6 +110,7 @@ class CustomWebView(
|
|
|
90
110
|
|
|
91
111
|
// Parse buildOptimisation property
|
|
92
112
|
buildOptimisation = properties.getProperty("buildOptimisation", "false").toBoolean()
|
|
113
|
+
profilerEnabled = PerfEventBuffer.isEnabled()
|
|
93
114
|
|
|
94
115
|
// Load allowed URLs from properties
|
|
95
116
|
allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
|
|
@@ -162,12 +183,26 @@ class CustomWebView(
|
|
|
162
183
|
}
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
private fun loadTopLevelUrl(url: String) {
|
|
187
|
+
if (BuildConfig.DEBUG) {
|
|
188
|
+
val navigation = profilerNavigationState.begin(url)
|
|
189
|
+
if (navigation.shouldReset) PerfEventBuffer.reset()
|
|
190
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
191
|
+
put("type", "boot-load-url")
|
|
192
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
193
|
+
put("url", navigation.url)
|
|
194
|
+
put("thread", Thread.currentThread().name)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
loadUrlInternal(url)
|
|
198
|
+
}
|
|
199
|
+
|
|
165
200
|
fun loadUrl(url: String) {
|
|
166
201
|
lastTargetUrl = url
|
|
167
202
|
offlinePageVisible = false
|
|
168
203
|
visibleOfflineSnapshotUrl = null
|
|
169
204
|
activeOfflineRouteOrigin = null
|
|
170
|
-
|
|
205
|
+
loadTopLevelUrl(url)
|
|
171
206
|
}
|
|
172
207
|
|
|
173
208
|
fun updateLastTargetUrl(url: String) {
|
|
@@ -185,7 +220,7 @@ class CustomWebView(
|
|
|
185
220
|
offlinePageVisible = true
|
|
186
221
|
visibleOfflineSnapshotUrl = null
|
|
187
222
|
activeOfflineRouteOrigin = null
|
|
188
|
-
|
|
223
|
+
loadTopLevelUrl(offlineAssetUrl)
|
|
189
224
|
if (BuildConfig.DEBUG) {
|
|
190
225
|
Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
|
|
191
226
|
}
|
|
@@ -228,7 +263,7 @@ class CustomWebView(
|
|
|
228
263
|
offlinePageVisible = false
|
|
229
264
|
visibleOfflineSnapshotUrl = snapshotUrl
|
|
230
265
|
updateActiveOfflineRoute(url, true)
|
|
231
|
-
|
|
266
|
+
loadTopLevelUrl(url)
|
|
232
267
|
if (BuildConfig.DEBUG) {
|
|
233
268
|
Log.d(TAG, "📴 Loading cached offline route through request interceptor: $url")
|
|
234
269
|
}
|
|
@@ -304,6 +339,7 @@ class CustomWebView(
|
|
|
304
339
|
|
|
305
340
|
fun destroy() {
|
|
306
341
|
job.cancel()
|
|
342
|
+
metricsMonitor.detachWebView()
|
|
307
343
|
webView.destroy()
|
|
308
344
|
}
|
|
309
345
|
|
|
@@ -335,6 +371,13 @@ class CustomWebView(
|
|
|
335
371
|
if (BuildConfig.DEBUG) {
|
|
336
372
|
Log.d(TAG, "🚀 Hardware acceleration enabled - Thread: ${Thread.currentThread().name}")
|
|
337
373
|
}
|
|
374
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
375
|
+
put("type", "hw-accel-change")
|
|
376
|
+
put("state", "on")
|
|
377
|
+
put("trigger", "cleanup")
|
|
378
|
+
put("thread", Thread.currentThread().name)
|
|
379
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
380
|
+
})
|
|
338
381
|
}
|
|
339
382
|
}
|
|
340
383
|
|
|
@@ -345,6 +388,13 @@ class CustomWebView(
|
|
|
345
388
|
if (BuildConfig.DEBUG) {
|
|
346
389
|
Log.d(TAG, "⚫ Hardware acceleration disabled - Thread: ${Thread.currentThread().name}")
|
|
347
390
|
}
|
|
391
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
392
|
+
put("type", "hw-accel-change")
|
|
393
|
+
put("state", "off")
|
|
394
|
+
put("trigger", "cache-serve")
|
|
395
|
+
put("thread", Thread.currentThread().name)
|
|
396
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
397
|
+
})
|
|
348
398
|
}
|
|
349
399
|
}
|
|
350
400
|
|
|
@@ -472,7 +522,7 @@ class CustomWebView(
|
|
|
472
522
|
}
|
|
473
523
|
offlinePageVisible = false
|
|
474
524
|
lastTargetUrl = target
|
|
475
|
-
|
|
525
|
+
loadTopLevelUrl(target)
|
|
476
526
|
} else if (BuildConfig.DEBUG) {
|
|
477
527
|
Log.w(TAG, "🔄 Retry requested but no target URL is known")
|
|
478
528
|
}
|
|
@@ -718,6 +768,13 @@ class CustomWebView(
|
|
|
718
768
|
|
|
719
769
|
@SuppressLint("SetJavaScriptEnabled")
|
|
720
770
|
private fun setupWebView() {
|
|
771
|
+
if (profilerEnabled && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
772
|
+
WebViewCompat.addDocumentStartJavaScript(
|
|
773
|
+
webView,
|
|
774
|
+
"window.__CATALYST_PROFILER_ENABLED = true;",
|
|
775
|
+
setOf("*")
|
|
776
|
+
)
|
|
777
|
+
}
|
|
721
778
|
if (BuildConfig.DEBUG) {
|
|
722
779
|
Log.d(TAG, "🌐 Setting up WebView on thread: ${Thread.currentThread().name}")
|
|
723
780
|
}
|
|
@@ -1057,14 +1114,53 @@ class CustomWebView(
|
|
|
1057
1114
|
Log.d(TAG, "⏳ Page load started for: $url - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
1058
1115
|
Log.d(FLOW_TAG, "PAGE started url=$url online=${NetworkUtils.getCurrentStatus(context).isOnline} offlinePageVisible=$offlinePageVisible visibleSnapshot=${visibleOfflineSnapshotUrl ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"}")
|
|
1059
1116
|
}
|
|
1117
|
+
|
|
1118
|
+
if (profilerEnabled) {
|
|
1119
|
+
val nativeNow = android.os.SystemClock.elapsedRealtime()
|
|
1120
|
+
view?.evaluateJavascript(
|
|
1121
|
+
"window.__CATALYST_PROFILER_ENABLED = true; window.__NATIVE_TIME_OFFSET = $nativeNow - performance.now();",
|
|
1122
|
+
null
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
if (profilerEnabled && url != null) {
|
|
1126
|
+
val nativeNow = android.os.SystemClock.elapsedRealtime()
|
|
1127
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1128
|
+
put("type", "boot-page-started")
|
|
1129
|
+
put("nativeTime", nativeNow)
|
|
1130
|
+
put("url", url)
|
|
1131
|
+
})
|
|
1132
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1133
|
+
put("type", "page-load-start")
|
|
1134
|
+
put("nativeTime", nativeNow)
|
|
1135
|
+
put("url", url)
|
|
1136
|
+
})
|
|
1137
|
+
}
|
|
1060
1138
|
}
|
|
1061
1139
|
|
|
1062
1140
|
override fun onPageFinished(view: WebView?, url: String?) {
|
|
1063
1141
|
progressBar.visibility = View.GONE
|
|
1064
1142
|
if(!isInitialPageLoaded){
|
|
1065
1143
|
isInitialPageLoaded = true
|
|
1144
|
+
metricsMonitor.markAppStartComplete()
|
|
1066
1145
|
}
|
|
1067
1146
|
url?.let { metricsMonitor.trackPageLoadEnd(it) }
|
|
1147
|
+
|
|
1148
|
+
val pageFinishedNative = android.os.SystemClock.elapsedRealtime()
|
|
1149
|
+
if (profilerEnabled && url != null) {
|
|
1150
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1151
|
+
put("type", "boot-page-finished")
|
|
1152
|
+
put("nativeTime", pageFinishedNative)
|
|
1153
|
+
put("url", url)
|
|
1154
|
+
})
|
|
1155
|
+
val pageStart = view?.tag as? Long
|
|
1156
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1157
|
+
put("type", "page-load-end")
|
|
1158
|
+
put("nativeTime", pageFinishedNative)
|
|
1159
|
+
put("url", url)
|
|
1160
|
+
put("durationMs", if (pageStart != null) System.currentTimeMillis() - pageStart else 0L)
|
|
1161
|
+
})
|
|
1162
|
+
if (view != null) PerfEventBuffer.scheduleFlush(view)
|
|
1163
|
+
}
|
|
1068
1164
|
if (BuildConfig.DEBUG) {
|
|
1069
1165
|
val startTime = view?.tag as? Long ?: return
|
|
1070
1166
|
val loadTime = System.currentTimeMillis() - startTime
|
|
@@ -1095,6 +1191,15 @@ class CustomWebView(
|
|
|
1095
1191
|
if (BuildConfig.DEBUG) {
|
|
1096
1192
|
Log.e(FLOW_TAG, "PAGE resource-error mainFrame=${request?.isForMainFrame} method=${request?.method} code=${error.errorCode} description=${error.description} url=${request?.url}")
|
|
1097
1193
|
}
|
|
1194
|
+
if (view != null && request?.isForMainFrame == true) {
|
|
1195
|
+
BridgeUtils.emitPerfEvent(view, JSONObject().apply {
|
|
1196
|
+
put("type", "page-load-error")
|
|
1197
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1198
|
+
put("url", request.url?.toString() ?: "")
|
|
1199
|
+
put("errorCode", error.errorCode)
|
|
1200
|
+
put("description", error.description?.toString() ?: "")
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
1098
1203
|
}
|
|
1099
1204
|
|
|
1100
1205
|
val failedUrl = request?.url?.toString()
|
|
@@ -1204,5 +1309,39 @@ class CustomWebView(
|
|
|
1204
1309
|
|
|
1205
1310
|
// Only enable WebView debugging in debug builds
|
|
1206
1311
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
|
1312
|
+
|
|
1313
|
+
if (profilerEnabled) {
|
|
1314
|
+
scrollGestureDetector = GestureDetector(context,
|
|
1315
|
+
object : GestureDetector.SimpleOnGestureListener() {
|
|
1316
|
+
override fun onScroll(
|
|
1317
|
+
e1: MotionEvent?,
|
|
1318
|
+
e2: MotionEvent,
|
|
1319
|
+
distanceX: Float,
|
|
1320
|
+
distanceY: Float
|
|
1321
|
+
): Boolean {
|
|
1322
|
+
if (!scrollSessionOpen) {
|
|
1323
|
+
scrollSessionOpen = true
|
|
1324
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
1325
|
+
put("type", "scroll-start")
|
|
1326
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
return false
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
)
|
|
1333
|
+
webView.setOnTouchListener { _, event ->
|
|
1334
|
+
scrollGestureDetector.onTouchEvent(event)
|
|
1335
|
+
if (scrollSessionOpen &&
|
|
1336
|
+
(event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL)) {
|
|
1337
|
+
scrollSessionOpen = false
|
|
1338
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
1339
|
+
put("type", "scroll-end")
|
|
1340
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1343
|
+
false
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1207
1346
|
}
|
|
1208
1347
|
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -274,6 +274,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
274
274
|
properties.setProperty("buildType", if (BuildConfig.DEBUG) "debug" else "release")
|
|
275
275
|
properties.setProperty("buildOptimisation", (!BuildConfig.DEBUG).toString())
|
|
276
276
|
}
|
|
277
|
+
io.yourname.androidproject.utils.PerfEventBuffer.configure(
|
|
278
|
+
properties.getProperty("profiler.enabled", "false").toBoolean()
|
|
279
|
+
)
|
|
277
280
|
|
|
278
281
|
configureEdgeToEdge()
|
|
279
282
|
|
|
@@ -286,16 +289,29 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
286
289
|
})
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
// Initialize MetricsMonitor
|
|
292
|
+
// Initialize MetricsMonitor — recordAppStart() resets the cold-start clock for this
|
|
293
|
+
// Activity lifecycle. The singleton may have been constructed in a previous session
|
|
294
|
+
// (hot restart / Activity recreation), so appStartTime would be stale without this.
|
|
290
295
|
metricsMonitor = MetricsMonitor.getInstance(this)
|
|
296
|
+
metricsMonitor.recordAppStart()
|
|
297
|
+
|
|
298
|
+
// Boot timing: record activity onCreate as the earliest boot event
|
|
299
|
+
if (BuildConfig.DEBUG) {
|
|
300
|
+
io.yourname.androidproject.utils.PerfEventBuffer.reset()
|
|
301
|
+
io.yourname.androidproject.utils.PerfEventBuffer.add(org.json.JSONObject().apply {
|
|
302
|
+
put("type", "boot-activity-created")
|
|
303
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
304
|
+
put("thread", Thread.currentThread().name)
|
|
305
|
+
})
|
|
306
|
+
}
|
|
291
307
|
|
|
292
308
|
// Setup UI
|
|
293
309
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
294
310
|
supportActionBar?.hide()
|
|
295
311
|
setContentView(binding.root)
|
|
296
312
|
|
|
297
|
-
// Initialize keyboard utility
|
|
298
|
-
keyboardUtil = KeyboardUtil(this, binding.webviewContainer)
|
|
313
|
+
// Initialize keyboard utility (pass webView so keyboard events reach WebPerfCollector)
|
|
314
|
+
keyboardUtil = KeyboardUtil(this, binding.webviewContainer, binding.webview)
|
|
299
315
|
keyboardUtil.initialize()
|
|
300
316
|
|
|
301
317
|
// Enable hardware acceleration for the window
|
|
@@ -351,6 +367,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
351
367
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
|
352
368
|
override fun handleOnBackPressed() {
|
|
353
369
|
if (customWebView.canGoBack()) {
|
|
370
|
+
if (BuildConfig.DEBUG) {
|
|
371
|
+
BridgeUtils.emitPerfEvent(customWebView.getWebView(), org.json.JSONObject().apply {
|
|
372
|
+
put("type", "navigation-back")
|
|
373
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
374
|
+
})
|
|
375
|
+
}
|
|
354
376
|
customWebView.goBack()
|
|
355
377
|
} else {
|
|
356
378
|
// Disable this callback and let the system handle back press
|
|
@@ -389,9 +411,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
389
411
|
customWebView.showOfflineRouteOrOfflinePage(currentUrl)
|
|
390
412
|
}
|
|
391
413
|
}
|
|
392
|
-
|
|
393
|
-
metricsMonitor.markAppStartComplete()
|
|
394
|
-
|
|
395
414
|
} catch (e: Exception) {
|
|
396
415
|
Log.e(TAG, "Failed to load initial URL: ${e.message}")
|
|
397
416
|
// TODO: Add HTML file workflow - error.html not available yet
|
|
@@ -453,6 +472,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
453
472
|
}
|
|
454
473
|
|
|
455
474
|
override fun onDestroy() {
|
|
475
|
+
io.yourname.androidproject.utils.PerfEventBuffer.reset()
|
|
456
476
|
// Log all performance metrics before destroying
|
|
457
477
|
if (BuildConfig.DEBUG) {
|
|
458
478
|
Log.d(TAG, "🏁 App shutting down - logging final metrics...")
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt
CHANGED
|
@@ -5,6 +5,10 @@ import android.os.Debug
|
|
|
5
5
|
import android.os.SystemClock
|
|
6
6
|
import android.util.Log
|
|
7
7
|
import android.view.Choreographer
|
|
8
|
+
import android.webkit.WebView
|
|
9
|
+
import io.yourname.androidproject.utils.BridgeUtils
|
|
10
|
+
import io.yourname.androidproject.utils.PerfEventBuffer
|
|
11
|
+
import org.json.JSONObject
|
|
8
12
|
import java.util.concurrent.TimeUnit
|
|
9
13
|
import java.util.concurrent.atomic.AtomicInteger
|
|
10
14
|
|
|
@@ -13,11 +17,29 @@ import java.util.concurrent.atomic.AtomicInteger
|
|
|
13
17
|
* Tracks key performance metrics and logs them on app destroy.
|
|
14
18
|
*/
|
|
15
19
|
class MetricsMonitor(private val context: Context) {
|
|
20
|
+
|
|
21
|
+
// WebView reference for emitting perf events — set via attachWebView()
|
|
22
|
+
@Volatile private var webView: WebView? = null
|
|
23
|
+
|
|
24
|
+
fun attachWebView(wv: WebView) {
|
|
25
|
+
webView = wv
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun detachWebView() {
|
|
29
|
+
// Flush any open fps-drop episode before losing the WebView reference
|
|
30
|
+
if (inFpsDropEpisode) emitFpsDropEpisode(endTime = SystemClock.elapsedRealtime())
|
|
31
|
+
webView = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun emitPerf(payload: JSONObject) {
|
|
35
|
+
webView?.let { BridgeUtils.emitPerfEvent(it, payload) }
|
|
36
|
+
}
|
|
16
37
|
private val TAG = "MetricsMonitor"
|
|
17
38
|
|
|
18
39
|
// App Startup metrics
|
|
19
40
|
private var appStartTime: Long = SystemClock.elapsedRealtime()
|
|
20
41
|
private var coldStartDuration: Long = 0
|
|
42
|
+
private var coldStartCompleted = false
|
|
21
43
|
|
|
22
44
|
// Page Load metrics
|
|
23
45
|
private val pageLoadTimes = mutableMapOf<String, Long>()
|
|
@@ -34,6 +56,15 @@ class MetricsMonitor(private val context: Context) {
|
|
|
34
56
|
private var avgFps = 0.0
|
|
35
57
|
private var fpsReadings = 0
|
|
36
58
|
|
|
59
|
+
// fps-drop episode tracking
|
|
60
|
+
private var inFpsDropEpisode = false
|
|
61
|
+
private var episodeStartNativeTime: Long = 0
|
|
62
|
+
private var episodeMinFps = Double.MAX_VALUE
|
|
63
|
+
private var episodeFpsSum = 0.0
|
|
64
|
+
private var episodeSampleCount = 0
|
|
65
|
+
private val FPS_EPISODE_MAX_MS = 10_000L // force-close after 10s
|
|
66
|
+
private var lastStableFps = 60.0 // fps reading just before episode started — used to compute deltaFps
|
|
67
|
+
|
|
37
68
|
// Memory metrics
|
|
38
69
|
private var initialMemory: Long = getMemoryUsage()
|
|
39
70
|
private var peakMemory: Long = initialMemory
|
|
@@ -74,8 +105,18 @@ class MetricsMonitor(private val context: Context) {
|
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
fun markAppStartComplete() {
|
|
108
|
+
if (coldStartCompleted) return
|
|
109
|
+
coldStartCompleted = true
|
|
77
110
|
coldStartDuration = SystemClock.elapsedRealtime() - appStartTime
|
|
78
111
|
Log.d(TAG, "App cold start completed in: $coldStartDuration ms")
|
|
112
|
+
// Buffer so it is delivered with the post-load batch flush when JS is ready.
|
|
113
|
+
// emitPerf (live path) would be a no-op here — JS may not have loaded yet.
|
|
114
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
115
|
+
put("type", "cold-start")
|
|
116
|
+
put("nativeTime", SystemClock.elapsedRealtime())
|
|
117
|
+
put("durationMs", coldStartDuration)
|
|
118
|
+
put("thread", Thread.currentThread().name)
|
|
119
|
+
})
|
|
79
120
|
}
|
|
80
121
|
|
|
81
122
|
fun trackPageLoadStart(url: String) {
|
|
@@ -144,8 +185,14 @@ class MetricsMonitor(private val context: Context) {
|
|
|
144
185
|
uiBlockingEvents++
|
|
145
186
|
}
|
|
146
187
|
|
|
147
|
-
fun recordLongTask() {
|
|
188
|
+
fun recordLongTask(durationMs: Long = longTaskThreshold) {
|
|
148
189
|
longTasksDetected++
|
|
190
|
+
emitPerf(JSONObject().apply {
|
|
191
|
+
put("type", "long-task")
|
|
192
|
+
put("nativeTime", SystemClock.elapsedRealtime())
|
|
193
|
+
put("durationMs", durationMs)
|
|
194
|
+
put("thread", Thread.currentThread().name)
|
|
195
|
+
})
|
|
149
196
|
}
|
|
150
197
|
|
|
151
198
|
private fun startFrameMonitoring() {
|
|
@@ -173,27 +220,106 @@ class MetricsMonitor(private val context: Context) {
|
|
|
173
220
|
val timeSpan = now - lastFpsCheckTime
|
|
174
221
|
lastFpsCheckTime = now
|
|
175
222
|
|
|
176
|
-
if (timeSpan
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
223
|
+
if (timeSpan <= 0) return
|
|
224
|
+
|
|
225
|
+
currentFps = frames * 1000.0 / timeSpan
|
|
226
|
+
|
|
227
|
+
// Update session-level min/max/avg
|
|
228
|
+
if (minFps == Double.MAX_VALUE) minFps = currentFps
|
|
229
|
+
minFps = minFps.coerceAtMost(currentFps)
|
|
230
|
+
maxFps = maxFps.coerceAtLeast(currentFps)
|
|
231
|
+
val totalFps = avgFps * fpsReadings + currentFps
|
|
232
|
+
fpsReadings++
|
|
233
|
+
avgFps = totalFps / fpsReadings
|
|
234
|
+
|
|
235
|
+
if (currentFps < 55.0) {
|
|
236
|
+
if (!inFpsDropEpisode) {
|
|
237
|
+
// Start new episode — capture the last stable fps as baseline for deltaFps
|
|
238
|
+
inFpsDropEpisode = true
|
|
239
|
+
episodeStartNativeTime = now - timeSpan // start of the measured window
|
|
240
|
+
episodeMinFps = currentFps
|
|
241
|
+
episodeFpsSum = currentFps
|
|
242
|
+
episodeSampleCount = 1
|
|
243
|
+
// lastStableFps already holds the previous reading (set in the else branch below)
|
|
244
|
+
} else {
|
|
245
|
+
// Accumulate into current episode
|
|
246
|
+
episodeMinFps = episodeMinFps.coerceAtMost(currentFps)
|
|
247
|
+
episodeFpsSum += currentFps
|
|
248
|
+
episodeSampleCount++
|
|
249
|
+
|
|
250
|
+
// Force-close if episode has run for >= 10s
|
|
251
|
+
val episodeDuration = now - episodeStartNativeTime
|
|
252
|
+
if (episodeDuration >= FPS_EPISODE_MAX_MS) {
|
|
253
|
+
emitFpsDropEpisode(endTime = now)
|
|
254
|
+
// Immediately start a new episode for the continuing drop
|
|
255
|
+
inFpsDropEpisode = true
|
|
256
|
+
episodeStartNativeTime = now
|
|
257
|
+
episodeMinFps = currentFps
|
|
258
|
+
episodeFpsSum = currentFps
|
|
259
|
+
episodeSampleCount = 1
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
// FPS is good — update lastStableFps and close episode if one was open
|
|
264
|
+
lastStableFps = currentFps
|
|
265
|
+
if (inFpsDropEpisode) {
|
|
266
|
+
emitFpsDropEpisode(endTime = now)
|
|
267
|
+
}
|
|
187
268
|
}
|
|
188
269
|
}
|
|
189
270
|
|
|
271
|
+
private fun emitFpsDropEpisode(endTime: Long) {
|
|
272
|
+
if (!inFpsDropEpisode) return
|
|
273
|
+
val durationMs = endTime - episodeStartNativeTime
|
|
274
|
+
val avgEpisodeFps = if (episodeSampleCount > 0) episodeFpsSum / episodeSampleCount else episodeMinFps
|
|
275
|
+
// deltaFps = how far FPS fell from the last stable reading before the episode
|
|
276
|
+
val deltaFps = lastStableFps - episodeMinFps
|
|
277
|
+
emitPerf(JSONObject().apply {
|
|
278
|
+
put("type", "fps-drop-episode")
|
|
279
|
+
put("startNativeTime", episodeStartNativeTime)
|
|
280
|
+
put("endNativeTime", endTime)
|
|
281
|
+
put("durationMs", durationMs)
|
|
282
|
+
put("minFps", episodeMinFps)
|
|
283
|
+
put("avgFps", avgEpisodeFps)
|
|
284
|
+
put("deltaFps", deltaFps) // drop magnitude from last stable fps
|
|
285
|
+
put("baselineFps", lastStableFps)
|
|
286
|
+
put("thread", Thread.currentThread().name)
|
|
287
|
+
})
|
|
288
|
+
inFpsDropEpisode = false
|
|
289
|
+
episodeMinFps = Double.MAX_VALUE
|
|
290
|
+
episodeFpsSum = 0.0
|
|
291
|
+
episodeSampleCount = 0
|
|
292
|
+
}
|
|
293
|
+
|
|
190
294
|
private fun monitorMemoryPeriodically() {
|
|
191
295
|
Thread {
|
|
192
296
|
while (!Thread.interrupted()) {
|
|
193
297
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
298
|
+
val memInfo = Debug.MemoryInfo()
|
|
299
|
+
Debug.getMemoryInfo(memInfo)
|
|
300
|
+
|
|
301
|
+
// PSS breakdown — all values in KB, convert to MB
|
|
302
|
+
val jvmMb = memInfo.dalvikPss / 1024.0 // Kotlin/Java heap
|
|
303
|
+
val webviewMb = memInfo.nativePss / 1024.0 // V8 + Blink + JNI (WebView proxy)
|
|
304
|
+
val otherMb = memInfo.otherPss / 1024.0 // graphics buffers, code, stack
|
|
305
|
+
val totalMb = memInfo.totalPss / 1024.0 // what Android OOM killer sees
|
|
306
|
+
|
|
307
|
+
currentMemory = memInfo.totalPss.toLong()
|
|
308
|
+
peakMemory = peakMemory.coerceAtLeast(currentMemory)
|
|
309
|
+
|
|
310
|
+
val now = SystemClock.elapsedRealtime()
|
|
311
|
+
emitPerf(JSONObject().apply {
|
|
312
|
+
put("type", "memory-snapshot")
|
|
313
|
+
put("nativeTime", now)
|
|
314
|
+
put("jvmMb", jvmMb)
|
|
315
|
+
put("webviewMb", webviewMb)
|
|
316
|
+
put("otherMb", otherMb)
|
|
317
|
+
put("totalMb", totalMb)
|
|
318
|
+
put("peakMb", peakMemory / 1024.0)
|
|
319
|
+
put("thread", Thread.currentThread().name)
|
|
320
|
+
if (coldStartDuration > 0) put("coldStartMs", coldStartDuration)
|
|
321
|
+
})
|
|
322
|
+
Thread.sleep(5000)
|
|
197
323
|
} catch (e: InterruptedException) {
|
|
198
324
|
break
|
|
199
325
|
}
|
|
@@ -324,7 +450,7 @@ class MetricsMonitor(private val context: Context) {
|
|
|
324
450
|
}
|
|
325
451
|
|
|
326
452
|
fun cleanup() {
|
|
327
|
-
|
|
453
|
+
if (inFpsDropEpisode) emitFpsDropEpisode(endTime = SystemClock.elapsedRealtime())
|
|
328
454
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
329
455
|
}
|
|
330
456
|
|
|
@@ -338,4 +464,4 @@ class MetricsMonitor(private val context: Context) {
|
|
|
338
464
|
}
|
|
339
465
|
}
|
|
340
466
|
}
|
|
341
|
-
}
|
|
467
|
+
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt
CHANGED
|
@@ -332,8 +332,11 @@ class NativeBridge(
|
|
|
332
332
|
|
|
333
333
|
@JavascriptInterface
|
|
334
334
|
fun openCamera(options: String?) {
|
|
335
|
+
val callId = "openCamera:${android.os.SystemClock.elapsedRealtime()}"
|
|
336
|
+
BridgeUtils.bridgeCallReceived(callId, "openCamera")
|
|
335
337
|
BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "open camera") {
|
|
336
338
|
mainActivity.runOnUiThread {
|
|
339
|
+
BridgeUtils.bridgeCallDispatched(callId)
|
|
337
340
|
if (CameraUtils.hasCameraPermission(mainActivity)) {
|
|
338
341
|
launchCamera()
|
|
339
342
|
} else {
|
|
@@ -492,10 +495,13 @@ class NativeBridge(
|
|
|
492
495
|
|
|
493
496
|
@JavascriptInterface
|
|
494
497
|
fun getDeviceInfo(options: String?) {
|
|
498
|
+
val callId = "getDeviceInfo:${android.os.SystemClock.elapsedRealtime()}"
|
|
499
|
+
BridgeUtils.bridgeCallReceived(callId, "getDeviceInfo")
|
|
495
500
|
BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_DEVICE_INFO_ERROR, "get device info") {
|
|
496
501
|
mainActivity.runOnUiThread {
|
|
497
502
|
val deviceInfo = DeviceInfoUtils.getDeviceInfo(mainActivity, properties)
|
|
498
503
|
BridgeUtils.logDebug(TAG, "Device info retrieved: $deviceInfo")
|
|
504
|
+
BridgeUtils.bridgeCallDispatched(callId)
|
|
499
505
|
BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_DEVICE_INFO_SUCCESS, deviceInfo.toString())
|
|
500
506
|
}
|
|
501
507
|
}
|