Repository: preactjs/preact-devtools
Pull request #388 (2022-06-04 13:49:52)

Improve stats capturing
- Simplify stats capturing
- Skip memoized subtrees


Patch:
diff --git a/src/adapter/10/options.ts b/src/adapter/10/options.ts
index a5c45a3..18d0e00 100644
--- a/src/adapter/10/options.ts
+++ b/src/adapter/10/options.ts
@@ -126,11 +126,14 @@ export function setupOptionsV10(
 
 	o._diff = o.__b = (vnode: VNode) => {
 		if (typeof vnode.type === "function") {
-			timings.start.set(vnode, performance.now());
 			const name = getDisplayName(vnode, config);
 			recordMark(`${name}_diff`);
 		}
 
+		if (vnode.type !== null) {
+			timings.start.set(vnode, performance.now());
+		}
+
 		if (prevBeforeDiff != null) prevBeforeDiff(vnode);
 	};
 
@@ -151,10 +154,13 @@ export function setupOptionsV10(
 				ownerStack.pop();
 			}
 
-			timings.end.set(vnode, performance.now());
 			endMark(getDisplayName(vnode, config));
 		}
 
+		if (vnode.type !== null) {
+			timings.end.set(vnode, performance.now());
+		}
+
 		if (prevAfterDiff) prevAfterDiff(vnode);
 	};
 
