import { Platform } from "react-native"; type ListenerStats = { adds: number; removes: number; active: number; }; type TimerStats = { created: number; fired: number; cleared: number; active: number; }; type WebSocketStats = { created: number; opened: number; closed: number; errored: number; active: number; }; type ComponentStats = { mounts: number; unmounts: number; renders: number; scrollEvents: number; nearBottomTransitions: number; metricUpdates: number; itemRenderCalls: number; wheelAttach: number; wheelDetach: number; inputChanges: number; keyPresses: number; lastRenderAtMs: number; }; type ScrollInvestigationStore = { markRender: (componentId: string) => void; markEvent: ( componentId: string, event: | "mount" | "unmount" | "scrollEvent" | "nearBottomTransition" | "metricUpdate" | "itemRenderCall" | "wheelAttach" | "wheelDetach" | "inputChange" | "keyPress", ) => void; snapshot: () => { listeners: { byType: Record; byCallsite: Record; activeUniqueKeys: number; activeByTypeAndTarget: Record>; }; timers: { timeout: TimerStats; interval: TimerStats; raf: TimerStats; }; websockets: { totals: WebSocketStats; activeByUrl: Record; }; components: Record; }; printSnapshot: (label?: string) => void; _installedAtMs: number; }; type ScrollInvestigationGlobal = typeof globalThis & { __PASEO_SCROLL_JANK_INVESTIGATION__?: ScrollInvestigationStore; __PASEO_SCROLL_JANK_INVESTIGATION_DISABLED__?: boolean; }; const TRACKED_EVENT_TYPES = new Set([ "wheel", "scroll", "pointermove", "pointerup", "pointercancel", ]); const SOURCE_LABEL = "[ScrollJankInvestigation]"; function shouldInstall(): boolean { const runtime = globalThis as ScrollInvestigationGlobal; const isDev = Boolean((globalThis as { __DEV__?: boolean }).__DEV__); return Platform.OS === "web" && isDev && !runtime.__PASEO_SCROLL_JANK_INVESTIGATION_DISABLED__; } function normalizeCapture(options?: AddEventListenerOptions | boolean): boolean { if (typeof options === "boolean") { return options; } return Boolean(options?.capture); } function describeEventTarget(target: EventTarget): string { const element = target as Element; if (element && typeof element === "object" && "tagName" in element) { const tagName = (element.tagName || "unknown").toLowerCase(); const testId = element.getAttribute?.("data-testid"); const role = element.getAttribute?.("role"); const id = (element as HTMLElement).id || null; const className = (element as HTMLElement).className; const classLabel = typeof className === "string" && className.trim().length > 0 ? className.trim().split(/\s+/).slice(0, 2).join(".") : null; const connectivityLabel = typeof (element as Node).isConnected === "boolean" ? (element as Node).isConnected ? "[connected]" : "[detached]" : null; return [ tagName, id ? `#${id}` : null, testId ? `[data-testid=${testId}]` : null, role ? `[role=${role}]` : null, classLabel ? `.${classLabel}` : null, connectivityLabel, ] .filter(Boolean) .join(""); } const ctorName = (target as { constructor?: { name?: string } }).constructor?.name; return ctorName || "unknown-target"; } function inferCallsite(): string { const stack = new Error().stack; if (!stack) { return "unknown"; } const frames = stack.split("\n"); for (const raw of frames.slice(2)) { const line = raw.trim(); if (!line) { continue; } if (line.includes("scroll-jank-investigation")) { continue; } if (line.includes("patchedAddEventListener")) { continue; } return line; } return "unknown"; } function ensureListenerStats(map: Map, type: string): ListenerStats { const existing = map.get(type); if (existing) { return existing; } const next: ListenerStats = { adds: 0, removes: 0, active: 0 }; map.set(type, next); return next; } function ensureComponentStats( map: Map, componentId: string, ): ComponentStats { const existing = map.get(componentId); if (existing) { return existing; } const next: ComponentStats = { mounts: 0, unmounts: 0, renders: 0, scrollEvents: 0, nearBottomTransitions: 0, metricUpdates: 0, itemRenderCalls: 0, wheelAttach: 0, wheelDetach: 0, inputChanges: 0, keyPresses: 0, lastRenderAtMs: 0, }; map.set(componentId, next); return next; } export function installScrollJankInvestigation(): void { if (!shouldInstall()) { return; } const runtime = globalThis as ScrollInvestigationGlobal; if (runtime.__PASEO_SCROLL_JANK_INVESTIGATION__) { return; } const targetIds = new WeakMap(); const listenerIds = new WeakMap(); const activeListenerKeys = new Set(); const activeListenerMeta = new Map(); const listenerStatsByType = new Map(); const listenerCallsiteCount = new Map(); const componentStatsById = new Map(); const activeWsByUrl = new Map(); const timeoutHandles = new Map(); const intervalHandles = new Map(); const rafHandles = new Map(); let nextTargetId = 1; let nextListenerId = 1; const timerStats = { timeout: { created: 0, fired: 0, cleared: 0, active: 0 } as TimerStats, interval: { created: 0, fired: 0, cleared: 0, active: 0 } as TimerStats, raf: { created: 0, fired: 0, cleared: 0, active: 0 } as TimerStats, }; const websocketStats: WebSocketStats = { created: 0, opened: 0, closed: 0, errored: 0, active: 0, }; const eventTargetProto = EventTarget.prototype as EventTarget & { addEventListener: EventTarget["addEventListener"]; removeEventListener: EventTarget["removeEventListener"]; }; const nativeAddEventListener = eventTargetProto.addEventListener; const nativeRemoveEventListener = eventTargetProto.removeEventListener; function getTargetId(target: EventTarget): string { const targetObj = target as unknown as object; const existing = targetIds.get(targetObj); if (existing) { return String(existing); } const next = nextTargetId++; targetIds.set(targetObj, next); return String(next); } function getListenerId(listener: EventListenerOrEventListenerObject): string { const listenerObj = listener as unknown as object; const existing = listenerIds.get(listenerObj); if (existing) { return String(existing); } const next = nextListenerId++; listenerIds.set(listenerObj, next); return String(next); } function toListenerKey( target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean, ): string { return [ getTargetId(target), type, normalizeCapture(options) ? "capture" : "bubble", getListenerId(listener), ].join("|"); } eventTargetProto.addEventListener = function patchedAddEventListener( this: EventTarget, type: string, listener: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean, ): void { nativeAddEventListener.call(this, type, listener as any, options as any); if (!listener) { return; } const stats = ensureListenerStats(listenerStatsByType, type); stats.adds += 1; const key = toListenerKey(this, type, listener, options); if (!activeListenerKeys.has(key)) { activeListenerKeys.add(key); stats.active += 1; activeListenerMeta.set(key, { type, target: describeEventTarget(this), }); } if (TRACKED_EVENT_TYPES.has(type)) { const callsite = inferCallsite(); const metricKey = `${type} :: ${callsite}`; listenerCallsiteCount.set(metricKey, (listenerCallsiteCount.get(metricKey) ?? 0) + 1); } }; eventTargetProto.removeEventListener = function patchedRemoveEventListener( this: EventTarget, type: string, listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean, ): void { nativeRemoveEventListener.call(this, type, listener as any, options as any); if (!listener) { return; } const stats = ensureListenerStats(listenerStatsByType, type); stats.removes += 1; const key = toListenerKey(this, type, listener, options); if (activeListenerKeys.delete(key)) { stats.active = Math.max(0, stats.active - 1); activeListenerMeta.delete(key); } }; const nativeSetTimeout = globalThis.setTimeout.bind(globalThis); const nativeClearTimeout = globalThis.clearTimeout.bind(globalThis); const nativeSetInterval = globalThis.setInterval.bind(globalThis); const nativeClearInterval = globalThis.clearInterval.bind(globalThis); const nativeRequestAnimationFrame = globalThis.requestAnimationFrame.bind(globalThis); const nativeCancelAnimationFrame = globalThis.cancelAnimationFrame.bind(globalThis); globalThis.setTimeout = ((handler: TimerHandler, timeout?: number, ...args: unknown[]) => { timerStats.timeout.created += 1; let timeoutId = -1; const wrapped = typeof handler === "function" ? (...handlerArgs: unknown[]) => { if (timeoutHandles.delete(timeoutId)) { timerStats.timeout.fired += 1; timerStats.timeout.active = timeoutHandles.size; } return handler(...handlerArgs); } : handler; timeoutId = nativeSetTimeout(wrapped, timeout, ...(args as any[])) as unknown as number; timeoutHandles.set(timeoutId, true); timerStats.timeout.active = timeoutHandles.size; return timeoutId as unknown as ReturnType; }) as unknown as typeof setTimeout; globalThis.clearTimeout = ((timeoutId?: number) => { if (typeof timeoutId === "number" && timeoutHandles.delete(timeoutId)) { timerStats.timeout.cleared += 1; timerStats.timeout.active = timeoutHandles.size; } return nativeClearTimeout(timeoutId); }) as typeof clearTimeout; globalThis.setInterval = ((handler: TimerHandler, timeout?: number, ...args: unknown[]) => { timerStats.interval.created += 1; let intervalId = -1; const wrapped = typeof handler === "function" ? (...handlerArgs: unknown[]) => { timerStats.interval.fired += 1; return handler(...handlerArgs); } : handler; intervalId = nativeSetInterval(wrapped, timeout, ...(args as any[])) as unknown as number; intervalHandles.set(intervalId, true); timerStats.interval.active = intervalHandles.size; return intervalId as unknown as ReturnType; }) as unknown as typeof setInterval; globalThis.clearInterval = ((intervalId?: number) => { if (typeof intervalId === "number" && intervalHandles.delete(intervalId)) { timerStats.interval.cleared += 1; timerStats.interval.active = intervalHandles.size; } return nativeClearInterval(intervalId); }) as typeof clearInterval; globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => { timerStats.raf.created += 1; let rafId = -1; const wrapped = (timestamp: number) => { if (rafHandles.delete(rafId)) { timerStats.raf.fired += 1; timerStats.raf.active = rafHandles.size; } callback(timestamp); }; rafId = nativeRequestAnimationFrame(wrapped) as unknown as number; rafHandles.set(rafId, true); timerStats.raf.active = rafHandles.size; return rafId as unknown as ReturnType; }) as typeof requestAnimationFrame; globalThis.cancelAnimationFrame = ((rafId: number) => { if (rafHandles.delete(rafId)) { timerStats.raf.cleared += 1; timerStats.raf.active = rafHandles.size; } return nativeCancelAnimationFrame(rafId); }) as typeof cancelAnimationFrame; const NativeWebSocket = globalThis.WebSocket; if (typeof NativeWebSocket === "function") { class InstrumentedWebSocket extends NativeWebSocket { constructor(url: string | URL, protocols?: string | string[]) { if (protocols === undefined) { super(url); } else { super(url, protocols); } const urlKey = String(url); websocketStats.created += 1; websocketStats.active += 1; activeWsByUrl.set(urlKey, (activeWsByUrl.get(urlKey) ?? 0) + 1); const handleOpen = () => { websocketStats.opened += 1; }; const handleError = () => { websocketStats.errored += 1; }; const handleClose = () => { websocketStats.closed += 1; websocketStats.active = Math.max(0, websocketStats.active - 1); const current = activeWsByUrl.get(urlKey) ?? 0; if (current <= 1) { activeWsByUrl.delete(urlKey); } else { activeWsByUrl.set(urlKey, current - 1); } this.removeEventListener("open", handleOpen); this.removeEventListener("error", handleError); this.removeEventListener("close", handleClose); }; this.addEventListener("open", handleOpen); this.addEventListener("error", handleError); this.addEventListener("close", handleClose); } } globalThis.WebSocket = InstrumentedWebSocket as typeof WebSocket; } const store: ScrollInvestigationStore = { markRender(componentId: string) { const stats = ensureComponentStats(componentStatsById, componentId); stats.renders += 1; stats.lastRenderAtMs = performance.now(); }, markEvent(componentId: string, event) { const stats = ensureComponentStats(componentStatsById, componentId); switch (event) { case "mount": stats.mounts += 1; return; case "unmount": stats.unmounts += 1; return; case "scrollEvent": stats.scrollEvents += 1; return; case "nearBottomTransition": stats.nearBottomTransitions += 1; return; case "metricUpdate": stats.metricUpdates += 1; return; case "itemRenderCall": stats.itemRenderCalls += 1; return; case "wheelAttach": stats.wheelAttach += 1; return; case "wheelDetach": stats.wheelDetach += 1; return; case "inputChange": stats.inputChanges += 1; return; case "keyPress": stats.keyPresses += 1; return; default: return; } }, snapshot() { const activeByTypeAndTarget: Record> = {}; for (const { type, target } of activeListenerMeta.values()) { const existingByType = activeByTypeAndTarget[type] ?? {}; existingByType[target] = (existingByType[target] ?? 0) + 1; activeByTypeAndTarget[type] = existingByType; } return { listeners: { byType: Object.fromEntries(listenerStatsByType.entries()), byCallsite: Object.fromEntries(listenerCallsiteCount.entries()), activeUniqueKeys: activeListenerKeys.size, activeByTypeAndTarget, }, timers: { timeout: { ...timerStats.timeout, active: timeoutHandles.size }, interval: { ...timerStats.interval, active: intervalHandles.size }, raf: { ...timerStats.raf, active: rafHandles.size }, }, websockets: { totals: { ...websocketStats }, activeByUrl: Object.fromEntries(activeWsByUrl.entries()), }, components: Object.fromEntries(componentStatsById.entries()), }; }, printSnapshot(label?: string) { console.log(`${SOURCE_LABEL} ${label ?? "snapshot"}`, this.snapshot()); }, _installedAtMs: Date.now(), }; runtime.__PASEO_SCROLL_JANK_INVESTIGATION__ = store; console.log( `${SOURCE_LABEL} installed`, "Use window.__PASEO_SCROLL_JANK_INVESTIGATION__.snapshot()", ); } function getStore(): ScrollInvestigationStore | null { const runtime = globalThis as ScrollInvestigationGlobal; return runtime.__PASEO_SCROLL_JANK_INVESTIGATION__ ?? null; } export function markScrollInvestigationRender(componentId: string): void { if (!shouldInstall()) { return; } getStore()?.markRender(componentId); } export function markScrollInvestigationEvent( componentId: string, event: | "mount" | "unmount" | "scrollEvent" | "nearBottomTransition" | "metricUpdate" | "itemRenderCall" | "wheelAttach" | "wheelDetach" | "inputChange" | "keyPress", ): void { if (!shouldInstall()) { return; } getStore()?.markEvent(componentId, event); }