Skip to content

Improve perf of ActionView::Helpers::TextHelper#excerpt for large strings. #39979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 27, 2020

Conversation

tgxworld
Copy link
Contributor

@tgxworld tgxworld commented Aug 4, 2020

Benchmark Script

begin
  require 'bundler/inline'
rescue LoadError => e
  $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
  raise e
end

gemfile(true) do
  source 'https://rubygems.org'

  gem 'actionview', "6.0.3"
  gem 'benchmark-ips'
  gem 'byebug'
end

require 'action_view'
require 'logger'
require 'benchmark/ips'
require 'byebug'

words_10000_string = <<~STRING
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec maximus dictum pharetra. Phasellus convallis feugiat pharetra. Pellentesque imperdiet tellus quis felis ullamcorper euismod. Quisque lacinia dui nec facilisis volutpat. In eget diam placerat, vulputate augue a, aliquam velit. Nunc varius scelerisque eros, sed aliquam orci imperdiet ac. Mauris vulputate pellentesque placerat. Integer dui nisi, fermentum laoreet dolor eu, fringilla gravida nisi. Suspendisse ipsum neque, rutrum nec turpis quis, laoreet vestibulum lacus. Integer pretium risus velit, eu semper purus laoreet a. Nulla in eleifend arcu, id vehicula justo.

Maecenas magna ligula, pretium at efficitur vitae, molestie nec nibh. Duis feugiat lorem non ipsum porta, ut hendrerit nisl blandit. Donec auctor, diam et consequat dignissim, sapien erat tempor libero, sit amet interdum massa nulla quis nibh. Pellentesque sit amet pellentesque nunc, vitae lacinia lorem. Sed in eleifend velit. Sed laoreet felis id elementum pellentesque. Donec a orci est. Vestibulum sollicitudin, tellus non vestibulum tincidunt, velit nunc egestas dolor, nec faucibus risus justo convallis ipsum.

Integer venenatis orci ut diam blandit, vitae lacinia libero posuere. Nullam ultrices nibh sit amet dapibus ultricies. Quisque pharetra orci a risus egestas feugiat. Donec mattis finibus leo ultrices mattis. Duis vehicula ligula augue, ut feugiat lectus placerat rutrum. Nullam rutrum vitae turpis ac tempor. Pellentesque facilisis arcu vitae gravida egestas. Vivamus euismod, nulla eu iaculis eleifend, nunc turpis tempor magna, nec posuere est magna in lectus. Nulla id scelerisque erat, ac egestas velit. Vestibulum vehicula id arcu nec cursus. Pellentesque hendrerit felis nisl, sed porttitor justo lobortis at. Cras id dictum ligula. Curabitur a consectetur leo. Sed laoreet ullamcorper enim id faucibus. Suspendisse euismod nulla sit amet porta tristique. Aliquam erat volutpat.

Donec mattis tincidunt purus ac egestas. Praesent tristique eget sapien nec venenatis. Nunc condimentum dapibus eleifend. Vestibulum non dui at sem bibendum tincidunt eget luctus nunc. Nunc efficitur orci velit, vitae sagittis ex ultricies ut. Curabitur mollis turpis ac justo suscipit, nec semper nisl euismod. Proin nibh mauris, laoreet in volutpat sit amet, semper in dui. Aliquam mollis libero sed metus fringilla, sed dapibus odio pretium. Aenean a lacus et urna maximus facilisis eu in eros. Sed mattis viverra augue, a tempor tellus maximus nec. Nullam aliquam risus massa. Phasellus elementum porta lectus vitae dignissim. Quisque nec imperdiet ligula, ut scelerisque justo. Sed mauris risus, pharetra nec mauris sit amet, viverra hendrerit ipsum. Sed eleifend leo leo, ac facilisis arcu tincidunt ac. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Duis nec tortor sit amet ex hendrerit fringilla ut id metus. Nunc ac felis eleifend, posuere nunc in, rutrum diam. Proin tempor tellus et rutrum malesuada. Nunc in nisl vel neque viverra feugiat eget at leo. Vivamus a mollis erat. Donec lacinia augue id turpis consectetur, congue venenatis felis viverra. Nullam sapien nulla, egestas quis ipsum quis, placerat efficitur ligula. Quisque et maximus ligula. Suspendisse ullamcorper ipsum ut nulla consequat, eget vehicula elit congue. Vivamus laoreet, mi ac scelerisque porta, turpis purus commodo velit, a ultricies urna nulla iaculis augue. Nunc bibendum fermentum quam, in dapibus ipsum iaculis id. Nulla magna mi, consequat sit amet molestie in, consectetur quis nisl. Morbi egestas turpis at rhoncus tristique. Quisque sagittis nec mi vitae auctor. Aenean sed orci aliquet, accumsan quam tempor, posuere tortor. Suspendisse leo mauris, tempor nec tempor at, imperdiet quis leo.

