Всем добрый день!
Не так давно пришлось мне заниматься профайлингом JavaScript-классов в IE. Методов у меня было много, вложенных объектов тоже хватало, и вставлять вручную в каждый метод код для замера времени выполнения немного ... напрягало
Поэтому я решился и написал простой до безобразия JS-класс, который умеет профайлить другие классы. Фактически он подменяет методы классов, вставляет свой код-обёртку для замеров времени и построения дерева вызовов, а внутри вызывает "родной" метод класса. Профайлит также и методы вложенных объектов. Проверялся только в IE 6.0.
Собственно класс называется
ObjectProfiler. У него есть 3 основных мембера:
startProfiling(obj) — собственно начинает профайлинг указанного объекта
excludeNames — массив имён мемберов (методов и полей) объекта, которые профайлить не следует
oncallfinished — сюда вешается хандлер на окончание очередного вызова метода (не вызывается при вложенных вызовах — т.е. когда, например, метод #1 класса вызывает другой метод класса)
Чтобы легче было использовать этот класс на деле, я написал другой класс,
ProfilerTextareaRenderer, который умеет отображать результаты профайлинга в <textarea>.
Ну и собственно пример использования (HTML):
<script language="javascript" src="ObjectProfiler.js"></script>
<script language="javascript" src="ProfilerTextareaRenderer.js"></script>
<script>
// Кастомный класс
function CustomClass()
{
// Поля
this._child1 = new CustomChild();
this._child2 = new CustomChild();
// Методы
this.run = function()
{
this.method1();
this.method2();
}
this.method1 = function()
{
this._child1.doLoop(1000000);
}
this.method2 = function()
{
this._child2.doLoop(100000);
}
}
function CustomChild()
{
this.doLoop = function(n)
{
for (var i = 0; i < n; ++i) {}
}
}
</script>
<script language="javascript">
// Создаём экземпляр нашего кастомного класса
var customObject = new CustomClass();
// Создаём экземпляр профайлера
var profiler = new ObjectProfiler();
// Исключаем некоторых мемберов объекта из профайлинга
profiler.excludeNames = [ "_child2" ];
// Запускаем профайлинг
profiler.startProfiling(customObject);
// Выводить результаты будем в <textarea id="profOutput">
var profRenderer = new ProfilerTextareaRenderer();
profRenderer.minMs = 1; // выводим только вызовы дольше 1 мс
profRenderer.registerProfiler(profiler);
window.onload = function() { profRenderer.initFromControl("profOutput"); }
</script>
<button onclick="customObject.run()">Run object method (with profiling)</button>
<textarea id="profOutput" style="width:100%;height:250px"></textarea>
Код профайлера в ответе на это сообщение.
А вот и код профайлера.
ObjectProfiler.js:
// -----------------------------------------------------------------------------
// Creates new ObjectProfiler object.
// Used for JavaScript objects time profiling
function ObjectProfiler()
{
this.calls = []; // calls array
this._context = null; // current call context
this.excludeNames = []; // array of excluded objects names
this.oncallfinished = null; // handler called when zero level call is finished
}
// -----------------------------------------------------------------------------
// Starts profiling of given object
ObjectProfiler.prototype.startProfiling = function(obj)
{
this._startProfilingInternal("this", obj);
};
// -----------------------------------------------------------------------------
// Internal: starts profiling of given object
ObjectProfiler.prototype._startProfilingInternal = function(objName, obj)
{
var excludeNames = this.excludeNames;
var excludeNamesLength = excludeNames.length;
var isExcludedName;
// Enumerate all object methods
var member, newMethod;
for (var name in obj) {
// Check name
isExcludedName = false;
for (var i = 0; i < excludeNamesLength; ++i) {
if (excludeNames[i] == name) {
isExcludedName = true;
break;
}
}
if (isExcludedName) continue;
member = obj[name];
if (typeof(member) == "function") {
// Replace each method with profiled call
newMethod = new Function("return arguments.callee.__prof._invokeOldMethod(arguments.callee, this, arguments);");
obj[name] = newMethod;
// Set properties for new method (they'll be used in _invokeOldMethod)
newMethod.__prof = this;
newMethod.__prof_old = member;
newMethod.__prof_name = name;
newMethod.__prof_objName = objName;
} else if (member != null && member.constructor != null && typeof(member) == "object") {
this._startProfilingInternal(name, member);
}
}
};
// -----------------------------------------------------------------------------
// Internal: calls given function over given object
ObjectProfiler.prototype._invokeOldMethod = function(newMethod, thisObj, args)
{
var profiler = newMethod.__prof;
var callContext = new this._CallContext(profiler._context, newMethod.__prof_name, newMethod.__prof_objName);
var parentCalls = (callContext.parent != null ? callContext.parent : profiler).calls;
parentCalls.push(callContext);
profiler._context = callContext;
var func = newMethod.__prof_old;
var startTime = new Date();
var res = func.apply(thisObj, args);
callContext.callMs = new Date() - startTime;
profiler._context = callContext.parent;
if (callContext.parent == null && profiler.oncallfinished != null) {
profiler.oncallfinished(callContext);
}
return res;
};
// -----------------------------------------------------------------------------
// Internal: creates new CallContext object
ObjectProfiler.prototype._CallContext = function(parent, name, objName)
{
this.parent = parent;
this.name = name;
this.objName = objName;
this.callMs = 0;
this.calls = [];
};
ProfilerTextareaRenderer.js:
// -----------------------------------------------------------------------------
// Creates new ProfilerTextareaRenderer object.
// Renders ObjectProfiler output into <textarea>
function ProfilerTextareaRenderer()
{
// Trick to save this
var _this = this;
this.minMs = 10; // minimal work time ms value with which method call result is rendered
this.textareaElement = null; // <textarea> element
// Handles zero-level call finished event
this._profilerCallfinishedHandler = function(callContext)
{
if (callContext.callMs >= _this.minMs) {
// Write log head
_this.writeLine();
_this.writeLine("************************************************************");
// Start writing call context
_this._writeCallContext(callContext, 0);
}
};
}
// -----------------------------------------------------------------------------
// Initializes class from <textarea> control (by given control ID)
ProfilerTextareaRenderer.prototype.initFromControl = function(clientId)
{
this.textareaElement = document.all[clientId];
this.writeLine("Profiling started");
};
// -----------------------------------------------------------------------------
// Registers renderer instance on profiler
ProfilerTextareaRenderer.prototype.registerProfiler = function(profiler)
{
profiler.oncallfinished = this._profilerCallfinishedHandler;
};
// -----------------------------------------------------------------------------
// Writes to <textarea>
ProfilerTextareaRenderer.prototype.write = function(text)
{
this.textareaElement.value += text;
};
// -----------------------------------------------------------------------------
// Writes line to <textarea>
ProfilerTextareaRenderer.prototype.writeLine = function(text)
{
if (text != null) {
this.write(text);
}
this.write("\n");
};
// -----------------------------------------------------------------------------
// Internal: writes call context <textarea>
ProfilerTextareaRenderer.prototype._writeCallContext = function(callContext, nestingLevel)
{
if (callContext.callMs >= this.minMs) {
// Write white space
for (var i = 0; i < nestingLevel; ++i) {
this.write(" ");
}
// Write results of this call
if (callContext.objName != "this") {
this.write(callContext.objName + ".");
}
this.writeLine(callContext.name + "() - " + callContext.callMs + " ms");
// Write results of nesting calls
++nestingLevel;
var calls = callContext.calls;
var callsLength = calls.length;
for (var i = 0; i < callsLength; ++i) {
this._writeCallContext(calls[i], nestingLevel);
}
}
};