UNPKG

gas2nel

Version:

A library for tracking and estimating resource usage metrics.

324 lines (290 loc) 11 kB
/** * @title Gas2nel Class * @dev A robust utility for tracking resource metrics during asynchronous function execution, * with a realistic "gas" cost estimation based on CPU, memory, network, and file I/O usage. * * @example * const { Gas2nel } = require('./Gas2nel'); * const gas2nel = new Gas2nel({ include: ["metric", "report"] }); * * async function exampleFn() { * // Simulated asynchronous operation * return 'Completed'; * } * * (async () => { * const result = await gas2nel.estimateGas(exampleFn); * console.log(result); * })(); */ const https = require('https'); const fs = require('fs'); const EventEmitter = require('events'); class Gas2nel extends EventEmitter { /** * @property {object} #metricWeights - Private weight coefficients for each tracked metric. * These values adjust the impact of each metric on the final gas estimation. */ #metricWeights = { cpuTimeMs: 0.35, cpuPercentage: 0.2, memoryRSS: 0.15, memoryHeapUsed: 0.1, memoryExternal: 0.05, sentBytes: 0.05, receivedBytes: 0.05, wallTimeMs: 0.05, }; constructor(options = {}) { super(); this.options = options; this.reset(); // Initialize bandwidth and file I/O metrics } /** * Resets network and file I/O counters to zero * @return {void} */ reset() { this.sentBytes = 0; // Tracks total bytes sent via https requests this.receivedBytes = 0; // Tracks total bytes received via https requests this.fileReadBytes = 0; // Tracks total bytes read from file system this.fileWriteBytes = 0; // Tracks total bytes written to file system } /** * Sets tracking options, e.g., to include specific metrics or generate reports * @param {object} options - Configuration options for tracking behavior * @returns {Gas2nel} - Returns the instance for chaining */ setOptions(options) { this.options = { ...this.options, ...options }; return this; } /** * Runs the function `fn`, captures metrics, and estimates gas usage based on those metrics. * @param {function} fn - Async function to monitor for resource usage * @param {...any} args - Arguments for the async function `fn` * @returns {object} - Object containing success status, data, gas estimate, metrics, and optional report */ async estimateGas(fn, ...args) { const { result, error, metrics } = await this.#captureMetrics(fn, ...args); const gas = this.#estimateRealisticGas(metrics); const output = { success: !error, data: error ? error.message : result, gas, }; if (this.options.include) { if (this.options.include.includes("metric")) output.metric = metrics; if (this.options.include.includes("report")) output.report = this.#generateReport(metrics); } return output; } /** * Tracks resources used by `fn`, including CPU, memory, and network I/O. * Also overrides methods for https bandwidth and file read/write monitoring. * @private * @param {function} fn - Async function to monitor * @param {...any} args - Arguments for `fn` * @returns {object} - Object containing result, error, and recorded metrics */ async #captureMetrics(fn, ...args) { this.reset(); const startCpu = process.cpuUsage(); // Record initial CPU state const startMem = process.memoryUsage(); // Record initial memory usage const startTime = process.hrtime.bigint(); // Record start time for wall time measurement // Preserve original methods to restore after tracking const originalRequest = https.request; const originalReadFile = fs.readFile; const originalWriteFile = fs.writeFile; const originalReadFileSync = fs.readFileSync; const originalWriteFileSync = fs.writeFileSync; // Override methods for tracking https and file I/O https.request = this.#trackhttpsBandwidth(originalRequest); fs.readFile = this.#trackFileRead(originalReadFile); fs.writeFile = this.#trackFileWrite(originalWriteFile); fs.readFileSync = this.#trackFileRead(originalReadFileSync); fs.writeFileSync = this.#trackFileWrite(originalWriteFileSync); let result, error; try { result = await fn(...args); // Execute the function } catch (err) { error = err; } // Restore original methods https.request = originalRequest; fs.readFile = originalReadFile; fs.writeFile = originalWriteFile; fs.readFileSync = originalReadFileSync; fs.writeFileSync = originalWriteFileSync; // Capture post-execution metrics const endCpu = process.cpuUsage(startCpu); const endMem = process.memoryUsage(); const endTime = process.hrtime.bigint(); const metrics = { cpuTimeMs: endCpu.user / 1000, // Convert to milliseconds wallTimeMs: Number((endTime - startTime) / BigInt(1e6)), // Wall time in ms memoryHeapUsed: endMem.heapUsed - startMem.heapUsed, memoryRSS: endMem.rss - startMem.rss, memoryExternal: endMem.external - startMem.external, cpuPercentage: (Number(endCpu.user) / Number(endTime - startTime)) * 100, sentBytes: this.sentBytes, receivedBytes: this.receivedBytes, fileReadBytes: this.fileReadBytes, fileWriteBytes: this.fileWriteBytes, }; return { result, error, metrics }; } /** * Estimates gas cost by weighing normalized metrics values according to metricWeights. * @private * @param {object} metrics - Resource usage metrics * @returns {number} - Estimated gas cost */ #estimateRealisticGas(metrics) { let gasEstimation = 0; for (const [metric, weight] of Object.entries(this.#metricWeights)) { const value = metrics[metric]; if (value !== undefined && typeof value === 'number') { const normalizedValue = this.#normalizeMetric(metric, value); gasEstimation += normalizedValue * weight; } } return gasEstimation; } /** * Normalizes metric values by dividing by assumed maximums for realistic scaling * @private * @param {string} metric - Metric name * @param {number} value - Raw metric value * @returns {number} - Normalized value */ #normalizeMetric(metric, value) { const maxValues = { cpuTimeMs: 10000, // 10 seconds wallTimeMs: 1000, // 1 second memoryHeapUsed: 500 * 1024 * 1024, // 500 MB memoryRSS: 1000 * 1024 * 1024, // 1 GB memoryExternal: 100 * 1024 * 1024, // 100 MB cpuPercentage: 100, // 100% CPU sentBytes: 10 * 1024 * 1024, // 10 MB receivedBytes: 10 * 1024 * 1024, // 10 MB fileReadBytes: 20 * 1024 * 1024, // 20 MB fileWriteBytes: 20 * 1024 * 1024, // 20 MB }; return value / (maxValues[metric] || 1); } /** * Generates a human-readable report from collected metrics * @private * @param {object} metrics - Resource usage metrics * @returns {object} - Report summarizing metric details */ #generateReport(metrics) { return { cpuTimeMs: metrics.cpuTimeMs, wallTimeMs: metrics.wallTimeMs, peakMemoryRSS: `${(metrics.memoryRSS / (1024 ** 2)).toFixed(2)} MB`, memoryHeapUsed: `${(metrics.memoryHeapUsed / (1024 ** 2)).toFixed(2)} MB`, memoryExternal: `${(metrics.memoryExternal / (1024 ** 2)).toFixed(2)} MB`, networkTransferred: `${((metrics.sentBytes + metrics.receivedBytes) / 1024).toFixed(2)} KB`, fileIO: `${((metrics.fileReadBytes + metrics.fileWriteBytes) / 1024).toFixed(2)} KB` }; } /** * Overrides https.request to monitor network traffic for bandwidth tracking * @private * @param {function} originalRequest - Original https.request function * @returns {function} - Wrapped https.request with bandwidth tracking */ #trackhttpsBandwidth(originalRequest) { return (...args) => { const req = originalRequest.apply(this, args); // Calculate header size and add to sentBytes const headersSize = JSON.stringify(req.getHeaders()).length; this.sentBytes += headersSize; // Intercept req.write to track sent data const originalWrite = req.write; req.write = (chunk, encoding, callback) => { if (chunk) { this.sentBytes += Buffer.byteLength(chunk, encoding); } return originalWrite.call(req, chunk, encoding, callback); }; // Intercept req.end to track any remaining sent data const originalEnd = req.end; req.end = (chunk, encoding, callback) => { if (chunk) { this.sentBytes += Buffer.byteLength(chunk, encoding); } return originalEnd.call(req, chunk, encoding, callback); }; // Track received data when the socket is active req.on('socket', socket => { socket.on('data', chunk => { this.receivedBytes += chunk.length; }); }); return req; }; } /** * Overrides fs.readFile to track file read operations (async) * @private * @param {function} originalReadFile - Original fs.readFile function * @returns {function} - Wrapped fs.readFile with read tracking */ #trackFileRead(originalReadFile) { return (path, options, callback) => { originalReadFile(path, options, (err, data) => { if (!err && data) { this.fileReadBytes += Buffer.byteLength(data); // Track the size of the read data } callback(err, data); }); }; } /** * Overrides fs.readFileSync to track file read operations (sync) * @private * @param {function} originalReadFileSync - Original fs.readFileSync function * @returns {function} - Wrapped fs.readFileSync with read tracking */ #trackFileReadSync(originalReadFileSync) { return (path, options) => { const data = originalReadFileSync(path, options); if (data) { this.fileReadBytes += Buffer.byteLength(data); // Track the size of the read data } return data; }; } /** * Overrides fs.writeFile to track file write operations (async) * @private * @param {function} originalWriteFile - Original fs.writeFile function * @returns {function} - Wrapped fs.writeFile with write tracking */ #trackFileWrite(originalWriteFile) { return (path, data, options, callback) => { if (data) { this.fileWriteBytes += Buffer.byteLength(data); // Track the size of the written data } originalWriteFile(path, data, options, callback); }; } /** * Overrides fs.writeFileSync to track file write operations (sync) * @private * @param {function} originalWriteFileSync - Original fs.writeFileSync function * @returns {function} - Wrapped fs.writeFileSync with write tracking */ #trackFileWriteSync(originalWriteFileSync) { return (path, data, options) => { if (data) { this.fileWriteBytes += Buffer.byteLength(data); // Track the size of the written data } return originalWriteFileSync(path, data, options); }; } } module.exports = Gas2nel;