latest log

酩酊状態で書いたエンジニアポエムです。酩酊状態で読んでください。

型情報(宣言)と、実装を分離できる仕組み prof.js を実装してみました

続き書きました: http://uupaa.hatenablog.com/entry/2012/11/02/154337


今朝方ピコーン*1 があったので頑張って1日で実装してみました。

ピコーンの内容は

・今あるソースコードを汚さずに、動的な型チェックのための仕組みを後付で追加する事ってできないかな?
・同時にパフォーマンス・チューニングに必要な情報も取れるのではないか
・宣言と実装を分離する方法があるのではないか
・今まではできなかった、戻り値の動的な型チェックもできるのではないか

です。

  • 監視対象の関数のIN/OUTで、呼び出し階層をネストした状態で吐き出すトレース機能
  • 監視対象の関数の呼び出し回数と、実行時間の統計情報
  • 宣言にもとづく引数の動的な型チェックと、戻り値の動的な型チェック

などを prof.js として実装してみました。スライドはこちら http://www.slideshare.net/uupaa/profjs


書き立てホヤホヤで、まだ洗練されていない状態のコードですが、200行ほどです。

var prof; // global.prof - prof.js library namespace

prof || (function(global) { // @arg Global: window or global

// --- library scope vars ----------------------------------
var _db = {},       // { path: { fn, owner, name }, ... }
    _tm = {},       // { path: { time, count }, ... }
    _trace = true;  // trace on/off

// --- header ----------------------------------------------
prof            = prof_dump;    // prof(find:String = ""):Object/String/undefined
prof_dump.on    = prof_on;      // prof.on()
prof_dump.off   = prof_off;     // prof.off()
prof_dump.add   = prof_add;     // prof.add(...:String)
prof_dump.clear = prof_clear;   // prof.clear()

// --- implement -------------------------------------------
function prof_dump(find) { // @arg String(= ""): find name. "" is dump all
                           // @ret Object/String/undefined: { time, count }
                           // @help: prof
                           // @desc: dump profile data (paste to Excel sheet)
    if (find) {
        return _tm[find];
    }
    var rv = [], header = ["function", "time", "count"].join("\t"),
        path, paths = Object.keys(_tm).sort();

    rv.push(header); // add header 

    while (path = paths.shift()) {
        rv.push([path, _tm[path].time, _tm[path].count].join("\t"));
    }

    rv.push(header); // add footer

    return "\n" + rv.join("\n") + "\n";
}

function prof_on() { // @help: prof#prof.on
                     // @desc: trace on
    _trace = true;
}

function prof_off() { // @help: prof#prof.off
                      // @desc: trace off
    _trace = false;
}

function prof_add(ooo) { // @var_args String: 'owner.fn(a:Integer/String, b:Object):Object'
                         // @help: prof#prof.add
                         // @desc: add profile data, and hook functions
    var i = 0, iz = arguments.length,
        fg, owner, fn, path;

    for (; i < iz; ++i) {
        fg = _parse(arguments[i].trim()); // { owner, fn, args, resultType }

        owner = global[fg.owner];         // owner object
        fn    = _drillFunctionObject(owner, fg.fn.concat());
        path  = fg.owner + "." + fg.fn.join("."); // function path, "owner.function"

        _db[path] = _hook(path, { fn: fn, owner: owner, name: fg.fn },
                          fg.args, fg.resultType);
        _tm[path] = { time: 0, count: 0 };
    }

    function _drillFunctionObject(owner, fnary) {
        var fn = owner[fnary.shift()];

        while (fnary.length) {
            fn = fn[fnary.shift()];
        }
        return fn;
    }
}

function prof_clear() { // @help: prof#prof.clear
                        // @desc: clear profile data, and unhook all functions
    for (var path in _db) {
        _unhook(path, _db[path]);
    }
    _db = {};
    _tm = {};
}

function _hook(path,         // @arg String: "owner.function" path
               obj,          // @arg Object: { fn, owner, name }
               args,         // @arg ObjectArray: [ { arg, type, def }, ... ]
               resultType) { // @arg String:
                             // @inner: hook function, arguments assert,
                             //         result type assert, trace,
                             //         profiling
    var owner = obj.owner, ary = obj.name.concat();

    while (ary.length > 1) {
        owner = owner[ary.shift()];
    }
    owner[ary[0]] = function() {

        // --- assert arguments ---
        if (typeof mm !== "undefined") {
            switch (args.length) {
            case 7: args[6].arg && mm.allow(arguments[6], args[6].type);
            case 6: args[5].arg && mm.allow(arguments[5], args[5].type);
            case 5: args[4].arg && mm.allow(arguments[4], args[4].type);
            case 4: args[3].arg && mm.allow(arguments[3], args[3].type);
            case 3: args[2].arg && mm.allow(arguments[2], args[2].type);
            case 2: args[1].arg && mm.allow(arguments[1], args[1].type);
            case 1: args[0].arg && mm.allow(arguments[0], args[0].type);
            }
        }
        // --- trace ---
        if (_trace && global.console && global.console.group) {
            global.console.group(path);
        }

        var now = Date.now();
        var rv = obj.fn.apply(obj.owner, arguments);

        _tm[path].time += (Date.now() - now);
        _tm[path].count++;

        // --- trace end ---
        if (_trace && global.console && global.console.groupEnd) {
            global.console.groupEnd();
        }
        // --- assert result type ---
        if (typeof mm !== "undefined") {
            if (resultType) {
                mm.allow(rv, resultType);
            }
        }
        return rv;
    };
    return obj;
}

function _unhook(path,  // @arg String: path
                 obj) { // @arg Object: { fn, owner, name }
                        // @inner: unhook function
    var owner = obj.owner, ary = obj.name.concat();

    while (ary.length > 1) {
        owner = owner[ary.shift()];
    }
    owner[ary[0]] = obj.fn;
}

function _parse(str) { // @arg String: 'a.fn2(a:Integer/String = ",", b:Array):Object'
                       // @ret Object: { owner, fn, args, resultType }
                       //       owner - String: "a"
                       //       fn    - StringArray: ["fn2"]
                       //       args  - ObjectArray:
                       //               [ { arg: "a", type: "Integer/String", def: "," },
                       //                 { arg: "b", type: "Array", def: undefined    } ],
                       //       resultType - String: "Object"
                       // @inner: parse function syntax

    var rv = { owner: "", fn: [], args: [], resultType: "" },
        ary, index = str.indexOf("(");

    if (index < 0) {
        throw new TypeError("BAD_FORMAT");
    }
    ary      = str.slice(0, index).replace("#", ".prototype.").split(".");
    rv.owner = ary.shift();
    rv.fn    = ary;
    str      = str.slice(index + 1);
    index    = str.lastIndexOf(")");
    if (index < 0) {
        throw new TypeError("BAD_FORMAT");
    }
    rv.args  = _parseArgs(str.slice(0, index));
    rv.resultType = (str.slice(index + 1) || "").replace(/^:/, "");
    return rv;
}

function _parseArgs(str) { // @arg String: 'arg:Type/MoreType = defaultValue'
                           // @ret ObjectArray: [ { arg, type, def }, ... ]
                           // @inner: parse argument, type, default value

    var rv = [], // [ { arg, type, def }, ... ]
        ary = [], quoted = 0;

    // split commas
    //      split    'a:String = ",",   b:String'
    //       to    [ 'a:String = ","', 'b:String' ]
    str.split(/\s*,\s*/).forEach(function(value) {
        if (/"$/.test(value) && ++quoted > 1) { // combine a quart consecutive
            ary[ary.length - 1] += ("," + value);
            quoted = 0;
            return;
        }
        ary.push(value)
    });

    // parse "arg:Type/MoreType = def"
    ary.map(function(value, index) {
        value.replace(/^(\w|\.\.\.)+:([\w/]+)(?:\s*=\s*(.*))?$/,
                      function(_, arg, type, def) {
            if (arg === "...") {
                rv.push( { arg: "", type: type, def: def } );
            } else if (def === void 0) {
                rv.push( { arg: arg, type: type, def: def } );
            } else {
                rv.push( { arg: arg, type: type += "/undefined", def: def } );
            }
        });
    });
    return rv;
}

})(this.self || global);

(ε・◇・)з o O ( 「既存のソースコードを改変せずに、必要に応じて型チェックを後から入れられる」ってトコがミソだね

*1: https://twitter.com/uupaa/statuses/263345149822775297