@@ -172,8 +178,10 @@ export function setupOptionsV10(
 
 	options.unmount = vnode => {
 		if (prevBeforeUnmount) prevBeforeUnmount(vnode);
-		timings.start.delete(vnode);
-		timings.end.delete(vnode);
+		if (vnode.type !== null) {
+			timings.start.delete(vnode);
+			timings.end.delete(vnode);
+		}
 		owners.delete(vnode);
 		renderer.onUnmount(vnode as any);
 	};
diff --git a/src/adapter/debug.ts b/src/adapter/debug.ts
index c32adab..a91c684 100644
--- a/src/adapter/debug.ts
+++ b/src/adapter/debug.ts
@@ -2,6 +2,7 @@ import { MsgTypes } from "./protocol/events";
 import { parseTable, flushTable } from "./protocol/string-table";
 import { ID, DevNodeType } from "../view/store/types";
 import { renderReasonToStr } from "./shared/renderReasons";
+import { parseStats } from "./shared/stats";
 
 export interface ParsedMsg {
 	rootId: number;
@@ -326,6 +327,12 @@ export function printCommit(data: number[]) {
 					i += 2 + count;
 					break;
 				}
+				case MsgTypes.COMMIT_STATS: {
+					const stats = parseStats(i + 1, data);
+					console.log("stats", stats.stats);
+					i = stats.i;
+					break;
+				}
 				default: {
 					console.log("Not implemented", data[i]);
 				}
diff --git a/src/adapter/protocol/events.ts b/src/adapter/protocol/events.ts
index a80cb6f..79d12af 100644
--- a/src/adapter/protocol/events.ts
+++ b/src/adapter/protocol/events.ts
@@ -3,7 +3,7 @@ import { Store } from "../../view/store/types";
 import { recordProfilerCommit } from "../../view/components/profiler/data/commits";
 import { ops2Tree } from "./operations";
 import { applyOperationsV1 } from "./legacy/operationsV1";
-import { Stats, stats2ops } from "../shared/stats";
+import { OperationInfo, Stats, stats2ops } from "../shared/stats";
 
 export enum MsgTypes {
 	ADD_ROOT = 1,
@@ -93,12 +93,24 @@ export function flush(commit: Commit) {
 	}
 	msg.push(...operations);
 	if (stats !== null) {
-		msg.push(...stats2ops(rootId, stats));
+		msg.push(...stats2ops(stats));
 	}
 
 	return { type: "operation_v2", data: msg };
 }
 
+function sumArrays(a: number[], b: number[]) {
+	for (let i = 0; i < b.length; i++) {
+		a[i] += b[i];
+	}
+}
+
+function sumOps(a: OperationInfo, b: OperationInfo) {
+	a.components += b.components;
+	a.elements += b.elements;
+	a.text += b.text;
+}
+
 /**
  * This is the heart of the devtools. Here we translate incoming events
  * and construct the tree data structure which all operations in the
@@ -141,33 +153,36 @@ export function applyOperationsV2(store: Store, data: number[]) {
 			store.stats.data.$ = stats;
 		} else {
 			store.stats.data.update(v => {
-				for (const key in stats) {
-					const next = (stats as any)[key];
-
-					if (key === "singleChildType") {
-						const old = (v as any)[key];
-						old.roots += next.roots;
-						old.classComponents += next.classComponents;
-						old.functionComponents += next.functionComponents;
-						old.fragments += next.fragments;
-						old.forwardRef += next.forwardRef;
-						old.memo += next.memo;
-						old.suspense += next.suspense;
-						old.elements += next.elements;
-						old.text += next.text;
-					} else {
-						if (typeof next === "object") {
-							const old = (v as any)[key];
-							next.children.forEach((nextValue: any, nextKey: any) => {
-								const oldChildren = old.children.get(nextKey) || 0;
-								old.children.set(nextKey, oldChildren + nextValue);
-							});
-							old.total += next.total;
-						} else {
-							(v as any)[key] += (stats as any)[key];
-						}
-					}
-				}
+				if (v === null) return;
+
+				sumArrays(v.roots, stats.roots);
+				sumArrays(v.classComponents, stats.classComponents);
+				sumArrays(v.functionComponents, stats.functionComponents);
+				sumArrays(v.fragments, stats.fragments);
+				sumArrays(v.forwardRef, stats.forwardRef);
+				sumArrays(v.memo, stats.memo);
+				sumArrays(v.suspense, stats.suspense);
+				sumArrays(v.elements, stats.elements);
+				v.text += stats.text;
+				sumArrays(v.keyed, stats.keyed);
+				sumArrays(v.unkeyed, stats.unkeyed);
+				sumArrays(v.mixed, stats.mixed);
+
+				sumOps(v.mounts, stats.mounts);
+				sumOps(v.updates, stats.updates);
+				sumOps(v.unmounts, stats.unmounts);
+
+				const single = v.singleChildType;
+				const singleNew = stats.singleChildType;
+
+				single.roots += singleNew.roots;
+				single.classComponents += singleNew.classComponents;
+				single.functionComponents += singleNew.functionComponents;
+				single.fragments += singleNew.fragments;
+				single.forwardRef += singleNew.forwardRef;
+				single.memo += singleNew.memo;
+				single.suspense += singleNew.suspense;
+				single.elements += singleNew.elements;
 
 				return v;
 			});
diff --git a/src/adapter/protocol/operations.ts b/src/adapter/protocol/operations.ts
index 55b6318..c8f24f6 100644
--- a/src/adapter/protocol/operations.ts
+++ b/src/adapter/protocol/operations.ts
@@ -141,10 +141,9 @@ export function ops2Tree(oldTree: Tree, existingRoots: ID[], ops: number[]) {
 				break;
 			}
 			case MsgTypes.COMMIT_STATS: {
-				const count = ops[i + 1];
-				const statsOps = ops.slice(i + 1, i + 1 + count);
-				stats = parseStats(statsOps);
-				i = i + 1 + count;
+				const statsData = parseStats(i + 1, ops);
+				i = statsData.i;
+				stats = statsData.stats;
 				break;
 			}
 			case MsgTypes.HOC_NODES: {
diff --git a/src/adapter/shared/renderer.ts b/src/adapter/shared/renderer.ts
index 50cb689..1abca2f 100644
--- a/src/adapter/shared/renderer.ts
+++ b/src/adapter/shared/renderer.ts
@@ -7,7 +7,7 @@ import { FilterState } from "../adapter/filter";
 import { Renderer } from "../renderer";
 import { startDrawing } from "../adapter/highlightUpdates";
 import { setIn, setInCopy } from "../shared/serialize";
-import { createStats } from "../shared/stats";
+import { createStats, OperationInfo } from "../shared/stats";
 import { ProfilerState } from "../adapter/profiler";
 import {
 	getVNodeById,
@@ -74,7 +74,23 @@ export function createRenderer<T extends SharedVNode>(
 
 	const domToVNode = new WeakMap<HTMLElement | Text, T>();
 
+	let unmountStats: OperationInfo = {
+		components: 0,
+		elements: 0,
+		text: 0,
+	};
+
 	function onUnmount(vnode: T) {
+		if (profiler.recordStats) {
+			if (bindings.isComponent(vnode)) {
+				unmountStats.components++;
+			} else if (bindings.isElement(vnode)) {
+				unmountStats.elements++;
+			} else {
+				unmountStats.text++;
+			}
+		}
+
 		if (!shouldFilter(vnode, filters, config, bindings)) {
 			if (hasVNodeId(ids, vnode)) {
 				currentUnmounts.push(getVNodeId(ids, vnode));
@@ -180,6 +196,13 @@ export function createRenderer<T extends SharedVNode>(
 
 			roots.forEach(root => {
 				const rootId = getVNodeId(ids, root);
+
+				unmountStats = {
+					components: 0,
+					elements: 0,
+					text: 0,
+				};
+
 				traverse(root, vnode => this.onUnmount(vnode), bindings);
 
 				const commit: Commit = {
@@ -191,7 +214,7 @@ export function createRenderer<T extends SharedVNode>(
 				};
 
 				if (commit.stats !== null) {
-					commit.stats.unmounts += commit.unmountIds.length;
+					commit.stats.unmounts = unmountStats;
 				}
 
 				const unmounts = flush(commit);
@@ -246,7 +269,13 @@ export function createRenderer<T extends SharedVNode>(
 			timingsByVNode.end.clear();
 
 			if (commit.stats !== null) {
-				commit.stats.unmounts += currentUnmounts.length;
+				commit.stats.unmounts = unmountStats;
+
+				unmountStats = {
+					components: 0,
+					elements: 0,
+					text: 0,
+				};
 			}
 
 			commit.unmountIds.push(...currentUnmounts);
diff --git a/src/adapter/shared/stats.ts b/src/adapter/shared/stats.ts
index fe6e5cd..2dcabdd 100644
--- a/src/adapter/shared/stats.ts
+++ b/src/adapter/shared/stats.ts
@@ -1,4 +1,4 @@
-import { DevNodeType, ID } from "../../view/store/types";
+import { DevNodeType } from "../../view/store/types";
 import { MsgTypes } from "../protocol/events";
 import { PreactBindings, SharedVNode } from "./bindings";
 import { getDevtoolsType, RendererConfig } from "./renderer";
@@ -10,27 +10,30 @@ export enum DiffType {
 	MIXED = 3,
 }
 
-export interface ComponentStats {
-	total: number;
-	children: number[];
+export type StatsChildren = [number, number, number, number, number];
+
+export interface OperationInfo {
+	components: number;
+	elements: number;
+	text: number;
 }
 
 export interface Stats {
-	roots: ComponentStats;
-	classComponents: ComponentStats;
-	functionComponents: ComponentStats;
-	fragments: ComponentStats;
-	forwardRef: ComponentStats;
-	memo: ComponentStats;
-	suspense: ComponentStats;
-	elements: ComponentStats;
+	roots: StatsChildren;
+	classComponents: StatsChildren;
+	functionComponents: StatsChildren;
+	fragments: StatsChildren;
+	forwardRef: StatsChildren;
+	memo: StatsChildren;
+	suspense: StatsChildren;
+	elements: StatsChildren;
 	text: number;
-	keyed: ComponentStats;
-	unkeyed: ComponentStats;
-	mixed: ComponentStats;
-	mounts: number;
-	updates: number;
-	unmounts: number;
+	keyed: StatsChildren;
+	unkeyed: StatsChildren;
+	mixed: StatsChildren;
+	mounts: OperationInfo;
+	updates: OperationInfo;
+	unmounts: OperationInfo;
 	singleChildType: {
 		roots: number;
 		classComponents: number;
@@ -44,74 +47,58 @@ export interface Stats {
 	};
 }
 
+function getChildCountIdx(count: number) {
+	return count > 4 ? 4 : count;
+}
+
 export function updateDiffStats(
 	stats: Stats,
 	diff: DiffType,
 	childCount: number,
 ) {
+	const idx = getChildCountIdx(childCount);
 	if (diff === DiffType.KEYED) {
-		stats.keyed.total++;
-		stats.keyed.children.push(childCount);
+		stats.keyed[idx]++;
 	} else if (diff === DiffType.UNKEYED) {
-		stats.unkeyed.total++;
-		stats.unkeyed.children.push(childCount);
+		stats.unkeyed[idx]++;
 	} else if (diff === DiffType.MIXED) {
-		stats.mixed.total++;
-		stats.mixed.children.push(childCount);
+		stats.mixed[idx]++;
+	}
+}
+
+export function updateOpStats<T extends SharedVNode>(
+	stats: Stats,
+	kind: "updates" | "mounts" | "unmounts",
+	vnode: T,
+	bindings: PreactBindings<T>,
+) {
+	if (bindings.isComponent(vnode)) {
+		stats[kind].components++;
+	} else if (bindings.isElement(vnode)) {
+		stats[kind].elements++;
+	} else {
+		stats[kind].text++;
 	}
 }
 
 // TODO: store update depth
 export function createStats(): Stats {
 	return {
-		roots: {
-			total: 0,
-			children: [],
-		},
-		classComponents: {
-			total: 0,
-			children: [],
-		},
-		functionComponents: {
-			total: 0,
-			children: [],
-		},
-		fragments: {
-			total: 0,
-			children: [],
-		},
-		forwardRef: {
-			total: 0,
-			children: [],
-		},
-		memo: {
-			total: 0,
-			children: [],
-		},
-		suspense: {
-			total: 0,
-			children: [],
-		},
-		elements: {
-			total: 0,
-			children: [],
-		},
+		roots: [0, 0, 0, 0, 0],
+		classComponents: [0, 0, 0, 0, 0],
+		functionComponents: [0, 0, 0, 0, 0],
+		fragments: [0, 0, 0, 0, 0],
+		forwardRef: [0, 0, 0, 0, 0],
+		memo: [0, 0, 0, 0, 0],
+		suspense: [0, 0, 0, 0, 0],
+		elements: [0, 0, 0, 0, 0],
 		text: 0,
-		keyed: {
-			total: 0,
-			children: [],
-		},
-		unkeyed: {
-			total: 0,
-			children: [],
-		},
-		mixed: {
-			total: 0,
-			children: [],
-		},
-		mounts: 0,
-		unmounts: 0,
-		updates: 0,
+		keyed: [0, 0, 0, 0, 0],
+		unkeyed: [0, 0, 0, 0, 0],
+		mixed: [0, 0, 0, 0, 0],
+		mounts: { components: 0, elements: 0, text: 0 },
+		unmounts: { components: 0, elements: 0, text: 0 },
+		updates: { components: 0, elements: 0, text: 0 },
 		singleChildType: {
 			roots: 0,
 			classComponents: 0,
@@ -126,58 +113,32 @@ export function createStats(): Stats {
 	};
 }
 
-export function stats2ops(rootId: ID, stats: Stats): number[] {
+export function stats2ops(stats: Stats): number[] {
 	return [
 		MsgTypes.COMMIT_STATS,
-		rootId,
-
-		stats.roots.total,
-		stats.roots.children.length,
-		...stats.roots.children,
-
-		stats.classComponents.total,
-		stats.classComponents.children.length,
-		...stats.classComponents.children,
-
-		stats.functionComponents.total,
-		stats.functionComponents.children.length,
-		...stats.functionComponents.children,
-
-		stats.fragments.total,
-		stats.fragments.children.length,
-		...stats.fragments.children,
-
-		stats.forwardRef.total,
-		stats.forwardRef.children.length,
-		...stats.forwardRef.children,
-
-		stats.memo.total,
-		stats.memo.children.length,
-		...stats.memo.children,
-
-		stats.suspense.total,
-		stats.suspense.children.length,
-		...stats.suspense.children,
-
-		stats.elements.total,
-		stats.elements.children.length,
-		...stats.elements.children,
-
+		...stats.roots,
+		...stats.classComponents,
+		...stats.functionComponents,
+		...stats.fragments,
+		...stats.forwardRef,
+		...stats.memo,
+		...stats.suspense,
+		...stats.elements,
 		stats.text,
 
-		stats.keyed.total,
-		stats.keyed.children.length,
-		...stats.keyed.children,
-		stats.unkeyed.total,
-		stats.unkeyed.children.length,
-		...stats.unkeyed.children,
-		stats.mixed.total,
-		stats.mixed.children.length,
-		...stats.mixed.children,
+		...stats.keyed,
+		...stats.unkeyed,
+		...stats.mixed,
 
-		stats.mounts,
-		stats.updates,
-		stats.unmounts,
+		stats.mounts.components,
+		stats.mounts.elements,
+		stats.mounts.text,
+		stats.updates.components,
+		stats.updates.elements,
+		stats.updates.text,
+		stats.unmounts.components,
+		stats.unmounts.elements,
+		stats.unmounts.text,
 
 		// Single child types
 		stats.singleChildType.roots,
@@ -192,27 +153,22 @@ export function stats2ops(rootId: ID, stats: Stats): number[] {
 	];
 }
 
-export interface ParsedComponentStats {
-	total: number;
-	children: Map<number, number>;
-}
-
 export interface ParsedStats {
-	roots: ParsedComponentStats;
-	classComponents: ParsedComponentStats;
-	functionComponents: ParsedComponentStats;
-	fragments: ParsedComponentStats;
-	forwardRef: ParsedComponentStats;
-	memo: ParsedComponentStats;
-	suspense: ParsedComponentStats;
-	elements: ParsedComponentStats;
+	roots: StatsChildren;
+	classComponents: StatsChildren;
+	functionComponents: StatsChildren;
+	fragments: StatsChildren;
+	forwardRef: StatsChildren;
+	memo: StatsChildren;
+	suspense: StatsChildren;
+	elements: StatsChildren;
 	text: number;
-	keyed: ParsedComponentStats;
-	unkeyed: ParsedComponentStats;
-	mixed: ParsedComponentStats;
-	mounts: number;
-	updates: number;
-	unmounts: number;
+	keyed: StatsChildren;
+	unkeyed: StatsChildren;
+	mixed: StatsChildren;
+	mounts: OperationInfo;
+	updates: OperationInfo;
+	unmounts: OperationInfo;
 	singleChildType: {
 		roots: number;
 		classComponents: number;
@@ -249,23 +205,18 @@ export function recordComponentStats<T extends SharedVNode>(
 	children: Array<T | null | undefined>,
 ) {
 	const childrenLen = children.length;
-	const type = vnode.type;
-	if (typeof type === "function") {
-		if (type.prototype && type.prototype.render) {
-			stats.classComponents.total++;
-			stats.classComponents.children.push(childrenLen);
-		} else {
-			if (type === config.Fragment) {
-				stats.fragments.total++;
-				stats.fragments.children.push(childrenLen);
-			}
+	const idx = getChildCountIdx(childrenLen);
 
-			stats.functionComponents.total++;
-			stats.functionComponents.children.push(childrenLen);
+	if (bindings.isComponent(vnode)) {
+		if (vnode.type === config.Fragment) {
+			stats.fragments[idx]++;
+		} else if (vnode.type.prototype && vnode.type.prototype.render) {
+			stats.classComponents[idx]++;
+		} else {
+			stats.functionComponents[idx]++;
 		}
-	} else if (type !== null) {
-		stats.elements.total++;
-		stats.elements.children.push(childrenLen);
+	} else if (bindings.isElement(vnode)) {
+		stats.elements[idx]++;
 	} else {
 		stats.text++;
 	}
@@ -273,16 +224,13 @@ export function recordComponentStats<T extends SharedVNode>(
 	const devType = getDevtoolsType(vnode, bindings);
 	switch (devType) {
 		case DevNodeType.ForwardRef:
-			stats.forwardRef.total++;
-			stats.forwardRef.children.push(childrenLen);
+			stats.forwardRef[idx]++;
 			break;
 		case DevNodeType.Memo:
-			stats.memo.total++;
-			stats.memo.children.push(childrenLen);
+			stats.memo[idx]++;
 			break;
 		case DevNodeType.Suspense:
-			stats.suspense.total++;
-			stats.suspense.children.push(childrenLen);
+			stats.suspense[idx]++;
 			break;
 	}
 
@@ -320,67 +268,57 @@ export function recordComponentStats<T extends SharedVNode>(
 	}
 }
 
-export function parseComponentStats(i: number, ops: number[]) {
-	const total = ops[i++];
-	const len = ops[i++];
-	const children = new Map();
-
-	let j = len;
-	while (j--) {
-		const value = children.get(ops[i + j]) || 0;
-		children.set(ops[i + j], value + 1);
-	}
-
-	i += len;
+export function parseChildrenStats(
+	i: number,
+	ops: number[],
+): { i: number; data: StatsChildren } {
+	const data: StatsChildren = [
+		ops[i++],
+		ops[i++],
+		ops[i++],
+		ops[i++],
+		ops[i++],
+	];
 
 	return {
 		i,
-		total,
-		children,
+		data,
 	};
 }
 
-export function parseStats(ops: number[]): ParsedStats {
-	let i = 1;
-	// const rootId = ops[i++];
-	const roots = parseComponentStats(i, ops);
-	i = roots.i;
-
-	const klass = parseComponentStats(i, ops);
-	i = klass.i;
-
-	const fnComps = parseComponentStats(i, ops);
-	i = fnComps.i;
-
-	const fragments = parseComponentStats(i, ops);
-	i = fragments.i;
-
-	const forwardRef = parseComponentStats(i, ops);
-	i = forwardRef.i;
-
-	const memo = parseComponentStats(i, ops);
-	i = memo.i;
-
-	const suspense = parseComponentStats(i, ops);
-	i = suspense.i;
+export function parseTypeStats(i: number, ops: number[]) {
+	const data = { components: ops[i++], elements: ops[i++], text: ops[i++] };
+	return {
+		i,
+		data,
+	};
+}
 
-	const elements = parseComponentStats(i, ops);
+export function parseStats(
+	i: number,
+	ops: number[],
+): { i: number; stats: ParsedStats } {
+	const roots = parseChildrenStats(i, ops);
+	const klass = parseChildrenStats(roots.i, ops);
+	const fnComps = parseChildrenStats(klass.i, ops);
+	const fragments = parseChildrenStats(fnComps.i, ops);
+	const forwardRef = parseChildrenStats(fragments.i, ops);
+	const memo = parseChildrenStats(forwardRef.i, ops);
+	const suspense = parseChildrenStats(memo.i, ops);
+	const elements = parseChildrenStats(suspense.i, ops);
 	i = elements.i;
 
 	const text = ops[i++];
 
-	const keyed = parseComponentStats(i, ops);
-	i = keyed.i;
-
-	const unkeyed = parseComponentStats(i, ops);
-	i = unkeyed.i;
-
-	const mixed = parseComponentStats(i, ops);
+	const keyed = parseChildrenStats(i, ops);
+	const unkeyed = parseChildrenStats(keyed.i, ops);
+	const mixed = parseChildrenStats(unkeyed.i, ops);
 	i = mixed.i;
 
-	const mounts = ops[i++];
-	const updates = ops[i++];
-	const unmounts = ops[i++];
+	const mounts = parseTypeStats(i, ops);
+	const updates = parseTypeStats(mounts.i, ops);
+	const unmounts = parseTypeStats(updates.i, ops);
+	i = unmounts.i;
 
 	const singleRoots = ops[i++];
 	const singleClassComponents = ops[i++];
@@ -392,24 +330,24 @@ export function parseStats(ops: number[]): ParsedStats {
 	const singleElements = ops[i++];
 	const singleText = ops[i++];
 
-	return {
-		roots,
-		classComponents: klass,
-		functionComponents: fnComps,
-		fragments,
-		forwardRef,
-		memo,
-		suspense,
-		elements,
+	const stats = {
+		roots: roots.data,
+		classComponents: klass.data,
+		functionComponents: fnComps.data,
+		fragments: fragments.data,
+		forwardRef: forwardRef.data,
+		memo: memo.data,
+		suspense: suspense.data,
+		elements: elements.data,
 		text,
 
-		keyed,
-		unkeyed,
-		mixed,
+		keyed: keyed.data,
+		unkeyed: unkeyed.data,
+		mixed: mixed.data,
 
-		mounts,
-		updates,
-		unmounts,
+		mounts: mounts.data,
+		updates: updates.data,
+		unmounts: unmounts.data,
 		singleChildType: {
 			roots: singleRoots,
 			classComponents: singleClassComponents,
@@ -422,4 +360,6 @@ export function parseStats(ops: number[]): ParsedStats {
 			text: singleText,
 		},
 	};
+
+	return { i, stats };
 }
diff --git a/src/adapter/shared/traverse.ts b/src/adapter/shared/traverse.ts
index fa80db7..31d71a9 100644
--- a/src/adapter/shared/traverse.ts
+++ b/src/adapter/shared/traverse.ts
@@ -13,7 +13,7 @@ import {
 import { ProfilerState } from "../adapter/profiler";
 import { getDevtoolsType, RendererConfig } from "./renderer";
 import { RenderReason, RenderReasonData } from "./renderReasons";
-import { createStats, DiffType, updateDiffStats } from "./stats";
+import { createStats, DiffType, updateDiffStats, updateOpStats } from "./stats";
 import { NodeType } from "../../constants";
 import { getDiffType, recordComponentStats } from "./stats";
 import { measureUpdate } from "../adapter/highlightUpdates";
@@ -196,7 +196,7 @@ function mount<T extends SharedVNode>(
 	renderReasonPre: Map<T, RenderReasonData> | null,
 ) {
 	if (commit.stats !== null) {
-		commit.stats.mounts++;
+		updateOpStats(commit.stats, "mounts", vnode, bindings);
 	}
 
 	const root = bindings.isRoot(vnode, config);
@@ -354,7 +354,7 @@ function update<T extends SharedVNode>(
 	renderReasonPre: Map<T, RenderReasonData> | null,
 ) {
 	if (commit.stats !== null) {
-		commit.stats.updates++;
+		updateOpStats(commit.stats, "updates", vnode, bindings);
 	}
 
 	let diff = DiffType.UNKNOWN;
@@ -421,7 +421,7 @@ function update<T extends SharedVNode>(
 			timingsByVNode,
 			renderReasonPre,
 		);
-		return true;
+		return;
 	}
 
 	const id = getVNodeId(ids, vnode);
@@ -429,45 +429,47 @@ function update<T extends SharedVNode>(
 	updateVNodeId(ids, id, vnode);
 
 	const didRender = timingsByVNode.end.has(vnode);
-	if (didRender) {
-		const name = bindings.getDisplayName(vnode, config);
-		if (filters.type.has("hoc")) {
-			const res = detectHocs(commit, name, id, hocs);
-			hocs = res.hocs;
-		}
+	if (!didRender) {
+		return;
+	}
 
-		commit.operations.push(
-			MsgTypes.UPDATE_VNODE_TIMINGS,
-			id,
-			(timingsByVNode.start.get(vnode) || 0) * 1000,
-			(timingsByVNode.end.get(vnode) || 0) * 1000,
-		);
+	const name = bindings.getDisplayName(vnode, config);
+	if (filters.type.has("hoc")) {
+		const res = detectHocs(commit, name, id, hocs);
+		hocs = res.hocs;
+	}
 
-		if (profiler.isProfiling && profiler.captureRenderReasons) {
-			const reason =
-				renderReasonPre !== null
-					? renderReasonPre.get(vnode) || null
-					: bindings.getRenderReasonPost(
-							ids,
-							bindings,
-							timingsByVNode,
-							oldVNode,
-							vnode,
-					  );
-			if (reason !== null) {
-				const count = reason.items ? reason.items.length : 0;
-				commit.operations.push(MsgTypes.RENDER_REASON, id, reason.type, count);
-				if (reason.items && count > 0) {
-					commit.operations.push(
-						...reason.items.map(str => getStringId(commit.strings, str)),
-					);
-				}
+	commit.operations.push(
+		MsgTypes.UPDATE_VNODE_TIMINGS,
+		id,
+		(timingsByVNode.start.get(vnode) || 0) * 1000,
+		(timingsByVNode.end.get(vnode) || 0) * 1000,
+	);
+
+	if (profiler.isProfiling && profiler.captureRenderReasons) {
+		const reason =
+			renderReasonPre !== null
+				? renderReasonPre.get(vnode) || null
+				: bindings.getRenderReasonPost(
+						ids,
+						bindings,
+						timingsByVNode,
+						oldVNode,
+						vnode,
+				  );
+		if (reason !== null) {
+			const count = reason.items ? reason.items.length : 0;
+			commit.operations.push(MsgTypes.RENDER_REASON, id, reason.type, count);
+			if (reason.items && count > 0) {
+				commit.operations.push(
+					...reason.items.map(str => getStringId(commit.strings, str)),
+				);
 			}
 		}
-
-		updateHighlight(profiler, vnode, bindings);
 	}
 
+	updateHighlight(profiler, vnode, bindings);
+
 	const oldChildren = oldVNode
 		? bindings
 				.getActualChildren(oldVNode)
@@ -599,9 +601,8 @@ export function createCommit<T extends SharedVNode>(
 
 	if (helpers.isRoot(vnode, config)) {
 		if (commit.stats !== null) {
-			commit.stats.roots.total++;
-			const children = helpers.getActualChildren(vnode);
-			commit.stats.roots.children.push(children.length);
+			const childrenLen = helpers.getActualChildren(vnode).length;
+			commit.stats.roots[childrenLen > 4 ? 4 : childrenLen]++;
 		}
 
 		parentId = -1;
diff --git a/src/view/components/stats/StatsPanel.tsx b/src/view/components/stats/StatsPanel.tsx
index 2fe420e..d551b03 100644
--- a/src/view/components/stats/StatsPanel.tsx
+++ b/src/view/components/stats/StatsPanel.tsx
@@ -7,18 +7,11 @@ import { Actions } from "../Actions";
 import { IconBtn } from "../IconBtn";
 import { RecordIcon, Refresh, NotInterested } from "../icons";
 import { useCallback } from "preact/hooks";
-import { ParsedStats } from "../../../adapter/shared/stats";
-
-function parseChildrenMap(data: Map<number, number>): Children {
-	const children: Children = [0, 0, 0, 0, 0];
-
-	data.forEach((v, k) => {
-		if (k >= 4) k = 4;
-		children[k] += v;
-	});
-
-	return children;
-}
+import {
+	OperationInfo,
+	ParsedStats,
+	StatsChildren,
+} from "../../../adapter/shared/stats";
 
 export function StatsRecordBtn() {
 	const store = useStore();
@@ -123,8 +116,6 @@ export function StatsPanel() {
 	);
 }
 
-export type Children = [number, number, number, number, number];
-
 export function ChildHeadings() {
 	return (
 		<tr>
@@ -139,10 +130,18 @@ export function ChildHeadings() {
 	);
 }
 
+function getTotal(arr: number[]): number {
+	return arr.reduce((acc, n) => acc + n, 0);
+}
+
+function getOpTotal(info: OperationInfo): number {
+	return info.components + info.elements + info.text;
+}
+
 export function ChildRow(props: {
 	label: string;
 	total: number;
-	count?: Children;
+	count?: StatsChildren;
 	testId?: string;
 }) {
 	const {
@@ -176,6 +175,26 @@ export function ChildRow(props: {
 	);
 }
 
+function OperationRow({ name, info }: { name: string; info: OperationInfo }) {
+	return (
+		<tr>
+			<td>{name}</td>
+			<td class={s.alignRight} data-testid={name + "-total"}>
+				{getOpTotal(info)}
+			</td>
+			<td class={s.alignRight} data-testid={name + "-components"}>
+				{info.components}
+			</td>
+			<td class={s.alignRight} data-testid={name + "-elements"}>
+				{info.elements}
+			</td>
+			<td class={s.alignRight} data-testid={name + "-text"}>
+				{info.text}
+			</td>
+		</tr>
+	);
+}
+
 export function StatsData({ stats }: { stats: ParsedStats }) {
 	return (
 		<Fragment>
@@ -202,21 +221,15 @@ export function StatsData({ stats }: { stats: ParsedStats }) {
 							<tr>
 								<th>Type</th>
 								<th>Total</th>
+								<th>Components</th>
+								<th>Elements</th>
+								<th>Text</th>
 							</tr>
 						</thead>
 						<tbody>
-							<tr>
-								<td>mount</td>
-								<td class={s.alignRight}>{stats.mounts}</td>
-							</tr>
-							<tr>
-								<td>update</td>
-								<td class={s.alignRight}>{stats.updates}</td>
-							</tr>
-							<tr>
-								<td>unmount</td>
-								<td class={s.alignRight}>{stats.unmounts}</td>
-							</tr>
+							<OperationRow name="mount" info={stats.mounts} />
+							<OperationRow name="update" info={stats.updates} />
+							<OperationRow name="unmount" info={stats.unmounts} />
 						</tbody>
 					</table>
 				</div>
@@ -231,18 +244,18 @@ export function StatsData({ stats }: { stats: ParsedStats }) {
 						<tbody>
 							<ChildRow
 								label="keyed"
-								total={stats.keyed.total}
-								count={parseChildrenMap(stats.keyed.children)}
+								total={getTotal(stats.keyed)}
+								count={stats.keyed}
 							/>
 							<ChildRow
 								label="unkeyed"
-								total={stats.unkeyed.total}
-								count={parseChildrenMap(stats.unkeyed.children)}
+								total={getTotal(stats.unkeyed)}
+								count={stats.unkeyed}
 							/>
 							<ChildRow
 								label="mixed"
-								total={stats.mixed.total}
-								count={parseChildrenMap(stats.mixed.children)}
+								total={getTotal(stats.mixed)}
+								count={stats.mixed}
 							/>
 						</tbody>
 					</table>
@@ -259,50 +272,50 @@ export function StatsData({ stats }: { stats: ParsedStats }) {
 							<ChildRow
 								label="Root"
 								testId="root"
-								total={stats.roots.total}
-								count={parseChildrenMap(stats.roots.children)}
+								total={getTotal(stats.roots)}
+								count={stats.roots}
 							/>
 							<ChildRow
 								label="Class Component"
 								testId="class-component"
-								total={stats.classComponents.total}
-								count={parseChildrenMap(stats.classComponents.children)}
+								total={getTotal(stats.classComponents)}
+								count={stats.classComponents}
 							/>
 							<ChildRow
 								label="Function Component*"
 								testId="function-component"
-								total={stats.functionComponents.total}
-								count={parseChildrenMap(stats.functionComponents.children)}
+								total={getTotal(stats.functionComponents)}
+								count={stats.functionComponents}
 							/>
 							<ChildRow
 								label="Fragment"
 								testId="fragment"
-								total={stats.fragments.total}
-								count={parseChildrenMap(stats.fragments.children)}
+								total={getTotal(stats.fragments)}
+								count={stats.fragments}
 							/>
 							<ChildRow
 								label="forwardRef"
 								testId="forwardref"
-								total={stats.forwardRef.total}
-								count={parseChildrenMap(stats.forwardRef.children)}
+								total={getTotal(stats.forwardRef)}
+								count={stats.forwardRef}
 							/>
 							<ChildRow
 								label="Memo"
 								testId="memo"
-								total={stats.memo.total}
-								count={parseChildrenMap(stats.memo.children)}
+								total={getTotal(stats.memo)}
+								count={stats.memo}
 							/>
 							<ChildRow
 								label="Suspense"
 								testId="suspense"
-								total={stats.suspense.total}
-								count={parseChildrenMap(stats.suspense.children)}
+								total={getTotal(stats.suspense)}
+								count={stats.suspense}
 							/>
 							<ChildRow
 								label="Element"
 								testId="element"
-								total={stats.elements.total}
-								count={parseChildrenMap(stats.elements.children)}
+								total={getTotal(stats.elements)}
+								count={stats.elements}
 							/>
 
 							<ChildRow label="Text**" testId="text" total={stats.text} />


Test patch:
diff --git a/src/adapter/shared/renderer.test.tsx b/src/adapter/shared/renderer.test.tsx
index 7f99e79..9926f57 100644
--- a/src/adapter/shared/renderer.test.tsx
+++ b/src/adapter/shared/renderer.test.tsx
@@ -73,6 +73,7 @@ describe("Renderer 10", () => {
 		expect(toSnapshot(spy.args[1][1])).to.deep.equal([
 			"rootId: 1",
 			"Update timings 1",
+			"Update timings 2",
 		]);
 	});
 
@@ -106,6 +107,7 @@ describe("Renderer 10", () => {
 			"rootId: 1",
 			"Add 3 <span> to parent 2",
 			"Update timings 1",
+			"Update timings 2",
 		]);
 	});
 
@@ -148,6 +150,7 @@ describe("Renderer 10", () => {
 		expect(toSnapshot(spy.args[1][1])).to.deep.equal([
 			"rootId: 1",
 			"Update timings 1",
+			"Update timings 2",
 		]);
 	});
 
@@ -170,6 +173,9 @@ describe("Renderer 10", () => {
 		expect(toSnapshot(spy.args[1][1])).to.deep.equal([
 			"rootId: 1",
 			"Update timings 1",
+			"Update timings 2",
+			"Update timings 4",
+			"Update timings 3",
 			"Reorder 2 [4, 3]",
 		]);
 	});
diff --git a/test-e2e/fixtures/apps/memo-stats.jsx b/test-e2e/fixtures/apps/memo-stats.jsx
new file mode 100644
index 0000000..686d089
--- /dev/null
+++ b/test-e2e/fixtures/apps/memo-stats.jsx
@@ -0,0 +1,29 @@
+import { h, render } from "preact";
+import { useState } from "preact/hooks";
+import { memo } from "preact/compat";
+
+function Value(props) {
+	return <p>{props.children}</p>;
+}
+
+function Display(props) {
+	return <Value>Counter: {props.value}</Value>;
+}
+
+const MemoDisplay = memo(Display);
+
+function Counter() {
+	const [v, set] = useState(0);
+
+	return (
+		<div style="padding: 2rem;">
+			<MemoDisplay value="42" />
+			<button onClick={() => set(v + 1)}>Increment</button>
+			<p data-testid="result" data-value={v}>
+				{v}
+			</p>
+		</div>
+	);
+}
+
+render(<Counter />, document.getElementById("app"));
diff --git a/test-e2e/tests/stats/stats-memo.test.ts b/test-e2e/tests/stats/stats-memo.test.ts
new file mode 100644
index 0000000..8c87144
--- /dev/null
+++ b/test-e2e/tests/stats/stats-memo.test.ts
@@ -0,0 +1,32 @@
+import {
+	newTestPage,
+	clickTab,
+	waitForSelector,
+	clickRecordButton,
+} from "../../test-utils";
+import { getText, waitForTestId, clickSelector } from "pentf/browser_utils";
+import { expect } from "chai";
+
+export const description = "Skip memoized components for stats";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "memo-stats");
+
+	await clickTab(devtools, "STATISTICS");
+	await waitForTestId(devtools, "stats-info");
+
+	await clickRecordButton(devtools);
+
+	await clickSelector(page, "button");
+	await waitForSelector(page, '[data-value="1"]');
+	await clickRecordButton(devtools);
+
+	const mountTotal = await getText(devtools, '[data-testid="mount-total"]');
+	expect(mountTotal).to.equal("0");
+
+	const updateTotal = await getText(devtools, '[data-testid="update-total"]');
+	expect(updateTotal).to.equal("8");
+
+	const unmountTotal = await getText(devtools, '[data-testid="unmount-total"]');
+	expect(unmountTotal).to.equal("0");
+}