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,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.InteractionCollector = void 0;
|
|
7
|
+
var _constants = require("../core/constants.js");
|
|
8
|
+
var _session = require("../core/session.js");
|
|
9
|
+
/**
|
|
10
|
+
* InteractionCollector.js
|
|
11
|
+
*
|
|
12
|
+
* Instruments: bugs #6, #8, #13, #17
|
|
13
|
+
* #6 blank screens → tap → no paint → LoAF blocked first render
|
|
14
|
+
* detected as: interaction span with no following rAF / LoAF overlap
|
|
15
|
+
* #8 long press context menu → pointerdown held > 500ms = native context menu will appear
|
|
16
|
+
* detected as: long-press span so devs see which elements trigger it
|
|
17
|
+
* #13 cart jank haywire clicks → interaction timestamps vs render timestamps diverge
|
|
18
|
+
* detected as: tap → paint delta > INTERACTION_SLOW_MS
|
|
19
|
+
* #17 prescription modal wrong → event queue backed up: pointerdown fires long after it was queued
|
|
20
|
+
* detected as: slow interaction span overlapping LoAF
|
|
21
|
+
*
|
|
22
|
+
* Spans emitted:
|
|
23
|
+
* catalyst:session/interaction|<tag>#<id> — currently disabled; preserved below as the
|
|
24
|
+
* old 2s grouping window for future re-enable.
|
|
25
|
+
* catalyst:interaction|<tag>#<id> — pointerdown → next rAF (Track: Interaction)
|
|
26
|
+
* detail: responseMs, loafOverlap, target, sessionId
|
|
27
|
+
* catalyst:long-press|<tag> — pointerdown held > 500ms (Track: Interaction)
|
|
28
|
+
* detail: target, heldMs
|
|
29
|
+
*
|
|
30
|
+
* Correlation:
|
|
31
|
+
* A lightweight interaction ID remains active from pointerdown to the next frame.
|
|
32
|
+
* Related work is tagged without emitting a parent session track.
|
|
33
|
+
*
|
|
34
|
+
* Disabled session nesting:
|
|
35
|
+
* On pointerdown, a 2-second interaction session opens.
|
|
36
|
+
* RenderCollector queries activeSessionId to tag its spans.
|
|
37
|
+
* Session closes after SESSION_WINDOW_MS (2s) or next pointerdown, whichever comes first.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const SESSION_WINDOW_MS = 2000;
|
|
41
|
+
const MAX_RECENT_INTERACTIONS = 50;
|
|
42
|
+
class InteractionCollector {
|
|
43
|
+
constructor(measure) {
|
|
44
|
+
this._measure = measure;
|
|
45
|
+
this._pendingTaps = new Map(); // pointerId → { startMark, startTime, target }
|
|
46
|
+
this._session = new _session.Session(_constants.PREFIX.SESSION_INTERACTION, measure, _constants.TRACK.INTERACTION);
|
|
47
|
+
this._sessionTimer = null;
|
|
48
|
+
this._sessionId = null;
|
|
49
|
+
this._sessionNetworkCalls = 0;
|
|
50
|
+
this._sessionFpsDrop = false;
|
|
51
|
+
this._activeInteraction = null;
|
|
52
|
+
this._recentInteractions = [];
|
|
53
|
+
this._interactionSeq = 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Public API — queried by native API and render collectors ──────────────
|
|
57
|
+
|
|
58
|
+
get activeInteractionId() {
|
|
59
|
+
return this._activeInteraction?.id ?? null;
|
|
60
|
+
}
|
|
61
|
+
interactionIdForRange(startTime, endTime = startTime) {
|
|
62
|
+
const interactions = this._activeInteraction ? [...this._recentInteractions, this._activeInteraction] : this._recentInteractions;
|
|
63
|
+
return interactions.slice().reverse().find(interaction => startTime <= (interaction.endTime ?? performance.now()) && endTime >= interaction.startTime)?.id ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Called by index.js to wire up insights notifications. */
|
|
67
|
+
setInsightsSource(fn) {
|
|
68
|
+
this._notifyInsights = fn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Called by index.js to feed the normalized perf store. */
|
|
72
|
+
setWaterfallSource(fn) {
|
|
73
|
+
this._notifyWaterfall = fn;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Called by RenderCollector when an fps-drop-episode fires. */
|
|
77
|
+
onFpsDrop() {
|
|
78
|
+
if (this._session.isOpen) {
|
|
79
|
+
this._sessionFpsDrop = true;
|
|
80
|
+
this._session.set("fpsDrop", true);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Called by NetworkTimingCollector when a fetch/XHR starts during an interaction. */
|
|
85
|
+
onNetworkCall() {
|
|
86
|
+
if (this._session.isOpen) {
|
|
87
|
+
this._sessionNetworkCalls++;
|
|
88
|
+
this._session.set("networkCalls", this._sessionNetworkCalls);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
init() {
|
|
92
|
+
document.addEventListener("pointerdown", this._onPointerDown.bind(this), {
|
|
93
|
+
passive: true
|
|
94
|
+
});
|
|
95
|
+
document.addEventListener("pointerup", this._onPointerUp.bind(this), {
|
|
96
|
+
passive: true
|
|
97
|
+
});
|
|
98
|
+
document.addEventListener("pointercancel", this._onPointerCancel.bind(this), {
|
|
99
|
+
passive: true
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
_openSession(target, startTime) {
|
|
103
|
+
// Close any existing session immediately — new tap = new session
|
|
104
|
+
this._closeSession();
|
|
105
|
+
this._sessionId = `${Math.round(startTime)}`;
|
|
106
|
+
this._sessionNetworkCalls = 0;
|
|
107
|
+
this._sessionFpsDrop = false;
|
|
108
|
+
this._session.open({
|
|
109
|
+
target,
|
|
110
|
+
sessionId: this._sessionId,
|
|
111
|
+
networkCalls: 0,
|
|
112
|
+
fpsDrop: false
|
|
113
|
+
}, startTime);
|
|
114
|
+
this._notifyWaterfall?.({
|
|
115
|
+
phase: "start",
|
|
116
|
+
target,
|
|
117
|
+
sessionId: this._sessionId,
|
|
118
|
+
startTime
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Auto-close after 2s
|
|
122
|
+
this._sessionTimer = setTimeout(() => this._closeSession(), SESSION_WINDOW_MS);
|
|
123
|
+
}
|
|
124
|
+
_closeSession() {
|
|
125
|
+
if (this._sessionTimer) {
|
|
126
|
+
clearTimeout(this._sessionTimer);
|
|
127
|
+
this._sessionTimer = null;
|
|
128
|
+
}
|
|
129
|
+
if (this._session.isOpen) {
|
|
130
|
+
const target = this._session.get("target");
|
|
131
|
+
const startTime = this._session.startTime;
|
|
132
|
+
const endTime = performance.now();
|
|
133
|
+
this._session.close({
|
|
134
|
+
sessionId: this._sessionId,
|
|
135
|
+
networkCalls: this._sessionNetworkCalls,
|
|
136
|
+
fpsDrop: this._sessionFpsDrop
|
|
137
|
+
}, endTime, this._sessionFpsDrop ? "error" : "secondary");
|
|
138
|
+
this._notifyWaterfall?.({
|
|
139
|
+
phase: "end",
|
|
140
|
+
target,
|
|
141
|
+
sessionId: this._sessionId,
|
|
142
|
+
networkCalls: this._sessionNetworkCalls,
|
|
143
|
+
fpsDrop: this._sessionFpsDrop,
|
|
144
|
+
startTime,
|
|
145
|
+
endTime,
|
|
146
|
+
durationMs: Math.round(endTime - startTime)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this._sessionId = null;
|
|
150
|
+
}
|
|
151
|
+
_onPointerDown(e) {
|
|
152
|
+
const startTime = performance.now();
|
|
153
|
+
const target = this._describeTarget(e.target);
|
|
154
|
+
const startMark = `${_constants.PREFIX.INTERACTION}:${e.pointerId}:start`;
|
|
155
|
+
const interactionId = `Tap #${++this._interactionSeq}`;
|
|
156
|
+
performance.mark(startMark, {
|
|
157
|
+
startTime
|
|
158
|
+
});
|
|
159
|
+
this._activeInteraction = {
|
|
160
|
+
id: interactionId,
|
|
161
|
+
startTime,
|
|
162
|
+
endTime: null
|
|
163
|
+
};
|
|
164
|
+
this._measure.marker(`${interactionId}: ${target}`, startTime, {
|
|
165
|
+
interactionId,
|
|
166
|
+
target
|
|
167
|
+
}, "secondary");
|
|
168
|
+
|
|
169
|
+
// The old 2s parent session
|
|
170
|
+
// this._openSession(target, startTime)
|
|
171
|
+
|
|
172
|
+
// Long-press detection timer
|
|
173
|
+
const longPressTimer = setTimeout(() => {
|
|
174
|
+
this._emitLongPress(e.pointerId, target, startTime);
|
|
175
|
+
}, _constants.THRESHOLD.LONG_PRESS_MS);
|
|
176
|
+
this._pendingTaps.set(e.pointerId, {
|
|
177
|
+
startMark,
|
|
178
|
+
startTime,
|
|
179
|
+
target,
|
|
180
|
+
interactionId,
|
|
181
|
+
longPressTimer
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Measure tap → next paint (interaction responsiveness)
|
|
185
|
+
requestAnimationFrame(() => {
|
|
186
|
+
const endTime = performance.now();
|
|
187
|
+
const tap = this._pendingTaps.get(e.pointerId);
|
|
188
|
+
if (!tap) return;
|
|
189
|
+
tap.frameComplete = true;
|
|
190
|
+
if (tap.released) this._pendingTaps.delete(e.pointerId);
|
|
191
|
+
this._rememberInteraction(interactionId, startTime, endTime);
|
|
192
|
+
const responseMs = Math.round(endTime - startTime);
|
|
193
|
+
if (responseMs > _constants.THRESHOLD.INTERACTION_SLOW_MS) {
|
|
194
|
+
this._notifyInsights?.({
|
|
195
|
+
target,
|
|
196
|
+
responseMs,
|
|
197
|
+
startTime,
|
|
198
|
+
endTime,
|
|
199
|
+
interactionId
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
this._measure.emit(`${interactionId}: ${target} - ${responseMs}ms`, startMark, endTime, {
|
|
203
|
+
responseMs,
|
|
204
|
+
loafOverlap: this._measure.overlapsLoaf(startTime, endTime),
|
|
205
|
+
target,
|
|
206
|
+
interactionId,
|
|
207
|
+
simulatorValid: false
|
|
208
|
+
}, _constants.TRACK.INTERACTION);
|
|
209
|
+
this._notifyWaterfall?.({
|
|
210
|
+
phase: "end",
|
|
211
|
+
target,
|
|
212
|
+
sessionId: interactionId,
|
|
213
|
+
networkCalls: 0,
|
|
214
|
+
fpsDrop: false,
|
|
215
|
+
startTime,
|
|
216
|
+
endTime,
|
|
217
|
+
durationMs: responseMs,
|
|
218
|
+
responseMs,
|
|
219
|
+
shortInteraction: true
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
_onPointerUp(e) {
|
|
224
|
+
const tap = this._pendingTaps.get(e.pointerId);
|
|
225
|
+
if (tap) {
|
|
226
|
+
clearTimeout(tap.longPressTimer);
|
|
227
|
+
tap.released = true;
|
|
228
|
+
if (tap.frameComplete) this._pendingTaps.delete(e.pointerId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
_onPointerCancel(e) {
|
|
232
|
+
const tap = this._pendingTaps.get(e.pointerId);
|
|
233
|
+
if (!tap) return;
|
|
234
|
+
clearTimeout(tap.longPressTimer);
|
|
235
|
+
this._pendingTaps.delete(e.pointerId);
|
|
236
|
+
if (this._activeInteraction?.id === tap.interactionId) this._activeInteraction = null;
|
|
237
|
+
}
|
|
238
|
+
_rememberInteraction(id, startTime, endTime) {
|
|
239
|
+
this._recentInteractions.push({
|
|
240
|
+
id,
|
|
241
|
+
startTime,
|
|
242
|
+
endTime
|
|
243
|
+
});
|
|
244
|
+
if (this._recentInteractions.length > MAX_RECENT_INTERACTIONS) {
|
|
245
|
+
this._recentInteractions.shift();
|
|
246
|
+
}
|
|
247
|
+
if (this._activeInteraction?.id === id) this._activeInteraction = null;
|
|
248
|
+
}
|
|
249
|
+
_emitLongPress(pointerId, target, startTime) {
|
|
250
|
+
const tap = this._pendingTaps.get(pointerId);
|
|
251
|
+
if (!tap) return;
|
|
252
|
+
const endTime = performance.now();
|
|
253
|
+
const heldMs = Math.round(endTime - startTime);
|
|
254
|
+
const markName = `${_constants.PREFIX.LONG_PRESS}:${Math.round(startTime)}`;
|
|
255
|
+
performance.mark(markName, {
|
|
256
|
+
startTime
|
|
257
|
+
});
|
|
258
|
+
this._measure.emit(`${tap.interactionId}: Long press ${target} - ${heldMs}ms`, markName, endTime, {
|
|
259
|
+
target,
|
|
260
|
+
heldMs,
|
|
261
|
+
interactionId: tap.interactionId,
|
|
262
|
+
simulatorValid: true
|
|
263
|
+
}, _constants.TRACK.INTERACTION, "tertiary");
|
|
264
|
+
}
|
|
265
|
+
_describeTarget(el) {
|
|
266
|
+
if (!el) return "unknown";
|
|
267
|
+
const tag = el.tagName?.toLowerCase() ?? "unknown";
|
|
268
|
+
const id = el.id ? `#${el.id}` : "";
|
|
269
|
+
const cls = el.classList?.length ? `.${[...el.classList].slice(0, 2).join(".")}` : "";
|
|
270
|
+
return `${tag}${id}${cls}`.slice(0, 40);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
exports.InteractionCollector = InteractionCollector;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.KeyboardCollector = void 0;
|
|
7
|
+
var _constants = require("../core/constants.js");
|
|
8
|
+
var _session = require("../core/session.js");
|
|
9
|
+
/**
|
|
10
|
+
* KeyboardCollector.js
|
|
11
|
+
*
|
|
12
|
+
* Instruments: bugs #3, #12
|
|
13
|
+
* #3 keyboard sticky scroll → keyboard-show → viewport-resize → scroll fires during session
|
|
14
|
+
* if sticky element recalcs during this window it causes thrash
|
|
15
|
+
* #12 search input no auto-focus → keyboard-show event should fire after route transition to search
|
|
16
|
+
* if it doesn't, the native WebView suppressed programmatic focus
|
|
17
|
+
*
|
|
18
|
+
* Native events received (via index.js router):
|
|
19
|
+
* keyboard-show { nativeTime, keyboardHeight }
|
|
20
|
+
* keyboard-hide { nativeTime }
|
|
21
|
+
*
|
|
22
|
+
* Spans emitted:
|
|
23
|
+
* catalyst:session/keyboard — keyboard-show → viewport settled (Track: Keyboard)
|
|
24
|
+
* detail: keyboardHeight, viewportDelta, hadScrollDuringSession
|
|
25
|
+
* catalyst:viewport-resize — visualViewport resize → next rAF (Track: Keyboard)
|
|
26
|
+
* detail: deltaHeight
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
class KeyboardCollector {
|
|
30
|
+
constructor(measure, nativeToWeb) {
|
|
31
|
+
this._measure = measure;
|
|
32
|
+
this._nativeToWeb = nativeToWeb;
|
|
33
|
+
this._session = new _session.Session(_constants.PREFIX.SESSION_KEYBOARD, measure, _constants.TRACK.INPUT);
|
|
34
|
+
}
|
|
35
|
+
init() {
|
|
36
|
+
this._observeVisualViewport();
|
|
37
|
+
}
|
|
38
|
+
setWaterfallSource(fn) {
|
|
39
|
+
this._notifyWaterfall = fn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Native event handlers ────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
onKeyboardShow(event) {
|
|
45
|
+
const webNow = this._nativeToWeb(event.nativeTime);
|
|
46
|
+
this._session.open({
|
|
47
|
+
keyboardHeight: event.keyboardHeight,
|
|
48
|
+
hadScroll: false
|
|
49
|
+
}, webNow);
|
|
50
|
+
}
|
|
51
|
+
onKeyboardHide(event) {
|
|
52
|
+
const webNow = this._nativeToWeb(event.nativeTime);
|
|
53
|
+
if (this._session.isOpen) {
|
|
54
|
+
this._closeSession(webNow);
|
|
55
|
+
}
|
|
56
|
+
// Emit a point mark so keyboard-hide is visible on the timeline
|
|
57
|
+
const hideMark = `catalyst:keyboard-hide:${Math.round(webNow)}`;
|
|
58
|
+
performance.mark(hideMark, {
|
|
59
|
+
startTime: webNow
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Called by ScrollCollector (via index.js) if scroll fires while keyboard open
|
|
64
|
+
onScrollDuringKeyboard() {
|
|
65
|
+
if (this._session.isOpen) {
|
|
66
|
+
this._session.set("hadScroll", true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Viewport resize ──────────────────────────────────────────────────────
|
|
71
|
+
// Fires when keyboard pushes/pulls the viewport.
|
|
72
|
+
// Also closes the keyboard session — viewport settling = keyboard done.
|
|
73
|
+
|
|
74
|
+
_observeVisualViewport() {
|
|
75
|
+
if (!window.visualViewport) return;
|
|
76
|
+
let lastHeight = window.visualViewport.height;
|
|
77
|
+
window.visualViewport.addEventListener("resize", () => {
|
|
78
|
+
const now = performance.now();
|
|
79
|
+
const newHeight = window.visualViewport.height;
|
|
80
|
+
const delta = Math.abs(newHeight - lastHeight);
|
|
81
|
+
lastHeight = newHeight;
|
|
82
|
+
if (delta < _constants.THRESHOLD.VIEWPORT_MIN_DELTA) return;
|
|
83
|
+
const markName = `${_constants.PREFIX.VIEWPORT_RESIZE}:start:${Math.round(now)}`;
|
|
84
|
+
performance.mark(markName, {
|
|
85
|
+
startTime: now
|
|
86
|
+
});
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
const endTime = performance.now();
|
|
89
|
+
const durationMs = Math.round(endTime - now);
|
|
90
|
+
this._measure.emit(_constants.PREFIX.VIEWPORT_RESIZE, markName, endTime, {
|
|
91
|
+
deltaHeight: Math.round(delta),
|
|
92
|
+
simulatorValid: true
|
|
93
|
+
}, _constants.TRACK.INPUT);
|
|
94
|
+
this._notifyWaterfall?.({
|
|
95
|
+
metric: "viewport-resize",
|
|
96
|
+
startTime: now,
|
|
97
|
+
endTime,
|
|
98
|
+
durationMs,
|
|
99
|
+
deltaHeight: Math.round(delta)
|
|
100
|
+
});
|
|
101
|
+
if (this._session.isOpen) {
|
|
102
|
+
this._closeSession(endTime);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
_closeSession(endTime) {
|
|
108
|
+
const startTime = this._session.startTime;
|
|
109
|
+
const detail = {
|
|
110
|
+
keyboardHeight: this._session.get("keyboardHeight"),
|
|
111
|
+
hadScrollDuringSession: this._session.get("hadScroll"),
|
|
112
|
+
simulatorValid: false
|
|
113
|
+
};
|
|
114
|
+
this._session.close(detail, endTime);
|
|
115
|
+
this._notifyWaterfall?.({
|
|
116
|
+
kind: "keyboard",
|
|
117
|
+
startTime,
|
|
118
|
+
endTime,
|
|
119
|
+
durationMs: Math.round(endTime - startTime),
|
|
120
|
+
...detail
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.KeyboardCollector = KeyboardCollector;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.NavigationCollector = void 0;
|
|
7
|
+
var _constants = require("../core/constants.js");
|
|
8
|
+
var _session = require("../core/session.js");
|
|
9
|
+
/**
|
|
10
|
+
* NavigationCollector.js
|
|
11
|
+
*
|
|
12
|
+
* Instruments: bugs #2, #7, #9, #11, #14
|
|
13
|
+
* #2 back-forward reloads screen → new nav session opens, cache miss burst follows
|
|
14
|
+
* #7 header disappears on back → route-transition span shows animation torn down early
|
|
15
|
+
* #9 forward transition broken → route-transition fires but no visual end mark
|
|
16
|
+
* #11 banner overlaps search box → nav session ends but layout-shift fires after
|
|
17
|
+
* #14 transitions not soft → skeleton→content swap visible as layout-shift during nav
|
|
18
|
+
*
|
|
19
|
+
* Spans emitted:
|
|
20
|
+
* catalyst:session/navigation — page start → LCP finalized (Track: Navigation)
|
|
21
|
+
* catalyst:route-transition|<from→to> — popstate/hashchange → rAF settle (Track: Navigation)
|
|
22
|
+
* catalyst:lcp|<ms>ms — LCP candidate finalized (Track: Navigation)
|
|
23
|
+
* catalyst:fcp|<ms>ms — FCP (iOS fallback) (Track: Navigation)
|
|
24
|
+
* catalyst:page-load|<url> — native page-load-start → page-load-end (Track: Navigation)
|
|
25
|
+
* catalyst:cold-start|<ms>ms — native app cold start duration (Track: Navigation)
|
|
26
|
+
* catalyst:boot/activity-created — batch: MainActivity.onCreate (Track: Navigation)
|
|
27
|
+
* catalyst:boot/webview-constructed — batch: CustomWebView init{} (Track: Navigation)
|
|
28
|
+
* catalyst:boot/load-url — batch: WebView.loadUrl() call (Track: Navigation)
|
|
29
|
+
* catalyst:boot/page-started — batch: onPageStarted callback (Track: Navigation)
|
|
30
|
+
* catalyst:boot/page-finished — batch: onPageFinished callback (Track: Navigation)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
class NavigationCollector {
|
|
34
|
+
constructor(measure, nativeToWeb) {
|
|
35
|
+
this._measure = measure;
|
|
36
|
+
this._nativeToWeb = nativeToWeb ?? (t => t);
|
|
37
|
+
this._session = new _session.Session(_constants.PREFIX.SESSION_NAV, measure, _constants.TRACK.NAVIGATION);
|
|
38
|
+
this._pendingLcp = null; // { entry, label } — buffered until finalized
|
|
39
|
+
this._lcpFinalized = false;
|
|
40
|
+
// Track pending native page-load spans: url → { markName, startWeb }
|
|
41
|
+
this._pendingPageLoads = new Map();
|
|
42
|
+
}
|
|
43
|
+
init() {
|
|
44
|
+
this._session.open();
|
|
45
|
+
this._observeLCP();
|
|
46
|
+
this._observeRouteTransitions();
|
|
47
|
+
this._setupLcpFinalization();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Called by index.js to feed the normalized perf store. */
|
|
51
|
+
setWaterfallSource(fn) {
|
|
52
|
+
this._notifyWaterfall = fn;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Called by other collectors to check if nav is still loading
|
|
56
|
+
get isLoading() {
|
|
57
|
+
return this._session.isOpen;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── LCP ──────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
_observeLCP() {
|
|
63
|
+
const types = PerformanceObserver.supportedEntryTypes ?? [];
|
|
64
|
+
const type = types.includes("largest-contentful-paint") ? "largest-contentful-paint" : types.includes("paint") ? "paint" : null;
|
|
65
|
+
if (!type) return;
|
|
66
|
+
const obs = new PerformanceObserver(list => {
|
|
67
|
+
for (const entry of list.getEntries()) {
|
|
68
|
+
if (type === "paint" && entry.name !== "first-contentful-paint") continue;
|
|
69
|
+
const label = type === "largest-contentful-paint" ? "lcp" : "fcp";
|
|
70
|
+
this._pendingLcp = {
|
|
71
|
+
entry,
|
|
72
|
+
label
|
|
73
|
+
}; // overwrite — keep latest candidate
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
obs.observe({
|
|
77
|
+
type,
|
|
78
|
+
buffered: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
_setupLcpFinalization() {
|
|
82
|
+
// Browser stops updating LCP candidate on first user interaction or page hide
|
|
83
|
+
const finalize = () => {
|
|
84
|
+
this._finalizeLcp();
|
|
85
|
+
document.removeEventListener("pointerdown", finalize);
|
|
86
|
+
document.removeEventListener("keydown", finalize);
|
|
87
|
+
};
|
|
88
|
+
document.addEventListener("pointerdown", finalize);
|
|
89
|
+
document.addEventListener("keydown", finalize);
|
|
90
|
+
document.addEventListener("visibilitychange", () => {
|
|
91
|
+
if (document.visibilityState === "hidden") this._finalizeLcp();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
_finalizeLcp() {
|
|
95
|
+
if (this._lcpFinalized || !this._pendingLcp) return;
|
|
96
|
+
this._lcpFinalized = true;
|
|
97
|
+
const {
|
|
98
|
+
entry,
|
|
99
|
+
label
|
|
100
|
+
} = this._pendingLcp;
|
|
101
|
+
this._pendingLcp = null;
|
|
102
|
+
const markName = `${_constants.PREFIX[label.toUpperCase()]}:mark`;
|
|
103
|
+
performance.mark(markName, {
|
|
104
|
+
startTime: 0
|
|
105
|
+
});
|
|
106
|
+
this._measure.emit(`${_constants.PREFIX[label.toUpperCase()]}|${Math.round(entry.startTime)}ms`, markName, entry.startTime, {
|
|
107
|
+
renderTime: entry.renderTime ?? null,
|
|
108
|
+
loadTime: entry.loadTime ?? null,
|
|
109
|
+
simulatorValid: false
|
|
110
|
+
}, _constants.TRACK.NAVIGATION);
|
|
111
|
+
this._notifyWaterfall?.({
|
|
112
|
+
label: label.toUpperCase(),
|
|
113
|
+
startTime: 0,
|
|
114
|
+
endTime: entry.startTime,
|
|
115
|
+
durationMs: Math.round(entry.startTime)
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// LCP finalized = page is loaded — close the navigation session
|
|
119
|
+
this._session.close({
|
|
120
|
+
simulatorValid: false
|
|
121
|
+
}, entry.startTime);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Route transitions ────────────────────────────────────────────────────
|
|
125
|
+
// Detects SPA route changes via popstate + hashchange.
|
|
126
|
+
// Emits a span from event fire → next rAF (visual settle).
|
|
127
|
+
//
|
|
128
|
+
// Covers:
|
|
129
|
+
// #7 — if rAF never fires cleanly, transition span shows the gap
|
|
130
|
+
// #9 — span fires on back but not on some forward routes → missing spans = broken routes
|
|
131
|
+
// #11 — nav session re-opens; layout-shift collector shows shift after session close
|
|
132
|
+
|
|
133
|
+
_observeRouteTransitions() {
|
|
134
|
+
let _fromPath = location.pathname + location.hash;
|
|
135
|
+
const onTransition = e => {
|
|
136
|
+
const from = _fromPath;
|
|
137
|
+
const to = location.pathname + location.hash;
|
|
138
|
+
_fromPath = to;
|
|
139
|
+
const startTime = performance.now();
|
|
140
|
+
const markName = `${_constants.PREFIX.ROUTE_TRANSITION}:start:${Math.round(startTime)}`;
|
|
141
|
+
performance.mark(markName, {
|
|
142
|
+
startTime
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Re-open nav session for the SPA route change.
|
|
146
|
+
// LCP only fires once per WebView load — it does not reset on pushState.
|
|
147
|
+
// So we can't wait for LCP to close the session. Instead, close it after
|
|
148
|
+
// the next rAF (visual settle) + a short timeout to catch deferred renders.
|
|
149
|
+
if (!this._session.isOpen) {
|
|
150
|
+
this._lcpFinalized = false;
|
|
151
|
+
this._session.open({}, startTime);
|
|
152
|
+
this._setupLcpFinalization();
|
|
153
|
+
// Fallback: if LCP doesn't fire (SPA), close after rAF + 1s
|
|
154
|
+
requestAnimationFrame(() => {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (this._session.isOpen && !this._lcpFinalized) {
|
|
157
|
+
this._session.close({
|
|
158
|
+
simulatorValid: true
|
|
159
|
+
}, performance.now());
|
|
160
|
+
}
|
|
161
|
+
}, 1000);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
requestAnimationFrame(() => {
|
|
165
|
+
const endTime = performance.now();
|
|
166
|
+
this._measure.emit(`${_constants.PREFIX.ROUTE_TRANSITION}|${from}→${to}`, markName, endTime, {
|
|
167
|
+
from,
|
|
168
|
+
to,
|
|
169
|
+
type: e.type,
|
|
170
|
+
// 'popstate' or 'hashchange'
|
|
171
|
+
simulatorValid: true
|
|
172
|
+
}, _constants.TRACK.NAVIGATION);
|
|
173
|
+
this._notifyWaterfall?.({
|
|
174
|
+
label: `Route ${to}`,
|
|
175
|
+
startTime,
|
|
176
|
+
endTime,
|
|
177
|
+
durationMs: Math.round(endTime - startTime)
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
window.addEventListener("popstate", onTransition);
|
|
182
|
+
window.addEventListener("hashchange", onTransition);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Native page-load events (from CustomWebview.kt) ─────────────────────
|
|
186
|
+
// page-load-start: fired by onPageStarted, carries nativeTime + url
|
|
187
|
+
// page-load-end: fired by onPageFinished, carries nativeTime + url + durationMs
|
|
188
|
+
// page-load-error: fired by onReceivedError, carries nativeTime + url + errorCode
|
|
189
|
+
|
|
190
|
+
onNativeEvent(event) {
|
|
191
|
+
switch (event.type) {
|
|
192
|
+
case "page-load-start":
|
|
193
|
+
{
|
|
194
|
+
const startWeb = this._nativeToWeb(event.nativeTime);
|
|
195
|
+
const markName = `${_constants.PREFIX.PAGE_LOAD}:start:${Math.round(startWeb)}`;
|
|
196
|
+
performance.mark(markName, {
|
|
197
|
+
startTime: startWeb
|
|
198
|
+
});
|
|
199
|
+
this._pendingPageLoads.set(event.url, {
|
|
200
|
+
markName,
|
|
201
|
+
startWeb
|
|
202
|
+
});
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "page-load-end":
|
|
206
|
+
{
|
|
207
|
+
const pending = this._pendingPageLoads.get(event.url);
|
|
208
|
+
if (!pending) break;
|
|
209
|
+
this._pendingPageLoads.delete(event.url);
|
|
210
|
+
const endWeb = this._nativeToWeb(event.nativeTime);
|
|
211
|
+
const durMs = event.durationMs ?? Math.round(endWeb - pending.startWeb);
|
|
212
|
+
const isSlow = durMs > 2500;
|
|
213
|
+
this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}|${event.url.split("/").pop() || event.url}`, pending.markName, endWeb, {
|
|
214
|
+
url: event.url,
|
|
215
|
+
durationMs: durMs,
|
|
216
|
+
simulatorValid: true
|
|
217
|
+
}, _constants.TRACK.NAVIGATION, isSlow ? "error" : "primary");
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "page-load-error":
|
|
221
|
+
{
|
|
222
|
+
const pending = this._pendingPageLoads.get(event.url);
|
|
223
|
+
const startWeb = pending ? pending.startWeb : this._nativeToWeb(event.nativeTime);
|
|
224
|
+
const markName = pending ? pending.markName : `${_constants.PREFIX.PAGE_LOAD}:err:${Math.round(startWeb)}`;
|
|
225
|
+
if (!pending) performance.mark(markName, {
|
|
226
|
+
startTime: startWeb
|
|
227
|
+
});
|
|
228
|
+
this._pendingPageLoads.delete(event.url);
|
|
229
|
+
const errEnd = this._nativeToWeb(event.nativeTime) + 1;
|
|
230
|
+
this._measure.emit(`${_constants.PREFIX.PAGE_LOAD}/error|${event.errorCode ?? "?"}`, markName, errEnd, {
|
|
231
|
+
url: event.url,
|
|
232
|
+
errorCode: event.errorCode,
|
|
233
|
+
description: event.description ?? "",
|
|
234
|
+
simulatorValid: true
|
|
235
|
+
}, _constants.TRACK.NAVIGATION, "error");
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "cold-start":
|
|
239
|
+
{
|
|
240
|
+
// emitted by markAppStartComplete() — point-in-time span at page init
|
|
241
|
+
const startWeb = this._nativeToWeb(event.nativeTime);
|
|
242
|
+
const markName = `${_constants.PREFIX.COLD_START}:mark`;
|
|
243
|
+
performance.mark(markName, {
|
|
244
|
+
startTime: Math.max(0, startWeb - event.durationMs)
|
|
245
|
+
});
|
|
246
|
+
this._measure.emit(`${_constants.PREFIX.COLD_START}|${event.durationMs}ms`, markName, startWeb, {
|
|
247
|
+
durationMs: event.durationMs,
|
|
248
|
+
simulatorValid: true
|
|
249
|
+
}, _constants.TRACK.NAVIGATION, event.durationMs > 3000 ? "error" : "secondary");
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Batch boot timing events ─────────────────────────────────────────────
|
|
256
|
+
// Emitted from PerfEventBuffer flush after onPageFinished.
|
|
257
|
+
// Each boot-* event is a point-in-time marker on Catalyst > Navigation.
|
|
258
|
+
|
|
259
|
+
onBatchEvent(event) {
|
|
260
|
+
if (event.type === "navigation-back") {
|
|
261
|
+
const t = this._nativeToWeb(event.nativeTime);
|
|
262
|
+
const markName = `catalyst:navigation/back:${Math.round(t)}`;
|
|
263
|
+
performance.mark(markName, {
|
|
264
|
+
startTime: Math.max(0, t)
|
|
265
|
+
});
|
|
266
|
+
this._measure.emit("catalyst:navigation/back", markName, Math.max(0, t) + 1, {
|
|
267
|
+
simulatorValid: false
|
|
268
|
+
}, _constants.TRACK.NAVIGATION, _constants.COLOR.SECONDARY);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const bootTypeMap = {
|
|
272
|
+
"boot-activity-created": "activity-created",
|
|
273
|
+
"boot-webview-constructed": "webview-constructed",
|
|
274
|
+
"boot-load-url": "load-url",
|
|
275
|
+
"boot-page-started": "page-started",
|
|
276
|
+
"boot-page-finished": "page-finished"
|
|
277
|
+
};
|
|
278
|
+
const label = bootTypeMap[event.type];
|
|
279
|
+
if (!label) return;
|
|
280
|
+
const t = this._nativeToWeb(event.nativeTime);
|
|
281
|
+
const markName = `catalyst:boot/${label}:${Math.round(t)}`;
|
|
282
|
+
performance.mark(markName, {
|
|
283
|
+
startTime: Math.max(0, t)
|
|
284
|
+
});
|
|
285
|
+
this._measure.emit(`catalyst:boot/${label}`, markName, Math.max(0, t) + 1,
|
|
286
|
+
// point span
|
|
287
|
+
{
|
|
288
|
+
nativeTime: event.nativeTime,
|
|
289
|
+
thread: event.thread ?? null,
|
|
290
|
+
simulatorValid: false
|
|
291
|
+
}, _constants.TRACK.NAVIGATION, _constants.COLOR.SECONDARY_LIGHT);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
exports.NavigationCollector = NavigationCollector;
|