import { Logger } from '@cirrusct/logging'; import { createError } from '@cirrusct/mr-core'; import * as path from 'path'; import { renderRequireTimesTemplate } from './renderTemplate'; export interface RequireTimesData { isRoot: boolean; sequence?: number; time?: number; relativeFilePath: string; module: NodeJS.Module; } export interface RequireTimesTreeEntry extends RequireTimesData { children: RequireTimesTreeEntry[]; } export interface RequireTimesMap { [name: string]: RequireTimesData; } export class RequireTimes { public get totalTime(): number | undefined { if (this._firstRequireBeforeTime) { return this._lastRequireAfterTime - this._firstRequireBeforeTime; } else { return undefined; } } public get times(): RequireTimesData[] { return this._times; } public get timesMap(): RequireTimesMap { return this._timesMap; } public get tree(): RequireTimesTreeEntry[] { const roots = this._times .filter(t => { return this._timesMap[t.module.parent.id] === undefined; }) .map(t => { return t.module.parent; }) .filter((i, pos, self) => { return self.indexOf(i) === pos; }); const getChildrenTreeEntries = (parent: NodeJS.Module): RequireTimesTreeEntry[] => { const childTimeEntries = this._times .filter(t => { return t.module.parent.id === parent.id; }) .sort((a, b) => { return b.time - a.time; }) .map(t => { return { ...t, children: getChildrenTreeEntries(t.module), }; }); return childTimeEntries; }; const rootsTree = roots.map(m => { const tree: RequireTimesTreeEntry = { isRoot: true, module: m, relativeFilePath: path.relative(this._cwd, m.filename), children: getChildrenTreeEntries(m), }; return tree; }); return rootsTree; } public get treeJson(): string { const mapNode = (node: RequireTimesTreeEntry) => { return { sequence: node.sequence, time: node.time, relativeFilePath: node.relativeFilePath, filename: node.module.filename, id: node.module.id, children: node.children.map(mapNode), isRoot: node.isRoot, }; }; const mapped = this.tree.map(mapNode); return JSON.stringify(mapped); } private _firstRequireBeforeTime: number; private _lastRequireAfterTime: number; private _lastSequence = 0; private _times: RequireTimesData[] = []; private _timesMap: RequireTimesMap = {}; private _cwd = process.cwd(); constructor(private programName: string, private extensions?: string[]) { if (!extensions) { this.extensions = ['.js']; } this.extensions.forEach(this.wrap); } public printConsoleTree = () => { const printTree = (node: RequireTimesTreeEntry, prefix: string) => { console.log(prefix + node.time + 'ms', node.module.id); node.children.forEach(c => { printTree(c, prefix + ' '); }); }; this.tree.forEach(t => { printTree(t, ''); }); }; public renderReport = (outputPath: string, logger: Logger) => { const model = { programName: this.programName, tree: this.tree, cwd: this._cwd, treeJson: this.treeJson, totalTime: this.totalTime, }; try { renderRequireTimesTemplate(outputPath, logger, model); } catch (e) { throw createError('renderReport Failed', e); } }; private wrap = (extension: string) => { const original = require.extensions[extension]; require.extensions[extension] = (module, filename) => { const start = Date.now(); if (!this._firstRequireBeforeTime) { this._firstRequireBeforeTime = start; } original(module, filename); this._lastRequireAfterTime = Date.now(); const diff = this._lastRequireAfterTime - start; if (!module.parent) { console.log(`${module.id} has not parent `); } const data: RequireTimesData = { isRoot: false, sequence: this._lastSequence++, relativeFilePath: path.relative(this._cwd, module.filename), time: diff, module, }; this._times.push(data); this._timesMap[data.module.id] = data; }; }; }