Cras eu vestibulum mi, sit amet iaculis tortor. Etiam at lacus et arcu semper semper vitae feugiat magna. Suspendisse pulvinar ornare sem vitae sagittis. Nulla bibendum augue at erat luctus, sagittis consequat est efficitur. In dolor tortor, ullamcorper at euismod in, congue in tellus. Duis finibus dui gravida, feugiat ligula a, interdum felis. Ut nec felis vel odio iaculis condimentum vel in nibh. Donec consectetur libero sed metus congue dictum. Nunc dapibus tempus lorem, non aliquet quam aliquet at. Praesent porta tellus ut pretium tristique. In in molestie nulla. Sed metus tortor, rhoncus vitae egestas ut, facilisis nec neque. Suspendisse vulputate nec mi ut ullamcorper. Nullam sed dui luctus tellus sagittis elementum id in velit. Cras sit amet ligula suscipit, tempor dui non, imperdiet tortor.

Vivamus at iaculis ante. Vestibulum urna metus, ornare vitae elit ullamcorper, dapibus viverra turpis. Fusce nec diam nibh. Phasellus suscipit, purus quis auctor laoreet, eros mi ullamcorper nulla, a efficitur leo turpis sed libero. Suspendisse consectetur eget lacus id ultricies. Fusce lacinia elementum ex, ac hendrerit dolor maximus sed. Aenean mattis, leo in pharetra mollis, urna elit vulputate ligula, ac gravida nunc nisi sit amet enim. Duis quis ex at ligula porta dignissim non eget nisl. Vivamus auctor venenatis bibendum. Nulla justo nibh, accumsan sed velit sit amet, faucibus viverra quam. Curabitur faucibus sagittis aliquet. Morbi venenatis vulputate fringilla.

Ut pellentesque massa ipsum. Nam blandit nisi vitae urna blandit, ac fringilla tortor elementum. Nunc nisl diam, malesuada id mattis accumsan, porttitor sit amet velit. Integer iaculis nibh vitae volutpat pharetra. Duis quam nibh, pellentesque sit amet fringilla non, volutpat a nulla. Quisque sit amet pharetra libero. Aliquam elementum libero lacus, vel tempor massa ornare non. Nunc malesuada tempor nulla sed sodales. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque ac ultrices felis, id elementum justo. Sed ultrices, libero nec tincidunt varius, purus mauris faucibus eros, sit amet posuere quam urna at urna.

Proin vehicula lectus ac enim pulvinar iaculis. Curabitur mattis consectetur sagittis. Mauris commodo eros sit amet nunc suscipit, quis efficitur est bibendum. Donec tempus dui tellus, quis viverra mauris pellentesque vel. Phasellus pharetra magna quis nunc dictum malesuada. Fusce dignissim enim eget ante tincidunt, a pretium metus tempus. Aenean nec congue leo. Vivamus id sem quis eros lacinia faucibus. Aenean fringilla nec dolor sed lacinia. Pellentesque rhoncus eleifend nibh, a rhoncus mi sollicitudin sed. Nunc volutpat ligula non euismod condimentum. Phasellus imperdiet sapien metus, quis posuere nisi bibendum ac.

Praesent lobortis, metus et varius commodo, sapien dolor rhoncus enim, at hendrerit tellus sapien non ligula. Ut volutpat, nunc nec gravida dapibus, lectus mi luctus lectus, vel egestas velit metus ac elit. Quisque molestie ex ac purus gravida, eget.
STRING

words_100_string = <<~STRING
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In accumsan lectus at augue fringilla vulputate. Praesent ut mi non libero imperdiet tristique. Duis turpis est, interdum id risus in, elementum semper libero. Sed semper mauris at aliquam scelerisque. Cras est lacus, venenatis consequat posuere id, aliquam vel mauris. Quisque eget cursus justo. Vestibulum elementum neque eu elementum scelerisque. Proin venenatis, lorem vel maximus sollicitudin, nulla augue euismod ex, pellentesque vehicula purus justo nec erat. Donec fringilla augue arcu, vel venenatis orci eleifend non. Suspendisse sit amet bibendum arcu. Nulla massa velit, cursus quis lorem vel, feugiat varius nunc. Nam condimentum.
STRING

