Шесть последовательных статей о слиянии Rails и Merb были опубликованы на www.engineyard.com с декабря 2009 до апреля 2010 года. Это вторая статья. Первая тут.
Следующим замечательным улучшением, которое мы надеялись внедрить в Rails из Merb, была лучшая производительность. Поскольку Merb появился после Rails, разработчики Merb имели возможность узнать, какие части Rails использовались чаще, и оптимизировать их производительность.
Мы хотели взять из Merb изменения, улучшающие производительность, и перенести их в Rails 3. В этом посте я расскажу о нескольких оптимизационных моментах, добавленных в Rails 3: уменьшение времени работы контроллера и (сильное) ускорение рендера коллекции партиалов.
Для начала мы сфокусировались на производительности нескольких специфических, но широко используемых частях Rails:
Это явно приблизительная оценка, но она покрыла большинство случаев, где производительность могла бы быть значительно лучше, но разработчик Rails не был в состоянии ее улучшить самостоятельно.
Первым шагом было усовершенствование издержек работы Rails контроллера. В Rails 2.3 нет способов их тестирования, потому что вы вынуждены использовать
Делая это, мы использовали fork Стефана Каеса (Stefan Kaes’ fork of ruby-prof), идущий вместе с
Когда мы посмотрели на процесс работы контроллера, оказалось, что доминирующую часть занимало создание ответа. Покопав��ись глубже, мы увидели, что ActionController устанавливал заголовки напрямую, потом парсил их снова перед тем, как вернуть ответ, чтобы получить дополнительную информацию. Хороший пример этого феномена — заголовок
Как видите, объект Response работал напрямую с заголовком Content-Type, парся часть заголовка при необходимости. Это было особенно проблематично, потому что Response совершал лишнюю работу над заголовками во время приготовлений перед отсылкой ответа клиенту:
То есть перед отправкой ответа Rails снова разбивал заголовок
Это не занимало сотни миллисекунд при запросе, но в сильно кешированных приложениях издержки могли быть выше стоимости вынимания объектов из кеша и возврата их клиенту.
Решением в данном случае было хранить тип контента и charset в полях объекта ответа и объединять их одной простой быстрой операцией при подготовке ответа.
Теперь мы просто находим переменные экземпляра и создаем один String. Множественные изменения этих строк кода снизили время изержек примерно с 400 мксек до 100 мксек. Конечно, не сильно большое количество времени, но оно могло реально ослабить чувствительные к производительности приложения.
Рендер коллекции партиалов представлял собой еще одну хорошую возможность для оптимизации. И в этом случае улучшение составило миллисекунды, а не микросекунды!
Для начала реализация в Rails 2.3:
Важной частью является то, что происходило внутри цикла, который мог случаться сотни раз в большой коллекции партиалов. В этом случае реализация в Merb имела бОльшую производительность, которую мы смогли перенести в Rails. Вот реализация Merb.
Сейчас нам понятно, что это далеко от идеала. Тут происходит много всего (и я лично хотел бы увидеть этот метод отрефакторенным). Но интересная часть — то, что происходит внутри цикла (начиная с
Для коллекции из 100 партиалов разница между издержками могла быть около 10 мс и 3 мс. Для коллекции маленьких партиалов это было заметно (и причина для инлайн партиалов, которые соответствовали для того, чтобы бы партиалами на первом месте).
В Rails 3, мы улучшили производительность за счет уменьшения того, что происходит внутри цикла. К сожалению, одна особенность Rails сделала оптимизацию немного труднее. В частности, вы могли отрендерить партиал, используя гетерогенную коллекцию (коллекцию, содержащую объекты Post, Article и Page, например) и Rails рендерили бы правильный шаблон для каждого объекта (объекты Article рендерятся в
Столкнувшись с этой проблемой, нам не удалось полностью оптимизировать гетерогенный случай, но мы сделали
Вот как теперь выглядит рендер коллекции в случае, когда мы знаем шаблон:
Что важно, сам цикл теперь маленький (даже проще чем то, что происходило внутри цикла в Merb). Что еще стоит отметить, это то, что в процессе улучшения производительности кода, мы создали объект PartialRenderer для отслеживания состояния. Хотя вы могли подумать, что создание нового объекта было бы обойтись дорого, оказывается, что создание объектов в Ruby относительно дешево и объекты могут предлагать возможности кеширования, которые гораздо сложнее в процедурном коде.
Для тех, кто хочет увидеть улучшения в картинках, вот несколько вещей: во-первых, улучшение между Rails 2.3 и Rails 3 на Ruby 1.9 (меньший столбик означает бОльшую скорость).

А вот для более дорогих операций:

Наконец, сравнение Rails 3 на четырех платформах (Ruby 1.8, Ruby 1.9, Rubinius и JRuby):

Как вы можете видеть, Rails 3 заметно быстрее, чем Rails 2.3, и что все платформы (включая Rubinius!) заметно улучшены по сравнению с Ruby 1.8. В общем, чудесный год для Ruby!
В следующем посте я расскажу про улучшения в API Rails 3 для авторов плагинов — следите за сообщениями и, как всегда, оставляйте комментарии!
Следующим замечательным улучшением, которое мы надеялись внедрить в Rails из Merb, была лучшая производительность. Поскольку Merb появился после Rails, разработчики Merb имели возможность узнать, какие части Rails использовались чаще, и оптимизировать их производительность.
Мы хотели взять из Merb изменения, улучшающие производительность, и перенести их в Rails 3. В этом посте я расскажу о нескольких оптимизационных моментах, добавленных в Rails 3: уменьшение времени работы контроллера и (сильное) ускорение рендера коллекции партиалов.
Для начала мы сфокусировались на производительности нескольких специфических, но широко используемых частях Rails:
- Непроизводительные издержки (маршрутизатор плюс цена за вход и выход из контроллера)
- render :text
- render :template
- render :partial
- рендер нескольких (10 и 100) одинаковых партиалов в цикле
- рендер нескольких (10 и 100) одинаковых партиалов с помощью collection
Это явно приблизительная оценка, но она покрыла большинство случаев, где производительность могла бы быть значительно лучше, но разработчик Rails не был в состоянии ее улучшить самостоятельно.
Непроизводительные издержки контроллера
Первым шагом было усовершенствование издержек работы Rails контроллера. В Rails 2.3 нет способов их тестирования, потому что вы вынуждены использовать
render :string, чтобы послать текст клиенту в ответ, что подразумевает выполнение всего процесса рендера. И все же, мы хотели уменьшить это насколько возможно.Делая это, мы использовали fork Стефана Каеса (Stefan Kaes’ fork of ruby-prof), идущий вместе с
CallStackPrinter (лучший способ визуализации профайловых данных из Ruby приложения, из всех, что я видел). Мы также написали несколько бенчмарков, которые могли дублировать профайлер в процессе его работы, чтобы сфокусироваться на конкретном участке и получить более точные данные.Когда мы посмотрели на процесс работы контроллера, оказалось, что доминирующую часть занимало создание ответа. Покопав��ись глубже, мы увидели, что ActionController устанавливал заголовки напрямую, потом парсил их снова перед тем, как вернуть ответ, чтобы получить дополнительную информацию. Хороший пример этого феномена — заголовок
Content-Type, который имеет два компонента (сам content-type и опциональный charset). Оба компонента были доступны в объекте Response через геттер и сеттер:Copy Source | Copy HTML<br/>def content_type=(mime_type)<br/> self.headers["Content-Type"] =<br/> if mime_type =~ /charset/ || (c = charset).nil?<br/> mime_type.to_s<br/> else<br/> "#{mime_type}; charset=#{c}"<br/> end<br/>end<br/> <br/># Returns the response's content MIME type, or nil if content type has been set.<br/>def content_type<br/> content_type = String(headers["Content-Type"] || headers["type"]).split(";")[ 0]<br/> content_type.blank? ? nil : content_type<br/>end<br/> <br/># Set the charset of the Content-Type header. Set to nil to remove it.<br/># If no content type is set, it defaults to HTML.<br/>def charset=(charset)<br/> headers["Content-Type"] =<br/> if charset<br/> "#{content_type || Mime::HTML}; charset=#{charset}"<br/> else<br/> content_type || Mime::HTML.to_s<br/> end<br/>end<br/> <br/>def charset<br/> charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]<br/> charset.blank? ? nil : charset.strip.split("=")[1]<br/>end <br/>Как видите, объект Response работал напрямую с заголовком Content-Type, парся часть заголовка при необходимости. Это было особенно проблематично, потому что Response совершал лишнюю работу над заголовками во время приготовлений перед отсылкой ответа клиенту:
Copy Source | Copy HTML<br/>def assign_default_content_type_and_charset!<br/> self.content_type ||= Mime::HTML<br/> self.charset ||= default_charset unless sending_file?<br/>end <br/>То есть перед отправкой ответа Rails снова разбивал заголовок
Content-Type по точке с запятой и потом делал еще работу со строками, чтобы соединить их вместе снова. И конечно, Response#content_type= использовался в других частях Rails, чтобы правильно установить его в зависимости от типа шаблона или внутри блоков respond_to.Это не занимало сотни миллисекунд при запросе, но в сильно кешированных приложениях издержки могли быть выше стоимости вынимания объектов из кеша и возврата их клиенту.
Решением в данном случае было хранить тип контента и charset в полях объекта ответа и объединять их одной простой быстрой операцией при подготовке ответа.
Copy Source | Copy HTML<br/>attr_accessor :charset, :content_type<br/> <br/>def assign_default_content_type_and_charset!<br/> return if headers[CONTENT_TYPE].present?<br/> <br/> @content_type ||= Mime::HTML<br/> @charset ||= self.class.default_charset<br/> <br/> type = @content_type.to_s.dup<br/> type < < "; charset=#{@charset}" unless @sending_file<br/> <br/> headers[CONTENT_TYPE] = type<br/>end <br/>Теперь мы просто находим переменные экземпляра и создаем один String. Множественные изменения этих строк кода снизили время изержек примерно с 400 мксек до 100 мксек. Конечно, не сильно большое количество времени, но оно могло реально ослабить чувствительные к производительности приложения.
Рендер коллекции партиалов
Рендер коллекции партиалов представлял собой еще одну хорошую возможность для оптимизации. И в этом случае улучшение составило миллисекунды, а не микросекунды!
Для начала реализация в Rails 2.3:
Copy Source | Copy HTML<br/>def render_partial_collection(options = {}) #:nodoc:<br/> return nil if options[:collection].blank?<br/> <br/> partial = options[:partial]<br/> spacer = options[:spacer_template] ? render(:partial => options[:spacer_template]) : ''<br/> local_assigns = options[:locals] ? options[:locals].clone : {}<br/> as = options[:as]<br/> <br/> index = 0<br/> options[:collection].map do |object|<br/> _partial_path ||= partial ||<br/> ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)<br/> template = _pick_partial_template(_partial_path)<br/> local_assigns[template.counter_name] = index<br/> result = template.render_partial(self, object, local_assigns.dup, as)<br/> index += 1<br/> result<br/> end.join(spacer).html_safe!<br/>end <br/>Важной частью является то, что происходило внутри цикла, который мог случаться сотни раз в большой коллекции партиалов. В этом случае реализация в Merb имела бОльшую производительность, которую мы смогли перенести в Rails. Вот реализация Merb.
Copy Source | Copy HTML<br/>with = [opts.delete(:with)].flatten<br/>as = (opts.delete(:as) || template.match(%r[(?:.*/)?_([^\./]*)])[1]).to_sym<br/> <br/># Ensure that as is in the locals hash even if it isn't passed in here<br/># so that it's included in the preamble.<br/>locals = opts.merge(:collection_index => -1, :collection_size => with.size, as => opts[as])<br/>template_method, template_location = _template_for(<br/> template,<br/> opts.delete(:format) || content_type,<br/> kontroller,<br/> template_path,<br/> locals.keys)<br/> <br/># this handles an edge-case where the name of the partial is _foo.* and your opts<br/># have :foo as a key.<br/>named_local = opts.key?(as)<br/> <br/>sent_template = with.map do |temp|<br/> locals[as] = temp unless named_local<br/> <br/> if template_method && self.respond_to?(template_method)<br/> locals[:collection_index] += 1<br/> send(template_method, locals)<br/> else<br/> raise TemplateNotFound, "Could not find template at #{template_location}.*"<br/> end<br/>end.join<br/> <br/>sent_template <br/>Сейчас нам понятно, что это далеко от идеала. Тут происходит много всего (и я лично хотел бы увидеть этот метод отрефакторенным). Но интересная часть — то, что происходит внутри цикла (начиная с
sent_template = with.map). В отличие от ActionView, который выяснял имя шаблона, брал объект шаблона, имя счетчика и т.д., Merb ограничил активность внутри цикла установкой нескольких значений Hash и вызовом метода.Для коллекции из 100 партиалов разница между издержками могла быть около 10 мс и 3 мс. Для коллекции маленьких партиалов это было заметно (и причина для инлайн партиалов, которые соответствовали для того, чтобы бы партиалами на первом месте).
В Rails 3, мы улучшили производительность за счет уменьшения того, что происходит внутри цикла. К сожалению, одна особенность Rails сделала оптимизацию немного труднее. В частности, вы могли отрендерить партиал, используя гетерогенную коллекцию (коллекцию, содержащую объекты Post, Article и Page, например) и Rails рендерили бы правильный шаблон для каждого объекта (объекты Article рендерятся в
_article.html.erb, и т.д.). Это означает, что не всегда есть возможность точно определить шаблон, который должен быть отрендерен.Столкнувшись с этой проблемой, нам не удалось полностью оптимизировать гетерогенный случай, но мы сделали
render :partial => "name", :collection => @array быстрее. Для достижения этого мы разбили логику на 2 пути: более быстрый для случая, когда мы знаем шаблон, и более медленный для случая, когда он должен быть определен в зависимости от объекта.Вот как теперь выглядит рендер коллекции в случае, когда мы знаем шаблон:
Copy Source | Copy HTML<br/>def collection_with_template(template = @template)<br/> segments, locals, as = [], @locals, @options[:as] || template.variable_name<br/> <br/> counter_name = template.counter_name<br/> locals[counter_name] = -1<br/> <br/> @collection.each do |object|<br/> locals[counter_name] += 1<br/> locals[as] = object<br/> <br/> segments < < template.render(@view, locals)<br/> end<br/> <br/> @template = template<br/> segments<br/>end <br/>Что важно, сам цикл теперь маленький (даже проще чем то, что происходило внутри цикла в Merb). Что еще стоит отметить, это то, что в процессе улучшения производительности кода, мы создали объект PartialRenderer для отслеживания состояния. Хотя вы могли подумать, что создание нового объекта было бы обойтись дорого, оказывается, что создание объектов в Ruby относительно дешево и объекты могут предлагать возможности кеширования, которые гораздо сложнее в процедурном коде.
Для тех, кто хочет увидеть улучшения в картинках, вот несколько вещей: во-первых, улучшение между Rails 2.3 и Rails 3 на Ruby 1.9 (меньший столбик означает бОльшую скорость).

А вот для более дорогих операций:

Наконец, сравнение Rails 3 на четырех платформах (Ruby 1.8, Ruby 1.9, Rubinius и JRuby):

Как вы можете видеть, Rails 3 заметно быстрее, чем Rails 2.3, и что все платформы (включая Rubinius!) заметно улучшены по сравнению с Ruby 1.8. В общем, чудесный год для Ruby!
В следующем посте я расскажу про улучшения в API Rails 3 для авторов плагинов — следите за сообщениями и, как всегда, оставляйте комментарии!