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.
Files changed (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkTimingCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
9
+ * NetworkTimingCollector.js
10
+ *
11
+ * Adds lightweight Catalyst timeline spans for fetch() and XMLHttpRequest.
12
+ * Chrome already has detailed network timing; these spans exist to attach
13
+ * network work to Catalyst interaction sessions via sessionId.
14
+ */
15
+
16
+ const FETCH_WRAPPED = "__catalystNetworkTimingFetchWrapped";
17
+ const XHR_WRAPPED = "__catalystNetworkTimingXhrWrapped";
18
+ const XHR_META = "__catalystNetworkTimingMeta";
19
+ const MAX_LABEL_LENGTH = 72;
20
+ class NetworkTimingCollector {
21
+ constructor(measure) {
22
+ this._measure = measure;
23
+ this._getInteraction = null;
24
+ this._requestSeq = 0;
25
+ }
26
+ setInteractionSource(fn) {
27
+ this._getInteraction = fn;
28
+ }
29
+ setWaterfallSource(fn) {
30
+ this._notifyWaterfall = fn;
31
+ }
32
+ init() {
33
+ if (typeof window === "undefined") return;
34
+ this._wrapFetch();
35
+ this._wrapXhr();
36
+ }
37
+ _wrapFetch() {
38
+ const originalFetch = window.fetch;
39
+ if (typeof originalFetch !== "function" || originalFetch[FETCH_WRAPPED]) return;
40
+ const collector = this;
41
+ function catalystFetch(input, init) {
42
+ const span = collector._start("fetch", init?.method ?? input?.method ?? "GET", init?.url ?? input?.url ?? input);
43
+ try {
44
+ return originalFetch.apply(this, arguments).then(response => {
45
+ collector._finish(span, {
46
+ status: response?.status ?? null,
47
+ ok: response?.ok ?? null
48
+ });
49
+ return response;
50
+ }, error => {
51
+ collector._finish(span, {
52
+ outcome: "error",
53
+ ok: false
54
+ });
55
+ throw error;
56
+ });
57
+ } catch (error) {
58
+ collector._finish(span, {
59
+ outcome: "error",
60
+ ok: false
61
+ });
62
+ throw error;
63
+ }
64
+ }
65
+ catalystFetch[FETCH_WRAPPED] = true;
66
+ window.fetch = catalystFetch;
67
+ }
68
+ _wrapXhr() {
69
+ const proto = window.XMLHttpRequest?.prototype;
70
+ if (!proto || proto[XHR_WRAPPED]) return;
71
+ if (typeof proto.open !== "function" || typeof proto.send !== "function") return;
72
+ const originalOpen = proto.open;
73
+ const originalSend = proto.send;
74
+ const collector = this;
75
+ proto.open = function catalystXhrOpen(method, url) {
76
+ this[XHR_META] = {
77
+ method,
78
+ url
79
+ };
80
+ return originalOpen.apply(this, arguments);
81
+ };
82
+ proto.send = function catalystXhrSend() {
83
+ const meta = this[XHR_META] ?? {};
84
+ const span = collector._start("xhr", meta.method ?? "GET", meta.url ?? "unknown");
85
+ let emitted = false;
86
+ const finish = (outcome = "complete") => {
87
+ if (emitted) return;
88
+ emitted = true;
89
+ const status = this.status;
90
+ collector._finish(span, {
91
+ status,
92
+ ok: collector._statusOk(status),
93
+ outcome
94
+ });
95
+ };
96
+ this.addEventListener?.("loadend", () => finish(), {
97
+ once: true
98
+ });
99
+ try {
100
+ return originalSend.apply(this, arguments);
101
+ } catch (error) {
102
+ finish("error");
103
+ throw error;
104
+ }
105
+ };
106
+ proto[XHR_WRAPPED] = true;
107
+ }
108
+ _start(api, method, rawUrl) {
109
+ const startTime = performance.now();
110
+ const normalizedMethod = String(method || "GET").toUpperCase();
111
+ const url = this._normalizeUrl(rawUrl);
112
+ const label = this._labelForUrl(url);
113
+ const requestId = `${api}:${++this._requestSeq}`;
114
+ const startMark = `${_constants.PREFIX.NETWORK_TIMING}:${requestId}:start`;
115
+ performance.mark(startMark, {
116
+ startTime
117
+ });
118
+ const interaction = this._getInteraction?.();
119
+ const interactionId = interaction?.activeInteractionId ?? null;
120
+ if (interactionId) interaction?.onNetworkCall?.();
121
+ return {
122
+ api,
123
+ method: normalizedMethod,
124
+ url,
125
+ label,
126
+ requestId,
127
+ startMark,
128
+ startTime,
129
+ interactionId
130
+ };
131
+ }
132
+ _finish(span, result = {}) {
133
+ const endTime = performance.now();
134
+ const durationMs = Math.round(endTime - span.startTime);
135
+ const outcome = result.outcome ?? "complete";
136
+ const ok = result.ok ?? this._statusOk(result.status);
137
+ const color = outcome !== "complete" || ok === false || durationMs > _constants.THRESHOLD.FETCH_SLOW_MS ? _constants.COLOR.ERROR : _constants.COLOR.SECONDARY;
138
+ const detail = {
139
+ api: span.api,
140
+ method: span.method,
141
+ url: span.url,
142
+ status: result.status ?? null,
143
+ ok,
144
+ outcome,
145
+ durationMs,
146
+ sessionId: span.interactionId,
147
+ requestId: span.requestId,
148
+ simulatorValid: true
149
+ };
150
+ this._notifyWaterfall?.({
151
+ ...detail,
152
+ startTime: span.startTime,
153
+ endTime
154
+ });
155
+ this._measure.emit(`${span.interactionId ? `[${span.interactionId}] ` : ""}Network: ${span.method} ${span.label} - ${durationMs}ms`, span.startMark, endTime, {
156
+ interactionId: span.interactionId,
157
+ ...detail,
158
+ sessionId: undefined
159
+ }, _constants.TRACK.NETWORK, color);
160
+ }
161
+ _normalizeUrl(rawUrl) {
162
+ const value = rawUrl?.url ?? rawUrl ?? "unknown";
163
+ try {
164
+ const parsed = new URL(String(value), window.location?.href);
165
+ return `${parsed.origin}${parsed.pathname}`;
166
+ } catch {
167
+ return String(value).split("?")[0].split("#")[0];
168
+ }
169
+ }
170
+ _labelForUrl(url) {
171
+ try {
172
+ const parsed = new URL(url, window.location?.href);
173
+ const label = parsed.origin === window.location?.origin ? parsed.pathname : `${parsed.host}${parsed.pathname}`;
174
+ return (label || "/").slice(0, MAX_LABEL_LENGTH);
175
+ } catch {
176
+ return String(url || "unknown").slice(0, MAX_LABEL_LENGTH);
177
+ }
178
+ }
179
+ _statusOk(status) {
180
+ return typeof status === "number" && status >= 200 && status < 400;
181
+ }
182
+ }
183
+ exports.NetworkTimingCollector = NetworkTimingCollector;
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RenderCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
9
+ * RenderCollector.js
10
+ *
11
+ * Instruments: bugs #1, #6, #10, #14, #15, #16
12
+ * #1 page slowness / cache miss → LoAF during navigation session = render blocked by loading
13
+ * #6 blank screens → LoAF blocks first render; LCP may never fire
14
+ * #10 footer flash on Drug/OTC → layout-shift fires just after navigation session closes
15
+ * #14 transitions not soft → layout-shift during nav = skeleton→content swap too abrupt
16
+ * #15 quantity selector slow → LoAF burst in the 2s after interaction (lazy chunk blocks)
17
+ * #16 prescription screen touch lag → same LoAF burst pattern after tap
18
+ *
19
+ * Spans emitted:
20
+ * catalyst:loaf|<ms>ms — long animation frame (Track: Render)
21
+ * detail: blockingDuration, scriptCount, sessionContext
22
+ * catalyst:layout-shift|<value> — unexpected layout shift (Track: Render)
23
+ * detail: value, sessionContext
24
+ * catalyst:fps-drop|<fps>fps-min|<ms>ms — native fps-drop episode (Track: Render)
25
+ * detail: minFps, avgFps, durationMs, sessionContext
26
+ * closes on recovery >55fps OR after 10s max
27
+ * catalyst:long-task|<ms>ms — native long task > 16ms (Track: Render)
28
+ * catalyst:mem/jvm|<MB>MB — JVM heap PSS — Kotlin/Java objects (Track: Render, blue)
29
+ * catalyst:mem/native|<MB>MB — native heap PSS — V8 + Blink + JNI, WebView proxy (Track: Render, orange)
30
+ * catalyst:mem/total|<MB>MB — total process PSS — what Android OOM killer sees (Track: Render, purple→red)
31
+ *
32
+ * Notifies:
33
+ * ScrollCollector.onLoaf() — so scroll session can increment its loafCount
34
+ * HookCollector.onLoaf() — so hook mount sessions can detect LoAF bursts
35
+ */
36
+
37
+ class RenderCollector {
38
+ constructor(measure, getSessionContext, nativeToWeb) {
39
+ this._measure = measure;
40
+ // getSessionContext() returns 'navigation'|'scroll'|'keyboard'|'none'
41
+ // provided by index.js so RenderCollector doesn't import other collectors
42
+ this._getSessionContext = getSessionContext;
43
+ this._nativeToWeb = nativeToWeb ?? (t => t);
44
+ this._loafListeners = []; // [fn] called on every LoAF
45
+ this._getInteraction = null; // set by index.js after construction
46
+ }
47
+
48
+ /** Called by index.js to wire up interaction session context. */
49
+ setInteractionSource(fn) {
50
+ this._getInteraction = fn;
51
+ }
52
+
53
+ /** Called by index.js to wire up insights notifications. */
54
+ setInsightsSource(fn) {
55
+ this._notifyInsights = fn;
56
+ }
57
+
58
+ /** Called by index.js to feed the normalized perf store. */
59
+ setWaterfallSource(fn) {
60
+ this._notifyWaterfall = fn;
61
+ }
62
+
63
+ // Allow other collectors to subscribe to LoAF events
64
+ addLoafListener(fn) {
65
+ this._loafListeners.push(fn);
66
+ }
67
+ init() {
68
+ this._observeLoAF();
69
+ this._observeLayoutShift();
70
+ }
71
+
72
+ // ─── Native MetricsMonitor events ────────────────────────────────────────
73
+
74
+ onNativeEvent(event) {
75
+ const t = this._nativeToWeb(event.nativeTime);
76
+ switch (event.type) {
77
+ case "fps-drop-episode":
78
+ {
79
+ const start = Math.max(0, this._nativeToWeb(event.startNativeTime));
80
+ const end = Math.max(start, this._nativeToWeb(event.endNativeTime));
81
+ const markName = `${_constants.PREFIX.FPS_DROP}:${Math.round(start)}`;
82
+ performance.mark(markName, {
83
+ startTime: start
84
+ });
85
+ const interactionId = this._getInteraction?.()?.interactionIdForRange(start, end) ?? null;
86
+ if (interactionId) this._getInteraction?.()?.onFpsDrop();
87
+ this._measure.emit(`${interactionId ? `[${interactionId}] ` : ""}FPS drop: ${event.minFps.toFixed(1)}fps - ${Math.round(event.durationMs)}ms`, markName, end, {
88
+ interactionId,
89
+ minFps: event.minFps,
90
+ avgFps: event.avgFps,
91
+ durationMs: event.durationMs,
92
+ sessionContext: this._getSessionContext(),
93
+ thread: event.thread ?? null,
94
+ simulatorValid: false
95
+ }, _constants.TRACK.RENDER, event.minFps < 30 ? "error" : "tertiary");
96
+ this._notifyWaterfall?.({
97
+ metric: "fps-drop",
98
+ startTime: start,
99
+ endTime: end,
100
+ durationMs: event.durationMs,
101
+ minFps: event.minFps,
102
+ avgFps: event.avgFps,
103
+ sessionId: interactionId
104
+ });
105
+ return {
106
+ interactionId,
107
+ startTime: start,
108
+ endTime: end
109
+ };
110
+ }
111
+ case "long-task":
112
+ {
113
+ const markName = `${_constants.PREFIX.LONG_TASK}:${Math.round(t)}`;
114
+ const interactionId = this._getInteraction?.()?.interactionIdForRange(t, t + event.durationMs) ?? null;
115
+ performance.mark(markName, {
116
+ startTime: t
117
+ });
118
+ this._measure.emit(`${interactionId ? `[${interactionId}] ` : ""}Long task: ${event.durationMs}ms`, markName, t + event.durationMs, {
119
+ interactionId,
120
+ durationMs: event.durationMs,
121
+ sessionContext: this._getSessionContext(),
122
+ thread: event.thread ?? null,
123
+ simulatorValid: true
124
+ }, _constants.TRACK.RENDER, event.durationMs > 100 ? "error" : "tertiary");
125
+ this._notifyWaterfall?.({
126
+ metric: "long-task",
127
+ startTime: t,
128
+ endTime: t + event.durationMs,
129
+ durationMs: event.durationMs,
130
+ sessionId: interactionId
131
+ });
132
+ break;
133
+ }
134
+ case "memory-snapshot":
135
+ {
136
+ const jvmMark = `catalyst:mem/jvm:${Math.round(t)}`;
137
+ performance.mark(jvmMark, {
138
+ startTime: t
139
+ });
140
+ this._measure.emit(`JVM memory: ${event.jvmMb.toFixed(1)}MB`, jvmMark, t + 1, {
141
+ memoryId: `memory-jvm-${Math.round(t)}`,
142
+ jvmMb: event.jvmMb,
143
+ peakMb: event.peakMb,
144
+ thread: event.thread ?? null,
145
+ simulatorValid: false,
146
+ ...(event.coldStartMs != null ? {
147
+ coldStartMs: event.coldStartMs
148
+ } : {})
149
+ }, _constants.TRACK.MEMORY, "primary");
150
+ const webviewMemoryId = `memory-webview-${Math.round(t)}`;
151
+ const webviewMark = `catalyst:mem/webview:${Math.round(t)}`;
152
+ performance.mark(webviewMark, {
153
+ startTime: t
154
+ });
155
+ this._measure.emit(`WebView memory: ${event.webviewMb.toFixed(1)}MB`, webviewMark, t + 1, {
156
+ memoryId: webviewMemoryId,
157
+ webviewMb: event.webviewMb,
158
+ otherMb: event.otherMb,
159
+ peakMb: event.peakMb,
160
+ thread: event.thread ?? null,
161
+ simulatorValid: false,
162
+ ...(event.coldStartMs != null ? {
163
+ coldStartMs: event.coldStartMs
164
+ } : {})
165
+ }, _constants.TRACK.MEMORY, "tertiary-dark");
166
+ const totalMark = `${_constants.PREFIX.MEMORY_TOTAL}:${Math.round(t)}`;
167
+ performance.mark(totalMark, {
168
+ startTime: t
169
+ });
170
+ this._measure.emit(`Total memory: ${event.totalMb.toFixed(1)}MB`, totalMark, t + 1, {
171
+ memoryId: `memory-total-${Math.round(t)}`,
172
+ totalMb: event.totalMb,
173
+ jvmMb: event.jvmMb,
174
+ webviewMb: event.webviewMb,
175
+ otherMb: event.otherMb,
176
+ peakMb: event.peakMb,
177
+ thread: event.thread ?? null,
178
+ simulatorValid: false,
179
+ ...(event.coldStartMs != null ? {
180
+ coldStartMs: event.coldStartMs
181
+ } : {})
182
+ }, _constants.TRACK.MEMORY, event.totalMb > 400 ? "error" : "secondary");
183
+ this._notifyInsights?.({
184
+ currentMb: event.webviewMb,
185
+ startTime: t,
186
+ endTime: t + 1,
187
+ memoryId: webviewMemoryId
188
+ });
189
+ this._notifyWaterfall?.({
190
+ metric: "memory",
191
+ startTime: t,
192
+ endTime: t + 1,
193
+ durationMs: 1,
194
+ totalMb: event.totalMb,
195
+ webviewMb: event.webviewMb
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ }
201
+
202
+ // ─── Long Animation Frames ────────────────────────────────────────────────
203
+
204
+ _observeLoAF() {
205
+ if (!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) return;
206
+ const obs = new PerformanceObserver(list => {
207
+ for (const entry of list.getEntries()) {
208
+ const start = entry.startTime;
209
+ const end = start + entry.duration;
210
+ const context = this._getSessionContext();
211
+
212
+ // Register with shared Measure so all collectors get overlap detection
213
+ this._measure.recordLoaf(start, end);
214
+
215
+ // Notify subscribers (ScrollCollector, HookCollector)
216
+ this._loafListeners.forEach(fn => fn(start, end));
217
+ const markName = `${_constants.PREFIX.LOAF}:${Math.round(start)}`;
218
+ performance.mark(markName, {
219
+ startTime: start
220
+ });
221
+ this._measure.emit(`${_constants.PREFIX.LOAF}|${Math.round(entry.duration)}ms`, markName, end, {
222
+ blockingDuration: entry.blockingDuration,
223
+ scriptCount: entry.scripts?.length ?? 0,
224
+ sessionContext: context,
225
+ simulatorValid: false
226
+ }, _constants.TRACK.RENDER);
227
+ this._notifyWaterfall?.({
228
+ metric: "loaf",
229
+ startTime: start,
230
+ endTime: end,
231
+ durationMs: entry.duration
232
+ });
233
+ }
234
+ });
235
+ obs.observe({
236
+ type: "long-animation-frame",
237
+ buffered: true
238
+ });
239
+ }
240
+
241
+ // ─── Layout shift ─────────────────────────────────────────────────────────
242
+
243
+ _observeLayoutShift() {
244
+ if (!PerformanceObserver.supportedEntryTypes?.includes("layout-shift")) return;
245
+ const obs = new PerformanceObserver(list => {
246
+ for (const entry of list.getEntries()) {
247
+ if (entry.hadRecentInput) continue; // user-initiated shift, ignore
248
+
249
+ const context = this._getSessionContext();
250
+ const markName = `catalyst:cls:${Math.round(entry.startTime)}`;
251
+ performance.mark(markName, {
252
+ startTime: entry.startTime
253
+ });
254
+ if (entry.value > 0.1) {
255
+ this._notifyInsights?.({
256
+ value: entry.value,
257
+ sessionContext: context,
258
+ startTime: entry.startTime,
259
+ endTime: entry.startTime + 1
260
+ });
261
+ }
262
+ this._measure.emit(`${_constants.PREFIX.LAYOUT_SHIFT}|${entry.value.toFixed(4)}`, markName, entry.startTime + 1,
263
+ // point-in-time event, minimal duration
264
+ {
265
+ value: entry.value,
266
+ sessionContext: context,
267
+ simulatorValid: false
268
+ }, _constants.TRACK.RENDER);
269
+ this._notifyWaterfall?.({
270
+ metric: "layout-shift",
271
+ startTime: entry.startTime,
272
+ endTime: entry.startTime + 1,
273
+ durationMs: 1,
274
+ value: entry.value
275
+ });
276
+ }
277
+ });
278
+ obs.observe({
279
+ type: "layout-shift",
280
+ buffered: true
281
+ });
282
+ }
283
+ }
284
+ exports.RenderCollector = RenderCollector;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ScrollCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ var _session = require("../core/session.js");
9
+ /**
10
+ * ScrollCollector.js
11
+ *
12
+ * Instruments: bugs #3, #13
13
+ * #3 keyboard sticky scroll bug → scroll fires during keyboard session → sticky recalc → layout thrash
14
+ * #13 cart computation jank → fps-drop-episode during scroll → scroll session marked janky
15
+ *
16
+ * Spans emitted:
17
+ * catalyst:session/scroll — scroll-start → scroll-end (Track: Input, shared with keyboard)
18
+ * detail: frameDropCount, minFps, janky, scrollDuration
19
+ *
20
+ * Native events consumed (via index.js _routeNativeEvent):
21
+ * scroll-start { nativeTime } — GestureDetector first onScroll() in CustomWebview.kt
22
+ * scroll-end { nativeTime } — GestureDetector ACTION_UP / ACTION_CANCEL
23
+ *
24
+ * fps-drop-episode routed here (Option B) when scroll session is open:
25
+ * index.js checks this._scroll._session.isOpen before calling onFpsDrop(event)
26
+ */
27
+
28
+ class ScrollCollector {
29
+ constructor(measure, nativeToWeb) {
30
+ this._measure = measure;
31
+ this._nativeToWeb = nativeToWeb;
32
+ this._session = new _session.Session(_constants.PREFIX.SESSION_SCROLL, measure, _constants.TRACK.INPUT);
33
+ this._insightsCb = null; // set by index.js via setInsightsSource
34
+ this._frameDropCount = 0;
35
+ this._minFps = 60;
36
+ }
37
+ init() {
38
+ // No DOM listener — scroll events come from native GestureDetector
39
+ }
40
+ setInsightsSource(cb) {
41
+ this._insightsCb = cb;
42
+ }
43
+ setWaterfallSource(fn) {
44
+ this._notifyWaterfall = fn;
45
+ }
46
+
47
+ // Called by RenderCollector when a LoAF fires (shared notification, kept for compat)
48
+ onLoaf() {
49
+ if (this._session.isOpen) {
50
+ this._session.increment("loafCount");
51
+ }
52
+ }
53
+
54
+ // ─── Native event handlers ────────────────────────────────────────────────
55
+
56
+ onScrollStart(event) {
57
+ if (this._session.isOpen) return; // already tracking, ignore duplicate start
58
+ const webNow = this._nativeToWeb(event.nativeTime);
59
+ this._frameDropCount = 0;
60
+ this._minFps = 60;
61
+ this._session.open({
62
+ loafCount: 0
63
+ }, webNow);
64
+ }
65
+ onScrollEnd(event) {
66
+ if (!this._session.isOpen) return;
67
+ const startTime = this._session.startTime;
68
+ const webNow = this._nativeToWeb(event.nativeTime);
69
+ const loafCount = this._session.get("loafCount") ?? 0;
70
+ const janky = this._frameDropCount > 0 || this._minFps < 55;
71
+ const durationMs = Math.round(webNow - startTime);
72
+ this._session.close({
73
+ loafCount,
74
+ frameDropCount: this._frameDropCount,
75
+ minFps: this._minFps,
76
+ janky,
77
+ simulatorValid: false
78
+ }, webNow);
79
+ if (janky && this._insightsCb) {
80
+ this._insightsCb({
81
+ minFps: this._minFps,
82
+ frameDropCount: this._frameDropCount,
83
+ startTime,
84
+ endTime: webNow
85
+ });
86
+ }
87
+ this._notifyWaterfall?.({
88
+ kind: "scroll",
89
+ startTime,
90
+ endTime: webNow,
91
+ durationMs,
92
+ loafCount,
93
+ frameDropCount: this._frameDropCount,
94
+ minFps: this._minFps,
95
+ janky
96
+ });
97
+ this._frameDropCount = 0;
98
+ this._minFps = 60;
99
+ }
100
+
101
+ // ─── fps-drop-episode correlation (Option B) ──────────────────────────────
102
+ // Called from index.js _routeNativeEvent when scroll session is open.
103
+
104
+ onFpsDrop(event) {
105
+ this._frameDropCount++;
106
+ if (event.minFps != null && event.minFps < this._minFps) {
107
+ this._minFps = event.minFps;
108
+ }
109
+ }
110
+ }
111
+ exports.ScrollCollector = ScrollCollector;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ThreadCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
9
+ * ThreadCollector.js
10
+ *
11
+ * Instruments: thread visibility gap during cache serving.
12
+ *
13
+ * Problem: when shouldInterceptRequest serves from cache it calls
14
+ * disableHardwareAcceleration() / enableHardwareAcceleration() on the WebView.
15
+ * This creates a software-render window that is invisible in the Render track
16
+ * because it happens on a bg thread and neither the thread name nor the
17
+ * hw-accel state appears in any existing span.
18
+ *
19
+ * Native events received (via index.js router):
20
+ * hw-accel-change { state: 'off'|'on', trigger: 'cache-serve'|'cleanup',
21
+ * thread, nativeTime }
22
+ *
23
+ * Spans emitted (on Render track):
24
+ * catalyst:hw-accel/off — software-render window open (error = bad, overlaps frames)
25
+ * catalyst:hw-accel/on — hardware acceleration restored (primary)
26
+ *
27
+ * Overlap detection:
28
+ * If a LoAF overlaps a software-render window → notifyInsights so
29
+ * InsightsCollector can fire an 'hw-accel-during-frame' critical insight.
30
+ */
31
+
32
+ class ThreadCollector {
33
+ constructor(measure, nativeToWeb) {
34
+ this._measure = measure;
35
+ this._nativeToWeb = nativeToWeb;
36
+
37
+ // Track open software-render windows: { startMark, startTime, thread }
38
+ this._openWindow = null;
39
+ this._notifyInsights = null;
40
+ }
41
+
42
+ /** Called by index.js to wire up insights notifications. */
43
+ setInsightsSource(fn) {
44
+ this._notifyInsights = fn;
45
+ }
46
+ init() {
47
+ // No web-side observers — all events come from native via onNativeEvent()
48
+ }
49
+ onNativeEvent(event) {
50
+ if (event.type !== "hw-accel-change") return;
51
+ const {
52
+ state,
53
+ trigger,
54
+ thread,
55
+ nativeTime
56
+ } = event;
57
+ const webTime = nativeTime != null ? this._nativeToWeb(nativeTime) : performance.now();
58
+ if (state === "off") {
59
+ // Software-render window opens — record start, wait for 'on' to close span
60
+ const markName = `${_constants.PREFIX.HW_ACCEL}/off:start:${Math.round(webTime)}`;
61
+ performance.mark(markName, {
62
+ startTime: webTime
63
+ });
64
+ this._openWindow = {
65
+ startMark: markName,
66
+ startTime: webTime,
67
+ thread: thread ?? null,
68
+ trigger: trigger ?? null
69
+ };
70
+ } else if (state === "on") {
71
+ const endTime = webTime;
72
+ if (this._openWindow) {
73
+ const {
74
+ startMark,
75
+ startTime,
76
+ thread: offThread,
77
+ trigger: offTrigger
78
+ } = this._openWindow;
79
+ this._openWindow = null;
80
+ const loafOverlap = this._measure.overlapsLoaf(startTime, endTime);
81
+ this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/sw-render-window|${offTrigger ?? "unknown"}`, startMark, endTime, {
82
+ trigger: offTrigger ?? null,
83
+ thread: offThread ?? null,
84
+ loafOverlap,
85
+ simulatorValid: false
86
+ }, _constants.TRACK.RENDER, loafOverlap ? _constants.COLOR.ERROR : _constants.COLOR.SECONDARY_DARK);
87
+ if (loafOverlap) {
88
+ this._notifyInsights?.({
89
+ durationMs: endTime - startTime,
90
+ thread: offThread,
91
+ trigger: offTrigger,
92
+ startTime,
93
+ endTime
94
+ });
95
+ }
96
+ }
97
+
98
+ // Emit a point-in-time marker for hw-accel restored
99
+ const onMark = `${_constants.PREFIX.HW_ACCEL}/on:${Math.round(endTime)}`;
100
+ performance.mark(onMark, {
101
+ startTime: endTime
102
+ });
103
+ this._measure.emit(`${_constants.PREFIX.HW_ACCEL}/restored`, onMark, endTime + 1, {
104
+ trigger: trigger ?? null,
105
+ thread: thread ?? null,
106
+ simulatorValid: false
107
+ }, _constants.TRACK.RENDER, _constants.COLOR.SECONDARY);
108
+ }
109
+ }
110
+ }
111
+ exports.ThreadCollector = ThreadCollector;