Производительность 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 Мбайта).