UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

226 lines 9.21 kB
import crypto from "node:crypto"; import path from "node:path"; import { formatTableV2 } from "@nomicfoundation/hardhat-utils/format"; import { ensureDir, getAllFilesMatching, readJsonFile, remove, writeJsonFile, } from "@nomicfoundation/hardhat-utils/fs"; import chalk from "chalk"; import debug from "debug"; const gasStatsLog = debug("hardhat:core:gas-analytics:gas-analytics-manager:gas-stats"); export class GasAnalyticsManagerImplementation { gasMeasurements = []; #gasStatsPath; constructor(gasStatsRootPath) { this.#gasStatsPath = path.join(gasStatsRootPath, "gas-stats"); } addGasMeasurement(gasMeasurement) { this.gasMeasurements.push(gasMeasurement); } async clearGasMeasurements(id) { const gasMeasurementsPath = await this.#getGasMeasurementsPath(id); await remove(gasMeasurementsPath); this.gasMeasurements = []; gasStatsLog("Cleared gas measurements from disk and memory"); } async saveGasMeasurements(id) { const gasMeasurementsPath = await this.#getGasMeasurementsPath(id); const filePath = path.join(gasMeasurementsPath, `${crypto.randomUUID()}.json`); await writeJsonFile(filePath, this.gasMeasurements); gasStatsLog("Saved gas measurements", id, filePath); } async reportGasStats(...ids) { await this._loadGasMeasurements(...ids); const gasStatsByContract = this._calculateGasStats(); const report = this._generateGasStatsReport(gasStatsByContract); console.log(report); console.log(); gasStatsLog("Printed markdown report"); } async #getGasMeasurementsPath(id) { const gasMeasurementsPath = path.join(this.#gasStatsPath, id); await ensureDir(gasMeasurementsPath); return gasMeasurementsPath; } /** * @private exposed for testing purposes only */ async _loadGasMeasurements(...ids) { this.gasMeasurements = []; for (const id of ids) { const gasMeasurementsPath = await this.#getGasMeasurementsPath(id); const filePaths = await getAllFilesMatching(gasMeasurementsPath); for (const filePath of filePaths) { const entries = await readJsonFile(filePath); for (const entry of entries) { this.gasMeasurements.push(entry); } gasStatsLog("Loaded gas measurements", id, filePath); } } } /** * @private exposed for testing purposes only */ _calculateGasStats() { const gasStatsByContract = new Map(); const measurementsByContract = this._aggregateGasMeasurements(); for (const [contractFqn, measurements] of measurementsByContract) { const contractGasStats = { functions: new Map(), }; if (measurements.deployment !== undefined) { contractGasStats.deployment = { gas: measurements.deployment.gas, size: measurements.deployment.size, }; } const overloadedFnNames = new Set(findDuplicates([...measurements.functions.keys()].map(getFunctionName))); for (const [functionSig, gasValues] of measurements.functions) { const functionName = getFunctionName(functionSig); const isOverloaded = overloadedFnNames.has(functionName); const stats = { min: Math.min(...gasValues), max: Math.max(...gasValues), avg: Math.round(avg(gasValues)), median: Math.round(median(gasValues)), calls: gasValues.length, }; contractGasStats.functions.set(isOverloaded ? functionSig : functionName, stats); } gasStatsByContract.set(contractFqn, contractGasStats); } return gasStatsByContract; } /** * @private exposed for testing purposes only */ _aggregateGasMeasurements() { const measurementsByContract = new Map(); for (const currentMeasurement of this.gasMeasurements) { let contractMeasurements = measurementsByContract.get(currentMeasurement.contractFqn); if (contractMeasurements === undefined) { contractMeasurements = { functions: new Map(), }; measurementsByContract.set(currentMeasurement.contractFqn, contractMeasurements); } if (currentMeasurement.type === "deployment") { contractMeasurements.deployment = { gas: currentMeasurement.gas, size: currentMeasurement.size, }; } else { let measurements = contractMeasurements.functions.get(currentMeasurement.functionSig); if (measurements === undefined) { measurements = []; contractMeasurements.functions.set(currentMeasurement.functionSig, measurements); } measurements.push(currentMeasurement.gas); } } return measurementsByContract; } /** * @private exposed for testing purposes only */ _generateGasStatsReport(gasStatsByContract) { const rows = []; if (gasStatsByContract.size > 0) { rows.push({ type: "title", text: chalk.bold("Gas Usage Statistics") }); } // Sort contracts alphabetically for consistent output const sortedContracts = [...gasStatsByContract.entries()].sort(([a], [b]) => a.localeCompare(b)); for (const [contractFqn, contractGasStats] of sortedContracts) { rows.push({ type: "section-header", text: chalk.cyan.bold(getUserFqn(contractFqn)), }); if (contractGasStats.functions.size > 0) { rows.push({ type: "header", cells: [ "Function name", "Min", "Average", "Median", "Max", "#calls", ].map((s) => chalk.yellow(s)), }); } // Sort functions by removing trailing ) and comparing alphabetically. // This ensures that overloaded functions with fewer params come first // (e.g., foo(uint256) comes before foo(uint256,uint256)). In other // scenarios, removing the trailing ) has no effect on the order. const sortedFunctions = [...contractGasStats.functions.entries()].sort(([a], [b]) => a.split(")")[0].localeCompare(b.split(")")[0])); for (const [functionDisplayName, gasStats] of sortedFunctions) { rows.push({ type: "row", cells: [ functionDisplayName, `${gasStats.min}`, `${gasStats.avg}`, `${gasStats.median}`, `${gasStats.max}`, `${gasStats.calls}`, ], }); } if (contractGasStats.deployment !== undefined) { rows.push({ type: "header", cells: ["Deployment Cost", "Deployment Size"].map((s) => chalk.yellow(s)), }); rows.push({ type: "row", cells: [ `${contractGasStats.deployment.gas}`, `${contractGasStats.deployment.size}`, ], }); } } return formatTableV2(rows); } } export function avg(values) { return values.reduce((a, c) => a + c, 0) / values.length; } export function median(values) { const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } export function getUserFqn(inputFqn) { if (inputFqn.startsWith("project/")) { return inputFqn.slice("project/".length); } if (inputFqn.startsWith("npm/")) { const withoutPrefix = inputFqn.slice("npm/".length); // Match "<pkg>@<version>/<rest>", where <pkg> may be scoped (@scope/pkg) const match = withoutPrefix.match(/^(@?[^@/]+(?:\/[^@/]+)*)@[^/]+\/(.*)$/); if (match !== null) { return `${match[1]}/${match[2]}`; } return withoutPrefix; } return inputFqn; } export function getFunctionName(signature) { return signature.split("(")[0]; } export function findDuplicates(arr) { const seen = new Set(); const duplicates = new Set(); for (const item of arr) { if (seen.has(item)) { duplicates.add(item); } else { seen.add(item); } } return [...duplicates]; } //# sourceMappingURL=gas-analytics-manager.js.map