Slide 1

Slide 1 text

Inspecting and Crafting Rails Akira Matsuda

Slide 2

Slide 2 text

Have You Heard a Rumour That Rails Is Slow?

Slide 3

Slide 3 text

For Example I Recently Saw This Page https://github.com/tbrand/whichisthe_fastest Rails is ranked at the 36th on the speed ranking out of 36 web frameworks

Slide 4

Slide 4 text

Let's See If It's True or Not

Slide 5

Slide 5 text

By Comparing with Something Else That Is Not Slow

Slide 6

Slide 6 text

I Heard Another Rumour About the Fastest Programming Language in the World

Slide 7

Slide 7 text

Of Course I'm Talking About

Slide 8

Slide 8 text

Of Course I'm Talking About

Slide 9

Slide 9 text

Let's Take Benchmarks and See How Slow (or Not Slow) Our Framework Is

Slide 10

Slide 10 text

Hello from Elixir & Phoenix % brew install elixir % mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez % mix phx.new hello % cd hello % mix deps.get

Slide 11

Slide 11 text

The Action # lib/hello_web/controllers/page_controller.ex use HelloWeb, :controller def index(conn, _params) do - render conn, "index.html" + send_resp conn, 200, "" end end

Slide 12

Slide 12 text

Result % curl http://localhost:4000/ => [info] GET / [debug] Processing with HelloWeb.PageController.index/2 Parameters: %{} Pipelines: [:browser] [info] Sent 200 in 109µs

Slide 13

Slide 13 text

OMG! This Is Really Fast! Even in development mode

Slide 14

Slide 14 text

The Log Looks Super Cool I've never seen µs logs in Rails A fast framework needs this, I guess

Slide 15

Slide 15 text

Rails' Turn % rails new rails_hello --skip-bootsnap --skip-spring --skip-listen --skip-puma --skip-active-record --skip- action-cable --skip-active-storage --skip-action-mailer --skip-sprockets --skip-javascript -B % cd rails_hello % git add .

Slide 16

Slide 16 text

Remove Unneeded Gems # Gemfile source 'https://rubygems.org' gem 'rails', '~> 5.2.0'

Slide 17

Slide 17 text

Bundle # So that we can easily patch the framework and the bundled gems % bundle --path vendor/bundle

Slide 18

Slide 18 text

The Action # app/controllers/hello_controller.rb class HelloController < ApplicationController def index render plain: 'Hello, Taipei!' end end # config/routes.rb Rails.application.routes.draw do root 'hello#index' end

Slide 19

Slide 19 text

Run the Server and Show the Hello Page % rails s -e production # We don't want to be bothered by the browser cookies % curl http://localhost:3000/

Slide 20

Slide 20 text

Result Started GET "/" for 127.0.0.1 at 2018-04-27 19:18:34 +0900 Processing by HelloController#index as */* Rendering text template Rendered text template (0.0ms) Completed 200 OK in 1ms (Views: 0.7ms)

Slide 21

Slide 21 text

Not That Bad Maybe we need a little bit more accurate number We want to see µs logs like Phoenix!

Slide 22

Slide 22 text

The Patch (Our First Patch Today!) # vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.0/lib/action_controller/log_subscriber.rb - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round} ms".dup + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{(event.duration * 1000).round}µs".dup

Slide 23

Slide 23 text

Result (µs) Completed 200 OK in 652μs (Views: 0.5ms)

Slide 24

Slide 24 text

The Fact Production mode Rails app is 5x Slower than development mode Phoenix app!

Slide 25

Slide 25 text

Phoenix Is So Impressive Great work, José!

Slide 26

Slide 26 text

How Can We Make Rails As Fast As Phoenix?

Slide 27

Slide 27 text

Let's Find Out Which Part of Rails Takes Time

Slide 28

Slide 28 text

There're Several Approaches to Find Performance Hot Spots Elapsed time Method calls Object allocations Memory usage

Slide 29

Slide 29 text

Measuring Elapsed Time # Just put something like this in your code p Benchmark.realtime { # do something }

Slide 30

Slide 30 text

It's a Quite Common Approach Nothing new here So I'm not going to explain this today

Slide 31

Slide 31 text

Object Allocations I heard that Rails allocates too much objects per each request That would cause memory bloat, GC pressure, etc. and so would largely impact on the total performance How can we measure this?

Slide 32

