src/vs/platform/profiling/electron-sandbox/profileAnalysisWorker.ts/bottomUp
function bottomUp(model: IProfileModel, topN: number, fullPaths: boolean = false) {

	const root = BottomUpNode.root();
	for (const node of model.nodes) {
		processNode(root, node, model);
		root.addNode(node);
	}

	const result = Object.values(root.children)
		.sort((a, b) => b.selfTime - a.selfTime)
		.slice(0, topN);


	const samples: BottomUpSample[] = [];

	for (const node of result) {

		const sample: BottomUpSample = {
			selfTime: Math.round(node.selfTime / 1000),
			totalTime: Math.round(node.aggregateTime / 1000),
			location: printCallFrame(node.callFrame, fullPaths),
			url: node.callFrame.url,
			caller: [],
			percentage: Math.round(node.selfTime / (model.duration / 100)),
			isSpecial: isSpecial(node.callFrame)
		};

		// follow the heaviest caller paths
		const stack = [node];
		while (stack.length) {
			const node = stack.pop()!;
			let top: BottomUpNode | undefined;
			for (const candidate of Object.values(node.children)) {
				if (!top || top.selfTime < candidate.selfTime) {
					top = candidate;
				}
			}
			if (top) {
				const percentage = Math.round(top.selfTime / (node.selfTime / 100));
				sample.caller.push({ percentage, location: printCallFrame(top.callFrame, false) });
				stack.push(top);
			}
		}

		samples.push(sample);
	}

	return samples;
}