Repository: preactjs/preact-devtools
Pull request #366 (2022-05-18 04:37:42)

Add new layout algorithm for flamegraph
The new one is more visually pleasing and doesn't have gaps between child nodes. It's more in line with what users expect it to look like.


Patch:
diff --git a/src/adapter/protocol/legacy/operationsV1.ts b/src/adapter/protocol/legacy/operationsV1.ts
index 969697f..76b94bf 100644
--- a/src/adapter/protocol/legacy/operationsV1.ts
+++ b/src/adapter/protocol/legacy/operationsV1.ts
@@ -1,7 +1,7 @@
 import { MsgTypes } from "../events";
 import { parseTable } from "../string-table";
 import { Store } from "../../../view/store/types";
-import { deepClone } from "../../../view/components/profiler/flamegraph/modes/adjustNodesToRight";
+import { deepClone } from "../../shared/utils";
 
 export function applyOperationsV1(store: Store, data: number[]) {
 	const nodes = new Map(store.nodes.$);
diff --git a/src/adapter/protocol/operations.ts b/src/adapter/protocol/operations.ts
index b082bf8..ec2b997 100644
--- a/src/adapter/protocol/operations.ts
+++ b/src/adapter/protocol/operations.ts
@@ -1,7 +1,7 @@
 import { ID, Tree } from "../../view/store/types";
 import { parseTable } from "./string-table";
 import { MsgTypes } from "./events";
-import { deepClone } from "../../view/components/profiler/flamegraph/modes/adjustNodesToRight";
+import { deepClone } from "../shared/utils";
 import { RenderReasonMap } from "../shared/renderReasons";
 import { ParsedStats, parseStats } from "../shared/stats";
 
diff --git a/src/adapter/shared/utils.ts b/src/adapter/shared/utils.ts
index e6cb216..9a2e756 100644
--- a/src/adapter/shared/utils.ts
+++ b/src/adapter/shared/utils.ts
@@ -15,3 +15,8 @@ export function traverse<T extends SharedVNode>(
 		}
 	}
 }
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export function deepClone<T extends object>(obj: T): T {
+	return JSON.parse(JSON.stringify(obj));
+}
diff --git a/src/view/components/profiler/components/CommitInfo/DebugInfo.tsx b/src/view/components/profiler/components/CommitInfo/DebugInfo.tsx
index b274f67..313187d 100644
--- a/src/view/components/profiler/components/CommitInfo/DebugInfo.tsx
+++ b/src/view/components/profiler/components/CommitInfo/DebugInfo.tsx
@@ -38,7 +38,7 @@ export function DebugProfilerInfo() {
 				<br />
 				<dt class={s.title}>tree:</dt>
 				<dd class={s.value}>
-					<TimeRange from={pos.start} to={pos.end} />
+					<TimeRange from={pos.x} to={pos.x + pos.width} />
 				</dd>
 				<br />
 				<dt class={s.title}>real:</dt>
diff --git a/src/view/components/profiler/data/commits.ts b/src/view/components/profiler/data/commits.ts
index bd98858..9b743d9 100644
--- a/src/view/components/profiler/data/commits.ts
+++ b/src/view/components/profiler/data/commits.ts
@@ -5,8 +5,7 @@ import {
 	RenderReasonMap,
 	RenderReasonData,
 } from "../../../../adapter/shared/renderReasons";
-import { FlameNodeTransform } from "../flamegraph/modes/flamegraph-utils";
-import { FlameTree, patchTree } from "../flamegraph/modes/patchTree";
+import { patchTree } from "../flamegraph/modes/patchTree";
 import { NodeTransform } from "../flamegraph/shared";
 import { toTransform } from "../flamegraph/ranked/ranked-utils";
 
@@ -66,7 +65,7 @@ export interface ProfilerState {
 	flamegraphType: Observable<FlamegraphType>;
 
 	// Flamegraph mode
-	flamegraphNodes: Observable<Map<ID, FlameNodeTransform>>;
+	flamegraphNodes: Observable<Map<ID, NodeTransform>>;
 	rankedNodes: Observable<NodeTransform[]>;
 }
 
@@ -145,26 +144,19 @@ export function createProfiler(): ProfilerState {
 	});
 
 	// FlamegraphNode
