import fs from "fs"; import { assertHardhatInvariant } from "./errors"; import { flagParallelChildren, TaskProfile } from "./task-profiling"; export interface Flamegraph { name: string; value: number; children: Flamegraph[]; parallel: boolean; } export function profileToFlamegraph(profile: TaskProfile): Flamegraph { assertHardhatInvariant( profile.end !== undefined, `Formatting invalid task profile for ${profile.name}. No end was recorded.` ); return { name: profile.name, // We assume this is a safe int, which is ok unless a task runs for months value: Number(profile.end - profile.start), children: profile.children.map((c) => profileToFlamegraph(c)), parallel: profile.parallel === true, }; } /** * Merges compatible children of toFold and its children. * * Compatible in a traditional Flamegraph means having the same name. We * modified that notion and also require their `parallel` flag to have the same * value. This means that parallel and non-parallel calls to the same function * are shown with two Flamegraph "blocks". * * The parallel block shows the max running time, instead of the sum of them. **/ function foldFramegraph(toFold: Flamegraph): Flamegraph { if (toFold.children.length === 0) { return { name: toFold.name, value: toFold.value, children: [], parallel: toFold.parallel, }; } const children = toFold.children.map((c) => foldFramegraph(c)); children.sort((a, b) => a.parallel === b.parallel ? 0 : b.parallel ? -1 : 1 ); children.sort((a, b) => a.name.localeCompare(b.name)); const foldedChildren = [children[0]]; const mergedChildren = new Set(); for (let i = 1; i < children.length; i++) { const latest = foldedChildren[foldedChildren.length - 1]; if ( children[i].name === latest.name && children[i].parallel === latest.parallel ) { if (latest.parallel) { latest.value = Math.max(latest.value, children[i].value); } else { latest.value += children[i].value; } latest.children.push(...children[i].children); mergedChildren.add(foldedChildren.length - 1); } else { foldedChildren.push(children[i]); } } for (const i of mergedChildren.values()) { foldedChildren[i] = foldFramegraph(foldedChildren[i]); } return { name: toFold.name, value: toFold.value, children: foldedChildren, parallel: toFold.parallel, }; } export function createFlamegraphHtmlFile(flamegraph: Flamegraph): string { const content = getFlamegraphFileContent(foldFramegraph(flamegraph)); const path = "flamegraph.html"; fs.writeFileSync(path, content, { encoding: "utf8" }); return path; } function getFlamegraphFileContent(flamegraph: Flamegraph): string { const data = JSON.stringify(foldFramegraph(flamegraph), undefined, 2); return ` Hardhat task flamegraph

Hardhat task flamegraph


`; } /** * Converts the TaskProfile into a flamegraph, saves it, and returns its path. */ export function saveFlamegraph(profile: TaskProfile): string { flagParallelChildren(profile); const flamegraph = profileToFlamegraph(profile); return createFlamegraphHtmlFile(flamegraph); }