Slide 32 text

ObjectSpace The easiest way to see object allocations is to investigate
 ObjectSpace.count_objects
 before and after processing the request

Slide 33

Slide 33 text

Using ObjectSpace # config/application.rb or somewhere like that class AllocationTracerMiddleware def initialize(app) @app = app; end def call(env) before = ObjectSpace.count_objects result = @app.call env after = ObjectSpace.count_objects pp after.each_pair {|k, v| after[k] = v - (before[k] || 0)}.select {|_k, v| v.positive?}. sort_by {|_k, v| -v} result ennd config.middleware.insert_before 0, AllocationTracerMiddleware

Slide 34

Slide 34 text

Maybe We'd Better Disable the GC Before Running This? # config.ru run Rails.application +GC.disable

Slide 35

Slide 35 text

The Result [[:T_ARRAY, 197], [:T_STRING, 159], [:T_HASH, 71], [:T_IMEMO, 52], [:T_OBJECT, 49], [:T_DATA, 31], [:T_MATCH, 8], [:T_STRUCT, 5], [:T_REGEXP, 1], [:T_CLASS, 1]]

Slide 36

Slide 36 text

Or For This Purpose, There's a Handy Gem Called allocation_tracer

Slide 37

Slide 37 text

GH/ko1/allocation_tracer

Slide 38

Slide 38 text

Just Bundle allocation_tracer Gem

Slide 39

Slide 39 text

Then Rewrite the Middleware to Be Something Like This class AllocationTracerMiddleware ObjectSpace::AllocationTracer.setup(%i{type}) def initialize(app) @app = app; end def call(env) result = nil allocations = ObjectSpace::AllocationTracer.trace { result = @app.call env } pp allocations.sort_by {|_k, v| -v.first} result end end

Slide 40

Slide 40 text

Result # We got a very similar result from the ObjectSpace one [[[:T_ARRAY], [197, 0, 0, 0, 0, 0]], [[:T_STRING], [164, 0, 0, 0, 0, 0]], [[:T_HASH], [72, 0, 0, 0, 0, 0]], [[:T_IMEMO], [52, 0, 0, 0, 0, 0]], [[:T_OBJECT], [49, 0, 0, 0, 0, 0]], [[:T_DATA], [31, 0, 0, 0, 0, 0]], [[:T_MATCH], [8, 0, 0, 0, 0, 0]], [[:T_STRUCT], [5, 0, 0, 0, 0, 0]], [[:T_CLASS], [1, 0, 0, 0, 0, 0]], [[:T_REGEXP], [1, 0, 0, 0, 0, 0]]]

Slide 41

Slide 41 text

Where in Our Code Do These Allocations Happen?

Slide 42

Slide 42 text

allocation_tracer Can Report Where the Objects Are Allocated class AllocationTracerMiddleware - ObjectSpace::AllocationTracer.setup(%i{type}) + ObjectSpace::AllocationTracer.setup(%i{path type}) def initialize(app) @app = app; end def call(env) result = nil allocations = ObjectSpace::AllocationTracer.trace { result = @app.call env } - pp allocations.sort_by {|_k, v| -v.first} + bundle_path = Rails.root.join('vendor/bundle/ruby/2.6.0/gems/').to_s + pp allocations.each_pair {|k, _v| k.first.sub!(bundle_path, '')}.sort_by {|_k, v| -v.first} result end end

Slide 43

Slide 43 text

