Pull to refresh

Слияние Rails и Merb: Производительность (Часть 2 из 6)

Reading time9 min
Views905
Original author: Yehuda Katz
Шесть последовательных статей о слиянии Rails и Merb были опубликованы на www.engineyard.com с декабря 2009 до апреля 2010 года. Это вторая статья. Первая тут.

Следующим замечательным улучшением, которое мы надеялись внедрить в 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 (меньший столбик означает бОльшую скорость).
image

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

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

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

В следующем посте я расскажу про улучшения в API Rails 3 для авторов плагинов — следите за сообщениями и, как всегда, оставляйте комментарии!
Tags:
Hubs:
+29
Comments8

Articles

Change theme settings