Оптимизируем производительность фреймворка Grails

· На чтение уйдёт 3 минут · (633 слов)

Производительность Grails

Рано или поздно, любой программист слышит, что производительность инструмента, который он использует или инструмента, который он сделал — невысока. Будь то какой-нибудь синтетический бенчмарк, или сложные распределённые вычисления. Будь то работа с базой данных или с файлами.

Все хотят максимальной производительности! Всегда! Немедленно!

Как известно, часто максимальная производительность достигается путём жертв. Пожертвовать удобством, настраиваемостью, масштабируемостью, поддерживаемостью — то и другое, и третье и четвёртое — можно принести на алтарь производительности.

Но насколько серьёзны потери? Насколько их можно избежать? Как можно выжать из проекта побольше производительности? Давайте посмотрим на примере Grails.

Hello world

В качестве тестового приложения будет использоваться примитивное MVC приложение. Контроллер ВООБЩЕ НИЧЕГО не делает, Вью примечателен тем, что наследует дефолтный зелёненький лэйаут (1), а также рендерит 2 подвьюшки (header, footer), состоящие просто из HTML комментария.

Нагрузка на подсистему Grails сводится к:

  • роутингу (/hello/index → HelloController → def index(), возможно с кэшированием)
  • загрузке и поиску views (аналогично, вероятно с использованием кэширования)
  • собственно рендеринг 3 вьюшек (View)

Чтобы выявить наиболее узкие места, вооружимся Yourkit Java Profiler, а также JMeter или ab2 (кому что больше нравится), и вперёд изучать ботлнеки (bottlenecks) Grails! Для простоты приложение будет запускаться внутри Jetty Runner. Тестирование производилось на ноутбуке с рабочего компьютера (непринципиально). Использовалась виртуальная машина Java 7, опция -server была включена.

Bottlenecks

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

На тот случай, если вы сами будете брать в руки профайлер — выбирайте режим TRACE, который популярно расскажет, сколько процессорного времени — «собственное».

Например, у меня профайлер показал, что огромное количество времени сервер провёл в методе org.codehaus.groovy.grails.web.util.WebUtils.forwardRequestForUrlMappingInfo (HttpServletRequest, HttpServletResponse, UrlMappingInfo, Map, boolean). Он скушал 6% всего процессорного времени.

Другими претендентами на оптимизацию будут:

  • org.codehaus.groovy.grails.web.util.StreamCharBuffer$StreamCharBufferWriter.write(String, int, int)
  • org.codehaus.groovy.grails.web.pages.TagLibraryLookup.tagNameKey(String, String)
  • org.codehaus.groovy.grails.web.util.GrailsPrintWriter.write(String)
  • org.codehaus.groovy.grails.web.util.StreamCharBuffer$AllocatedBuffer.writeString(String, int, int)
  • org.codehaus.groovy.grails.web.util.StreamCharBuffer$StreamCharBufferWriter.write(String)
  • org.codehaus.groovy.grails.web.util.StreamCharBuffer.isConnectedMode()
  • org.codehaus.groovy.grails.web.util.StreamCharBuffer.allocateSpace()
  • org.codehaus.groovy.grails.plugins.web.api.CommonWebApi.getRequest(Object)
  • grails.util.Environment.getCurrent()
  • org.codehaus.groovy.grails.web.util.StreamCharBuffer$StreamCharBufferWriter.markUsed()
  • org.codehaus.groovy.grails.web.pages.GroovyPageOutputStack.push(Writer, boolean)
  • org.codehaus.groovy.grails.web.pages.TagLibraryLookup.lookupTagLibrary(String, String)
  • org.codehaus.groovy.grails.web.pages.TagLibraryLookup.doesTagReturnObject(String, String)
  • org.codehaus.groovy.grails.web.pages.GroovyPage.invokeTag(String, String, int, Map, int)
  • и даже org.slf4j.impl.GrailsLog4jLoggerAdapter.isDebugEnabled()

Как видно, я отобрал только методы из grails-core, т.к. оптимизация самого Groovy лежит вне области наших интересов на данный момент. Методы преимущественно весьма легковесные, но есть одно большое «но»: вызываются они десятки тысяч раз на менее чем тысячу запросов.

Вот например метод isDebugEnabled() вызывался 5719 раз из метода org.codehaus.groovy.grails.web.pages.GroovyPageBinding.internalSetVariable (Binding, String, Object). И org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch (HttpServletRequest, HttpServletResponse) также отличился (1633 раза). Больше всего, этот метод любят в Spring Framework, так что эта часть также не подлежит оптимизации.

Итак, если ещё раз внимательно приглядеться к списку, видим, что почти все «горячие» методы — родом из подпроекта grails-web проекта grails-core. Давайте повнимательнее изучим org.codehaus.groovy.grails.web.

Вот например метод

   protected String tagNameKey(String namespace, String tagName) {
       return namespace + ':' + tagName;
   }

Заглянув в профайлер, мы видим, что компилятор всё сделал «правильно»: 1 контруктор, 2 append, и toString(). Но доподлинно неизвестно, насколько медленнее использование композитного ключа, например. Или ещё лучше, использования пары HashMap.

Ещё особо интересной была бы оптимизация метода org.codehaus.groovy.grails.web.util.GrailsPrintWriter.write(String). Однако, несмотря на то, что он вызывается повсеместно и в совокупности почти 100000 раз, оптимизации он практически не поддаётся. Разве что крохоборской (вроде того, что объект проверяют на null и перед передачей объекта в этот метод). Разумеется, проверять объект до вызова — «дешевле»: не теряется дорогостоящее время на вызов метода, затем на возврат.

Не крохоборское решение — довольно сложное, можно убрать из этого метода try … catch, перехватывая их где-нибудь в более подходящем месте. Ведь если однажды во время записи к клиенту в браузер случился IOException, крайне маловероятно, что вот уже через 810 наносекунд всё будет в порядке.

До безобразия медленный write — очевидно, и является причиной того, что в то время как JSP по производительности примерно эквивалентны простому сервлету, то GSP до этого ещё очень-очень долго.
Другое решение — попробовать потыкать аннотации groovy++ в коде grails-web. К сожалению, модули слишком огромные для того, чтобы можно было бы просто сделать свою оптимизированную версию (1,5 Мбайта).

Полезное