Result [[["/Users/a_matsuda/.rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/logger.rb", :T_STRING], [25, 0, 0, 0, 0, 0]], [["actionpack-5.2.0/lib/action_dispatch/http/response.rb", :T_ARRAY], [25, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/inflector/methods.rb", :T_STRING], [15, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/tagged_logging.rb", :T_ARRAY], [15, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/tagged_logging.rb", :T_STRING], [15, 0, 0, 0, 0, 0]], [["rack-2.0.5/lib/rack/utils.rb", :T_STRING], [13, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/hash_with_indifferent_access.rb", :T_HASH], [11, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/hash_with_indifferent_access.rb", :T_ARRAY], [11, 0, 0, 0, 0, 0]], [["actionpack-5.2.0/lib/action_dispatch/http/response.rb", :T_STRING], [11, 0, 0, 0, 0, 0]], [["/Users/a_matsuda/.rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/securerandom.rb", :T_STRING], [10, 0, 0, 0, 0, 0]], [["actionpack-5.2.0/lib/action_dispatch/http/parameter_filter.rb", :T_ARRAY], [10, 0, 0, 0, 0, 0]], [["rack-2.0.5/lib/rack/request.rb", :T_STRING], [10, 0, 0, 0, 0, 0]],

Slide 44

Slide 44 text

active_support/tagged_logging.rb Creates 30 Objects

Slide 45

Slide 45 text

Because of This Code Where tags.size == 1 Is Almost Always 1 def tags_text tags = current_tags if tags.any? tags.collect { |tag| "[#{tag}] " }.join end end

Slide 46

Slide 46 text

This Can Be Very Easily Improved for the Majority Case Where tags.size == 1 def tags_text tags = current_tags - if tags.any? + if tags.one? + "[#{tags[0]}] " + elsif tags.any? tags.collect { |tag| "[#{tag}] " }.join end end

Slide 47

Slide 47 text

And So I Did So Yesterday https://github.com/rails/rails/commit/ ac93e7b5

Slide 48

Slide 48 text

Current Result [[["actionpack-5.2.0/lib/action_dispatch/http/response.rb", :T_ARRAY], [25, 0, 0, 0, 0, 0]], [["/Users/a_matsuda/.rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/logger.rb", :T_STRING], [25, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/inflector/methods.rb", :T_STRING], [15, 0, 0, 0, 0, 0]], [["rack-2.0.5/lib/rack/utils.rb", :T_STRING], [13, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/hash_with_indifferent_access.rb", :T_HASH], [11, 0, 0, 0, 0, 0]], [["actionpack-5.2.0/lib/action_dispatch/http/response.rb", :T_STRING], [11, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/hash_with_indifferent_access.rb", :T_ARRAY], [11, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/tagged_logging.rb", :T_STRING], [10, 0, 0, 0, 0, 0]], [["actionpack-5.2.0/lib/action_dispatch/http/parameter_filter.rb", :T_ARRAY], [10, 0, 0, 0, 0, 0]], [["/Users/a_matsuda/.rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/securerandom.rb", :T_STRING], [10, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/tagged_logging.rb", :T_ARRAY], [10, 0, 0, 0, 0, 0]], [["rack-2.0.5/lib/rack/request.rb", :T_STRING], [10, 0, 0, 0, 0, 0]], [["activesupport-5.2.0/lib/active_support/tagged_logging.rb", :T_IMEMO], [10, 0, 0, 0, 0, 0]],

Slide 49

Slide 49 text

hash_with_indifferent_access.rb A major culprit for Rails' too many object allocations Historically, we needed this because we could not create dynamic Hash instances with Symbol keys Since Ruby did not GC Symbols, if we create Symbols from user-input data, that would cause symbol-fixation vulnerability

Slide 50

Slide 50 text

But Recent Versions of Ruby GCs Dynamic Symbols! Maybe it's time to throw HWIA away, and let all Hashs be simply Symbol-based?

Slide 51

Slide 51 text

Assuming that We Get the env instance as a Symbol-based Hash class EnvSymbolizer def initialize(app) @app = app; end def call(env) env.symbolize_keys! @app.call env ennd config.middleware.insert_before 0, EnvSymbolizer

Slide 52

Slide 52 text

ERROR ActionController::UnknownHttpMethod: , accepted HTTP methods are OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, VERSION-CONTROL, REPORT, CHECKOUT, CHECKIN, UNCHECKOUT, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE-CONTROL, MKACTIVITY, ORDERPATCH, ACL, SEARCH, MKCALENDAR, and PATCH

Slide 53

Slide 53 text

Of Course It Doesn't Work, So Let's Make This Work

Slide 54

Slide 54 text

And So I Tried I needed to patch 89 files in actionpack, actionview, activesupport, rack, webrick, etc. I don't think the patch is completely done, but I could make it return some kind of
 "200 OK" response

Slide 55

Slide 55 text

Result [[[:T_ARRAY], [178, 0, 0, 0, 0, 0]], [[:T_STRING], [162, 0, 0, 0, 0, 0]], [[:T_HASH], [63, 0, 0, 0, 0, 0]], [[:T_IMEMO], [62, 0, 0, 0, 0, 0]], [[:T_OBJECT], [48, 0, 0, 0, 0, 0]], [[:T_DATA], [32, 0, 0, 0, 0, 0]], [[:T_MATCH], [8, 0, 0, 0, 0, 0]], [[:T_STRUCT], [5, 0, 0, 0, 0, 0]], [[:T_CLASS], [1, 0, 0, 0, 0, 0]], [[:T_REGEXP], [1, 0, 0, 0, 0, 0]]]

Slide 56

Slide 56 text

Result Before (Reprise) [[[:T_ARRAY], [197, 0, 0, 0, 0, 0]], [[:T_STRING], [164, 0, 0, 0, 0, 0]], [[:T_HASH], [72, 0, 0, 0, 0, 0]], [[:T_IMEMO], [52, 0, 0, 0, 0, 0]], [[:T_OBJECT], [49, 0, 0, 0, 0, 0]], [[:T_DATA], [31, 0, 0, 0, 0, 0]], [[:T_MATCH], [8, 0, 0, 0, 0, 0]], [[:T_STRUCT], [5, 0, 0, 0, 0, 0]], [[:T_CLASS], [1, 0, 0, 0, 0, 0]], [[:T_REGEXP], [1, 0, 0, 0, 0, 0]]]

Slide 57

Slide 57 text

Result I could reduce some object allocations

Slide 58

Slide 58 text

Speed Completed 200 OK in 663µs (Views: 0.5ms) Completed 200 OK in 559µs (Views: 0.4ms) Completed 200 OK in 619µs (Views: 0.5ms) Completed 200 OK in 567µs (Views: 0.4ms) Completed 200 OK in 597µs (Views: 0.5ms)

Slide 59

Slide 59 text

10%-ish Faster Than Before? Still needs more work

Slide 60

Slide 60 text

Speed (Reprise) Completed 200 OK in 663µs (Views: 0.5ms) Completed 200 OK in 559µs (Views: 0.4ms) Completed 200 OK in 619µs (Views: 0.5ms) Completed 200 OK in 567µs (Views: 0.4ms) Completed 200 OK in 597µs (Views: 0.5ms)

Slide 61

Slide 61 text

You See, Most of The Time Is Spent for Views But what exactly the "view" does when we're not rendering a template?

Slide 62

Slide 62 text

What Is Displayed Here As the "Views:" Runtime? module ActionController module Instrumentation module ClassMethods # A hook which allows other frameworks to log what happened during # controller process action. This method should return an array # with the messages to be added. def log_process_action(payload) #:nodoc: messages, view_runtime = [], payload[:view_runtime] messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime messages ennnnd

Slide 63

Slide 63 text

How Is the view_runtime Value Set? module ActionController module Instrumentation def render(*args) render_output = nil self.view_runtime = cleanup_view_runtime do Benchmark.ms { render_output = super } end render_output ennnd

Slide 64

Slide 64 text

So, the view_runtime Should Be Almost Equivalent to the Time Spent for `render` Method

Slide 65

Slide 65 text

We Can Make This Sure by Adding Our Own Benchmark class HelloController < ApplicationController def index p Benchmark.realtime { render plain: 'Hello, Taipei!' } end end

Slide 66

Slide 66 text

Results # logfile Completed 200 OK in 581µs (Views: 0.4ms) # Benchmark.realtime 0.0004210001789033413

Slide 67

Slide 67 text

What Is Measured When We Don't Explicitly Call `render` in the Contrller?

Slide 68

Slide 68 text

The Framework Calls `default_render` module ActionController module BasicImplicitRender # :nodoc: def send_action(method, *args) super.tap { default_render unless performed? } ennnd

Slide 69

Slide 69 text

Then `default_render` Finds a Template from the Controller Class Name and the Action Name So basically it's a quick template lookup + render

Slide 70

Slide 70 text

Now Let's Go Back to This Code and See What Ruby Is Doing class HelloController < ApplicationController def index render plain: 'Hello, Taipei!' end end

Slide 71

Slide 71 text

Tracing Method Calls with TracePoint def index trace = TracePoint.new(:call) {|t| p "#{t.defined_class}##{t.method_id}"} trace.enable render plain: 'Hello, Taipei!' trace.disable end

Slide 72

Slide 72 text

Result "ActionController::Instrumentation#render" "ActionController::Instrumentation#cleanup_view_runtime" "##ms" "##realtime" "ActionController::Rendering#render" "AbstractController::Rendering#render" "AbstractController::Rendering#_normalize_render" "ActionController::Rendering#_normalize_args" "ActionView::Rendering#_normalize_args" "AbstractController::Rendering#_normalize_args" "ActionController::Rendering#_process_variant" "ActionDispatch::Http::MimeNegotiation#variant" "Object#present?" "ActionController::Rendering#_normalize_options" "ActionController::Rendering#_normalize_text" "ActionView::Layouts#_normalize_options" "ActionView::Rendering#_normalize_options" "AbstractController::Rendering#_normalize_options" "ActionView::ViewPaths#_prefixes" "ActionView::ViewPaths::ClassMethods#_prefixes" "ActionView::Layouts#_include_layout?" "ActionController::Renderers#render_to_body" "ActionController::Renderers#_render_to_body_with_renderer" "ActionController::Base#_renderers" "##_renderers" "Set#each" "ActionController::Rendering#render_to_body" "ActionView::Rendering#render_to_body" "ActionController::Streaming#_process_options" "ActionController::Rendering#_process_options" "AbstractController::Rendering#_process_options" "ActionController::Streaming#_render_template" "ActionView::Rendering#_render_template" "ActionView::Rendering#view_context" "ActionView::Rendering#view_context_class" "ActionView::Rendering::ClassMethods#view_context_class" "ActionView::Rendering#view_renderer" "ActionView::ViewPaths#lookup_context" "ActionView::Renderer#initialize" "AbstractController::Rendering#view_assigns" "ActionController::Base#_protected_ivars" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "Set#include?" "ActionDispatch::Routing::UrlFor#initialize" "ActionView::Base#initialize" "ActiveSupport::InheritableOptions#initialize" "ActionView::Base#assign" "ActionView::Helpers::ControllerHelper#assign_controller" "ActiveSupport::Configurable#config" "ActiveSupport::InheritableOptions#inheritable_copy" "ActiveSupport::InheritableOptions#initialize" "ActionController::FormBuilder#default_form_builder" "##_default_form_builder" "ActionView::Context#_prepare_context" "ActionView::OutputFlow#initialize" "ActionView::Rendering#view_renderer" "ActionView::Renderer#render" "ActionView::Renderer#render_template" "ActionView::AbstractRenderer#initialize" "ActionView::TemplateRenderer#render" "ActionView::AbstractRenderer#extract_details" "ActionView::LookupContext#registered_details" "ActionView::TemplateRenderer#determine_template" "ActionView::Template::Text#initialize" "##[]" "##type_klass" "##[]" "##lookup_by_extension" "ActionView::Template::Text#formats" "Mime::Type#ref" "ActionView::AbstractRenderer#prepend_formats" "ActionView::LookupContext::Accessors#formats" "ActionView::LookupContext#formats=" "ActionView::LookupContext::Accessors#formats=" "Object#present?" "ActionView::LookupContext::DetailsCache#_set_detail" "ActionView::Template::Text#formats" "Mime::Type#ref" "ActionView::TemplateRenderer#render_template" "ActionView::TemplateRenderer#render_with_layout" "ActionView::Template::Text#identifier" "NilClass#try" "ActionView::AbstractRenderer#instrument" "##instrument" "ActiveSupport::Notifications::Fanout#listening?" "ActiveSupport::Notifications::Fanout#listeners_for" "Concurrent::Map#[]" "Concurrent::Collection::NonConcurrentMapBackend#[]" "##instrumenter" "ActiveSupport::PerThreadRegistry#instance" "ActiveSupport::Notifications::InstrumentationRegistry#instrumenter_for" "ActiveSupport::Notifications::Instrumenter#instrument" "ActiveSupport::Notifications::Instrumenter#start" "ActiveSupport::Notifications::Fanout#start" "ActiveSupport::Notifications::Fanout#listeners_for" "Concurrent::Map#[]" "Concurrent::Collection::NonConcurrentMapBackend#[]" "ActiveSupport::Notifications::Fanout::Subscribers::Evented#start" "ActionView::LogSubscriber#start" "ActionView::LogSubscriber#log_rendering_start" "ActiveSupport::LogSubscriber#info" "ActionView::LogSubscriber#logger" "##logger" "ActionView::LogSubscriber#logger" "##logger" "Logger#info" "ActiveSupport::Logger#add" "ActiveSupport::LoggerThreadSafeLevel#level" "ActiveSupport::LoggerThreadSafeLevel#local_level" "ActiveSupport::LoggerThreadSafeLevel#local_log_id" "Concurrent::Map#[]" "Concurrent::Collection::NonConcurrentMapBackend#[]" "Logger#add" "ActionView::LogSubscriber#from_rails_root" "ActionView::LogSubscriber#rails_root" "Logger#format_severity" "Logger#format_message" "ActiveSupport::TaggedLogging::Formatter#call" "ActiveSupport::TaggedLogging::Formatter#tags_text" "ActiveSupport::TaggedLogging::Formatter#current_tags" "Logger::Formatter#call" "Logger::Formatter#format_datetime" "Logger::Formatter#msg2str" "Logger::LogDevice#write" "MonitorMixin#mon_synchronize" "MonitorMixin#mon_enter" "MonitorMixin#mon_exit" "MonitorMixin#mon_check_owner" "ActiveSupport::LogSubscriber#start" "ActionView::LogSubscriber#logger" "##logger" "ActiveSupport::Subscriber#start" "ActiveSupport::Notifications::Event#initialize" "ActiveSupport::Subscriber#event_stack" "ActiveSupport::PerThreadRegistry#instance" "ActiveSupport::SubscriberQueueRegistry#get_queue" "ActiveSupport::Subscriber#event_stack" "ActiveSupport::PerThreadRegistry#instance" "ActiveSupport::SubscriberQueueRegistry#get_queue" "ActionView::Template::Text#render" "ActionView::Template::Text#to_str" "ActiveSupport::Notifications::Instrumenter#finish_with_state" "ActiveSupport::Notifications::Fanout#finish" "ActiveSupport::Notifications::Fanout::Subscribers::Evented#finish" "ActiveSupport::LogSubscriber#finish" "ActionView::LogSubscriber#logger" "##logger" "ActiveSupport::Subscriber#finish" "ActiveSupport::Subscriber#event_stack" "ActiveSupport::PerThreadRegistry#instance" "ActiveSupport::SubscriberQueueRegistry#get_queue" "ActionView::LogSubscriber#render_template" "ActiveSupport::LogSubscriber#info" "ActionView::LogSubscriber#logger" "##logger" "ActionView::LogSubscriber#logger" "##logger" "Logger#info" "ActiveSupport::Logger#add" "ActiveSupport::LoggerThreadSafeLevel#level" "ActiveSupport::LoggerThreadSafeLevel#local_level" "ActiveSupport::LoggerThreadSafeLevel#local_log_id" "Concurrent::Map#[]" "Concurrent::Collection::NonConcurrentMapBackend#[]" "Logger#add" "ActionView::LogSubscriber#from_rails_root" "ActionView::LogSubscriber#rails_root" "ActiveSupport::Notifications::Event#duration" "Time#minus_with_coercion" "Time#minus_with_duration" "##===" "ActiveSupport::NumericWithFormat#to_s" "Logger#format_severity" "Logger#format_message" "ActiveSupport::TaggedLogging::Formatter#call" "ActiveSupport::TaggedLogging::Formatter#tags_text" "ActiveSupport::TaggedLogging::Formatter#current_tags" "Logger::Formatter#call" "Logger::Formatter#format_datetime" "Logger::Formatter#msg2str" "Logger::LogDevice#write" "MonitorMixin#mon_synchronize" "MonitorMixin#mon_enter" "MonitorMixin#mon_exit" "MonitorMixin#mon_check_owner" "ActionView::Rendering#rendered_format" "ActionView::ViewPaths#lookup_context" "##[]" "##type_klass" "##[]" "##lookup_by_extension" "ActionController::Rendering#_set_rendered_content_type" "ActionDispatch::Response#content_type" "ActionDispatch::Response#parsed_content_type_header" "ActionDispatch::Response#get_header" "##[]" "##__getobj__" "ActionDispatch::Response#parse_content_type" "Mime::Type#to_s" "ActionController::Metal#content_type=" "ActionDispatch::Response#content_type=" "ActionDispatch::Response#parse_content_type" "ActionDispatch::Response#parsed_content_type_header" "ActionDispatch::Response#get_header" "##[]" "##__getobj__" "ActionDispatch::Response#parse_content_type" "##default_charset" "ActionDispatch::Response#set_content_type" "ActionDispatch::Response#set_header" "ActionDispatch::Response::Header#[]=" "ActionDispatch::Response#sending?" "MonitorMixin#mon_synchronize" "MonitorMixin#mon_enter" "MonitorMixin#mon_exit" "MonitorMixin#mon_check_owner" "ActionDispatch::Response#sent?" "MonitorMixin#mon_synchronize" "MonitorMixin#mon_enter" "MonitorMixin#mon_exit" "MonitorMixin#mon_check_owner" "##[]=" "##__getobj__" "ActionController::Metal#response_body=" "ActionDispatch::Response#reset_body!" "ActionDispatch::Response#build_buffer" "ActionDispatch::Response::Buffer#initialize" "ActionDispatch::Response#body=" "MonitorMixin#mon_synchronize" "MonitorMixin#mon_enter" "ActionDispatch::Response#munge_body_object" "ActionDispatch::Response#build_buffer" "ActionDispatch::Response::Buffer#initialize" "MonitorMixin#mon_exit" "MonitorMixin#mon_check_owner"

Slide 73

Slide 73 text

Turned Out That a Simple `render :plain` Call Invokes 244 Methods I'm not going to step into this deeper, but there has to be a room for improvements Somebody please improve this!

Slide 74

Slide 74 text

Investigating More Details

Slide 75

Slide 75 text

Investigating More Details Use memory_profiler gem

Slide 76

Slide 76 text

GH/SamSaffron/memory_profiler Shows more detailed report with memory usage, allocated objects by gem, etc.

Slide 77

Slide 77 text

Result Total allocated: 36487 bytes (501 objects) Total retained: 9984 bytes (110 objects) ... allocated memory by location ----------------------------------- 1440 (gem_path)/activesupport-5.2.0/lib/active_support/inflector/methods.rb:72 1248 (ruby_path)/logger.rb:597 1194 (gem_path)/rack-2.0.5/lib/rack/utils.rb:469 1008 (gem_path)/activesupport-5.2.0/lib/active_support/tagged_logging.rb:23 968 (gem_path)/actionpack-5.2.0/lib/action_dispatch/http/response.rb:413 960 (gem_path)/activesupport-5.2.0/lib/active_support/notifications/instrumenter.rb:60 ... Allocated String Report ----------------------------------- 9 "" 6 (ruby_path)/logger.rb:597 2 (gem_path)/actionpack-5.2.0/lib/action_dispatch/http/request.rb:255 1 (gem_path)/actionpack-5.2.0/lib/action_dispatch/journey/router/utils.rb:20 9 "Hello" 9 (gem_path)/activesupport-5.2.0/lib/active_support/inflector/methods.rb:72 6 "/" 2 (gem_path)/rack-2.0.5/lib/rack/request.rb:404 1 (gem_path)/actionpack-5.2.0/lib/action_dispatch/journey/gtg/simulator.rb:26 1 (gem_path)/actionpack-5.2.0/lib/action_dispatch/journey/path/pattern.rb:158 1 (gem_path)/actionpack-5.2.0/lib/action_dispatch/journey/router/utils.rb:22

Slide 78

Slide 78 text

Why Don't We Try a Little Bit More Real-world-ish Use Case?

Slide 79

Slide 79 text

Same "Hello, Taipei!" Using Action View # app/controllers/hello_controller.rb class HelloController < ApplicationController def index - render plain: 'Hello, Taipei!' end end # app/views/hello/index.html.erb (new) +Hello, Taipei!

Slide 80

Slide 80 text

Result Total allocated: 68099 bytes (889 objects) Total retained: 14543 bytes (139 objects)

Slide 81

Slide 81 text

Very Interesting Part in the Report Allocated String Report ----------------------------------- ... 5 "/stylesheets/application.css" 1 (gem_path)/actionview-5.2.0/lib/action_view/helpers/asset_url_helper.rb:213 1 (gem_path)/actionview-5.2.0/lib/action_view/helpers/asset_url_helper.rb:264 1 (gem_path)/actionview-5.2.0/lib/action_view/helpers/tag_helper.rb:93 1 (gem_path)/activesupport-5.2.0/lib/active_support/core_ext/string/output_safety.rb:39 1 (gem_path)/activesupport-5.2.0/lib/active_support/multibyte/unicode.rb:229 ... 3 "" 1 (gem_path)/actionview-5.2.0/lib/action_view/helpers/asset_tag_helper.rb:143 1 (gem_path)/actionview-5.2.0/lib/action_view/helpers/tag_helper.rb:239 1 (gem_path)/activesupport-5.2.0/lib/active_support/core_ext/string/output_safety.rb:174

Slide 82

Slide 82 text

Why Does Action View Duplicates the Same Strings So Many Times? Let's see actionview-5.2.0/lib/action_view/ helpers/tag_helper.rb:93 as an example

Slide 83

Slide 83 text

The Code def tag_option(key, value, escape) if value.is_a?(Array) value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) else value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s end %(#{key}="#{value.gsub('"'.freeze, '"'.freeze)}") # the line 93 end

Slide 84

Slide 84 text

Do You See What's Wrong with the Line 93?

Slide 85

Slide 85 text

The Answer String#gsub here duplicates the String instance, so we'd better use gsub! here We usually don't have to care about such a small difference, but this method is called against all tags' all attributes, so this gsub would create so many String objects

Slide 86

Slide 86 text

The Patch def tag_option(key, value, escape) if value.is_a?(Array) value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) else - value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s + value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup end - %(#{key}="#{value.gsub('"'.freeze, '"'.freeze)}") + value.gsub!('"'.freeze, '"'.freeze) + %(#{key}="#{value}") end

Slide 87

Slide 87 text

The Patch https://github.com/rails/rails/commit/ 9276ea89d2b0be9fdd1ad6590857f8d45a38 c267 https://github.com/rails/rails/commit/ 7d2400ab61c8e3ed95e14d03ba3844e8ba2e 36e4

Slide 88

Slide 88 text

Result Total allocated: 67740 bytes (878 objects) Total retained: 14543 bytes (139 objects)

Slide 89

Slide 89 text

Another String Duplication # action_view/helpers/asset_tag_helper.rb def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys early_hints_links = [] sources_tags = sources.uniq.map { |source| href = path_to_stylesheet(source, path_options) early_hints_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", "href" => href }.merge!(options) tag(:link, tag_options) }.join("\n").html_safe request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request sources_tags end

Slide 90

Slide 90 text

What's Happening Here tag() Returns an HTML safe String join("\n") makes the String unsafe even if the collection has only one element ['a'.html_safe].join("\n").html_safe? #=> false Then we need to call html_safe against it again

Slide 91

Slide 91 text

But This Method Won't Be That Serious Performance Hot Spot Because it's just a public helper method

Slide 92

Slide 92 text

Possible Patch path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys early_hints_links = [] - sources_tags = sources.uniq.map { |source| + sources_tags = sources.uniq.each_with_object("".html_safe) { |source, result| href = path_to_stylesheet(source, path_options) early_hints_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", "href" => href }.merge!(options) - tag(:link, tag_options) - }.join("\n").html_safe + result << "\n" unless result.empty? + result << tag(:link, tag_options) + }

Slide 93

Slide 93 text

But I Didn't Commit This Because I Couldn't See Any Significant Performance Change

Slide 94

Slide 94 text

I'm Rather Interested in active_support/ core_ext/string/output_safety.rb So many String duplications are happening in this file When creating a new SafeBuffer Or via ActiveSupport::Multibyte::Unicode.tidy_bytes

Slide 95

Slide 95 text

Maybe We Can Create tidy_bytes! Method Using String#scrub! But I'm not sure how to deal with the Rubinius implementation

Slide 96

Slide 96 text

Or Maybe It's Time to Think About Totally Newly Designed SafeBuffer?

Slide 97

Slide 97 text

(Time for Showing More Hacks If Time Allows) Tuning the template more A JSON API for the next benchmark Dealing with the database, and making it faster And so on...

Slide 98

Slide 98 text

So, I Hope You Got Some Ideas for Your Weekend Hack

Slide 99

Slide 99 text

And I Hope You Got Some Tools in Your Hands Now

Slide 100

Slide 100 text

There're So Many Problems and Rooms for Improvement in Rails

Slide 101

Slide 101 text

(We Need to Get Rid of the Reputation of "the Slowest Framework"...)

Slide 102

Slide 102 text

Ruby Is Getting Faster and Faster, So Let's Make Rails Faster Too!

Slide 103

Slide 103 text

Go Ahead and Do Your Hack!

Slide 104

Slide 104 text

end

Slide 105

Slide 105 text

end Name: Akira Matsuda GitHub: @amatsuda Twitter: @a_matsuda