@lzwme/feps-webpack-plugin
Version:
This plug-in is used for function execution performance statistics. It calculates the execution time by injecting statistical code and finds slow functions.
247 lines (228 loc) • 7.3 kB
JavaScript
/*
* @Author: lzw
* @Date: 2021-12-13 19:09:25
* @LastEditors: lzw
* @LastEditTime: 2022-01-07 11:05:40
* @Description:
*/
// @ts-check
const webpack = require('webpack');
const path = require('path');
const comm = require('./common');
const DependencyTemplate = require('webpack/lib/DependencyTemplate');
let markedFnTotal = 0;
class FEPSTemplate extends DependencyTemplate {
/**
* @param {FEPSDependency} dep the dependency for which the template should be applied
* @param {comm.ReplaceSource} source the current replace source which can be modified
* @param {comm.DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
// @ts-ignore
apply(dep, source, templateContext) {
const module = dep.moduleProxy || templateContext.module; // templateContext.moduleGraph.getModule(dep)
/** @type {comm.Program} */
// @ts-ignore
const ast = module.ast;
if (!ast) return;
// @ts-ignore
delete module.ast;
// module.cleanupForCache();
/** @type {comm.AnyNode[]} */
const result = [];
const recursiveProps = node => {
let hit = false;
Object.keys(node).forEach(key => {
const item = node[key];
if (!item || typeof item !== 'object') return;
if (Array.isArray(item)) {
item.forEach(d => {
if (recursiveProps(d)) hit = true;
});
} else {
if (item.type && item.loc && item.range) {
travel(item);
hit = true;
}
}
return hit;
});
return hit;
};
/**
* @param {comm.AnyNode | comm.AnyNode[]} nodes
*/
const travel = nodes => {
if (!Array.isArray(nodes)) nodes = [nodes];
nodes.forEach(n => {
if (!n) return;
if (dep.options.excludeNodeType.includes(n.type)) return;
switch (n.type) {
case 'Literal':
case 'Identifier':
case 'TaggedTemplateExpression':
case 'TemplateLiteral':
case 'ExportAllDeclaration':
case 'ImportDeclaration':
case 'ThisExpression':
case 'Super':
case 'EmptyStatement':
// case 'NewExpression':
// 忽略的类型
break;
case 'VariableDeclaration':
if (n.declarations[0].init) {
const node = n.declarations[0];
// @ts-ignore
if (!node.init.id) node.init.id = node.id;
travel(node.init);
}
break;
case 'ArrowFunctionExpression':
// 忽略表达式返回
if (!n.expression) result.push(n);
break;
case 'FunctionExpression':
case 'FunctionDeclaration':
result.push(n);
break;
case 'ClassExpression':
case 'ClassDeclaration':
travel(n.body.body);
break;
case 'MethodDefinition':
if (n.kind === 'method') {
result.push(n);
} else if (n.kind === 'constructor') {
travel(n.value.body.body);
}
break;
case 'ExpressionStatement':
travel(n.expression);
break;
case 'ExportNamedDeclaration':
travel(n.declaration);
break;
case 'Property':
case 'PropertyDefinition':
travel(n.value);
break;
case 'ObjectExpression':
travel(n.properties);
break;
case 'ExportDefaultDeclaration':
travel([n.declaration]);
break;
case 'ArrayExpression':
case 'ArrayPattern':
travel(n.elements);
break;
case 'LogicalExpression':
travel([n.left, n.right]);
break;
case 'ConditionalExpression':
travel([n.test, n.alternate, n.consequent]);
break;
case 'MemberExpression':
travel(n.property);
break;
case 'IfStatement':
travel([n.test, n.alternate, n.consequent]);
break;
case 'CallExpression':
travel(n.callee);
break;
default: {
if (!recursiveProps(n)) {
if (dep.options.debug) console.warn(`[${comm.PLUGIN_NAME}][warn]`, '未处理的节点类型:', n.type, dep.relativePath, n);
}
// else if (dep.options.debug) {
// console.log(`[${comm.PLUGIN_NAME}][warn]`, '该节点类型使用了通用处理逻辑', n.type, dep.relativePath, n);
// }
}
}
});
};
travel(ast.body);
if (!result.length) return;
let markedFnCount = 0;
/**
* @param {string} name
* @param {*} node
*/
const reWrite = (name, node) => {
if (!node.body || node.__feps) return;
if (node.body.end - node.body.start < dep.options.minFnCodeLength) return;
node.__feps = true;
source.insert(node.body.start, `{ markTime('${name}'); const $r = (${node.async ? 'async ' : ''}() => `);
source.insert(node.body.end, `)(); markTime('${name}'); return $r;}`);
markedFnCount++;
};
result.forEach(node => {
if (node.type === 'MethodDefinition') {
// @ts-ignore
reWrite(node.key.name, node.value);
} else {
// @ts-ignore
let nodeDesc = node.id && node.id.name;
if (!nodeDesc) nodeDesc = path.basename(dep.relativePath) + `:${node.loc.start.line}-${node.loc.start.column}`;
reWrite(nodeDesc, node);
}
});
if (markedFnCount) source.insert(0, dep.markTimeCode);
markedFnTotal += markedFnCount;
if (dep.options.debug) {
const color = comm.getColors();
console.log(
color.gray(`[${comm.PLUGIN_NAME}]`),
color.greenBright(`marked/markedTotal:`),
color.yellowBright(`${markedFnCount}/${markedFnTotal}`),
color.cyan(dep.relativePath)
);
}
}
}
class FEPSDependency extends webpack.Dependency {
// @ts-ignore
get type() {
return comm.PLUGIN_NAME;
}
/**
* Use the constructor to save any information you need for later
* @param {string} relativePath
* @param {comm.FepsOptions} options
* @param {import('webpack').Module} module
*/
constructor(relativePath, options, module = null) {
super();
this.relativePath = relativePath;
this.options = options;
this.moduleProxy = module;
this.markTimeCode = comm.getMarkTimeCode(options.timeLimit, options.logger, relativePath);
}
serialize(context) {
const { write } = context;
write(this.options);
write(this.relativePath);
super.serialize(context);
}
deserialize(context) {
super.deserialize(context);
}
}
// for webpack5
if (webpack.util.serialization) {
webpack.util.serialization.register(FEPSDependency, './BenchmarkPlugin', null, {
serialize(instance, context) {
instance.serialize(context);
},
deserialize(context) {
const { read } = context;
const dep = new FEPSDependency(read(), read());
dep.deserialize(context);
return dep;
},
});
}
FEPSDependency.Template = FEPSTemplate;
module.exports = FEPSDependency;