Статьи Архив статей

Перевод: Мациевский Николай aka sunnybear
Опубликована: 4 марта 2009

Профилирование вызовов JavaScript-функций

Примечание: ниже расположен перевод статьи JavaScript Function Call Profiling, в которой John Resig рассматривает способы профилирования JavaScript-кода, в частности, измерения времени выполнения вызовов функций. Мои комментарии далее курсивом.

В связи с недавним выходом jQuery 1.3.2 я нахожусь в постоянном поиске дополнительных способов для профилирования и оптимизации jQuery.

В прошлом году я провел исследование сайтов, которые используют jQuery, для того, чтобы выяснить, какие именно CSS-селекторы используются (практически, одновременно с John я провел собственное исследование частоты использования CSS-селекторов, однако оно затрагивало только сами CSS-файлы, результаты получились практически те же самые). С помощью этой информации я смог создать движок выборки по селекторам Sizzle, который специализирован на селекторах определенного вида (Sizzle на данный момент является самым быстрым движком для выбора элементов по CSS-селектору, незначительно уступая только Peppy и YASS).

Дополнительно я написал дополнение для глубокого профилирования jQuery, которое помогло обнаружить методы, которые выполняются чересчур долго на реальных сайтах с jQuery. Это помогло внести ряд улучшений в jQuery 1.2.6, 1.3, и 1.3.2.

А что дальше? Неплохо бы начать с уточнения самих оптимизационных методов, которые, очевидно, являются не такими эффективными, как нам хотелось бы — ведь непонятно, где именно нужно оптимизировать. Можно пойти по одному пути и замерить число вызовов функции из какого-либо метода. В этом нам может помочь Firebug, который позволяет увидеть эту информация в соответствующей вкладке (еще и наряду со значениями времени выполнения каждого метода). К несчастью, очень неудобно вручную вбивать код, затем проверять результаты в консоле и определять, насколько они неудовлетворительны и изменились ли они с прошлого раза. Если бы только был способ получать эти числа в автоматическом режиме.

Профилирующие методы FireUnit

Вчера я немного улучшил описанную ситуацию и добавил пару новых методов в FireUnit.

fireunit.getProfile();

Можно запустить этот код сразу после того, как вы использовали console.profile(); и console.profileEnd(); для перехвата проблемного участка кода — и вы получите полный вывод профилирующей информации. Например, если вы запустите это:

Вызовы методов jQUery

то получите из fireunit.getProfile() следующий объект JavaScript:

{
    "time": 8.443,
    "calls": 611,
    "data":[
    {
	"name":"makeArray()",
	"calls":1,
	"percent":23.58,
	"ownTime":1.991,
	"time":1.991,
	"avgTime":1.991,
	"minTime":1.991,
	"maxTime":1.991,
	"fileName":"jquery.js (line 2059)"
    },
    // etc.
]}
fireunit.profile( fn );

Второй метод, добавленный в FireUnit, позволяет легко и непринужденно запускать и профилировать отдельные вызовы функций. Грубо говоря, этот метод запускает профайлер, исполняет функцию, останавливает профайлер, а затем возвращает результаты из getProfile(). Дополнительно он проверяет возможные исключения при вызове функции и гарантирует, что профайлер надежно выключен (просто небольшая перестраховка с моей стороны).

Это можно использовать примерно следующим образом:

fireunit.profile(function(){
    document.getElementsByClassName("foo");
});

Как это все применять

Во-первых, вам обязательно нужно установить последнюю версию FireUnit. Я создал копию последней версии кода в виде расширения, если вы захотите его установить:

При запуске вам нужно будет убедиться:

  1. обе вкладки Console и Script в Firebug включены;
  2. свойство extensions.firebug.throttleMessages в about:config выставлено в false.

Результаты

Я создал тестовую страницу, на которой можно быстро запустить несколько методов jQuery, чтобы можно было посмотреть на время их выполнения.

Ниже приведены результаты вызовов из jQuery 1.3.2 («методом» я называю метод jQuery, который я запускаю с определенными параметрами, «вызовы» — это число вызовов функции, которое происходит во время работы метода, «O(n)» является грубой оценкой сложности вызова функции, подробнее описано здесь):

МетодВызовыO(n)
.addClass("test");5426n
.addClass("test");5926n
.removeClass("test");7548n
.removeClass("test");6106n
.css("color", "red");4955n
.css({color: "red", border: "1px solid red"});8879n
.remove();237722n+n2
.append("<p>test</p>");3073n
.append("<p>test</p><p>test</p><p>test</p><p>test</p><p>test</p>");3193n
.show();3944n
.hide();3944n
.html("<p>test</p>");287593n+n2
.empty();284523n+n2
.is("div");110n
.filter("div");2162n
.find("div");156416n

Достаточно подробно по поводу производительности JavaScript Относительно O(n) изложено в презентации Douglas Crockford.

Как можно видеть из этой таблицы по значениям O(n), большинство методов jQuery вызывают, по крайней мере, по одной функции на каждый элемент, который им нужно обработать. addClass запускает около 6 функций на каждый элемент, filter — примерно две, а is — только одну.

Также мы легко видим проблемные методы, напоминающие большие черные дыры, в которые утекает процессорное время — .remove(), .empty() и .html(). Все они имеют сложность вызовов функций n2, что является значительной проблемой относительно производительности. (Все эти числа выросли по очень простой причине: .html() использует .empty(), .empty() использует .remove(), а .remove() работает крайне неэффективно.) Если не начать профилировать вызовы функций на медленное выполнение (к слову сказать, большинство внутренних методов jQuery выполняются легко и непринужденно), то и не удастся обнаружить код, написанный неэффективно.

Я немного поразмыслил над описанным случаем и понял, что метод .remove() может быть существенно упрощен. Я завел тикет и предложил обновление, которое привело к значительно улучшенным числам:

МетодВызовыO(n)
.remove();2983n
.html("<p>test</p>");5075n
.empty();2002n

Я просто восхищен своим новым инструментом. Автоматизированный процесс профилирования кода открывает широкие просторы для исследовательской деятельности. Даже не используя ничего другого, кроме вышеописанного метода, я уже могу значительно улучшить каждый метод jQuery.

Также весьма интересно интегрировать описанное профилирование как часть самого процесса разработки — чтобы сразу замечать очевидные недочеты в производительности — но я это оставлю на ближайшее будущее.

Позиция John относительно философии развития jQuery заслуживает отдельного слова. John изначательно закладывается на совместимость со всеми браузерами, чтобы охватить максимальную аудиторию пользователей и разработчиков. А только после этого (когда стало понятно, что jQuery работает не так эффективно, как хотелось бы) он начал ее существенно оптимизировать. С точки зрения маркетинга — очень грамотной подход. YASS однако стоится по противоположному принципу: сначала производительность, а потом уже функциональность.

Читать дальше

Все комментарии (habrahabr.ru)