catalyst-core 0.2.0-beta.2 → 0.3.0-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/babel.native.config.cjs +8 -0
- package/bin/catalyst.js +49 -105
- package/changelog.md +8 -0
- package/dist/caching.js +1 -1
- package/dist/index.jsx +8 -0
- package/dist/logger.js +74 -3
- 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/androidSetup.js +300 -8
- package/dist/native/bridge/WebBridge.js +481 -59
- package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
- package/dist/native/bridge/errors.js +250 -26
- package/dist/native/bridge/hooks/index.js +125 -1
- package/dist/native/bridge/hooks/useCamera.js +347 -9
- package/dist/native/bridge/hooks/useDataProtection.js +200 -2
- package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
- package/dist/native/bridge/hooks/useFilePicker.js +429 -2
- package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
- package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
- package/dist/native/bridge/hooks/useIntent.js +95 -1
- package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
- package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
- package/dist/native/bridge/hooks/useNotification.js +318 -2
- package/dist/native/bridge/hooks/useSafeArea.js +84 -2
- package/dist/native/bridge/hooks/useVideoStream.js +434 -6
- package/dist/native/bridge/hooks/utils.js +27 -1
- package/dist/native/bridge/hooks.js +16 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
- package/dist/native/bridge/perf/core/constants.js +126 -0
- package/dist/native/bridge/perf/core/measure.js +189 -0
- package/dist/native/bridge/perf/core/session.js +100 -0
- package/dist/native/bridge/perf/core/store.js +554 -0
- package/dist/native/bridge/perf/core/summary.js +174 -0
- package/dist/native/bridge/perf/core/traceExport.js +240 -0
- package/dist/native/bridge/perf/core/waterfalls.js +146 -0
- package/dist/native/bridge/perf/index.js +402 -0
- package/dist/native/bridge/safeArea.js +53 -5
- package/dist/native/bridge/standardizedHooks.js +595 -82
- package/dist/native/bridge/useBaseHook.js +274 -38
- package/dist/native/bridge/utils/CameraUtils.js +201 -56
- package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
- package/dist/native/bridge/utils/NativeBridge.js +425 -141
- package/dist/native/buildAndroid/assets.js +510 -13
- package/dist/native/buildAndroid/build.js +296 -19
- package/dist/native/buildAndroid/config.js +87 -1
- package/dist/native/buildAndroid/index.js +374 -1
- package/dist/native/buildAppAndroid.js +34 -2
- package/dist/native/buildAppIos.js +62 -2
- package/dist/native/buildIos/assets.js +609 -8
- package/dist/native/buildIos/build.js +639 -20
- package/dist/native/buildIos/config.js +294 -12
- package/dist/native/buildIos/index.js +343 -6
- package/dist/native/buildIos/plugins.js +343 -16
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +232 -7
- 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/dist/native/package.json +3 -0
- package/dist/native/plugin-bridge/PluginBridge.js +227 -1
- package/dist/native/pluginComposerAndroid.js +407 -23
- package/dist/native/pluginComposerIos.js +435 -11
- package/dist/native/renameAndroidProject.js +1094 -88
- package/dist/native/setupEmulatorIos.js +289 -22
- package/dist/native/setupServer.js +160 -19
- package/dist/native/terminalProgress.js +199 -11
- package/dist/native/utils.js +193 -15
- package/dist/offline/catalyst-sw.js +145 -1
- package/dist/offline/registerServiceWorker.js +33 -1
- package/dist/otel.js +375 -46
- package/dist/router/ClientRouter.js +14 -2
- package/dist/router/ServerRouter.js +9 -1
- package/dist/router.js +1 -1
- package/dist/scripts/build.js +112 -3
- package/dist/scripts/checkVersion.js +3 -1
- package/dist/scripts/cleanCache.js +30 -1
- package/dist/scripts/generateOfflineManifest.js +116 -1
- package/dist/scripts/loadEnvironmentVariables.js +32 -6
- package/dist/scripts/plugins.js +309 -1
- package/dist/scripts/preServerInit.js +13 -0
- package/dist/scripts/scriptUtils.js +54 -3
- package/dist/scripts/serve.js +51 -4
- package/dist/scripts/start.js +47 -10
- package/dist/scripts/validator.js +149 -1
- package/dist/sentry.cjs +127 -0
- package/dist/sentry.js +125 -5
- package/dist/server/expressServer.js +250 -5
- package/dist/server/manifestCache.js +36 -0
- package/dist/server/renderer/ChunkExtractor.js +144 -0
- package/dist/server/renderer/document/Body.jsx +59 -0
- package/dist/server/renderer/document/FastRefresh.jsx +20 -0
- package/dist/server/renderer/document/Head.jsx +60 -0
- package/dist/server/renderer/document/index.jsx +2 -0
- package/dist/server/renderer/extract.js +149 -31
- package/dist/server/renderer/handler.jsx +433 -0
- package/dist/server/renderer/index.js +8 -1
- package/dist/server/renderer/render.js +47 -0
- package/dist/server/utils/botDetectionMiddleware.js +29 -0
- package/dist/server/utils/cjsRequire.js +10 -0
- package/dist/server/utils/userAgentUtil.js +87 -9
- package/dist/server/utils/validator.js +164 -1
- package/dist/vite/FastRefresh.jsx +20 -0
- package/dist/vite/inject-cache-key-plugin.js +276 -0
- package/dist/vite/loadCustomViteConfig.js +22 -0
- package/dist/vite/manifest-categorization-plugin.js +455 -0
- package/dist/vite/node-loader.mjs +458 -0
- package/dist/vite/scssParams.js +21 -0
- package/dist/vite/vite.config.client.js +130 -0
- package/dist/vite/vite.config.js +422 -0
- package/dist/vite/vite.config.server.js +84 -0
- package/dist/web-router/components/MetaTag.jsx +34 -0
- package/dist/web-router/components/RouterDataProvider.jsx +332 -0
- package/dist/web-router/components/Split.jsx +200 -0
- package/dist/web-router/components/SplitInview.jsx +106 -0
- package/dist/web-router/components/SsrRequestContext.jsx +14 -0
- package/dist/web-router/context.jsx +12 -0
- package/dist/web-router/hooks.jsx +72 -0
- package/dist/web-router/types/components/MetaTag.d.ts +2 -0
- package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
- package/dist/web-router/types/context.d.ts +14 -0
- package/dist/web-router/types/hooks.d.ts +1 -0
- package/dist/web-router/types/index.d.ts +3 -0
- package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +119 -0
- package/mcp_v2/conversion-tasks.json +35 -17
- package/mcp_v2/knowledge-base.json +124 -13
- package/mcp_v2/lib/helpers.js +10 -0
- package/mcp_v2/mcp.js +4 -4
- package/mcp_v2/package.json +1 -0
- package/mcp_v2/tools/build.js +116 -20
- package/mcp_v2/tools/conversion.js +106 -5
- package/mcp_v2/tools/tasks.js +18 -14
- package/package.json +35 -68
- package/babel.config.js +0 -32
- package/dist/index.js +0 -1
- package/dist/scripts/devBuild.js +0 -3
- package/dist/scripts/devServe.js +0 -3
- package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
- package/dist/scripts/registerAliases.js +0 -6
- package/dist/server/renderer/document/Body.js +0 -11
- package/dist/server/renderer/document/Head.js +0 -9
- package/dist/server/renderer/document/index.js +0 -1
- package/dist/server/renderer/handler.js +0 -47
- package/dist/server/startServer.js +0 -57
- package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
- package/dist/webpack/babel.config.client.js +0 -1
- package/dist/webpack/babel.config.ssr.js +0 -1
- package/dist/webpack/base.babel.js +0 -7
- package/dist/webpack/development.client.babel.js +0 -10
- package/dist/webpack/production.client.babel.js +0 -4
- package/dist/webpack/production.ssr.babel.js +0 -8
- package/dist/webpack/scssParams.js +0 -9
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
|
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt
CHANGED
|
@@ -5,7 +5,9 @@ import android.util.Log
|
|
|
5
5
|
import android.util.LruCache
|
|
6
6
|
import android.webkit.WebResourceResponse
|
|
7
7
|
import io.yourname.androidproject.BuildConfig
|
|
8
|
+
import io.yourname.androidproject.utils.PerfEventBuffer
|
|
8
9
|
import kotlinx.coroutines.*
|
|
10
|
+
import org.json.JSONObject
|
|
9
11
|
import java.io.*
|
|
10
12
|
import java.net.HttpURLConnection
|
|
11
13
|
import java.net.URL
|
|
@@ -36,6 +38,24 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
36
38
|
(Runtime.getRuntime().maxMemory() / 1024 / memoryFraction).toInt()
|
|
37
39
|
)
|
|
38
40
|
|
|
41
|
+
private fun emitCachePerfEvent(type: String, url: String, startMs: Long, statusCode: Int? = null) {
|
|
42
|
+
if (!BuildConfig.DEBUG) return
|
|
43
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
44
|
+
put("type", type)
|
|
45
|
+
put("url", url)
|
|
46
|
+
put("nativeStartMs", startMs)
|
|
47
|
+
put("durationMs", android.os.SystemClock.elapsedRealtime() - startMs)
|
|
48
|
+
put("resourceType", when {
|
|
49
|
+
url.endsWith(".js") -> "script"
|
|
50
|
+
url.endsWith(".css") -> "stylesheet"
|
|
51
|
+
url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".svg") -> "image"
|
|
52
|
+
else -> "other"
|
|
53
|
+
})
|
|
54
|
+
put("thread", Thread.currentThread().name)
|
|
55
|
+
statusCode?.let { put("statusCode", it) }
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
init {
|
|
40
60
|
try {
|
|
41
61
|
cacheDir.mkdirs()
|
|
@@ -81,6 +101,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
81
101
|
@Suppress("UNUSED_PARAMETER")
|
|
82
102
|
fun getCachedResponseSync(url: String, headers: Map<String, String>): WebResourceResponse? {
|
|
83
103
|
return try {
|
|
104
|
+
val startMs = android.os.SystemClock.elapsedRealtime()
|
|
84
105
|
val cacheKey = generateCacheKey(url)
|
|
85
106
|
val currentTime = System.currentTimeMillis()
|
|
86
107
|
|
|
@@ -94,6 +115,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
94
115
|
Log.d(TAG, "✅ Serving from memory cache (sync): $url")
|
|
95
116
|
Log.d(FLOW_TAG, "ASSET cache-hit source=memory ageMs=$age url=$url")
|
|
96
117
|
}
|
|
118
|
+
emitCachePerfEvent("cache-hit-memory", url, startMs)
|
|
97
119
|
return memoryCacheEntry.toResponse()
|
|
98
120
|
}
|
|
99
121
|
if (BuildConfig.DEBUG) {
|
|
@@ -113,6 +135,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
113
135
|
Log.d(TAG, "✅ Serving from disk cache (sync): $url")
|
|
114
136
|
Log.d(FLOW_TAG, "ASSET cache-hit source=disk ageMs=$fileAge bytes=${cacheFile.length()} mime=${metadata.mimeType} encoding=${metadata.encoding} url=$url")
|
|
115
137
|
}
|
|
138
|
+
emitCachePerfEvent("cache-hit-disk", url, startMs)
|
|
116
139
|
return response
|
|
117
140
|
}
|
|
118
141
|
if (BuildConfig.DEBUG) {
|
|
@@ -125,6 +148,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
125
148
|
Log.d(TAG, "❌ No valid cache available (sync): $url")
|
|
126
149
|
Log.d(FLOW_TAG, "ASSET cache-miss url=$url")
|
|
127
150
|
}
|
|
151
|
+
emitCachePerfEvent("cache-miss-fetch", url, startMs)
|
|
128
152
|
null
|
|
129
153
|
} catch (e: Exception) {
|
|
130
154
|
Log.e(TAG, "Error in getCachedResponseSync for URL: $url: ${e.message}")
|
|
@@ -357,6 +381,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
357
381
|
cacheKey: String
|
|
358
382
|
): WebResourceResponse? {
|
|
359
383
|
var connection: HttpURLConnection? = null
|
|
384
|
+
val startMs = android.os.SystemClock.elapsedRealtime()
|
|
360
385
|
try {
|
|
361
386
|
connection = URL(url).openConnection() as HttpURLConnection
|
|
362
387
|
headers.forEach { (key, value) ->
|
|
@@ -440,6 +465,7 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
440
465
|
if (BuildConfig.DEBUG) {
|
|
441
466
|
Log.d(FLOW_TAG, "ASSET network-fetch success status=${connection.responseCode} bytes=${responseBytes.size} contentType=$contentType replayMime=$mimeType replayEncoding=$encoding url=$url")
|
|
442
467
|
}
|
|
468
|
+
emitCachePerfEvent("network-fetch-complete", url, startMs, connection.responseCode)
|
|
443
469
|
return response
|
|
444
470
|
}
|
|
445
471
|
if (BuildConfig.DEBUG) {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt
CHANGED
|
@@ -5,6 +5,8 @@ import android.webkit.WebView
|
|
|
5
5
|
import io.yourname.androidproject.CatalystConstants
|
|
6
6
|
import org.json.JSONObject
|
|
7
7
|
import android.os.Looper
|
|
8
|
+
import android.os.SystemClock
|
|
9
|
+
import io.yourname.androidproject.BuildConfig
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Common utilities for native bridge operations
|
|
@@ -95,11 +97,64 @@ object BridgeUtils {
|
|
|
95
97
|
ON_TRANSITION_CANCELLED("ON_TRANSITION_CANCELLED"),
|
|
96
98
|
ON_TRANSITION_TIMEOUT("ON_TRANSITION_TIMEOUT"),
|
|
97
99
|
|
|
100
|
+
// Perf telemetry
|
|
101
|
+
CATALYST_PERF_EVENT("CATALYST_PERF_EVENT"),
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Emit a performance event directly to WebPerfCollector via
|
|
106
|
+
* window.__catalystPerfEvent(). Bypasses the WebBridge callback system
|
|
107
|
+
* to keep perf overhead minimal — no JS parsing, no handler lookup.
|
|
108
|
+
*
|
|
109
|
+
* Must be called from any thread — posts to main thread automatically.
|
|
110
|
+
*
|
|
111
|
+
* @param webView The WebView instance
|
|
112
|
+
* @param payload JSONObject with at minimum a "type" field
|
|
113
|
+
*/
|
|
114
|
+
fun emitPerfEvent(webView: WebView, payload: JSONObject) {
|
|
115
|
+
if (!PerfEventBuffer.isEnabled()) return
|
|
116
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
117
|
+
// Capture caller thread name before marshalling to main thread
|
|
118
|
+
if (!payload.has("emitThread")) {
|
|
119
|
+
payload.put("emitThread", Thread.currentThread().name)
|
|
120
|
+
}
|
|
121
|
+
webView.post { emitPerfEvent(webView, payload) }
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
// Use JSON.parse inside the JS call so we never deal with quote escaping
|
|
126
|
+
val jsonStr = payload.toString().replace("\\", "\\\\").replace("'", "\\'")
|
|
127
|
+
webView.evaluateJavascript("window.__catalystPerfEvent && window.__catalystPerfEvent('$jsonStr')", null)
|
|
128
|
+
} catch (e: Exception) {
|
|
129
|
+
Log.e(TAG, "emitPerfEvent failed: ${e.message}")
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record that a @JavascriptInterface method was invoked (t0).
|
|
135
|
+
* Pair with bridgeCallDispatched() at notifyWeb() call site.
|
|
136
|
+
* No-op in release builds.
|
|
137
|
+
*
|
|
138
|
+
* @param callId Unique identifier for this call (e.g. "getDeviceInfo:<timestamp>")
|
|
139
|
+
* @param method Human-readable method name for ApiCollector.js
|
|
140
|
+
*/
|
|
141
|
+
fun bridgeCallReceived(callId: String, method: String) {
|
|
142
|
+
if (!BuildConfig.DEBUG) return
|
|
143
|
+
PerfEventBuffer.bridgeCallReceived(callId, method)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Record that native dispatched the callback back to JS (t1).
|
|
148
|
+
* Must be called just before (or just after) notifyWeb().
|
|
149
|
+
*/
|
|
150
|
+
fun bridgeCallDispatched(callId: String) {
|
|
151
|
+
if (!BuildConfig.DEBUG) return
|
|
152
|
+
PerfEventBuffer.bridgeCallDispatched(callId)
|
|
153
|
+
}
|
|
154
|
+
|
|
100
155
|
/**
|
|
101
156
|
* Send a callback to the web layer
|
|
102
|
-
*
|
|
157
|
+
*
|
|
103
158
|
* @param webView The WebView instance
|
|
104
159
|
* @param event The web event to send
|
|
105
160
|
* @param data The data to send (can be null)
|
|
@@ -2,27 +2,34 @@ package io.yourname.androidproject.utils
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.graphics.Rect
|
|
5
|
+
import android.os.SystemClock
|
|
5
6
|
import android.util.TypedValue
|
|
6
7
|
import android.view.View
|
|
7
8
|
import android.view.ViewGroup
|
|
8
9
|
import android.view.ViewTreeObserver
|
|
10
|
+
import android.webkit.WebView
|
|
9
11
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
12
|
+
import org.json.JSONObject
|
|
10
13
|
|
|
11
14
|
class KeyboardUtil(
|
|
12
15
|
private val activity: Activity,
|
|
13
|
-
private val webViewContainer: View
|
|
16
|
+
private val webViewContainer: View,
|
|
17
|
+
private val webView: WebView? = null,
|
|
14
18
|
) {
|
|
15
19
|
private var originalHeight: Int = 0
|
|
16
20
|
private var listener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
17
|
-
|
|
21
|
+
|
|
18
22
|
// Keyboard detection threshold in dp - commonly used values are 120-150dp
|
|
19
23
|
private val keyboardThresholdDp = 120f
|
|
20
|
-
|
|
24
|
+
|
|
21
25
|
// Calculate minimum keyboard height in pixels based on device density
|
|
22
26
|
private val keyboardThresholdPx: Int by lazy {
|
|
23
27
|
dpToPx(keyboardThresholdDp)
|
|
24
28
|
}
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
// Track keyboard state to avoid spamming events on every layout pass
|
|
31
|
+
private var keyboardVisible = false
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* Convert density-independent pixels to pixels
|
|
28
35
|
*/
|
|
@@ -33,7 +40,7 @@ class KeyboardUtil(
|
|
|
33
40
|
activity.resources.displayMetrics
|
|
34
41
|
).toInt()
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
|
|
37
44
|
/**
|
|
38
45
|
* Convert pixels to density-independent pixels
|
|
39
46
|
*/
|
|
@@ -43,7 +50,7 @@ class KeyboardUtil(
|
|
|
43
50
|
|
|
44
51
|
fun initialize() {
|
|
45
52
|
val rootView = activity.findViewById<ViewGroup>(android.R.id.content)
|
|
46
|
-
|
|
53
|
+
|
|
47
54
|
webViewContainer.post {
|
|
48
55
|
originalHeight = webViewContainer.height
|
|
49
56
|
}
|
|
@@ -53,21 +60,33 @@ class KeyboardUtil(
|
|
|
53
60
|
val screenHeight = activity.resources.displayMetrics.heightPixels
|
|
54
61
|
val visibleHeight = rect.height()
|
|
55
62
|
val keyboardHeight = screenHeight - visibleHeight
|
|
56
|
-
|
|
63
|
+
|
|
57
64
|
// Use density-independent threshold and consider screen size
|
|
58
65
|
// Also add a minimum percentage of screen height as additional validation
|
|
59
66
|
val minScreenPercentage = (screenHeight * 0.15).toInt() // 15% of screen height
|
|
60
67
|
val effectiveThreshold = maxOf(keyboardThresholdPx, minScreenPercentage)
|
|
61
|
-
|
|
68
|
+
val isNowVisible = keyboardHeight > effectiveThreshold
|
|
69
|
+
|
|
70
|
+
if (isNowVisible) {
|
|
62
71
|
// Keyboard visible - resize
|
|
63
72
|
val params = webViewContainer.layoutParams as ConstraintLayout.LayoutParams
|
|
64
73
|
params.height = originalHeight - keyboardHeight
|
|
65
74
|
webViewContainer.layoutParams = params
|
|
75
|
+
|
|
76
|
+
if (!keyboardVisible) {
|
|
77
|
+
keyboardVisible = true
|
|
78
|
+
emitKeyboardEvent("keyboard-show", keyboardHeight)
|
|
79
|
+
}
|
|
66
80
|
} else {
|
|
67
81
|
// Keyboard hidden - restore
|
|
68
82
|
val params = webViewContainer.layoutParams as ConstraintLayout.LayoutParams
|
|
69
83
|
params.height = originalHeight
|
|
70
84
|
webViewContainer.layoutParams = params
|
|
85
|
+
|
|
86
|
+
if (keyboardVisible) {
|
|
87
|
+
keyboardVisible = false
|
|
88
|
+
emitKeyboardEvent("keyboard-hide", 0)
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
|
@@ -79,4 +98,25 @@ class KeyboardUtil(
|
|
|
79
98
|
rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
|
|
80
99
|
}
|
|
81
100
|
}
|
|
101
|
+
|
|
102
|
+
// ─── Perf telemetry ──────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Emit keyboard show/hide event to WebPerfCollector.
|
|
106
|
+
* Uses SystemClock.elapsedRealtime() so the timestamp is alignable with
|
|
107
|
+
* native Perfetto traces via window.__NATIVE_TIME_OFFSET.
|
|
108
|
+
*/
|
|
109
|
+
private fun emitKeyboardEvent(type: String, keyboardHeight: Int) {
|
|
110
|
+
val wv = webView ?: return
|
|
111
|
+
try {
|
|
112
|
+
val payload = JSONObject().apply {
|
|
113
|
+
put("type", type)
|
|
114
|
+
put("nativeTime", SystemClock.elapsedRealtime())
|
|
115
|
+
put("keyboardHeight", keyboardHeight)
|
|
116
|
+
}
|
|
117
|
+
BridgeUtils.emitPerfEvent(wv, payload)
|
|
118
|
+
} catch (e: Exception) {
|
|
119
|
+
// Non-critical — swallow silently
|
|
120
|
+
}
|
|
121
|
+
}
|
|
82
122
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package io.yourname.androidproject.utils
|
|
2
|
+
|
|
3
|
+
internal class PerfDeliveryState<T> {
|
|
4
|
+
data class Batch<T>(val id: String, val payload: T, val generation: Long, val retryCount: Int = 0)
|
|
5
|
+
|
|
6
|
+
private var generation = 0L
|
|
7
|
+
private var sequence = 0L
|
|
8
|
+
private var inFlight: Batch<T>? = null
|
|
9
|
+
|
|
10
|
+
fun hasInFlight(): Boolean = inFlight != null
|
|
11
|
+
|
|
12
|
+
fun current(): Batch<T>? = inFlight
|
|
13
|
+
|
|
14
|
+
fun begin(payload: T): Batch<T>? {
|
|
15
|
+
if (inFlight != null) return null
|
|
16
|
+
return Batch("catalyst-${++sequence}", payload, generation).also { inFlight = it }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun isCurrent(batch: Batch<T>): Boolean = inFlight?.id == batch.id && batch.generation == generation
|
|
20
|
+
|
|
21
|
+
fun acknowledge(batch: Batch<T>): Boolean {
|
|
22
|
+
if (!isCurrent(batch)) return false
|
|
23
|
+
inFlight = null
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun retry(batch: Batch<T>, maxRetries: Int): Batch<T>? {
|
|
28
|
+
if (!isCurrent(batch) || batch.retryCount >= maxRetries) return null
|
|
29
|
+
return batch.copy(retryCount = batch.retryCount + 1).also { inFlight = it }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun reset() {
|
|
33
|
+
generation++
|
|
34
|
+
inFlight = null
|
|
35
|
+
}
|
|
36
|
+
}
|