Template Toolkit

Версия от 18:28, 28 августа 2009; VitaliyFilippov (обсуждение | вклад) (Bugzilla 2)

Template::Toolkit (а также TTk или просто TT) — шаблонизатор, Perl-библиотека для отделения уровня представления от уровня приложения. Короче говоря, относительно простой язык программирования, упрощающий генерацию динамических страниц, в основном, хотя и не обязательно — HTML-страниц.

Статья Template Toolkit в русскоязычной Википедии говорит нам о том, что по сути своей это «легковесная» библиотека, в отличие от «полновесных» фреймворков для построения веб-приложений.

Do you want to try some new features? By joining the beta, you will get access to experimental features, at the risk of encountering bugs and issues.

Ок Нет, спасибо

«Не используйте Template Toolkit!»

Или же очередная серия «Заметок об идиотизме».

Главные минусы TT — во-первых, его производительность, а во-вторых, как это ни парадоксально — богатство возможностей, провоцирующее на создание сложных шаблонов и перенос в них половины логики приложения. В итоге идея о простых шаблонах, которые может редактировать даже дизайнер, накрывается медным тазом. Для примера достаточно взглянуть на шаблоны и код Bugzilla.

Производительность TT страдает в первую очередь от Template::Stash (от англ. stash — тайник, «заначка») — зачем-то введённого отдельного уровня хранения данных — по сути, собственной реализации хеш-таблицы со множеством наворотов, проверок и вообще лишних действий, созданной, по всей видимости, для обеспечения нетривиальных возможностей вроде установки функции-getter’а вместо значение переменной шаблона (возможность, которой за весь опыт веб-разработки лично мне пользоваться не приходилось). В Stash при каждом получении значения какой-либо переменной передаётся ссылка на массив — путь к этой переменной, что как минимум сразу делает невозможным «компилированный» доступ к переменным.

Так как обычный Stash — просто кошмар неиллюзорный, а Template Toolkit был (да и до сих пор) весьма популярен, в районе года 2000 появилась его XS-версия (то есть, переписанная на C) — Template::Stash::XS. После этого TT-шаблонам, всё-таки, полегчало. Но, во-первых, Stash никогда не был единственным замедляющим работу TT уровнем, во-вторых, даже Stash::XS всё равно работает не так быстро, как мог бы, а в-третьих, Stash::XS долгое время имел различные баги — например, поддержка UTF-8 в Template::Stash::XS появилась только 4 июля 2009 года.

Пример: Bugzilla

Bugzilla — популярный и распространённый баг-трекер, «система контроля ошибок», разработанная организацией Mozilla, написанный на Perl’е.

Изначально Bugzilla вообще не была веб-приложением, а работала на основе библиотеки Tk. Однако это было давно и неправда и, как и распространённые версии 2.x, так и версии 3.x используют Template Toolkit.

Bugzilla 2

Bugzilla 2, с точки зрения качества кода, вообще была очень сомнительным поделием, поддерживающим только запуск через CGI и содержащим большое количество старого кода, рассчитанного на то, что недоступны ни транзакции, ни даже библиотека Perl DBI, файлы-вложения хранились в виде BLOB’ов в базе, интерфейс был весьма аскетичен и местами неудобен, и т. д. и т. п. Тем не менее, свои задача Bugzilla 2 выполняла хорошо, и почти никаких проблем с надёжностью не испытывала. Для генерации страниц Bugzilla 2 использует Template Toolkit и, несмотря на CGI, показывает приличную производительность (нет, конечно же не веб-2.0, но для баг-трекера вполне) благодаря относительной простоте кода и, что важно, шаблонов.

Bugzilla 3…

Bugzilla 3 же показала большой прогресс в области удаления старого кода и написания нового. Появились транзакции, DBI, mod_perl, поддержка UTF-8, более приятный интерфейс, защита от дублирования изменений, для почты стали наконец-то использоваться нормальные (ну, такие же) шаблоны для писем (это с большой оговоркой: если Template Toolkit вообще считать нормальным). Также авторы начали понимать, что Bugzillе совершенно необходимы возможности расширения и постепенно добавляют таковые — в 3.0 появляются Custom Fields (дополнительные поля у багов) и чуть-чуть хуков, в 3.2 настраиваемый жизненный цикл бага, поддержка расширений авторизации и ещё чуть-чуть хуков, и т. п.

Но есть и беда: Bugzilla всё ещё использует Template Toolkit, а разрабатывается длительное время и большим числом людей разной степени вшивости :) в итоге люди начинают использовать ну, прямо-таки все возможности Template Toolkit’а, что ведёт к уже упомянутому анти-паттерну разработки: перемещению 50 % логики в шаблоны.

...И её тормоза