-	const flamegraphNodes = watch<FlameTree>(() => {
+	const flamegraphNodes = watch<Map<ID, NodeTransform>>(() => {
 		const commit = activeCommit.$;
 		if (!commit || flamegraphType.$ !== FlamegraphType.FLAMEGRAPH) {
 			return new Map();
 		}
 
-		let prevCommit = null;
 		for (let i = activeCommitIdx.$ - 1; i >= 0; i--) {
 			if (i >= commits.$.length) {
 				return new Map();
 			}
-
-			const search = commits.$[i];
-			if (search.rootId === commit.rootId) {
-				prevCommit = search;
-				break;
-			}
 		}
 
-		return patchTree(prevCommit, commit);
+		return patchTree(commit);
 	});
 
 	const rankedNodes = watch<NodeTransform[]>(() => {
diff --git a/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx b/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx
index a433a15..4055f9f 100644
--- a/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx
+++ b/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx
@@ -7,6 +7,7 @@ import { placeFlamegraph } from "./flamegraph-utils";
 import { formatTime } from "../../util";
 import { useObserver, useStore } from "../../../../store/react-bindings";
 import { HocLabels } from "../../../elements/TreeView";
+import { getSelfDuration } from "./patchTree";
 
 export interface FlamegraphLayoutProps {
 	commit: CommitData;
@@ -47,7 +48,7 @@ export function FlamegraphLayout({
 				const node = commit.nodes.get(pos.id)!;
 				let appendix = "";
 				if (!pos.commitParent && pos.weight !== -1) {
-					const self = formatTime(commit.selfDurations.get(node.id)!);
+					const self = formatTime(getSelfDuration(commit, node));
 					const total = formatTime(node.endTime - node.startTime);
 					appendix = ` (${self} of ${total})`;
 				}
diff --git a/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.ts b/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.ts
deleted file mode 100644
index 6e7ba25..0000000
--- a/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ID, DevNode, Tree } from "../../../../store/types";
-import { FlameTree } from "./patchTree";
-
-export function adjustNodesToRight(
-	tree: Tree,
-	flame: FlameTree,
-	id: ID,
-	delta: number,
-	stopParent: ID,
-	ignore: Set<ID>,
-) {
-	const children: ID[] = [];
-
-	let item = tree.get(id);
-	let prevParent = item;
-	while (item && (item = tree.get(item.parent))) {
-		if (ignore.has(item.id)) break;
-
-		flame.get(item.id)!.end += delta;
-
-		const idx = item.children.indexOf(prevParent!.id);
-		if (idx < item.children.length - 1) {
-			children.push(...item.children.slice(idx + 1));
-		}
-
-		// Stop traversing upwards if we reach the user defined limit
-		if (item.id === stopParent) break;
-
-		prevParent = item;
-	}
-
-	let childId;
-	while ((childId = children.pop())) {
-		if (ignore.has(childId)) continue;
-
-		const pos = flame.get(childId)!;
-		pos.start += delta;
-		pos.end += delta;
-
-		const node = tree.get(childId)!;
-		children.push(...node.children);
-	}
-}
-
-export function cloneTree(tree: Map<ID, DevNode>) {
-	const clone = new Map<ID, DevNode>();
-	tree.forEach((node, id) => clone.set(id, JSON.parse(JSON.stringify(node))));
-	return clone;
-}
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-export function deepClone<T extends object>(obj: T): T {
-	return JSON.parse(JSON.stringify(obj));
-}
diff --git a/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts
index 5879240..0dfc3a2 100644
--- a/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts
+++ b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts
@@ -1,6 +1,5 @@
 import { NodeTransform } from "../shared";
-import { ID, DevNodeType, DevNode } from "../../../../store/types";
-import { FlameTree } from "./patchTree";
+import { ID, DevNode } from "../../../../store/types";
 
 export interface FlameNodeTransform extends NodeTransform {
 	/** Relative start position to root node (without zoom) */
@@ -14,71 +13,56 @@ export interface FlameNodeTransform extends NodeTransform {
  */
 export function placeFlamegraph(
 	tree: Map<ID, DevNode>,
-	transforms: FlameTree,
+	idToTransform: Map<ID, NodeTransform>,
 	rootId: ID,
 	selectedId: ID,
 	canvasWidth: number,
-): FlameNodeTransform[] {
-	const selectedNode = tree.get(selectedId);
-	// Collect parents upfront, so that we already know which
-	// nodes should be maximized in the next traversal
-	const parents = new Set<ID>([selectedId]);
-	let item: DevNode | undefined = selectedNode;
-	while (item && (item = tree.get(item.parent))) {
-		parents.add(item.id);
-	}
+): NodeTransform[] {
+	const maximizedIds = new Set<ID>([selectedId]);
+	const commitParentIds = new Set<ID>();
 
-	const selectedPos = transforms.get(selectedId);
-	if (!selectedPos) return [];
+	let parentId = tree.get(rootId)!.parent;
+	while (parentId !== -1) {
+		const node = tree.get(parentId);
+		if (node === undefined) break;
+		commitParentIds.add(parentId);
+		parentId = node.parent;
+	}
 
-	const root = tree.get(rootId)!;
-	const scale =
-		(canvasWidth || 1) / (selectedPos.end - selectedPos.start || 0.01);
-	const offset = selectedPos.start;
+	parentId = tree.get(selectedId)!.parent;
+	while (parentId !== -1) {
+		const node = tree.get(parentId);
+		if (node === undefined) break;
+		maximizedIds.add(parentId);
+		parentId = node.parent;
+	}
 
-	// Use this value to keep track of when we enter the current
-	// sub-tree that we're zoomed in into. Once we processed all
-	// nodes of that we need to reset this value.
-	let visitedSelectedRow = -1;
+	let offset = 0;
+	let scale = 1;
 
-	const stack = [root];
-	while ((item = stack.pop())) {
-		// Track when we enter and leave the currently focused sub-tree
-		if (item.id === selectedId) {
-			visitedSelectedRow = item.depth;
-		} else if (item.depth <= visitedSelectedRow) {
-			visitedSelectedRow = -1;
-		}
+	const selectedPos = idToTransform.get(selectedId);
+	if (selectedPos !== undefined) {
+		offset = selectedPos.x;
+		scale = canvasWidth / selectedPos.width;
+	}
 
-		if (item.type !== DevNodeType.Group) {
-			const pos = transforms.get(item.id)!;
-			// If we are a parent node of the selected node or the selected
-			// node itself, we need to maximize it by stretching it over
-			// the whole canvas.
-			if (parents.has(item.id)) {
-				pos.maximized = true;
-				pos.width = canvasWidth;
-				pos.x = 0;
-				pos.visible = true;
-			}
-			// At this point the node isn't maximized. If we're not inside
-			// the currently zoomed in sub-tree, we must be a node that
-			// should be hidden.
-			else if (visitedSelectedRow === -1) {
-				pos.visible = false;
-			}
-			// Looks like the node is visible and we have to position it
-			// inside the visible canvas.
-			else {
-				pos.maximized = false;
-				pos.visible = true;
-				pos.x = (pos.start - offset) * scale;
-				pos.width = (pos.end - pos.start) * scale;
-			}
+	return Array.from(idToTransform.values()).map(pos => {
+		let start;
+		let width;
+		if (maximizedIds.has(pos.id)) {
+			start = 0;
+			width = canvasWidth;
+		} else {
+			start = (pos.x - offset) * scale;
+			width = pos.width * scale;
 		}
 
-		stack.push(...item.children.map(id => tree.get(id)!));
-	}
-
-	return Array.from(transforms.values());
+		return {
+			...pos,
+			x: start,
+			width,
+			maximized: maximizedIds.has(pos.id),
+			visible: pos.x >= 0 && pos.x <= canvasWidth,
+		};
+	});
 }
diff --git a/src/view/components/profiler/flamegraph/modes/patchTree.ts b/src/view/components/profiler/flamegraph/modes/patchTree.ts
index 27e6cca..db15695 100644
--- a/src/view/components/profiler/flamegraph/modes/patchTree.ts
+++ b/src/view/components/profiler/flamegraph/modes/patchTree.ts
@@ -1,460 +1,90 @@
-import { Tree, ID, DevNode, DevNodeType } from "../../../../store/types";
-import { adjustNodesToRight } from "./adjustNodesToRight";
+import { ID, DevNode } from "../../../../store/types";
 import { FlameNodeTransform } from "./flamegraph-utils";
 import { getGradient } from "../../data/gradient";
 import { CommitData } from "../../data/commits";
+import { NodeTransform } from "../shared";
 
 export type FlameTree = Map<ID, FlameNodeTransform>;
 
-function createTransform(): FlameNodeTransform {
-	return {
-		id: -1,
-		weight: -1,
-		commitParent: false,
-		row: -1,
-		start: -1,
-		end: -1,
-
-		// Only used for local transforms (zooming, canvas resizing)
-		visible: true,
-		maximized: false,
-		width: 0,
-		x: 0,
-	};
-}
-
-function createGroup(): DevNode {
-	return {
-		id: -1,
-		type: DevNodeType.Group,
-		children: [],
-		depth: -1,
-		hocs: null,
-		endTime: -1,
-		startTime: -1,
-		name: "__PREACT_GROUP__",
-		key: "",
-		parent: -1,
-	};
-}
-
-export function getStartPosition(tree: Tree, flame: FlameTree, node: DevNode) {
-	const parent = tree.get(node.parent)!;
-
-	// Get START position (where to move the tree) by:
-	//  - previous sibling end (if previous siblings are present)
-	//  - otherwise: parent start
-	const idx = parent.children.indexOf(node.id);
-	return idx > 0
-		? flame.get(parent.children[idx - 1])!.end
-		: flame.get(parent.id)!.start;
-}
-
-export function getEndPosition(tree: Tree, flame: FlameTree, id: ID) {
-	const node = tree.get(id)!;
-	const parent = tree.get(node.parent)!;
-	// Get END position (where to move the tree) by:
-	//  - next sibling start (if next siblings are present)
-	//  - otherwise: parent end
-	const idx = parent.children.indexOf(id);
-	return idx + 1 < parent.children.length
-		? flame.get(parent.children[idx + 1])!.start
-		: flame.get(parent.id)!.end;
-}
-
 /**
- * Move node as far to the end as possible, where they are usually
- * found. This prevents sub-trees from moving left to right between
- * each commit.
- */
-export function moveToEnd(tree: Tree, flame: FlameTree, item: DevNode) {
-	const end = getEndPosition(tree, flame, item.id);
-	const pos = flame.get(item.id)!;
-	if (end > pos.end) {
-		const offset = end - pos.end;
-
-		const stack = [item.id];
-		let id;
-		while ((id = stack.pop())) {
-			const pos = flame.get(id);
-			const node = tree.get(id);
-			if (!pos || !node) continue;
-
-			pos.start += offset;
-			pos.end += offset;
-
-			stack.push(...node.children);
-		}
-	}
-}
-
-/**
- * Place static (memoized) trees. Contrary to non-static ones these
- * should not expand parent nodes, but scale down to fit into the
- * remaining space.
+ * Place commit tree. When a node's children are bigger than it's parent,
+ * the parent and all next siblings will be expanded (pushed) to the right.
  */
-export function placeStaticTrees(
-	tree: Tree,
-	flame: FlameTree,
-	staticRoots: DevNode[],
-) {
-	for (let i = staticRoots.length - 1; i >= 0; i--) {
-		const root = staticRoots[i];
-		const needsResize: DevNode[] = [];
-
-		const offsetStack = [-root.startTime];
-		const subtreeLevels = [-1];
-
-		const stack = [root.id];
-		let id;
-		while ((id = stack.pop())) {
-			const node = tree.get(id);
-			if (!node) continue;
-
-			const parent = tree.get(node.parent);
-			// Check if node is root of a re-queued sub-tree
-			if (
-				parent &&
-				(node.startTime >= parent.endTime || node.endTime >= parent.endTime)
-			) {
-				const start = getStartPosition(tree, flame, node);
-				offsetStack.push(-(node.startTime - start));
-				subtreeLevels.push(node.depth);
-				needsResize.push(node);
-			}
-
-			const offset = offsetStack[offsetStack.length - 1];
-			const start = node.startTime + offset;
-			const end = node.endTime + offset;
-
-			const flameParent = flame.get(node.parent);
-			if (flameParent && flameParent.end < end) {
-				needsResize.push(node);
-			}
-
-			const pos = createTransform();
-			pos.id = node.id;
-			pos.row = node.depth;
-			pos.start = start;
-			pos.end = end;
-			pos.weight = -1;
-			flame.set(node.id, pos);
-
-			// Push children to stack in reverse to ensure that we
-			// always traverse the tree in pre-order
-			for (let j = node.children.length - 1; j >= 0; j--) {
-				stack.push(node.children[j]);
+export function patchTree(commit: CommitData) {
+	const { nodes, commitRootId } = commit;
+
+	const idToTransform = new Map<ID, NodeTransform>();
+	placeNode(commit, idToTransform, commit.rootId);
+
+	let direct: DevNode | undefined = nodes.get(commitRootId);
+	if (direct !== undefined) {
+		while ((direct = nodes.get(direct.parent))) {
+			const transform = idToTransform.get(direct.id);
+			if (transform) {
+				transform.commitParent = true;
 			}
 		}
-
-		needsResize.forEach(node => {
-			const delta =
-				flame.get(node.id)!.end - getEndPosition(tree, flame, node.id);
-			adjustNodesToRight(tree, flame, node.id, delta, root.id, new Set());
-
-			// Move nodes to end for a less jarring visual experience
-			if (delta < 0) {
-				moveToEnd(tree, flame, node);
-			}
-		});
 	}
 
-	// At this point each static tree is consistent in itself, but we
-	// need to move them the correct position in commit tree and scale
-	// the static one to fit the available space.
-
-	// 1. Group static siblings
-	// 2. Move static siblings next to each other
-	// 3. Move Group to final start position
-	// 4. Scale down group to available width if necessary
-
-	// Build groups
-	let groupId = -1;
-	const groups: DevNode[] = [];
-	for (let i = 0; i < staticRoots.length; i++) {
-		const root = staticRoots[i];
-
-		const group = createGroup();
-		group.id = groupId--;
-		group.children.push(root.id);
-		group.parent = root.parent;
-
-		const parent = tree.get(root.parent);
-		if (parent) {
-			const idx = parent.children.indexOf(root.id);
-			let j = 1;
-			const i2 = i;
-			while (idx + j < parent.children.length) {
-				if (i2 + j > staticRoots.length - 1) {
-					break;
-				}
-
-				if (staticRoots[i2 + j].parent === parent.id) {
-					group.children.push(staticRoots[i2 + j].id);
-					i++;
-				}
-				j++;
-			}
-		}
-
-		groups.push(group);
-	}
-
-	// Move siblings next to each other
-	groups.forEach(group => {
-		const root = tree.get(group.children[0])!;
-		const rootPos = flame.get(root.id)!;
-		let offset = rootPos.end;
-
-		for (let i = 1; i < group.children.length; i++) {
-			const child = group.children[i];
-			const pos = flame.get(child)!;
-
-			const stack = [child];
-			let item;
-			while ((item = stack.pop())) {
-				const childPos = flame.get(item)!;
-				const oldStart = childPos.start;
-				childPos.start = offset;
-				childPos.end = offset + (childPos.end - oldStart);
-
-				const node = tree.get(item)!;
-				stack.push(...node.children);
-			}
-
-			offset = pos.end;
-		}
-
-		// Move group to final position
-		const parent = tree.get(root.parent);
-		if (parent) {
-			const firstId = group.children[0];
-			const lastId = group.children[group.children.length - 1];
-
-			const firstIdx = parent.children.indexOf(firstId);
-			const lastIdx = parent.children.indexOf(lastId);
-
-			const start =
-				firstIdx > 0
-					? flame.get(parent.children[firstIdx - 1])!.end
-					: flame.get(parent.id)!.start;
-			const end =
-				lastIdx + 1 < parent.children.length
-					? flame.get(parent.children[lastIdx + 1])!.end
-					: flame.get(parent.id)!.end;
-
-			const firstPos = flame.get(firstId)!;
-			const lastPos = flame.get(lastId)!;
-			offset = start - firstPos.start;
-
-			const available = end - start;
-			const actual = flame.get(lastId)!.end - flame.get(firstId)!.start;
-			const scale = available < actual ? available / actual : 1;
+	return idToTransform;
+}
 
-			const stack = [...group.children];
-			let item;
-			while ((item = stack.pop())) {
-				const childPos = flame.get(item)!;
-				childPos.start = childPos.start * scale + offset;
-				childPos.end = childPos.end * scale + offset;
+export function getSelfDuration(commit: CommitData, node: DevNode) {
+	const duration = node.endTime - node.startTime;
+	const children = node.children.reduce((acc, childId) => {
+		const child = commit.nodes.get(childId);
+		if (!child) return 0;
 
-				const node = tree.get(item)!;
-				stack.push(...node.children);
-			}
+		return acc + (child.endTime - child.startTime);
+	}, 0);
 
-			offset = lastPos.end;
-		}
-	});
+	const selfDuration = duration - children;
+	return selfDuration <= 0 ? 0 : selfDuration;
 }
 
-/**
- * Place commit tree. When a node's children are bigger than it's parent,
- * the parent and all next siblings will be expanded (pushed) to the right.
- */
-export function patchTree(
-	prevCommit: CommitData | null,
+function placeNode(
 	commit: CommitData,
-): FlameTree {
-	const { nodes: tree, rootId, commitRootId, maxSelfDuration } = commit;
-
-	const flame: FlameTree = new Map();
-
-	if (tree.size === 0) {
-		return flame;
-	}
+	idToTransform: Map<ID, NodeTransform>,
+	id: ID,
+) {
+	const node = commit.nodes.get(id)!;
+	if (!node) return;
 
-	const commitParents = new Set<ID>();
-	let direct: DevNode | undefined = tree.get(commitRootId)!;
-	while ((direct = tree.get(direct.parent))) {
-		commitParents.add(direct.id);
+	let start = 0;
+	if (node.parent !== -1) {
+		const parentPos = idToTransform.get(node.parent)!;
+		start = parentPos.x + parentPos.width;
 	}
 
-	const renderedNodes = new Set<ID>();
-
-	const root = tree.get(rootId)!;
-	const commitRoot = tree.get(commitRootId)!;
-
-	// Nodes that represent roots of non-static sub-trees that
-	// we inserted. We may need to resize parents and push siblings
-	// to the right if the sub-tree node doesn't fit into the
-	// available space.
-	const maybeGrow: DevNode[] = [];
-
-	const staticRoots: DevNode[] = [];
-
-	// Check if we are in mount mode
-	// Walk tree, because we may encounter re-queued sub-trees
-	// that need a different offset value.
-	const offsetStack = [-root.startTime];
-	const subtreeLevels = [-1];
-
-	const stack = [rootId];
-	let item;
-	while ((item = stack.pop())) {
-		const node = tree.get(item);
-		if (!node) continue;
-
-		// Add to rendered nodes if we're traversing into the actively
-		// committed tree
-		if (node.id === commitRootId || renderedNodes.has(node.parent)) {
-			renderedNodes.add(node.id);
-		}
-
-		// Check if we've traversed out of the subtree and need
-		// to go back to the previous offset value
-		if (node.depth === subtreeLevels[subtreeLevels.length - 1]) {
-			offsetStack.pop();
-			subtreeLevels.pop();
-		}
-
-		if (node.id === commitRootId && rootId !== commitRootId) {
-			const parent = tree.get(commitRoot.parent)!;
-			const idx = parent.children.indexOf(commitRootId);
-			const start =
-				idx > 0
-					? flame.get(parent.children[idx - 1])!.end
-					: flame.get(parent.id)!.start;
-
-			const commitOffset = start - commitRoot.startTime;
-
-			offsetStack.push(commitOffset);
-			subtreeLevels.push(node.depth);
-		}
-
-		// Check if we're dealing with a memoized tree
-		if (renderedNodes.has(node.id) && node.startTime < commitRoot.startTime) {
-			staticRoots.push(node);
-			continue;
-		}
-
-		const parent = tree.get(node.parent);
-		// Check if node is root of a re-queued sub-tree
-		if (node.id !== commitRootId && parent) {
-			const start = getStartPosition(tree, flame, node);
-
-			const offset = offsetStack[offsetStack.length - 1];
-			if (node.startTime + offset < start) {
-				offsetStack.push(start - node.startTime);
-				subtreeLevels.push(node.depth);
-			} else if (
-				node.startTime >= parent.endTime ||
-				node.endTime >= parent.endTime
-			) {
-				offsetStack.push(start - node.startTime);
-				subtreeLevels.push(node.depth);
-
-				const nextOffset = offsetStack[offsetStack.length - 1];
-				if (
-					node.startTime + nextOffset >= parent.endTime ||
-					node.endTime + nextOffset >= parent.endTime
-				) {
-					maybeGrow.push(node);
-				}
-			}
-		}
-
-		const offset = offsetStack[offsetStack.length - 1];
-		const start = node.startTime + offset;
-		const end = node.endTime + offset;
-
-		const flameParent = flame.get(node.parent);
-		if (flameParent) {
-			if (flameParent.end < end) {
-				maybeGrow.push(node);
-			}
-		}
-
-		const pos = createTransform();
-		pos.id = node.id;
-		pos.weight = !renderedNodes.has(node.id)
-			? -1
-			: getGradient(maxSelfDuration, commit.selfDurations.get(node.id) || 0);
-		pos.commitParent = commitParents.has(node.id);
-		pos.row = node.depth;
-		pos.start = start;
-		pos.end = end;
-		flame.set(node.id, pos);
-
-		if (start < 0) {
-			// eslint-disable-next-line no-console
-			console.warn(
-				`< 0 ${node.name} #${node.id} ${end} ${offset} ${node.startTime}`,
-			);
+	const selfDuration = getSelfDuration(commit, node);
 
-			pos.start += -start;
-			pos.end += -start;
-		}
-
-		// Push children to stack in reverse to ensure that we
-		// always traverse the tree in pre-order
-		for (let i = node.children.length - 1; i >= 0; i--) {
-			stack.push(node.children[i]);
-		}
-	}
-
-	// Check if nodes that were flagged for potential enlargement
-	// actually need to be resized
-	while ((item = maybeGrow.pop())) {
-		let delta = flame.get(item.id)!.end - getEndPosition(tree, flame, item.id);
+	const nodePos: NodeTransform = {
+		id,
+		row: node.depth,
+		x: start,
+		commitParent: false,
+		maximized: false,
+		visible: false,
+		weight: commit.selfDurations.has(id) ? getGradient(50, selfDuration) : -1,
+		width: selfDuration,
+	};
+	idToTransform.set(id, nodePos);
 
-		if (delta < 0 && item.id === commitRootId && prevCommit !== null) {
-			const prev = prevCommit.nodes.get(commitRootId);
-			if (prev) {
-				delta = item.endTime - item.startTime - (prev.endTime - prev.startTime);
-			}
-		}
-		adjustNodesToRight(
-			tree,
-			flame,
-			item.id,
-			delta,
-			rootId,
-			new Set(staticRoots.map(node => node.id)),
-		);
+	for (let i = 0; i < node.children.length; i++) {
+		const childId = node.children[i];
+		const child = commit.nodes.get(childId);
+		if (!child) continue;
 
-		// Move nodes to end for a less jarring visual experience
-		if (delta < 0) {
-			moveToEnd(tree, flame, item);
-		}
-	}
+		const selfDuration = getSelfDuration(commit, child);
 
-	placeStaticTrees(tree, flame, staticRoots);
+		placeNode(commit, idToTransform, childId);
 
-	// Prettify: We'll expand the commitRoot tree to fit available space
-	if (commitRoot.parent > -1) {
-		const end = getEndPosition(tree, flame, commitRootId);
-		const pos = flame.get(commitRootId)!;
-		if (end > pos.end) {
-			adjustNodesToRight(
-				tree,
-				flame,
-				commitRootId,
-				pos.end - end,
-				-1,
-				new Set(),
-			);
+		// Expand parents upwards by self duration
+		let parentId = id;
+		while (parentId !== -1) {
+			const parent = commit.nodes.get(parentId)!;
+			idToTransform.get(parentId)!.width += selfDuration;
+			parentId = parent.parent;
 		}
 	}
-
-	return flame;
 }


Test patch:
diff --git a/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.test.ts b/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.test.ts
deleted file mode 100644
index 646982e..0000000
--- a/src/view/components/profiler/flamegraph/modes/adjustNodesToRight.test.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { flames } from "../testHelpers";
-import { expect } from "chai";
-import { adjustNodesToRight } from "./adjustNodesToRight";
-
-function toTimings(res: ReturnType<typeof flames>) {
-	return Array.from(res.idMap.values()).map(node => {
-		const t = res.transformMap.get(node.id)!;
-		return {
-			name: node.name,
-			start: t.start,
-			end: t.end,
-		};
-	});
-}
-
-describe("adjustNodesToRight", () => {
-	it("should traverse parents", () => {
-		const tree = flames`
-    App *****************
-      Foo **   Bar ******
-                Bob ***
-    `;
-
-		expect(toTimings(tree)).to.deep.equal([
-			{ name: "App", start: 0, end: 210 },
-			{ name: "Foo", start: 20, end: 80 },
-			{ name: "Bar", start: 110, end: 210 },
-			{ name: "Bob", start: 120, end: 190 },
-		]);
-		adjustNodesToRight(
-			tree.idMap,
-			tree.transformMap,
-			tree.byName("Bob")!.id,
-			10,
-			-1,
-			new Set(),
-		);
-		expect(toTimings(tree)).to.deep.equal([
-			{ name: "App", start: 0, end: 220 },
-			{ name: "Foo", start: 20, end: 80 },
-			{ name: "Bar", start: 110, end: 220 },
-			{ name: "Bob", start: 120, end: 190 },
-		]);
-	});
-
-	it("should map to right", () => {
-		const tree = flames`
-    App *****************
-      Foo **   Bar ******
-       Faz *    Bob ***
-    `;
-
-		expect(toTimings(tree)).to.deep.equal([
-			{ name: "App", start: 0, end: 210 },
-			{ name: "Foo", start: 20, end: 80 },
-			{ name: "Faz", start: 30, end: 80 },
-			{ name: "Bar", start: 110, end: 210 },
-			{ name: "Bob", start: 120, end: 190 },
-		]);
-		adjustNodesToRight(
-			tree.idMap,
-			tree.transformMap,
-			tree.byName("Faz")!.id,
-			10,
-			-1,
-			new Set(),
-		);
-		expect(toTimings(tree)).to.deep.equal([
-			{ name: "App", start: 0, end: 220 },
-			{ name: "Foo", start: 20, end: 90 },
-			{ name: "Faz", start: 30, end: 80 },
-			{ name: "Bar", start: 120, end: 220 },
-			{ name: "Bob", start: 130, end: 200 },
-		]);
-	});
-});
diff --git a/src/view/components/profiler/flamegraph/modes/patchTree.test.ts b/src/view/components/profiler/flamegraph/modes/patchTree.test.ts
index ccbe1e2..35d88f8 100644
--- a/src/view/components/profiler/flamegraph/modes/patchTree.test.ts
+++ b/src/view/components/profiler/flamegraph/modes/patchTree.test.ts
@@ -1,18 +1,19 @@
 import { expect } from "chai";
-import { patchTree, FlameTree } from "./patchTree";
+import { patchTree } from "./patchTree";
 import { flames } from "../testHelpers";
 import { Tree, DevNode, ID } from "../../../../store/types";
+import { NodeTransform } from "../shared";
 
-export function toTimings(tree: Tree, flame: FlameTree) {
+export function toTimings(tree: Tree, flame: Map<ID, NodeTransform>) {
 	return Array.from(flame.values())
-		.map(x => {
-			const node = tree.get(x.id)!;
+		.map(pos => {
+			const node = tree.get(pos.id)!;
 			return {
-				id: x.id,
+				id: pos.id,
 				name: node.name,
 				children: node.children,
-				start: x.start,
-				end: x.end,
+				start: pos.x,
+				end: pos.x + pos.width,
 			};
 		})
 		.sort((a, b) => a.id - b.id);
@@ -21,7 +22,7 @@ export function toTimings(tree: Tree, flame: FlameTree) {
 export function patch(tree: Tree, rootId: ID, commitRootId: ID) {
 	return toTimings(
 		tree,
-		patchTree(null, {
+		patchTree({
 			nodes: tree,
 			rootId,
 			commitRootId,
@@ -71,7 +72,7 @@ describe("patchTree", () => {
 		const actual = patch(b.idMap, 1, 1);
 		expect(actual).to.deep.equal([
 			{ name: "App", id: 1, start: 0, end: 110, children: [2] },
-			{ name: "Bar", id: 2, start: 10, end: 70, children: [] },
+			{ name: "Bar", id: 2, start: 50, end: 110, children: [] },
 		]);
 	});
 
@@ -79,14 +80,14 @@ describe("patchTree", () => {
 		const tree = flames`
 			App **
 			 Bar ***
-			 	Bob *
+			  Bob *
 		`;
 
 		const actual = patch(tree.idMap, 1, 2);
 		expect(actual).to.deep.equal([
 			{ name: "App", id: 1, start: 0, end: 70, children: [2] },
 			{ name: "Bar", id: 2, start: 0, end: 70, children: [3] },
-			{ name: "Bob", id: 3, start: 10, end: 60, children: [] },
+			{ name: "Bob", id: 3, start: 20, end: 70, children: [] },
 		]);
 	});
 
@@ -108,8 +109,8 @@ describe("patchTree", () => {
 		expect(actual).to.deep.equal([
 			// TODO: App is detected as a static tree
 			{ name: "App", id: 1, start: 0, end: 90, children: [2] },
-			{ name: "Bar", id: 2, start: 0, end: 70, children: [3] },
-			{ name: "Bob", id: 3, start: 10, end: 60, children: [] },
+			{ name: "Bar", id: 2, start: 20, end: 90, children: [3] },
+			{ name: "Bob", id: 3, start: 40, end: 90, children: [] },
 		]);
 	});
 
@@ -134,11 +135,11 @@ describe("patchTree", () => {
 			{
 				name: "Bar",
 				id: 2,
-				start: 0,
-				end: 80,
+				start: 30,
+				end: 110,
 				children: [],
 			},
-			{ name: "Bob", id: 3, start: 80, end: 140, children: [] },
+			{ name: "Bob", id: 3, start: 110, end: 170, children: [] },
 		]);
 	});
 
@@ -172,8 +173,8 @@ describe("patchTree", () => {
 		const actual = patch(b.idMap, 1, 1);
 		expect(actual).to.deep.equal([
 			{ name: "App", id: 1, start: 0, end: 90, children: [2] },
-			{ name: "Bar", id: 2, start: 0, end: 70, children: [3] },
-			{ name: "Bob", id: 3, start: 10, end: 60, children: [] },
+			{ name: "Bar", id: 2, start: 20, end: 90, children: [3] },
+			{ name: "Bob", id: 3, start: 40, end: 90, children: [] },
 		]);
 	});
 });
diff --git a/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts b/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts
index e68904d..1e0ad23 100644
--- a/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts
+++ b/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts
@@ -12,6 +12,8 @@ import { getFlameNodes } from "./utils";
 
 export const description = "Correctly position memoized sibling sub-trees";
 
+export const skip = () => true;
+
 export async function run(config: any) {
 	const { page, devtools } = await newTestPage(config, "memo2");