class TextHelper
  extend ActionView::Helpers::TextHelper
end

class FastTextHelper
  extend ActionView::Helpers::TextHelper

  private

  def self.cut_excerpt_part(part_position, part, separator, options)
    return "", "" unless part

    radius   = options.fetch(:radius, 100)
    omission = options.fetch(:omission, "...")

    if separator != ""
      part = part.split(separator)
      part.delete("")
    end

    affix = part.length > radius ? omission : ""

    part =
      if part_position == :first
        part.last(radius)
      else
        part.first(radius)
      end

    if separator != ""
      part = part.join(separator)
    end

    return affix, part
  end
end

Benchmark.ips do |x|
  x.report("fast 10_000 words") do
    FastTextHelper.excerpt(words_10000_string, 'commodo', radius: 150)
  end

  x.report("slow 10_000 words") do
    TextHelper.excerpt(words_10000_string, 'commodo', radius: 150)
  end

  x.report("fast 100 words") do
    FastTextHelper.excerpt(words_100_string, 'venenatis', radius: 150)
  end

  x.report("slow 100 words") do
    TextHelper.excerpt(words_100_string, 'venenatis', radius: 150)
  end

  x.compare!
end

Results:

Warming up --------------------------------------
   fast 10_000 words     7.130k i/100ms
   slow 10_000 words   186.000  i/100ms
      fast 100 words    16.924k i/100ms
      slow 100 words     1.328k i/100ms
Calculating -------------------------------------
   fast 10_000 words    111.210k (±18.4%) i/s -    527.620k in   5.072596s
   slow 10_000 words      1.880k (± 2.0%) i/s -      9.486k in   5.047067s
      fast 100 words    161.487k (± 6.1%) i/s -    812.352k in   5.047552s
      slow 100 words     13.357k (± 2.3%) i/s -     67.728k in   5.073195s

Comparison:
      fast 100 words:   161487.4 i/s
   fast 10_000 words:   111209.8 i/s - 1.45x  (± 0.00) slower
      slow 100 words:    13357.3 i/s - 12.09x  (± 0.00) slower
   slow 10_000 words:     1880.2 i/s - 85.89x  (± 0.00) slower

@rails-bot rails-bot bot added the actionview label Aug 4, 2020
@tgxworld tgxworld force-pushed the speed_up_text_helper_excerpt branch 2 times, most recently from 63b25ce to ae87891 Compare August 4, 2020 09:06
@@ -467,18 +467,25 @@ def cut_excerpt_part(part_position, part, separator, options)
radius = options.fetch(:radius, 100)
omission = options.fetch(:omission, "...")

part = part.split(separator)
Copy link
Contributor Author

@tgxworld tgxworld Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very expensive when separator is "", we end up generating a giant array of characters for a large string.

@tgxworld tgxworld force-pushed the speed_up_text_helper_excerpt branch from ae87891 to 0460290 Compare August 4, 2020 09:08
Copy link
Member

@eugeneius eugeneius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you ✂️ the benchmark from the commit message? 10,000 words of lorem ipsum is a bit much 😅

else
part.first(radius)
affix = part.length > radius ? omission : ""
part = part.public_send(part_position == :first ? :last : :first, radius)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is complex enough that I prefer the longer version from your benchmark script:

Suggested change
part = part.public_send(part_position == :first ? :last : :first, radius)
part =
if part_position == :first
part.last(radius)
else
part.first(radius)
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to require active_support/core_ext/string/access in order to use String#first and String#last here.

@eugeneius
Copy link
Member

The pull request title and commit message both mention truncate, when the optimisation is actually for excerpt.

@tgxworld tgxworld force-pushed the speed_up_text_helper_excerpt branch from 0460290 to 86a264b Compare December 23, 2020 08:17
@tgxworld tgxworld changed the title Improve perf of ActionView::Helpers::TextHelper#truncate for large strings. Improve perf of ActionView::Helpers::TextHelper#excerpt for large strings. Dec 23, 2020
@tgxworld tgxworld force-pushed the speed_up_text_helper_excerpt branch from 86a264b to a40d3de Compare December 23, 2020 08:20
@tgxworld
Copy link
Contributor Author

@eugeneius Sorry for the late reply as I got caught up with a project at work. I've updated the PR and commit as per your review.

@eugeneius eugeneius merged commit f307197 into rails:master Dec 27, 2020
@eugeneius
Copy link
Member

Thanks @tgxworld!

@tgxworld tgxworld deleted the speed_up_text_helper_excerpt branch March 5, 2021 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants