Спустя более чем пол года в приложении для которого писался класс AsyncCaller появились некоторые проблемы с производительностью на очень больших для браузера объемах данных, в связи с чем мне пришлось заняться профилированием производительности и памяти (для профилирования памяти использовал JavaScript Memory Validator -
http://www.softwareverify.com/javasc...ory/index.html). В результате чего так же удалось обнаружить изъян в представленном выше классе AsyncCaller.
Проблема в том, что в методе OnReadyStateChange (который вызывается через каждые 10 миллисекунд и проверяет не пришел ли респонс) в случае если респонс еще не пришел/не загружен до конца в в метод установки таймера передается безымянная функция, котора служит только для того, что бы через 10 миллисекунд опять вызвать OnReadyStateChange. В безымянную функцию передается единственный параметр self через замыкание, для того что бы вызвать OnReadyStateChange нужного объекта. Вот этот код:
Код:
|
if (this.httpReq.readyState == 4)
{
this.StopTimer();
RemoveEvent(window, "unload", this.OnWindowUnload);
this.completeCallback(this.httpReq.status, this.httpReq.statusText, this.httpReq.responseText, this.state);
}
else
{
var self = this;
this.timerID = setTimeout(
function()
{
self.OnReadyStateChange();
},
this.pollInterval);
}
|
Сама суть проблемы в том, что если этот код выполняется часто (а например при 10 секундном ожидании респонса он выполнится 1000 раз), то он приводит к созданию большого количества объектов функций, которые конечно будут подобраны сборщиком мусора, но до того как он сработает, они расходуют лишнюю память.
Что бы избежать этого я переписал класс, отказавшись от замыкания. Кроме того увеличил poolInterval до 100, т.е. метод OnReadyStateChange (который проверяет пришел ли респонс) будет вызываться 10 раз в секунду (тогда как в оригинальной версии вызывался 100 раз). Переписанный код класса ниже, ключевые изменения выделены красным.
В качестве PS. cразу оговорюсь по коду - AsyncCaller.handles это объект, читай хештаблица, а не массив, потому что он гарантированно будет разреженным (кроме того класс может использоваться одновременно для посылки нескольких запросов поэтому пустота будет не только в начале массива, но и заполненная часть так же может иметь пропуски, хотя их скорее всего их будет 1 - 2, т.к. одновременный запуск десятков асинхронных запросов не оправдан, а в IE 6 их вообще нельзя запустить более 2 одновременно). Хотя здесь можно было бы использовать и массив JavaScript, так как они тоже должны хорошо работать с разреженными массивами, это скорее уже дело вкуса. Сам я реализацию с массивом на производительность не тестировал.
Код:
|
// Constructor.
// completeCallback - callback that will be executed when request is completed
// state - (optional parameter) state parameter that will be supplied as 4-th argument of completeCallback function when it is called
function AsyncCaller(completeCallback, state)
{
if (!completeCallback)
throw "Callback is not specified for AsyncCaller constructor";
var httpReq = this.GetHttpRequest();
if (!httpReq)
throw "Unable to create an instance of XMLHttpRequest.";
this.httpReq = httpReq;
this.completeCallback = completeCallback;
this.state = state;
this.pollInterval = 100; // ms.
var handle = AsyncCaller.handleCount++;
this.handle = handle;
AsyncCaller.handles[handle] = this;
// Ensure async call is aborted when document is unloaded
var self = this;
this.OnWindowUnload = function() { self.AbortExecution(); }
AddEvent(window, "unload", this.OnWindowUnload);
}
// Object handles to references map (we use an instance of Object as a Dictionary)
AsyncCaller.handles = {};
AsyncCaller.handleCount = 0;
// Returns a new initialized instance of XMLHttpRequest class
AsyncCaller.prototype.GetHttpRequest = function()
{
var result = null;
if (window.XMLHttpRequest)
{
// Mozilla, Safari,
result = new XMLHttpRequest();
if (result.overrideMimeType)
{
result.overrideMimeType('text/xml');
}
}
else if (window.ActiveXObject)
{
// IE
try
{
result = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
result = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{}
}
}
return result;
}
// Call GET or POST method asynchronously. When it is completed callback passed to constructor will be executed
AsyncCaller.prototype.Call = function(uri, postRequest, headers)
{
if (this.timerID != null)
{
throw "This method cannot be called untill the Ajax request initiated by previous call of this method is not completed.";
}
var method, body;
if (postRequest)
{
method = "POST";
body = postRequest;
}
else
{
method = "GET";
body = null;
}
this.httpReq.open(method, uri, true);
this.httpReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
this.httpReq.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
this.httpReq.setRequestHeader("Cache-Control", "no-cache");
if (headers)
{
for (var i = 0; i < headers.length; i++)
{
if (headers[i] && headers[i] != "")
{
var colonIndex = headers[i].indexOf(":");
if (colonIndex != -1)
{
var headerName = headers[i].substring(0, colonIndex);
var headerValue = headers[i].substring(colonIndex + 1);
this.httpReq.setRequestHeader(headerName, headerValue);
}
}
}
}
this.StartTimer();
this.httpReq.send(body);
}
// Internal callback that is called with the interval defined by property 'pollInterval'
// untill request is completed
function OnReadyStateChange(callerHandle)
{
var instance = Handle2Reference(callerHandle);
if (instance != null)
{
if (instance.httpReq.readyState == 4)
{
instance.StopTimer();
RemoveEvent(window, "unload", instance.OnWindowUnload);
ReleaseHandle(instance.handle);
instance.completeCallback(instance.httpReq.status, instance.httpReq.statusText, instance.httpReq.responseText, instance.state);
}
else
instance.timerID = setTimeout("OnReadyStateChange(" + instance.handle + ")", instance.pollInterval);
}
}
// Returns a reference to an object by its handle
function Handle2Reference(handle)
{
if (AsyncCaller.handles.hasOwnProperty(handle))
return AsyncCaller.handles[handle];
return null;
}
// Release handle
function ReleaseHandle(handle)
{
if (AsyncCaller.handles.hasOwnProperty(handle))
{
AsyncCaller.handles[handle] = null;
delete AsyncCaller.handles[handle];
}
}
AsyncCaller.prototype.StartTimer = function()
{
this.StopTimer();
this.timerID = setTimeout("OnReadyStateChange(" + this.handle + ")", this.pollInterval);
}
AsyncCaller.prototype.StopTimer = function()
{
if (this.timerID != null)
{
clearTimeout(this.timerID);
this.timerID = null;
}
}
// Aborts execution of async request
AsyncCaller.prototype.AbortExecution = function()
{
this.StopTimer();
this.httpReq.abort();
ReleaseHandle(this.handle);
} |