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
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.InsightsCollector = void 0;
|
|
7
|
+
var _constants = require("../core/constants.js");
|
|
8
|
+
/**
|
|
9
|
+
* InsightsCollector.js
|
|
10
|
+
*
|
|
11
|
+
* Watches accumulated perf data and emits actionable warnings to the
|
|
12
|
+
* Catalyst > Insights track in Chrome DevTools.
|
|
13
|
+
*
|
|
14
|
+
* Each insight is a point-in-time span with:
|
|
15
|
+
* - severity: 'critical' | 'warning' | 'info'
|
|
16
|
+
* - rule: short machine-readable id (e.g. 'slow-page-load')
|
|
17
|
+
* - message: one-sentence human diagnosis
|
|
18
|
+
* - fix: immediate actionable recommendation
|
|
19
|
+
* - evidence: the numbers that triggered the rule
|
|
20
|
+
*
|
|
21
|
+
* Insights are emitted in two modes:
|
|
22
|
+
* 1. Reactive — emitted immediately when a threshold breach is detected
|
|
23
|
+
* (e.g. an fps-drop episode during interaction)
|
|
24
|
+
* 2. Summary — emitted once after SESSION_SUMMARY_DELAY_MS of inactivity
|
|
25
|
+
* to give a rolled-up view of what happened in the last session
|
|
26
|
+
*
|
|
27
|
+
* How to read this in DevTools:
|
|
28
|
+
* Performance tab → Catalyst > Insights track
|
|
29
|
+
* Click any span → Tooltip shows: severity, rule, message, fix, evidence
|
|
30
|
+
* Red spans = critical (user-visible jank/failure)
|
|
31
|
+
* Yellow (tertiary) = warning
|
|
32
|
+
* Blue (secondary) = info / optimization opportunity
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const SESSION_SUMMARY_DELAY_MS = 3000;
|
|
36
|
+
function round(value) {
|
|
37
|
+
return Number.isFinite(value) ? Math.round(value) : value;
|
|
38
|
+
}
|
|
39
|
+
class InsightsCollector {
|
|
40
|
+
constructor(measure) {
|
|
41
|
+
this._measure = measure;
|
|
42
|
+
this._summaryTimer = null;
|
|
43
|
+
this._insightSeq = 0;
|
|
44
|
+
|
|
45
|
+
// Counters reset per session
|
|
46
|
+
this._fpsDrops = []; // [{ minFps, durationMs }]
|
|
47
|
+
this._slowLoads = []; // [{ url, durationMs }]
|
|
48
|
+
this._loafsDuringNav = 0;
|
|
49
|
+
this._slowInteractions = []; // [{ target, responseMs }]
|
|
50
|
+
this._highMemory = []; // [{ currentMb }]
|
|
51
|
+
this._layoutShifts = []; // [{ value }]
|
|
52
|
+
this._coldStartMs = null; // always attached to other insights once known
|
|
53
|
+
}
|
|
54
|
+
init() {
|
|
55
|
+
// No web observers — fed via notify*() methods from index.js
|
|
56
|
+
}
|
|
57
|
+
setStoreSource(fn) {
|
|
58
|
+
this._notifyStore = fn;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Feed methods — called by index.js after routing native events ─────────
|
|
62
|
+
|
|
63
|
+
notifyFpsDrop({
|
|
64
|
+
minFps,
|
|
65
|
+
avgFps,
|
|
66
|
+
durationMs,
|
|
67
|
+
duringInteraction,
|
|
68
|
+
deltaFps,
|
|
69
|
+
baselineFps,
|
|
70
|
+
startTime,
|
|
71
|
+
endTime,
|
|
72
|
+
interactionId
|
|
73
|
+
}) {
|
|
74
|
+
const source = {
|
|
75
|
+
startTime,
|
|
76
|
+
endTime,
|
|
77
|
+
related: {
|
|
78
|
+
interactionId
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
this._fpsDrops.push({
|
|
82
|
+
minFps,
|
|
83
|
+
durationMs
|
|
84
|
+
});
|
|
85
|
+
if (duringInteraction) {
|
|
86
|
+
this._emitReactive({
|
|
87
|
+
severity: "critical",
|
|
88
|
+
rule: "fps-drop-during-tap",
|
|
89
|
+
message: `FPS dropped to ${minFps.toFixed(1)} for ${Math.round(durationMs)}ms right after a tap`,
|
|
90
|
+
fix: "Heavy computation is blocking the render thread after interaction — compare spans with the same interaction ID",
|
|
91
|
+
evidence: {
|
|
92
|
+
minFps,
|
|
93
|
+
avgFps,
|
|
94
|
+
durationMs,
|
|
95
|
+
deltaFps,
|
|
96
|
+
baselineFps
|
|
97
|
+
},
|
|
98
|
+
...source
|
|
99
|
+
});
|
|
100
|
+
} else if (minFps < 30) {
|
|
101
|
+
this._emitReactive({
|
|
102
|
+
severity: "warning",
|
|
103
|
+
rule: "severe-fps-drop",
|
|
104
|
+
message: `Severe FPS drop: ${minFps.toFixed(1)}fps min for ${Math.round(durationMs)}ms`,
|
|
105
|
+
fix: "Look for LoAF spans overlapping this window on the Render track — a long script or layout is stalling frames",
|
|
106
|
+
evidence: {
|
|
107
|
+
minFps,
|
|
108
|
+
avgFps,
|
|
109
|
+
durationMs,
|
|
110
|
+
deltaFps,
|
|
111
|
+
baselineFps
|
|
112
|
+
},
|
|
113
|
+
...source
|
|
114
|
+
});
|
|
115
|
+
} else if (deltaFps != null && deltaFps >= 10) {
|
|
116
|
+
// Sudden spike: FPS fell sharply even if absolute value is not critical
|
|
117
|
+
this._emitReactive({
|
|
118
|
+
severity: "warning",
|
|
119
|
+
rule: "fps-spike-drop",
|
|
120
|
+
message: `Sudden FPS spike: dropped ${deltaFps.toFixed(1)}fps (${baselineFps?.toFixed(1)} → ${minFps.toFixed(1)}) in one frame window`,
|
|
121
|
+
fix: "A sudden spike drop — often a one-off heavy layout or image decode. Check Render track for a LoAF aligned with this episode.",
|
|
122
|
+
evidence: {
|
|
123
|
+
minFps,
|
|
124
|
+
avgFps,
|
|
125
|
+
durationMs,
|
|
126
|
+
deltaFps,
|
|
127
|
+
baselineFps
|
|
128
|
+
},
|
|
129
|
+
...source
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
this._scheduleSummary();
|
|
133
|
+
}
|
|
134
|
+
notifySlowLoad({
|
|
135
|
+
url,
|
|
136
|
+
durationMs,
|
|
137
|
+
startTime,
|
|
138
|
+
endTime
|
|
139
|
+
}) {
|
|
140
|
+
this._slowLoads.push({
|
|
141
|
+
url,
|
|
142
|
+
durationMs
|
|
143
|
+
});
|
|
144
|
+
if (durationMs > 4000) {
|
|
145
|
+
this._emitReactive({
|
|
146
|
+
severity: "critical",
|
|
147
|
+
rule: "page-load-too-slow",
|
|
148
|
+
message: `Page load took ${Math.round(durationMs)}ms (${url.split("/").pop() || url})`,
|
|
149
|
+
fix: "Check Cache track for cache-miss-fetch bursts; consider preloading route chunks or warming the WebView cache",
|
|
150
|
+
evidence: {
|
|
151
|
+
url,
|
|
152
|
+
durationMs
|
|
153
|
+
},
|
|
154
|
+
startTime,
|
|
155
|
+
endTime
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
this._scheduleSummary();
|
|
159
|
+
}
|
|
160
|
+
notifyColdStart({
|
|
161
|
+
durationMs,
|
|
162
|
+
startTime,
|
|
163
|
+
endTime
|
|
164
|
+
}) {
|
|
165
|
+
this._coldStartMs = durationMs; // attach to all future insights automatically
|
|
166
|
+
const severity = durationMs > 3000 ? "critical" : durationMs > 1800 ? "warning" : "info";
|
|
167
|
+
const label = durationMs > 3000 ? "slow" : durationMs > 1800 ? "acceptable" : "fast";
|
|
168
|
+
this._emitReactive({
|
|
169
|
+
severity,
|
|
170
|
+
rule: "cold-start-time",
|
|
171
|
+
message: `Cold start: ${Math.round(durationMs)}ms (${label}) — app launched to first page finish`,
|
|
172
|
+
fix: durationMs > 3000 ? "Cold start >3s — check if WebView.loadUrl() is called before layout inflation completes; defer non-critical MainActivity init; consider prewarming WebView in Application.onCreate()" : durationMs > 1800 ? "Cold start >1.8s — consider moving WebView construction earlier (Application.onCreate vs MainActivity.onCreate); check if large local assets block onPageFinished" : "Cold start within target (≤1.8s). No action needed.",
|
|
173
|
+
evidence: {
|
|
174
|
+
durationMs,
|
|
175
|
+
targetMs: 1800,
|
|
176
|
+
acceptableMs: 3000
|
|
177
|
+
},
|
|
178
|
+
startTime,
|
|
179
|
+
endTime
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
notifyLoafDuringNav({
|
|
183
|
+
durationMs,
|
|
184
|
+
startTime,
|
|
185
|
+
endTime
|
|
186
|
+
}) {
|
|
187
|
+
this._loafsDuringNav++;
|
|
188
|
+
if (this._loafsDuringNav === 1) {
|
|
189
|
+
this._emitReactive({
|
|
190
|
+
severity: "warning",
|
|
191
|
+
rule: "loaf-blocking-nav",
|
|
192
|
+
message: `Long animation frame (${Math.round(durationMs)}ms) fired during navigation — may cause blank screen`,
|
|
193
|
+
fix: "Defer non-critical JS execution after page-load-end; split large bundles with dynamic import()",
|
|
194
|
+
evidence: {
|
|
195
|
+
durationMs,
|
|
196
|
+
loafsDuringNav: this._loafsDuringNav
|
|
197
|
+
},
|
|
198
|
+
startTime,
|
|
199
|
+
endTime
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
notifySlowInteraction({
|
|
204
|
+
target,
|
|
205
|
+
responseMs,
|
|
206
|
+
startTime,
|
|
207
|
+
endTime,
|
|
208
|
+
interactionId
|
|
209
|
+
}) {
|
|
210
|
+
this._slowInteractions.push({
|
|
211
|
+
target,
|
|
212
|
+
responseMs
|
|
213
|
+
});
|
|
214
|
+
if (responseMs > 300) {
|
|
215
|
+
this._emitReactive({
|
|
216
|
+
severity: "critical",
|
|
217
|
+
rule: "interaction-unresponsive",
|
|
218
|
+
message: `Tap on '${target}' took ${responseMs}ms to paint (threshold ${_constants.THRESHOLD.INTERACTION_SLOW_MS}ms)`,
|
|
219
|
+
fix: "A LoAF is delaying the paint — inspect work overlapping the interaction",
|
|
220
|
+
evidence: {
|
|
221
|
+
target,
|
|
222
|
+
responseMs,
|
|
223
|
+
thresholdMs: _constants.THRESHOLD.INTERACTION_SLOW_MS
|
|
224
|
+
},
|
|
225
|
+
startTime,
|
|
226
|
+
endTime,
|
|
227
|
+
related: {
|
|
228
|
+
interactionId
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
this._scheduleSummary();
|
|
233
|
+
}
|
|
234
|
+
notifyHighMemory({
|
|
235
|
+
currentMb,
|
|
236
|
+
startTime,
|
|
237
|
+
endTime,
|
|
238
|
+
memoryId
|
|
239
|
+
}) {
|
|
240
|
+
const source = {
|
|
241
|
+
startTime,
|
|
242
|
+
endTime,
|
|
243
|
+
related: {
|
|
244
|
+
memoryId
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
// currentMb is webviewMb (V8 + Blink + JNI PSS) — the WebView engine memory.
|
|
248
|
+
// A healthy WebView app idles at 80-120MB. Growth beyond that signals
|
|
249
|
+
// a real leak (un-released image bitmaps, accumulating JS closures, JNI objects).
|
|
250
|
+
// Thresholds are intentionally higher than total-PSS thresholds used previously
|
|
251
|
+
// because we now track the native slice, not the whole process.
|
|
252
|
+
this._highMemory.push({
|
|
253
|
+
currentMb
|
|
254
|
+
});
|
|
255
|
+
if (currentMb > 120) {
|
|
256
|
+
this._emitReactive({
|
|
257
|
+
severity: "critical",
|
|
258
|
+
rule: "memory-critical",
|
|
259
|
+
message: `WebView memory at ${Math.round(currentMb)}MB — above 120MB critical threshold (V8/Blink/JNI leak likely)`,
|
|
260
|
+
fix: "Check for un-released image bitmaps, accumulating JS closures, or JNI objects not returned to GC. Compare WebView memory growth across snapshots on the Memory track.",
|
|
261
|
+
evidence: {
|
|
262
|
+
webviewMb: currentMb
|
|
263
|
+
},
|
|
264
|
+
...source
|
|
265
|
+
});
|
|
266
|
+
} else if (currentMb > 80) {
|
|
267
|
+
this._emitReactive({
|
|
268
|
+
severity: "warning",
|
|
269
|
+
rule: "memory-high",
|
|
270
|
+
message: `WebView memory at ${Math.round(currentMb)}MB — above 80MB warning threshold (healthy idle is <80MB)`,
|
|
271
|
+
fix: "Monitor WebView memory on the Memory track — sustained growth = leak. Check image cache eviction and dispose WebView listeners on route unmount.",
|
|
272
|
+
evidence: {
|
|
273
|
+
webviewMb: currentMb
|
|
274
|
+
},
|
|
275
|
+
...source
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
notifyHwAccelDuringFrame({
|
|
280
|
+
durationMs,
|
|
281
|
+
thread,
|
|
282
|
+
trigger,
|
|
283
|
+
startTime,
|
|
284
|
+
endTime
|
|
285
|
+
}) {
|
|
286
|
+
this._emitReactive({
|
|
287
|
+
severity: "critical",
|
|
288
|
+
rule: "hw-accel-during-frame",
|
|
289
|
+
message: `Software rendering was active for ${Math.round(durationMs)}ms during a dropped frame (hw-accel disabled by ${trigger ?? "cache-serve"})`,
|
|
290
|
+
fix: "Cache serving on shouldInterceptRequest disables hardware acceleration — move cache I/O off the WebView thread or batch cache warming on app start to reduce mid-session hw-accel toggles",
|
|
291
|
+
evidence: {
|
|
292
|
+
durationMs,
|
|
293
|
+
thread: thread ?? "unknown",
|
|
294
|
+
trigger: trigger ?? "unknown"
|
|
295
|
+
},
|
|
296
|
+
startTime,
|
|
297
|
+
endTime
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
notifyLayoutShift({
|
|
301
|
+
value,
|
|
302
|
+
sessionContext,
|
|
303
|
+
startTime,
|
|
304
|
+
endTime
|
|
305
|
+
}) {
|
|
306
|
+
this._layoutShifts.push({
|
|
307
|
+
value
|
|
308
|
+
});
|
|
309
|
+
if (value > 0.25) {
|
|
310
|
+
this._emitReactive({
|
|
311
|
+
severity: "warning",
|
|
312
|
+
rule: "large-layout-shift",
|
|
313
|
+
message: `Large layout shift (CLS ${value.toFixed(3)}) during '${sessionContext}' phase`,
|
|
314
|
+
fix: "Reserve space for async-loaded content (images, ads, banners) using explicit height; avoid injecting DOM above visible content",
|
|
315
|
+
evidence: {
|
|
316
|
+
value,
|
|
317
|
+
sessionContext
|
|
318
|
+
},
|
|
319
|
+
startTime,
|
|
320
|
+
endTime
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
notifyScrollJank({
|
|
325
|
+
minFps,
|
|
326
|
+
frameDropCount,
|
|
327
|
+
startTime,
|
|
328
|
+
endTime
|
|
329
|
+
}) {
|
|
330
|
+
const severity = minFps < 30 ? "critical" : "warning";
|
|
331
|
+
const rule = minFps < 30 ? "scroll-jank-critical" : "scroll-jank-warning";
|
|
332
|
+
this._emitReactive({
|
|
333
|
+
severity,
|
|
334
|
+
rule,
|
|
335
|
+
message: `Scroll jank: ${Math.round(frameDropCount)} frame drop(s), min ${minFps.toFixed(1)}fps during scroll`,
|
|
336
|
+
fix: minFps < 30 ? "Severe scroll jank — check for layout thrash triggered during scroll. Look for LoAF spans on Render track overlapping the Input track scroll session." : "Scroll dropped below 55fps — common causes: sticky header recalc, cart badge updates, or image decode on scroll. Check Render track LoAF spans inside the scroll session.",
|
|
337
|
+
evidence: {
|
|
338
|
+
minFps,
|
|
339
|
+
frameDropCount
|
|
340
|
+
},
|
|
341
|
+
startTime,
|
|
342
|
+
endTime
|
|
343
|
+
});
|
|
344
|
+
this._scheduleSummary();
|
|
345
|
+
}
|
|
346
|
+
notifyCacheSummary({
|
|
347
|
+
hitRatePct,
|
|
348
|
+
total,
|
|
349
|
+
avgFetchMs,
|
|
350
|
+
startTime,
|
|
351
|
+
endTime
|
|
352
|
+
}) {
|
|
353
|
+
if (total < 5) return; // not enough data
|
|
354
|
+
if (hitRatePct < 50) {
|
|
355
|
+
this._emitReactive({
|
|
356
|
+
severity: "warning",
|
|
357
|
+
rule: "low-cache-hit-rate",
|
|
358
|
+
message: `Cache hit rate ${hitRatePct}% (${total} resources) — majority served via network`,
|
|
359
|
+
fix: "Add missed URLs to the allowedUrls cache list; check cache max-age and disk eviction policy",
|
|
360
|
+
evidence: {
|
|
361
|
+
hitRatePct,
|
|
362
|
+
total,
|
|
363
|
+
avgFetchMs
|
|
364
|
+
},
|
|
365
|
+
startTime,
|
|
366
|
+
endTime
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Reactive insight emission ─────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
_emitReactive({
|
|
374
|
+
severity,
|
|
375
|
+
rule,
|
|
376
|
+
message,
|
|
377
|
+
fix,
|
|
378
|
+
evidence = {},
|
|
379
|
+
startTime,
|
|
380
|
+
endTime,
|
|
381
|
+
related = {}
|
|
382
|
+
}) {
|
|
383
|
+
const start = Number.isFinite(startTime) ? startTime : performance.now();
|
|
384
|
+
const end = Number.isFinite(endTime) ? Math.max(start + 1, endTime) : start + 2;
|
|
385
|
+
const markName = `${_constants.PREFIX.INSIGHT}:${rule}:${Math.round(start)}:${++this._insightSeq}`;
|
|
386
|
+
const color = severity === "critical" ? "error" : severity === "warning" ? "tertiary" : "secondary";
|
|
387
|
+
const title = this._titleFor(rule, evidence, message);
|
|
388
|
+
const category = this._categoryFor(rule);
|
|
389
|
+
const detail = {
|
|
390
|
+
severity,
|
|
391
|
+
rule,
|
|
392
|
+
title,
|
|
393
|
+
category,
|
|
394
|
+
message,
|
|
395
|
+
fix,
|
|
396
|
+
...related,
|
|
397
|
+
...(this._coldStartMs != null ? {
|
|
398
|
+
"coldStart.ms": String(Math.round(this._coldStartMs))
|
|
399
|
+
} : {}),
|
|
400
|
+
...Object.fromEntries(Object.entries(evidence).filter(([, value]) => value != null).map(([key, value]) => [`evidence.${key}`, String(value)]))
|
|
401
|
+
};
|
|
402
|
+
performance.mark(markName, {
|
|
403
|
+
startTime: start
|
|
404
|
+
});
|
|
405
|
+
this._notifyStore?.({
|
|
406
|
+
severity,
|
|
407
|
+
rule,
|
|
408
|
+
title,
|
|
409
|
+
category,
|
|
410
|
+
message,
|
|
411
|
+
fix,
|
|
412
|
+
evidence,
|
|
413
|
+
...related,
|
|
414
|
+
startTime: start,
|
|
415
|
+
endTime: end,
|
|
416
|
+
durationMs: end - start
|
|
417
|
+
});
|
|
418
|
+
this._measure.emit(`${related.interactionId ? `[${related.interactionId}] ` : ""}Insight: ${title}`, markName, end, detail, _constants.TRACK.INSIGHTS, color);
|
|
419
|
+
}
|
|
420
|
+
_titleFor(rule, evidence, message) {
|
|
421
|
+
switch (rule) {
|
|
422
|
+
case "fps-drop-during-tap":
|
|
423
|
+
return `FPS drop after tap: ${round(evidence.minFps)}fps`;
|
|
424
|
+
case "severe-fps-drop":
|
|
425
|
+
return `FPS drop: ${round(evidence.minFps)}fps for ${round(evidence.durationMs)}ms`;
|
|
426
|
+
case "fps-spike-drop":
|
|
427
|
+
return `FPS spike drop: -${round(evidence.deltaFps)}fps`;
|
|
428
|
+
case "page-load-too-slow":
|
|
429
|
+
return `Page load slow: ${round(evidence.durationMs)}ms`;
|
|
430
|
+
case "cold-start-time":
|
|
431
|
+
return `Cold start: ${round(evidence.durationMs)}ms`;
|
|
432
|
+
case "loaf-blocking-nav":
|
|
433
|
+
return `Render blocked during navigation: ${round(evidence.durationMs)}ms`;
|
|
434
|
+
case "interaction-unresponsive":
|
|
435
|
+
return `Slow tap: ${evidence.target ?? "unknown"} ${round(evidence.responseMs)}ms`;
|
|
436
|
+
case "memory-critical":
|
|
437
|
+
case "memory-high":
|
|
438
|
+
return `Memory high: ${round(evidence.webviewMb)}MB`;
|
|
439
|
+
case "hw-accel-during-frame":
|
|
440
|
+
return `Software render window: ${round(evidence.durationMs)}ms`;
|
|
441
|
+
case "large-layout-shift":
|
|
442
|
+
return `Large layout shift: ${Number.isFinite(evidence.value) ? evidence.value.toFixed(3) : "unknown"}`;
|
|
443
|
+
case "scroll-jank-critical":
|
|
444
|
+
case "scroll-jank-warning":
|
|
445
|
+
return `Scroll jank: ${round(evidence.minFps)}fps`;
|
|
446
|
+
case "low-cache-hit-rate":
|
|
447
|
+
return `Cache hit rate low: ${evidence.hitRatePct}%`;
|
|
448
|
+
case "session-summary":
|
|
449
|
+
return "Session issues";
|
|
450
|
+
default:
|
|
451
|
+
return message ?? rule;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
_categoryFor(rule) {
|
|
455
|
+
if (rule.includes("fps") || rule.includes("loaf") || rule.includes("layout")) return "render";
|
|
456
|
+
if (rule.includes("page-load") || rule.includes("cold-start")) return "navigation";
|
|
457
|
+
if (rule.includes("interaction") || rule.includes("tap")) return "interaction";
|
|
458
|
+
if (rule.includes("memory")) return "memory";
|
|
459
|
+
if (rule.includes("scroll")) return "scroll";
|
|
460
|
+
if (rule.includes("cache")) return "cache";
|
|
461
|
+
if (rule.includes("session")) return "session";
|
|
462
|
+
return "general";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─── Session summary ───────────────────────────────────────────────────────
|
|
466
|
+
// Emitted after SESSION_SUMMARY_DELAY_MS of quiet — a rolled-up health check.
|
|
467
|
+
|
|
468
|
+
_scheduleSummary() {
|
|
469
|
+
if (this._summaryTimer) clearTimeout(this._summaryTimer);
|
|
470
|
+
this._summaryTimer = setTimeout(() => this._emitSummary(), SESSION_SUMMARY_DELAY_MS);
|
|
471
|
+
}
|
|
472
|
+
_emitSummary() {
|
|
473
|
+
this._summaryTimer = null;
|
|
474
|
+
const problems = [];
|
|
475
|
+
if (this._fpsDrops.length > 0) problems.push(`${this._fpsDrops.length} fps-drop episode(s), min ${Math.min(...this._fpsDrops.map(f => f.minFps)).toFixed(0)}fps`);
|
|
476
|
+
if (this._slowLoads.length > 0) problems.push(`${this._slowLoads.length} slow page load(s)`);
|
|
477
|
+
if (this._slowInteractions.length > 0) problems.push(`${this._slowInteractions.length} slow tap(s)`);
|
|
478
|
+
if (this._highMemory.length > 0) problems.push(`peak ${Math.max(...this._highMemory.map(m => m.currentMb)).toFixed(0)}MB memory`);
|
|
479
|
+
const severity = this._fpsDrops.some(f => f.minFps < 20) ? "critical" : problems.length > 2 ? "warning" : "info";
|
|
480
|
+
const message = problems.length === 0 ? "Session clean — no threshold breaches detected" : `Session issues: ${problems.join(" · ")}`;
|
|
481
|
+
const fix = problems.length === 0 ? "No action needed" : "Expand each red/yellow span above for per-issue fixes";
|
|
482
|
+
this._emitReactive({
|
|
483
|
+
severity: severity === "info" ? "info" : severity,
|
|
484
|
+
rule: "session-summary",
|
|
485
|
+
message,
|
|
486
|
+
fix,
|
|
487
|
+
evidence: {
|
|
488
|
+
fpsDropEpisodes: this._fpsDrops.length,
|
|
489
|
+
slowPageLoads: this._slowLoads.length,
|
|
490
|
+
slowInteractions: this._slowInteractions.length,
|
|
491
|
+
highMemoryReadings: this._highMemory.length,
|
|
492
|
+
layoutShifts: this._layoutShifts.length
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Reset counters for next session
|
|
497
|
+
this._fpsDrops = [];
|
|
498
|
+
this._slowLoads = [];
|
|
499
|
+
this._loafsDuringNav = 0;
|
|
500
|
+
this._slowInteractions = [];
|
|
501
|
+
this._highMemory = [];
|
|
502
|
+
this._layoutShifts = [];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
exports.InsightsCollector = InsightsCollector;
|