Практически это приводит к следующим эффектам. Например, открытие бага с большим числом комментариев (например, с 703-емя — это наш максимум в [ЗИС]'е) в Bugzilla 2.x, даже с учётом того, что это CGI-скрипт (то есть перезапускается интерпретатор), занимает примерно 1.5 секунды, а в 3.x, даже с учётом того, что это mod_perl (то есть интерпретатор не перезапускается и модули тоже не перезагружаются — хочется быстрее, да?)… примерно 22 секунд, из которых 6 тратятся на работу Template::Stash. После некоторых манипуляций средней хитрости время с 22 сек было доведено до 5.5 секунд, но это всё равно уже никак не 1.5 секунды 2-ой багзиллы, а для дальнейшего прогресса «локальных» улучшений уже мало — нужно глобально менять, так сказать, «архитектурные элементы». Та же история, хоть и менее катастрофическая, с поиском багов — было полсекунды, стало полторы. Некритично, конечно, но где же оно, заявленное «this greatly improves performance and highly decreases the memory footprint»?..

А в процессе манипуляций был выявлен замечательный идиотизм, который надо, во-первых, увековечить, а во-вторых, и зафиксить тоже не помешает. Код багзиллы, к счастью, доступен online на Mozilla Cross-Referencer. Мы будем рассматривать ветку Bugzilla 3.4 (вдруг потом баг всё-таки пофиксят). Но вообще он присутствует и в 3.2, и в 3.0.

Итак. В коде Bugzilla::Template есть такая замечательная функция, как quoteUrls, отвечающая за преобразование слов «bug XXXX» и «comment XX» в комментариях в ссылки на баги и оборачивание обычных ссылок и e-mail адресов в гипертекстовые ссылки. А в ней есть следующая строчка:

161 sub quoteUrls {
…
247     my $bug_word = get_text('term', { term => 'bug' });

Строчка относится к получению настоящего названия термина «баг» — в Bugzilla 3 можно баги называть не багами, а «задачами», «проблемами», «требованиями» и вообще как душе угодно. Функция quoteUrls при выводе страницы по разу вызывается для каждого комментария. А что делает get_text? Обратимся к модулю Bugzilla::Util:

597 sub get_text {
598     my ($name, $vars) = @_;
599     my $template = Bugzilla->template_inner;
600     $vars ||= {};
601     $vars->{'message'} = $name;
602     my $message;
603     $template->process('global/message.txt.tmpl', $vars, \$message)
604         || ThrowTemplateError($template->error());
605     # Remove the indenting that exists in messages.html.tmpl.
606     $message =~ s/^    //gm;
607     return $message;
608 }

А теперь заглянем в шаблон global/message.txt.tmpl — он почти ничего не делает, но включает в себя другой шаблон: global/messages.html.tmpl, отвечающий за вывод огромного числа различных информационных сообщений (между прочим, цепочкой ELSIF’ов вместо кучи блоков и именного PROCESS), и в частности — отдельным ELSIF’ом — вывод термина [% terms.$term %], ради которого, собственно, и вызывается get_text('term', { term => 'bug' }). Иными словами, это 33 килобайта, выполняющиеся заново при обработке каждого коммента к багу. В нашем случае — каждого из 703… На это уходило почти 10 секунд из всех 20-и, то есть почти 50 % времени.

Порассуждаем:

  • Во-первых, функция get_text с первым аргументом 'term' вызывается в коде Bugzilla только в одном месте (см.выше), а все term’ы содержатся в отдельном шаблоне global/variables.none.tmpl. Поэтому получение термина можно вынести в отдельную функцию.
  • Во-вторых, нет никакой нужды выбирать нужную функцию в global/messages.html.tmpl цепочкой ELSIF’ов — можно применить блоки Template Toolkit’а, или даже просто отдельные шаблончики. Хотя самое забавное, что время-то как раз съедали не ELSIF’ы, а постоянные дёрганья global/field-descs.none.tmpl, представляющего собой по сути всего лишь пару вызовов Template::Stash::set(), это очередная загадка TT.
  • В-третьих, что самое главное — нет никакой нужды фильтровать тексты комментариев заново при каждом выводе страницы! Зачем выполнять лишнюю работу? Фильтрованный HTML-текст комментариев можно просто сохранить в базу данных рядом с нефильтрованным и спокойно выводить вообще без преобразования.

Вторая глобальная манипуляция — это именно разрешение Template::Stash::XS, зачем-то специально запрещённого в коде Bugzilla 3.x. Наверное, это имело под собой какие-то основания, но когда баг открывается 20 секунд, все основания идут в задницу, и это позволило сэкономить ещё где-то 5 секунд.

Другие манипуляции были более мелкие — отказ от Text::Wrap и оптимизация регулярных выражений в quoteUrls — и дали соответственный выигрыш: 1.5 секунды.

Данные профилировщика

Чтобы не быть голословным, я даже приведу данные профилировщика. Использовался Devel::NYTProf, написанный ребятами из New York Times (кстати, это очень хороший Perl-профилировщик, достойный отдельной статьи). Профилировалась Bugzilla не под mod_perl’ом, а под HTTP::Server::Simple — проблем там никаких не возникает, потому что он тоже CGI (см. Платформы для запуска Perl веб-приложений). Всё тестировалось на «супер-баге» с 703 комментами.

А ещё отсюда видно, что даже после оптимизации больше всего времени всё равно отъедает TT, и что осуществляется 61282 вызова Template::Stash::XS::get(), съедающих почти секунду времени. Такое количество какбэ намекает на возросшую (во многом бесполезно) сложность шаблонов Bugzilla.

Так-то.

Ссылки