preact-perf-tracker 0.1.0
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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/index.cjs +744 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.mjs +737 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/index.test.ts +269 -0
- package/src/__tests__/instrumentation.test.ts +490 -0
- package/src/__tests__/utils.test.ts +167 -0
- package/src/index.ts +130 -0
- package/src/instrumentation.ts +412 -0
- package/src/overlay.ts +210 -0
- package/src/toolbar.ts +230 -0
- package/src/types.ts +147 -0
- package/src/utils.ts +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["getReport","clearReport","getReportSummary","getReportSummary","getReportInternal","getReportSummaryInternal"],"sources":["../src/types.ts","../src/utils.ts","../src/instrumentation.ts","../src/overlay.ts","../src/toolbar.ts","../src/index.ts"],"sourcesContent":["import type { VNode, Component, Options as PreactOptions } from 'preact';\n\nexport interface InternalVNode<P = Record<string, unknown>> extends VNode<P> {\n\t/** Component instance (`_component`) */\n\t__c: InternalComponent | null;\n\t/** First DOM node (`_dom`) */\n\t__e: Element | Text | null;\n\t/** Children VNodes (`_children`) */\n\t__k: Array<InternalVNode | null> | null;\n\t/** Parent VNode (`_parent`) */\n\t__: InternalVNode | null;\n\t/** Diff flags / start offset (`_flags`) */\n\t__b: number;\n\t/** Index in parent children array (`_index`) */\n\t__i: number;\n}\n\nexport interface InternalComponent extends Component<any, any> {\n\t/** Associated VNode (`_vnode`) */\n\t__v: InternalVNode;\n\t/** Pending / next state (`_nextState`) */\n\t__s: Record<string, unknown>;\n\t/** Dirty flag (`_dirty`) */\n\t__d: boolean;\n\t/** Force-update flag */\n\t__f: boolean;\n\t/** Hooks state container */\n\t__H: HooksState | null;\n\t/** displayName – set by user or build tooling */\n\tdisplayName?: string;\n\t/** Previous props snapshot (set by us for change detection) */\n\t__prevProps?: Record<string, unknown>;\n\t/** Previous state snapshot (set by us for change detection) */\n\t__prevState?: Record<string, unknown>;\n\t/** Previous hooks snapshot (set by us for hook-state change detection) */\n\t__prevHooks?: unknown[];\n}\n\nexport interface HooksState {\n\t/** Ordered hook state list */\n\t__: HookState[];\n\t/** Pending effect hooks */\n\t__h: HookState[];\n}\n\nexport interface HookState {\n\t/** Current value (useState ⇒ state value, useRef ⇒ ref object, …) */\n\t__: unknown;\n\t/** Deps array for effects / memo / callback */\n\t__H?: unknown[];\n}\n\nexport interface InternalOptions extends PreactOptions {\n\t__b?(vnode: InternalVNode): void;\n\t__r?(vnode: InternalVNode): void;\n\t__c?(vnode: InternalVNode, commitQueue: InternalComponent[]): void;\n\t__h?(component: InternalComponent, index: number, hookType: number): void;\n}\n\n\nexport interface Options {\n\t/**\n\t * Enable / disable tracking.\n\t * @default true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Log render info to the console.\n\t * @default false\n\t */\n\tlog?: boolean;\n\n\t/**\n\t * Show the floating toolbar.\n\t * @default true\n\t */\n\tshowToolbar?: boolean;\n\n\t/**\n\t * Outline animation speed.\n\t * @default \"fast\"\n\t */\n\tanimationSpeed?: 'slow' | 'fast' | 'off';\n\n\t/**\n\t * Called when a component renders.\n\t */\n\tonRender?: (info: RenderInfo) => void;\n\n\t/**\n\t * Called when a commit cycle begins (before diffing).\n\t */\n\tonCommitStart?: () => void;\n\n\t/**\n\t * Called when a commit cycle ends (after diffed queue flushed).\n\t */\n\tonCommitFinish?: () => void;\n}\n\nexport enum ChangeType {\n\tProps = 1,\n\tState = 2,\n\tContext = 4,\n\tForce = 8,\n}\n\nexport interface Change {\n\ttype: ChangeType;\n\tname: string;\n\tprevValue?: unknown;\n\tnextValue?: unknown;\n}\n\nexport interface RenderInfo {\n\tcomponentName: string;\n\tphase: 'mount' | 'update' | 'unmount';\n\tselfTime: number;\n\tchanges: Change[];\n\ttimestamp: number;\n\tdomNode: Element | null;\n}\n\nexport interface ReportEntry {\n\tcount: number;\n\ttotalSelfTime: number;\n\tdisplayName: string | null;\n\ttype: unknown;\n}\n\nexport interface ReportSummaryEntry {\n\tdisplayName: string;\n\tcount: number;\n\ttotalSelfTime: number;\n\tavgSelfTime: number;\n}\n\nexport interface OutlineData {\n\trect: DOMRect;\n\talpha: number;\n\tcolor: string;\n\tcount: number;\n\tname: string;\n\tselfTimeMs: number;\n\ttimestamp: number;\n}\n","import type { InternalVNode } from './types';\n\nexport function getDisplayName(vnode: InternalVNode): string | null {\n\tconst type = vnode.type;\n\tif (typeof type === 'string') return null;\n\tif (typeof type === 'function') {\n\t\tconst name = (type as any).displayName || type.name || null;\n\t\tif (name === 'type' || name === 'anonymous') return null;\n\t\treturn name;\n\t}\n\treturn null;\n}\n\nexport function getComponentDOMNode(vnode: InternalVNode): Element | null {\n\tlet dom = vnode.__e;\n\tif (dom instanceof Element) return dom;\n\n\tconst children = vnode.__k;\n\tif (children) {\n\t\tfor (let i = 0; i < children.length; i++) {\n\t\t\tconst child = children[i];\n\t\t\tif (!child) continue;\n\t\t\tif (child.__e instanceof Element) return child.__e;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function shallowDiff(\n\tprev: Record<string, unknown> | null | undefined,\n\tnext: Record<string, unknown> | null | undefined,\n): string[] {\n\tconst changed: string[] = [];\n\tif (!prev && !next) return changed;\n\tif (!prev || !next) {\n\t\treturn Object.keys(next || prev || {});\n\t}\n\tconst allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);\n\tfor (const key of allKeys) {\n\t\tif (key === 'children') continue;\n\t\tif (!Object.is(prev[key], next[key])) {\n\t\t\tchanged.push(key);\n\t\t}\n\t}\n\treturn changed;\n}\n\nexport function isComponentVNode(vnode: InternalVNode): boolean {\n\treturn typeof vnode.type === 'function';\n}\n\nexport function snapshot(\n\tobj: Record<string, unknown> | null | undefined,\n): Record<string, unknown> | null {\n\tif (!obj) return null;\n\treturn Object.assign({}, obj);\n}\n\nexport const now =\n\ttypeof performance !== 'undefined'\n\t\t? () => performance.now()\n\t\t: () => Date.now();\n","import { options, Fragment } from 'preact';\nimport {\n\tChangeType,\n\ttype InternalVNode,\n\ttype InternalComponent,\n\ttype InternalOptions,\n\ttype RenderInfo,\n\ttype Change,\n\ttype Options,\n\ttype ReportEntry,\n\ttype ReportSummaryEntry,\n} from './types';\nimport {\n\tgetDisplayName,\n\tgetComponentDOMNode,\n\tshallowDiff,\n\tisComponentVNode,\n\tsnapshot,\n\tnow,\n} from './utils';\n\nconst renderStartTimes = new WeakMap<InternalComponent, number>();\nconst defaultOptions: Options = {\n\tenabled: true,\n\tlog: false,\n\tshowToolbar: true,\n\tanimationSpeed: 'fast',\n};\n\n/**\n * Whether our permanent hook wrappers have been installed.\n * Once installed they stay in place for the lifetime of the page;\n * `isHooked` toggles them between active / pass-through so that\n * libraries hooking *after* us are never clobbered on unhook.\n */\nlet hooksInstalled = false;\n\nlet savedOriginalHooks: {\n\t__b?: InternalOptions['__b'];\n\t__r?: InternalOptions['__r'];\n\tdiffed?: InternalOptions['diffed'];\n\t__c?: InternalOptions['__c'];\n\tunmount?: InternalOptions['unmount'];\n} | null = null;\n\nlet activeOptions: Options = {\n\t...defaultOptions,\n};\n\nconst reportData = new Map<unknown, ReportEntry>();\n\nconst renderListeners = new Set<(info: RenderInfo) => void>();\nlet legacyOverlayRenderListener: ((info: RenderInfo) => void) | null = null;\n\nlet isHooked = false;\nlet inCommit = false;\n\n\nfunction detectChanges(\n\tcomponent: InternalComponent,\n\tvnode: InternalVNode,\n\tisMounting: boolean,\n): Change[] {\n\tconst changes: Change[] = [];\n\n\tif (isMounting) return changes;\n\n\tconst prevProps = component.__prevProps;\n\tconst nextProps = vnode.props as Record<string, unknown>;\n\tconst changedPropKeys = shallowDiff(prevProps, nextProps);\n\tfor (const key of changedPropKeys) {\n\t\tchanges.push({\n\t\t\ttype: ChangeType.Props,\n\t\t\tname: key,\n\t\t\tprevValue: prevProps?.[key],\n\t\t\tnextValue: nextProps[key],\n\t\t});\n\t}\n\n\tconst prevState = component.__prevState;\n\tconst nextState = component.__s ?? component.state;\n\tif (prevState && nextState) {\n\t\tconst changedStateKeys = shallowDiff(\n\t\t\tprevState as Record<string, unknown>,\n\t\t\tnextState as Record<string, unknown>,\n\t\t);\n\t\tfor (const key of changedStateKeys) {\n\t\t\tchanges.push({\n\t\t\t\ttype: ChangeType.State,\n\t\t\t\tname: key,\n\t\t\t\tprevValue: (prevState as Record<string, unknown>)[key],\n\t\t\t\tnextValue: (nextState as Record<string, unknown>)[key],\n\t\t\t});\n\t\t}\n\t}\n\n\tif (component.__f) {\n\t\tchanges.push({\n\t\t\ttype: ChangeType.Force,\n\t\t\tname: 'forceUpdate',\n\t\t});\n\t}\n\n\treturn changes;\n}\n\nfunction emitRender(info: RenderInfo) {\n\tfor (const listener of renderListeners) {\n\t\tlistener(info);\n\t}\n}\n\n\nfunction onBeforeDiff(vnode: InternalVNode) {\n\tif (!activeOptions.enabled) return;\n\n\tif (!inCommit) {\n\t\tinCommit = true;\n\t\tactiveOptions.onCommitStart?.();\n\t}\n}\n\nfunction onBeforeRender(vnode: InternalVNode) {\n\tif (!activeOptions.enabled) return;\n\tif (!isComponentVNode(vnode)) return;\n\tif (vnode.type === Fragment) return;\n\n\tconst component = vnode.__c as InternalComponent | null;\n\tif (!component) return;\n\n\trenderStartTimes.set(component, now());\n}\n\nfunction onDiffed(vnode: InternalVNode) {\n\tif (!activeOptions.enabled) return;\n\tif (!isComponentVNode(vnode)) return;\n\tif (vnode.type === Fragment) return;\n\n\tconst component = vnode.__c as InternalComponent | null;\n\tif (!component) return;\n\n\tconst startTime = renderStartTimes.get(component);\n\tconst selfTime = startTime != null ? now() - startTime : 0;\n\trenderStartTimes.delete(component);\n\n\tconst isMounting = component.__prevProps === undefined;\n\tconst changes = detectChanges(component, vnode, isMounting);\n\tconst componentName = getDisplayName(vnode) || 'Anonymous';\n\tconst domNode = getComponentDOMNode(vnode);\n\n\tconst info: RenderInfo = {\n\t\tcomponentName,\n\t\tphase: isMounting ? 'mount' : 'update',\n\t\tselfTime,\n\t\tchanges,\n\t\ttimestamp: now(),\n\t\tdomNode,\n\t};\n\n\tcomponent.__prevProps = snapshot(\n\t\tvnode.props as Record<string, unknown>,\n\t) as Record<string, unknown>;\n\tcomponent.__prevState = snapshot(\n\t\t(component.__s ?? component.state) as Record<string, unknown>,\n\t) as Record<string, unknown>;\n\n\tconst type = vnode.type;\n\tlet entry = reportData.get(type);\n\tif (!entry) {\n\t\tentry = {\n\t\t\tcount: 0,\n\t\t\ttotalSelfTime: 0,\n\t\t\tdisplayName: componentName,\n\t\t\ttype,\n\t\t};\n\t\treportData.set(type, entry);\n\t}\n\tentry.count++;\n\tentry.totalSelfTime += selfTime;\n\n\tif (activeOptions.log) {\n\t\tlogRender(info);\n\t}\n\n\tactiveOptions.onRender?.(info);\n\temitRender(info);\n}\n\nfunction onCommit(_vnode: InternalVNode, _commitQueue: InternalComponent[]) {\n\tif (!activeOptions.enabled) return;\n\n\tif (inCommit) {\n\t\tinCommit = false;\n\t\tactiveOptions.onCommitFinish?.();\n\t}\n}\n\nfunction onUnmount(vnode: InternalVNode) {\n\tif (!activeOptions.enabled) return;\n\tif (!isComponentVNode(vnode)) return;\n\tif (vnode.type === Fragment) return;\n\n\tconst componentName = getDisplayName(vnode) || 'Anonymous';\n\tconst domNode = getComponentDOMNode(vnode);\n\n\tconst info: RenderInfo = {\n\t\tcomponentName,\n\t\tphase: 'unmount',\n\t\tselfTime: 0,\n\t\tchanges: [],\n\t\ttimestamp: now(),\n\t\tdomNode,\n\t};\n\n\tactiveOptions.onRender?.(info);\n\temitRender(info);\n}\n\n\nfunction logRender(info: RenderInfo) {\n\tconst parts: string[] = [\n\t\t`%c[preact-perf-tracker]%c ${info.componentName}`,\n\t\t'color: #8b5cf6; font-weight: bold',\n\t\t'color: inherit',\n\t];\n\n\tconst meta: string[] = [info.phase];\n\tif (info.selfTime >= 0.01) {\n\t\tmeta.push(`${info.selfTime.toFixed(2)}ms`);\n\t}\n\tif (info.changes.length > 0) {\n\t\tmeta.push(\n\t\t\tinfo.changes\n\t\t\t\t.map(\n\t\t\t\t\t(c) =>\n\t\t\t\t\t\t`${c.type === 1 ? 'prop' : c.type === 2 ? 'state' : c.type === 4 ? 'ctx' : 'force'}:${c.name}`,\n\t\t\t\t)\n\t\t\t\t.join(', '),\n\t\t);\n\t}\n\tparts[0] += ` (${meta.join(' · ')})`;\n\n\tconsole.log(...parts);\n}\n\n\n/**\n * Install the Preact options hooks for render tracking.\n *\n * The wrappers are installed once and stay in place for the lifetime\n * of the page. `isHooked` toggles them between \"active\" (running\n * our instrumentation) and \"pass-through\" (just forwarding to the\n * previously chained hook). This avoids the destructive restore\n * that would clobber hooks installed by other libraries (e.g.\n * hooks / signals) that chain after us.\n */\nexport function hookIntoPreact() {\n\tif (isHooked) return;\n\tisHooked = true;\n\n\tif (hooksInstalled) return;\n\thooksInstalled = true;\n\n\tconst opts = options as InternalOptions;\n\n\tsavedOriginalHooks = {\n\t\t__b: opts.__b,\n\t\t__r: opts.__r,\n\t\tdiffed: opts.diffed,\n\t\t__c: opts.__c,\n\t\tunmount: opts.unmount,\n\t};\n\n\tconst prev__b = opts.__b;\n\topts.__b = (vnode: InternalVNode) => {\n\t\tif (isHooked) onBeforeDiff(vnode);\n\t\tprev__b?.(vnode);\n\t};\n\n\tconst prev__r = opts.__r;\n\topts.__r = (vnode: InternalVNode) => {\n\t\tif (isHooked) onBeforeRender(vnode);\n\t\tprev__r?.(vnode);\n\t};\n\n\tconst prevDiffed = opts.diffed;\n\topts.diffed = (vnode) => {\n\t\tif (isHooked) onDiffed(vnode as InternalVNode);\n\t\tprevDiffed?.(vnode);\n\t};\n\n\tconst prev__c = opts.__c;\n\topts.__c = (vnode: InternalVNode, queue: InternalComponent[]) => {\n\t\tif (isHooked) onCommit(vnode, queue);\n\t\t(prev__c as any)?.(vnode, queue);\n\t};\n\n\tconst prevUnmount = opts.unmount;\n\topts.unmount = (vnode) => {\n\t\tif (isHooked) onUnmount(vnode as InternalVNode);\n\t\tprevUnmount?.(vnode);\n\t};\n}\n\n/**\n * Disable our instrumentation hooks. The wrappers stay in the\n * options chain as transparent pass-throughs so that hooks installed\n * by other libraries after us are never lost.\n */\nexport function unhookFromPreact() {\n\tisHooked = false;\n}\n\n\nexport function getActiveOptions(): Readonly<Options> {\n\treturn activeOptions;\n}\n\nexport function setActiveOptions(opts: Partial<Options>) {\n\tObject.assign(activeOptions, opts);\n}\n\nexport function resetActiveOptions() {\n\tactiveOptions = { ...defaultOptions };\n}\n\n/**\n * Get render report for all tracked components, or a specific type.\n */\nexport function getReport(\n\ttype?: unknown,\n): ReportEntry | Map<unknown, ReportEntry> | null {\n\tif (type !== undefined) {\n\t\treturn reportData.get(type) ?? null;\n\t}\n\treturn new Map(reportData);\n}\n\nexport function clearReport() {\n\treportData.clear();\n}\n\n/**\n * Register a listener that fires for every tracked render.\n */\nexport function addRenderListener(fn: (info: RenderInfo) => void) {\n\trenderListeners.add(fn);\n}\n\nexport function removeRenderListener(fn: (info: RenderInfo) => void) {\n\trenderListeners.delete(fn);\n}\n\n/**\n * Backward-compatible single-listener API used by older integrations/tests.\n */\nexport function setOverlayRenderListener(\n\tfn: ((info: RenderInfo) => void) | null,\n) {\n\tif (legacyOverlayRenderListener) {\n\t\tremoveRenderListener(legacyOverlayRenderListener);\n\t\tlegacyOverlayRenderListener = null;\n\t}\n\tif (fn) {\n\t\tlegacyOverlayRenderListener = fn;\n\t\taddRenderListener(fn);\n\t}\n}\n\n/**\n * Get a sorted summary for quick insights (highest total self-time first).\n */\nexport function getReportSummary(limit = 10): ReportSummaryEntry[] {\n\tconst normalizedLimit = Math.max(1, Math.floor(limit));\n\treturn Array.from(reportData.values())\n\t\t.map((entry) => ({\n\t\t\tdisplayName: entry.displayName || 'Anonymous',\n\t\t\tcount: entry.count,\n\t\t\ttotalSelfTime: entry.totalSelfTime,\n\t\t\tavgSelfTime: entry.count > 0 ? entry.totalSelfTime / entry.count : 0,\n\t\t}))\n\t\t.sort((a, b) => {\n\t\t\tif (b.totalSelfTime !== a.totalSelfTime) {\n\t\t\t\treturn b.totalSelfTime - a.totalSelfTime;\n\t\t\t}\n\t\t\treturn b.count - a.count;\n\t\t})\n\t\t.slice(0, normalizedLimit);\n}\n\nexport function isInstrumented() {\n\treturn isHooked;\n}\n\n/**\n * @internal — test-only. Fully tears down hook wrappers so the next\n * `hookIntoPreact()` call will re-install fresh ones. Needed in test\n * harnesses where every test must start from a clean slate.\n */\nexport function __resetHooks() {\n\tisHooked = false;\n\thooksInstalled = false;\n\tif (savedOriginalHooks) {\n\t\tconst opts = options as InternalOptions;\n\t\topts.__b = savedOriginalHooks.__b;\n\t\topts.__r = savedOriginalHooks.__r;\n\t\topts.diffed = savedOriginalHooks.diffed;\n\t\topts.__c = savedOriginalHooks.__c;\n\t\topts.unmount = savedOriginalHooks.unmount;\n\t\tsavedOriginalHooks = null;\n\t}\n}\n","import type { RenderInfo, OutlineData } from './types';\nimport {\n\taddRenderListener,\n\tremoveRenderListener,\n\tgetActiveOptions,\n} from './instrumentation';\n\n\nconst OUTLINE_DURATION_MS = 750;\nconst FADE_SPEED: Record<string, number> = {\n\tfast: 1.5,\n\tslow: 0.6,\n\toff: 999,\n};\n\nfunction getOutlineColor(count: number): string {\n\tif (count <= 1) return 'rgba(128, 90, 213, ALPHA)';\n\tif (count <= 4) return 'rgba(168, 85, 247, ALPHA)';\n\tif (count <= 10) return 'rgba(234, 88, 12, ALPHA)';\n\treturn 'rgba(239, 68, 68, ALPHA)';\n}\n\nfunction borderWidthForTime(ms: number): number {\n\tif (ms < 1) return 1;\n\tif (ms < 8) return 2;\n\tif (ms < 16) return 3;\n\treturn 4;\n}\n\n\nconst outlines = new Map<Element, OutlineData>();\n\nlet canvas: HTMLCanvasElement | null = null;\nlet ctx: CanvasRenderingContext2D | null = null;\nlet rafId: number | null = null;\nlet isRunning = false;\n\n\nfunction ensureCanvas(): CanvasRenderingContext2D {\n\tif (canvas && ctx) return ctx;\n\n\tcanvas = document.createElement('canvas');\n\tcanvas.id = 'preact-scan-overlay';\n\tObject.assign(canvas.style, {\n\t\tposition: 'fixed',\n\t\ttop: '0',\n\t\tleft: '0',\n\t\twidth: '100vw',\n\t\theight: '100vh',\n\t\tpointerEvents: 'none',\n\t\tzIndex: '2147483646',\n\t} satisfies Partial<CSSStyleDeclaration>);\n\tdocument.documentElement.appendChild(canvas);\n\n\tctx = canvas.getContext('2d')!;\n\tresizeCanvas();\n\n\twindow.addEventListener('resize', resizeCanvas);\n\treturn ctx;\n}\n\nfunction resizeCanvas() {\n\tif (!canvas) return;\n\tconst dpr = window.devicePixelRatio || 1;\n\tcanvas.width = window.innerWidth * dpr;\n\tcanvas.height = window.innerHeight * dpr;\n\tctx?.scale(dpr, dpr);\n}\n\n\nfunction drawFrame() {\n\tif (!ctx || !canvas) return;\n\n\tconst speed = FADE_SPEED[getActiveOptions().animationSpeed ?? 'fast'] ?? 1.5;\n\tconst nowMs = performance.now();\n\n\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\n\tfor (const [element, outline] of outlines) {\n\t\tif (!element.isConnected) {\n\t\t\toutlines.delete(element);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst elapsed = nowMs - outline.timestamp;\n\t\tconst progress = Math.min(elapsed / OUTLINE_DURATION_MS, 1);\n\t\toutline.alpha = Math.max(0, 1 - progress * speed);\n\n\t\tif (outline.alpha <= 0.01) {\n\t\t\toutlines.delete(element);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst rect = element.getBoundingClientRect();\n\t\tif (rect.width === 0 && rect.height === 0) continue;\n\t\toutline.rect = rect;\n\n\t\tdrawOutline(outline);\n\t}\n\n\tif (outlines.size > 0) {\n\t\trafId = requestAnimationFrame(drawFrame);\n\t} else {\n\t\tstopLoop();\n\t\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\t}\n}\n\nfunction drawOutline(outline: OutlineData) {\n\tif (!ctx) return;\n\n\tconst { rect, alpha, color, count, name } = outline;\n\tconst resolvedColor = color.replace('ALPHA', String(alpha));\n\tconst borderWidth = borderWidthForTime(outline.selfTimeMs);\n\n\tctx.strokeStyle = resolvedColor;\n\tctx.lineWidth = borderWidth;\n\tctx.strokeRect(\n\t\trect.x + borderWidth / 2,\n\t\trect.y + borderWidth / 2,\n\t\trect.width - borderWidth,\n\t\trect.height - borderWidth,\n\t);\n\n\tctx.fillStyle = color.replace('ALPHA', String(alpha * 0.08));\n\tctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n\n\tif (alpha > 0.3) {\n\t\tconst renderTimeLabel =\n\t\t\toutline.selfTimeMs >= 0.01 ? ` ${outline.selfTimeMs.toFixed(2)}ms` : '';\n\t\tconst label =\n\t\t\tcount > 1 ? `${name} ×${count}${renderTimeLabel}` : `${name}${renderTimeLabel}`;\n\t\tconst fontSize = 10;\n\t\tctx.font = `600 ${fontSize}px ui-monospace, SFMono-Regular, Menlo, monospace`;\n\n\t\tconst textMetrics = ctx.measureText(label);\n\t\tconst padding = 3;\n\t\tconst labelHeight = fontSize + padding * 2;\n\t\tconst labelWidth = textMetrics.width + padding * 2;\n\n\t\tlet labelX = rect.x;\n\t\tlet labelY = rect.y - labelHeight - 1;\n\t\tif (labelY < 0) labelY = rect.y + rect.height + 1;\n\n\t\tctx.fillStyle = color.replace('ALPHA', String(Math.min(alpha, 0.9)));\n\t\tctx.fillRect(labelX, labelY, labelWidth, labelHeight);\n\n\t\tctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;\n\t\tctx.fillText(label, labelX + padding, labelY + fontSize + padding - 1);\n\t}\n}\n\n\nfunction onRender(info: RenderInfo) {\n\tif (info.phase === 'unmount') {\n\t\tif (info.domNode) outlines.delete(info.domNode);\n\t\treturn;\n\t}\n\n\tconst el = info.domNode;\n\tif (!el) return;\n\n\tconst existing = outlines.get(el);\n\tconst count = existing ? existing.count + 1 : 1;\n\n\toutlines.set(el, {\n\t\trect: el.getBoundingClientRect(),\n\t\talpha: 1,\n\t\tcolor: getOutlineColor(count),\n\t\tcount,\n\t\tname: info.componentName,\n\t\tselfTimeMs: info.selfTime,\n\t\ttimestamp: performance.now(),\n\t});\n\n\tensureLoop();\n}\n\n\nfunction ensureLoop() {\n\tif (isRunning) return;\n\tisRunning = true;\n\tensureCanvas();\n\trafId = requestAnimationFrame(drawFrame);\n}\n\nfunction stopLoop() {\n\tisRunning = false;\n\tif (rafId != null) {\n\t\tcancelAnimationFrame(rafId);\n\t\trafId = null;\n\t}\n}\n\n\nexport function startOverlay() {\n\taddRenderListener(onRender);\n}\n\nexport function stopOverlay() {\n\tremoveRenderListener(onRender);\n\tstopLoop();\n\toutlines.clear();\n\tif (canvas) {\n\t\tcanvas.remove();\n\t\tcanvas = null;\n\t\tctx = null;\n\t}\n\twindow.removeEventListener('resize', resizeCanvas);\n}\n","import {\n\tgetActiveOptions,\n\tsetActiveOptions,\n\tclearReport,\n\tgetReportSummary,\n} from './instrumentation';\n\n\nlet rootContainer: HTMLDivElement | null = null;\nlet shadowRoot: ShadowRoot | null = null;\nlet renderCount = 0;\nlet fps = 0;\nlet rendersPerSecond = 0;\nlet frameCount = 0;\nlet lastFpsTime = performance.now();\nlet fpsRafId: number | null = null;\nlet rendersThisSecond = 0;\n\n\nfunction updateFps() {\n\tframeCount++;\n\tconst now = performance.now();\n\tif (now - lastFpsTime >= 1000) {\n\t\tfps = frameCount;\n\t\trendersPerSecond = rendersThisSecond;\n\t\trendersThisSecond = 0;\n\t\tframeCount = 0;\n\t\tlastFpsTime = now;\n\t\tupdateDisplay();\n\t}\n\tfpsRafId = requestAnimationFrame(updateFps);\n}\n\n\nexport function notifyToolbarRender() {\n\trenderCount++;\n\trendersThisSecond++;\n\tupdateDisplay();\n}\n\n\nconst TOOLBAR_STYLES = `\n:host {\n all: initial;\n}\n.toolbar {\n position: fixed;\n bottom: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 2147483647;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n background: #0a0a0a;\n border: 1px solid #27272a;\n border-radius: 10px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n font-size: 12px;\n color: #e4e4e7;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);\n user-select: none;\n cursor: default;\n line-height: 1;\n}\n.toolbar-title {\n font-weight: 700;\n color: #a78bfa;\n padding-right: 4px;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.toolbar-title svg {\n width: 14px;\n height: 14px;\n}\n.stat {\n color: #a1a1aa;\n padding: 0 4px;\n}\n.stat-value {\n color: #e4e4e7;\n font-weight: 600;\n}\n.separator {\n width: 1px;\n height: 14px;\n background: #27272a;\n}\n.toggle-btn {\n background: none;\n border: 1px solid #3f3f46;\n border-radius: 6px;\n color: #e4e4e7;\n padding: 3px 8px;\n font-size: 11px;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n}\n.toggle-btn:hover {\n background: #27272a;\n border-color: #52525b;\n}\n.toggle-btn.active {\n background: #7c3aed;\n border-color: #8b5cf6;\n}\n`;\n\nfunction createToolbarDOM(): ShadowRoot {\n\trootContainer = document.createElement('div');\n\trootContainer.id = 'preact-tracker-toolbar';\n\tshadowRoot = rootContainer.attachShadow({ mode: 'open' });\n\n\tconst style = document.createElement('style');\n\tstyle.textContent = TOOLBAR_STYLES;\n\tshadowRoot.appendChild(style);\n\n\tconst toolbar = document.createElement('div');\n\ttoolbar.className = 'toolbar';\n\ttoolbar.innerHTML = `\n\t\t<span class=\"toolbar-title\">\n\t\t\t<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n\t\t\t\t<circle cx=\"12\" cy=\"12\" r=\"10\"/>\n\t\t\t\t<line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/>\n\t\t\t\t<line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n\t\t\t</svg>\n\t\t\tpreact-perf-tracker\n\t\t</span>\n\t\t<span class=\"separator\"></span>\n\t\t<span class=\"stat\">renders: <span class=\"stat-value\" data-renders>0</span></span>\n\t\t<span class=\"separator\"></span>\n\t\t<span class=\"stat\">r/s: <span class=\"stat-value\" data-rps>0</span></span>\n\t\t<span class=\"separator\"></span>\n\t\t<span class=\"stat\">fps: <span class=\"stat-value\" data-fps>--</span></span>\n\t\t<span class=\"separator\"></span>\n\t\t<span class=\"stat\">hot: <span class=\"stat-value\" data-hot>--</span></span>\n\t\t<span class=\"separator\"></span>\n\t\t<button class=\"toggle-btn active\" data-toggle>Enabled</button>\n\t\t<button class=\"toggle-btn\" data-reset>Reset</button>\n\t\t<button class=\"toggle-btn\" data-copy>Copy</button>\n\t`;\n\n\tconst toggleBtn = toolbar.querySelector('[data-toggle]') as HTMLButtonElement;\n\ttoggleBtn.addEventListener('click', () => {\n\t\tconst opts = getActiveOptions();\n\t\tconst next = !opts.enabled;\n\t\tsetActiveOptions({ enabled: next });\n\t\ttoggleBtn.textContent = next ? 'Enabled' : 'Disabled';\n\t\ttoggleBtn.classList.toggle('active', next);\n\t});\n\n\tconst resetBtn = toolbar.querySelector('[data-reset]') as HTMLButtonElement;\n\tresetBtn.addEventListener('click', () => {\n\t\tclearReport();\n\t\trenderCount = 0;\n\t\trendersThisSecond = 0;\n\t\trendersPerSecond = 0;\n\t\tupdateDisplay();\n\t});\n\n\tconst copyBtn = toolbar.querySelector('[data-copy]') as HTMLButtonElement;\n\tcopyBtn.addEventListener('click', async () => {\n\t\tconst summary = getReportSummary(25);\n\t\tconst payload = JSON.stringify(summary, null, 2);\n\t\tif (!navigator.clipboard?.writeText) return;\n\t\tawait navigator.clipboard.writeText(payload);\n\t\tconst old = copyBtn.textContent;\n\t\tcopyBtn.textContent = 'Copied';\n\t\twindow.setTimeout(() => {\n\t\t\tcopyBtn.textContent = old;\n\t\t}, 1200);\n\t});\n\n\tshadowRoot.appendChild(toolbar);\n\tconst legacyMarker = document.createElement('span');\n\tlegacyMarker.id = 'preact-scan-toolbar';\n\tlegacyMarker.style.display = 'none';\n\tshadowRoot.appendChild(legacyMarker);\n\tdocument.documentElement.appendChild(rootContainer);\n\n\treturn shadowRoot;\n}\n\nfunction updateDisplay() {\n\tif (!shadowRoot) return;\n\n\tconst rendersEl = shadowRoot.querySelector('[data-renders]');\n\tconst rpsEl = shadowRoot.querySelector('[data-rps]');\n\tconst fpsEl = shadowRoot.querySelector('[data-fps]');\n\tconst hotEl = shadowRoot.querySelector('[data-hot]');\n\n\tconst hottest = getReportSummary(1)[0];\n\tconst hotLabel = hottest\n\t\t? `${hottest.displayName} (${hottest.totalSelfTime.toFixed(1)}ms)`\n\t\t: '--';\n\n\tif (rendersEl) rendersEl.textContent = String(renderCount);\n\tif (rpsEl) rpsEl.textContent = String(rendersPerSecond);\n\tif (fpsEl) fpsEl.textContent = String(fps);\n\tif (hotEl) hotEl.textContent = hotLabel;\n}\n\n\nexport function createToolbar() {\n\tif (rootContainer) return;\n\tcreateToolbarDOM();\n\tfpsRafId = requestAnimationFrame(updateFps);\n}\n\nexport function destroyToolbar() {\n\tif (fpsRafId != null) {\n\t\tcancelAnimationFrame(fpsRafId);\n\t\tfpsRafId = null;\n\t}\n\tif (rootContainer) {\n\t\trootContainer.remove();\n\t\trootContainer = null;\n\t\tshadowRoot = null;\n\t}\n\trenderCount = 0;\n\trendersThisSecond = 0;\n\trendersPerSecond = 0;\n\tfps = 0;\n\tframeCount = 0;\n\tlastFpsTime = performance.now();\n}\n","import type { Options, RenderInfo, ReportEntry } from './types';\nimport {\n\thookIntoPreact,\n\tunhookFromPreact,\n\tsetActiveOptions,\n\tgetActiveOptions,\n\tgetReport as getReportInternal,\n\tgetReportSummary as getReportSummaryInternal,\n\tclearReport as clearReportInternal,\n\tresetActiveOptions,\n\taddRenderListener,\n\tremoveRenderListener,\n} from './instrumentation';\nimport { startOverlay, stopOverlay } from './overlay';\nimport { createToolbar, destroyToolbar, notifyToolbarRender } from './toolbar';\n\n\nlet started = false;\nconst toolbarRenderListener = (_info: RenderInfo) => {\n\tnotifyToolbarRender();\n};\n\n\n/**\n * Start tracking component renders.\n *\n * ```ts\n * import { install } from 'preact-perf-tracker';\n *\n * install({\n * enabled: true,\n * log: false,\n * showToolbar: true,\n * });\n * ```\n */\nexport function install(options: Options = {}): void {\n\tresetActiveOptions();\n\tsetOptions(options);\n\n\tconst opts = getActiveOptions();\n\n\tif (opts.enabled === false && opts.showToolbar !== true) {\n\t\treturn;\n\t}\n\n\tstart();\n}\n\n/**\n * Update options at runtime.\n */\nexport function setOptions(options: Partial<Options>): void {\n\tsetActiveOptions(options);\n}\n\n/**\n * Get the current options.\n */\nexport function getOptions(): Readonly<Options> {\n\treturn getActiveOptions();\n}\n\n/**\n * Get a report of all tracked components, or a single component type.\n */\nexport function getReport(\n\ttype?: unknown,\n): ReportEntry | Map<unknown, ReportEntry> | null {\n\treturn getReportInternal(type);\n}\n\n/**\n * Get the top report entries sorted by total self-time.\n */\nexport function getReportSummary(limit = 10) {\n\treturn getReportSummaryInternal(limit);\n}\n\n/**\n * Clear all accumulated report data.\n */\nexport function clearReport(): void {\n\tclearReportInternal();\n}\n\n/**\n * Stop tracking and clean up all resources.\n */\nexport function stop(): void {\n\tif (!started) return;\n\tstarted = false;\n\tremoveRenderListener(toolbarRenderListener);\n\tunhookFromPreact();\n\tstopOverlay();\n\tdestroyToolbar();\n}\n\n\nfunction start() {\n\tif (started) return;\n\tstarted = true;\n\n\thookIntoPreact();\n\taddRenderListener(toolbarRenderListener);\n\tstartOverlay();\n\n\tconst opts = getActiveOptions();\n\tif (opts.showToolbar !== false) {\n\t\tif (typeof document !== 'undefined') {\n\t\t\tif (document.readyState === 'loading') {\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', () => createToolbar(), {\n\t\t\t\t\tonce: true,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tcreateToolbar();\n\t\t\t}\n\t\t}\n\t}\n}\n\n\nexport type {\n\tOptions,\n\tRenderInfo,\n\tReportEntry,\n\tReportSummaryEntry,\n\tChange,\n\tChangeType,\n} from './types';\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "preact-perf-tracker",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"author": "Jovi De Croock <jovi@resynapse.dev>",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "Track re-renders happening in Preact.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/JoviDeCroock/preact-perf-tracker#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/JoviDeCroock/preact-perf-tracker.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/JoviDeCroock/preact-perf-tracker/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"preact",
|
|
18
|
+
"perf",
|
|
19
|
+
"re-render",
|
|
20
|
+
"tracker"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"require": "./dist/index.cjs",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.mjs",
|
|
30
|
+
"module": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown src/index.ts",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"changeset": "changeset",
|
|
43
|
+
"version": "changeset version",
|
|
44
|
+
"release": "pnpm build && changeset publish",
|
|
45
|
+
"prepublishOnly": "pnpm build"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"preact": "^10.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
52
|
+
"@changesets/cli": "^2.29.8",
|
|
53
|
+
"@preact/preset-vite": "^2.10.3",
|
|
54
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
55
|
+
"preact": "^10.28.3",
|
|
56
|
+
"tsdown": "^0.20.3",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"vitest": "^4.0.18",
|
|
59
|
+
"vitest-browser-preact": "^1.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createElement } from 'preact';
|
|
3
|
+
import { render } from 'preact';
|
|
4
|
+
import {
|
|
5
|
+
install,
|
|
6
|
+
stop,
|
|
7
|
+
setOptions,
|
|
8
|
+
getOptions,
|
|
9
|
+
getReport,
|
|
10
|
+
getReportSummary,
|
|
11
|
+
clearReport,
|
|
12
|
+
} from '../index';
|
|
13
|
+
import { setActiveOptions } from '../instrumentation';
|
|
14
|
+
import type { RenderInfo } from '../types';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let scratch: HTMLDivElement;
|
|
18
|
+
|
|
19
|
+
async function act(fn: () => void) {
|
|
20
|
+
fn();
|
|
21
|
+
// Flush Preact's microtask-based rerender queue
|
|
22
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
scratch = document.createElement('div');
|
|
27
|
+
document.body.appendChild(scratch);
|
|
28
|
+
// Reset options to defaults between tests
|
|
29
|
+
setActiveOptions({
|
|
30
|
+
enabled: true,
|
|
31
|
+
log: false,
|
|
32
|
+
showToolbar: false,
|
|
33
|
+
animationSpeed: 'off',
|
|
34
|
+
onRender: undefined,
|
|
35
|
+
onCommitStart: undefined,
|
|
36
|
+
onCommitFinish: undefined,
|
|
37
|
+
});
|
|
38
|
+
clearReport();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
stop();
|
|
43
|
+
render(null, scratch);
|
|
44
|
+
scratch.remove();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
describe('scan()', () => {
|
|
49
|
+
it('starts tracking and creates the toolbar', async () => {
|
|
50
|
+
install({ showToolbar: true });
|
|
51
|
+
await act(() => {});
|
|
52
|
+
|
|
53
|
+
const toolbar = document.getElementById('preact-tracker-toolbar');
|
|
54
|
+
expect(toolbar).not.toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not start if enabled=false and showToolbar is not true', () => {
|
|
58
|
+
install({ enabled: false, showToolbar: false });
|
|
59
|
+
|
|
60
|
+
// Toolbar should not exist
|
|
61
|
+
const toolbar = document.getElementById('preact-tracker-toolbar');
|
|
62
|
+
expect(toolbar).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('tracks renders after calling scan()', async () => {
|
|
66
|
+
const renders: RenderInfo[] = [];
|
|
67
|
+
install({
|
|
68
|
+
showToolbar: false,
|
|
69
|
+
onRender: (info) => renders.push(info),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function App() {
|
|
73
|
+
return createElement('div', null, 'hello');
|
|
74
|
+
}
|
|
75
|
+
await act(() => render(createElement(App, null), scratch));
|
|
76
|
+
|
|
77
|
+
expect(renders.length).toBe(1);
|
|
78
|
+
expect(renders[0].componentName).toBe('App');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
describe('stop()', () => {
|
|
84
|
+
it('removes the toolbar from the DOM', () => {
|
|
85
|
+
install({ showToolbar: true });
|
|
86
|
+
expect(document.getElementById('preact-tracker-toolbar')).not.toBeNull();
|
|
87
|
+
|
|
88
|
+
stop();
|
|
89
|
+
expect(document.getElementById('preact-tracker-toolbar')).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('stops tracking renders after stop()', async () => {
|
|
93
|
+
const renders: RenderInfo[] = [];
|
|
94
|
+
install({
|
|
95
|
+
showToolbar: false,
|
|
96
|
+
onRender: (info) => renders.push(info),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function Before() {
|
|
100
|
+
return createElement('div', null, 'before');
|
|
101
|
+
}
|
|
102
|
+
await act(() => render(createElement(Before, null), scratch));
|
|
103
|
+
const countBefore = renders.length;
|
|
104
|
+
|
|
105
|
+
stop();
|
|
106
|
+
|
|
107
|
+
function After() {
|
|
108
|
+
return createElement('div', null, 'after');
|
|
109
|
+
}
|
|
110
|
+
await act(() => render(createElement(After, null), scratch));
|
|
111
|
+
|
|
112
|
+
// No new renders tracked
|
|
113
|
+
expect(renders.length).toBe(countBefore);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('is safe to call stop() multiple times', () => {
|
|
117
|
+
install({ showToolbar: false });
|
|
118
|
+
expect(() => {
|
|
119
|
+
stop();
|
|
120
|
+
stop();
|
|
121
|
+
stop();
|
|
122
|
+
}).not.toThrow();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
describe('setOptions() / getOptions()', () => {
|
|
128
|
+
it('updates options at runtime', () => {
|
|
129
|
+
install({ showToolbar: false, log: false });
|
|
130
|
+
|
|
131
|
+
setOptions({ log: true });
|
|
132
|
+
expect(getOptions().log).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('preserves unmodified options', () => {
|
|
136
|
+
install({ showToolbar: false, animationSpeed: 'slow' });
|
|
137
|
+
|
|
138
|
+
setOptions({ log: true });
|
|
139
|
+
expect(getOptions().animationSpeed).toBe('slow');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
describe('getReport() / clearReport()', () => {
|
|
145
|
+
it('returns a Map of all tracked components', async () => {
|
|
146
|
+
install({ showToolbar: false });
|
|
147
|
+
|
|
148
|
+
function Foo() {
|
|
149
|
+
return createElement('span', null, 'foo');
|
|
150
|
+
}
|
|
151
|
+
function Bar() {
|
|
152
|
+
return createElement('span', null, 'bar');
|
|
153
|
+
}
|
|
154
|
+
await act(() =>
|
|
155
|
+
render(
|
|
156
|
+
createElement('div', null, createElement(Foo, null), createElement(Bar, null)),
|
|
157
|
+
scratch,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const report = getReport() as Map<unknown, any>;
|
|
162
|
+
expect(report).toBeInstanceOf(Map);
|
|
163
|
+
expect(report.size).toBeGreaterThanOrEqual(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns a single entry for a specific type', async () => {
|
|
167
|
+
install({ showToolbar: false });
|
|
168
|
+
|
|
169
|
+
function Target() {
|
|
170
|
+
return createElement('em', null, 'target');
|
|
171
|
+
}
|
|
172
|
+
await act(() => render(createElement(Target, null), scratch));
|
|
173
|
+
|
|
174
|
+
const entry = getReport(Target) as any;
|
|
175
|
+
expect(entry).not.toBeNull();
|
|
176
|
+
expect(entry.displayName).toBe('Target');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('clears all data with clearReport()', async () => {
|
|
180
|
+
install({ showToolbar: false });
|
|
181
|
+
|
|
182
|
+
function X() {
|
|
183
|
+
return createElement('span', null);
|
|
184
|
+
}
|
|
185
|
+
await act(() => render(createElement(X, null), scratch));
|
|
186
|
+
|
|
187
|
+
clearReport();
|
|
188
|
+
const report = getReport() as Map<unknown, any>;
|
|
189
|
+
expect(report.size).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns a summary list with a limit', async () => {
|
|
193
|
+
install({ showToolbar: false });
|
|
194
|
+
|
|
195
|
+
function SummaryTarget() {
|
|
196
|
+
return createElement('span', null, 'summary');
|
|
197
|
+
}
|
|
198
|
+
await act(() => render(createElement(SummaryTarget, null), scratch));
|
|
199
|
+
await act(() => render(createElement(SummaryTarget, null), scratch));
|
|
200
|
+
|
|
201
|
+
const summary = getReportSummary(1);
|
|
202
|
+
expect(summary.length).toBe(1);
|
|
203
|
+
expect(summary[0].displayName).toBe('SummaryTarget');
|
|
204
|
+
expect(summary[0].count).toBeGreaterThanOrEqual(2);
|
|
205
|
+
expect(summary[0].avgSelfTime).toBeGreaterThanOrEqual(0);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
describe('overlay', () => {
|
|
211
|
+
it('creates the overlay canvas element', async () => {
|
|
212
|
+
install({ showToolbar: false });
|
|
213
|
+
|
|
214
|
+
function Vis() {
|
|
215
|
+
return createElement('div', null, 'visible');
|
|
216
|
+
}
|
|
217
|
+
await act(() => render(createElement(Vis, null), scratch));
|
|
218
|
+
|
|
219
|
+
const canvas = document.getElementById('preact-scan-overlay');
|
|
220
|
+
expect(canvas).not.toBeNull();
|
|
221
|
+
expect(canvas!.tagName).toBe('CANVAS');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('removes the canvas on stop()', async () => {
|
|
225
|
+
install({ showToolbar: false });
|
|
226
|
+
|
|
227
|
+
function Vis2() {
|
|
228
|
+
return createElement('div', null, 'visible');
|
|
229
|
+
}
|
|
230
|
+
await act(() => render(createElement(Vis2, null), scratch));
|
|
231
|
+
|
|
232
|
+
stop();
|
|
233
|
+
const canvas = document.getElementById('preact-scan-overlay');
|
|
234
|
+
expect(canvas).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
describe('integration: full render cycle', () => {
|
|
240
|
+
it('tracks mount → update → unmount lifecycle', async () => {
|
|
241
|
+
const phases: string[] = [];
|
|
242
|
+
install({
|
|
243
|
+
showToolbar: false,
|
|
244
|
+
onRender: (info) => {
|
|
245
|
+
if (info.componentName === 'Lifecycle') {
|
|
246
|
+
phases.push(info.phase);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
function Lifecycle(props: { value: number }) {
|
|
252
|
+
return createElement('div', null, String(props.value));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Mount
|
|
256
|
+
await act(() => render(createElement(Lifecycle, { value: 0 }), scratch));
|
|
257
|
+
expect(phases).toContain('mount');
|
|
258
|
+
|
|
259
|
+
// Update
|
|
260
|
+
await act(() => render(createElement(Lifecycle, { value: 1 }), scratch));
|
|
261
|
+
expect(phases).toContain('update');
|
|
262
|
+
|
|
263
|
+
// Unmount
|
|
264
|
+
await act(() => render(null, scratch));
|
|
265
|
+
expect(phases).toContain('unmount');
|
|
266
|
+
|
|
267
|
+
expect(phases).toEqual(['mount', 'update', 'unmount']);
|
|
268
|
+
});
|
|
269
|
+
});
|