型情報(宣言)と、実装を分離できる仕組み prof.js を実装してみました
今朝方ピコーン*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 ( 「既存のソースコードを改変せずに、必要に応じて型チェックを後から入れられる」ってトコがミソだね