22 February 2014

接到主管的任务安排:统计每个接口的端到端耗时,及接口请求成功的回调函数执行耗时。只要求先在测试环境中收集一些数据,然后决定是否部署上线。

好在框架在接口这一层做了封装,每一个远程接口都是RemoteInterface类的实例。所以只要在这个文件中的发送请求前,接收到响应后,和调用回调函数完成这三处进行埋点计时即可,之后把这三个点的时间戳相减,然后发送一条pingback log到服务器即可完成数据初步收集。

唯一不好处理的是,希望在发送的log中同时能携带接口的名字。但接口的名字存储在调用RemoteInterface实例的那一层,上层在创建RemoteInterface实例和调用send方法时都没有传入接口名字。remoteInterface对象内部拿不到上层的接口名字。现在要想办法能给remoteInterface对象加属性_name_

当然可以手动改上层代码,但是涉及到的文件太多,手动改的话,纯粹体力耐力劳动!而且,如果以后停止统计了,那可能还要在手动一个一个的删除。所以就想到用UglifyJS进行批量修改(包括RemoteInterface类的那三处埋点计时和发送代码)。

任务变成:找到所有的new RemoteInterface语句,在这条语句的后面,加一条语句,给new操作返回的对象加一个属性name,值为文件名。

先贴代码,再描述方案:

define(function (require, exports, module) {
    var RemoteInterface = require('../kit/remoteInterface');
    module.exports = Q.Class('partnerVideoListInterface', {
        construct:function (param) {
            this._remoteInterface = new RemoteInterface({
                partnerVideoListData: {}
            });
            this._remoteInterface_pps = new RemoteInterface({
                partnerVideoListData_pps: {}
            });
        }
    });
});

现在希望给每个new RemoteInterface({})返回的对象加一个属性`_name_,其值为文件名,以便在统计RemoteInterface性能时能从实例内部拿到文件名。期待结果(第8行和第12行)如下:

define(function (require, exports, module) {
    var RemoteInterface = require('../kit/remoteInterface');
    module.exports = Q.Class('partnerVideoListInterface', {
        construct:function (param) {
            this._remoteInterface = new RemoteInterface({
                partnerVideoListData: {}
            });
            this._remoteInterface._name_ = "PartnerVideoListInterface"
            this._remoteInterface_pps = new RemoteInterface({
                partnerVideoListData_pps: {}
            });
            this._remoteInterface_pps._name_ = "PartnerVideoListInterface"
        }
    });
});

解决思路就是通过UglifyJS提供的语法树API来定位到new RemoteInterface(),然后在这条语句的后面加一条语句(第8行和第12行,手动加代码也行,但是文件数量太多,能在部署系统中搞定最佳)。

UglifyJS主要有两种语法树的操作API:一种是TreeWalker,用于遍历读取节点信息;一种是TreeTransformer,继承TreeWalker,用于修改语法树。

细化一下方案:

  1. 从define函数调用AST_Call为入口(因为define是全局变量),找到实参列表args的最后一个参数factory函数的第一个形参argnames[0],从而定位到require形参变量的声明(类型为AST_SymbolFunarg)。

  2. 找到对require的函数调用,且参数(文件路径)的文件名部分为"remoteInterface",并定位到所赋给的变量名"RemoteInterface"。

  3. 找到所有以new形式调用第2步找到的"RemoteInterface"的AST_New节点,并向上定位该行语句,new调用结果所赋給的变量名,该行语句所在的语句块。

  4. 新建一条赋值语句,右边为字符串常量文件名,左边为.属性访问操作,.右边属性为name,左边为上面一步所记录的new调用结果所赋给的变量名的拷贝。

  5. 语句块的body是一个数组,数组元素为每一条语句。可以通过indexOf获取语句在body数组的位置,然后在这个位置之后插入地4步创建的语句。

批处理代码如下:

var Fs = require("fs");
var Path = require("path");
var UglifyJS = require("uglify-js");
var hooker = process.hooker

hooker.on("before-art-template-preprocess", function(path){
    var code = Fs.readFileSync(path, "utf-8");
    if(code && code.indexOf("remoteInterface") == -1){
        return;
    }
    var astToplevel;
    try{
        astToplevel = UglifyJS.parse(code);
    }catch(e){
        console.log("in " + path);
        throw e;
    }
    astToplevel.figure_out_scope();
    
    var remoteInterfaceWalker = new UglifyJS.TreeWalker(function(node, descend){
    //找到define
    if(node instanceof UglifyJS.AST_Call && node.expression.name == "define"){
        requireNode = remoteInterfaceVarDef = remoteInterfaceSymbolVar = remoteInterfaceNew = null;
        var factoryNode = node.args &&  node.args.length > 0 && node.args[node.args.length -1];
        //找到define的最后一个参数factory函数的第一个参数require的变量名 及作用域
        if(factoryNode instanceof UglifyJS.AST_Function && factoryNode.argnames &&
            factoryNode.argnames.length > 0){
            requireNode = factoryNode.argnames[0];
        }
    }   
    //找到require "remoteInterface"赋值语句,并跟踪所赋给的变量名 及作用域
    else if(node instanceof UglifyJS.AST_VarDef && node.value instanceof UglifyJS.AST_Call &&
        node.value.expression instanceof UglifyJS.AST_SymbolRef && 
        node.value.expression.name == requireNode.name && node.value.expression.scope == requireNode.scope){
        var args = node.value.args;
        if(args && args.length > 0 && args[0] instanceof UglifyJS.AST_String && 
            args[0].value && args[0].value.indexOf("/remoteInterface") !== -1){
            remoteInterfaceVarDef = node;
            remoteInterfaceSymbolVar = node.name;
        }
    }
    //找到 this.remoteInterface = new RemoteInterface(...)语句,在之后再加一句
    // this.remoteInterface._name_ = filename语句
    }   
});

未完待续...



blog comments powered by Disqus