runtimeanalyz.ing

← Runtime Analysis

Configuring what gets recorded

By default the recorder wraps the methods defined under app and lib. It can record any directory you name, including your gems, which are off by default because of what recording them costs.

“Every object in your application” on the home page means every object whose class is defined in your own code, which by default is the code under app and lib. Every setting that widens or narrows that is on this page.

The default

Every setting, at the value it has when you leave it out:

RuntimeAnalysis.configure do |c|
  c.included       = %w[app lib]              # directories whose methods are recorded
  c.excluded       = []                       # directories left out
  c.excluded_files = []                       # single files left out
  c.include_gems                              # none; name gems to record your calls into them, one call deep
  c.class_builders = [[Data, :define], [Struct, :new]] # methods that build a class whose fields are recorded
  c.tier           = :calls                   # :calls who called what · :classes adds argument classes · :objects adds object ids and returns · :raises adds exceptions
  c.adaptive       = false                    # true records a path in full the first time, then samples it
  c.mechanism      = :native                  # the C wrapper in front of each method · :ruby one written in Ruby, for a JIT
  c.recording_dir  = "./tmp/runtime_analysis" # where the stream files and run.json are written
  c.rotate         = nil                      # or { max_bytes: "64MB", max_age: 3600, max_trees: 10_000 }
  c.retain         = nil                      # or "1GB": a cap on the bytes kept across files
  c.environment    = nil                      # Rails.env under Rails; written to run.json
  c.label          = nil                      # a name for the run, such as "suite"; written to run.json
end
RuntimeAnalysis.start!                        # before your application code loads

A method is recorded when the file that defines it is under an included directory. With the default, that is every method in app and lib, whoever calls it. A call into your code is recorded even when a gem made it. A call from your code into a gem is not recorded, because the gem’s method has no wrapper.

The recorder wraps methods as they are defined. Methods that already exist when it starts stay unwrapped, so start it before your application code loads.

Narrowing it

Leave out a directory or a single file:

RuntimeAnalysis.configure do |c|
  c.excluded = %w[app/views lib/generated]
  c.excluded_files = %w[app/models/legacy_importer.rb]
end

Recording your gems too

included takes any directory, so a gem’s install path works the same way as app:

RuntimeAnalysis.configure do |c|
  c.included = ["app", "lib", Gem.loaded_specs["devise"].full_gem_path]
end

Pass Bundler.bundle_path.to_s and every gem in your bundle is included.

Recording gems has these conditions:

  • The recorder has to start before the gem loads. In a Rails app that means before Bundler.require in config/application.rb, and before require "rails/all" if Rails itself is to be recorded, which puts it in config/boot.rb. An initializer runs after the gems have loaded, which is too late for them.
  • Started that early, the recorder is loaded before Rails, so its Railtie does not register and requests are not named as entry points. Spec and test examples still are.
  • Methods written in C have no source file and are not wrapped. String#upcase and Array#map stay unrecorded whatever you include. Sampled recording of core methods is planned and not built.

Why gems are off by default

Recording is limited to your own code by default because of what recording gems costs.

The recorder adds its cost to every call of every wrapped method. A request passes through Rack, Rails and your other gems before and after it reaches your code, so including them wraps many more methods and records many more calls for the same request. On Campfire’s test suite, recording app and lib cost too little to measure. Recording every gem in the bundle as well added 111 percent to CPU time at tier 1 and 172 percent at tier 4, and wrote about 440 times as many rows. The performance page has the tables.

The tools answer questions about your code: who calls this method, what it returns, which class answered here. Rows from inside a gem are not used for those answers, and they make the recording larger.

If you need one gem’s internals, include that gem alone, turn on adaptive mode, and read what recording costs first.

Recording your calls into a gem

include_gems records the calls your own code makes into a gem, which shows what your code got back from it: that name.camelize returned a String, that occurrence.id was read here.

RuntimeAnalysis.configure do |c|
  c.include_gems "activesupport", "values"       # one call deep
  c.include_gems "sidekiq", depth: 2             # and the call that one makes
end

At depth 1 a gem method is recorded when a line of your own code called it. What the gem does inside itself is not recorded, and neither is a call that reaches the gem through another gem. When the gem calls back into your code, your method is recorded as usual and the depth count starts again.

A gem method gets a wrapper when your source names it, and the rest of the gem stays unwrapped. The recorder reads the names out of your included directories when it starts, which took about 25 milliseconds on the two apps we measured.

A method Ruby defines in C has no file and belongs to no gem, so include_gems does not record String#upcase. The readers of a Data or a Struct are recorded through class_builders, described next.

Fields of a Data, a Struct, or a class a gem builds for you

Occurrence = Data.define(:id, :finish_position)

Ruby writes the readers of a Data and a Struct in C. They have no source file, so the recorder cannot tell from a path that they belong to your code. It watches Data.define and Struct.new instead. When a line of your own code calls one, the fields named on that line are recorded like any method you wrote, and that line is recorded as where they are defined. A Struct’s writers are recorded as well.

The other methods the builder puts on the class, such as ==, to_h and inspect, are not recorded. A method you write in the builder’s block is recorded at the line where you wrote it.

A gem that builds classes the same way can be added:

RuntimeAnalysis.configure do |c|
  c.class_builders << [Value, :new]              # the values gem
end

Each pair is a class or module and the name of a method on it. The fields are the Symbols and Strings passed to that method which the class it returns then answers to. A class the builder makes for a gem’s own code is not recorded, so the Struct.new calls that gems make all the time add nothing to the recording.

A class built before recording starts is not seen, so start recording before your code loads.

The other settings

SettingDefaultWhat it does
includedapp, libDirectories whose methods are recorded
excludednoneDirectories left out
excluded_filesnoneSingle files left out
include_gemsnoneGems to record your calls into, with how many calls deep
class_buildersData and StructMethods that build a class whose fields are recorded
tier:callsHow much is captured per call: :calls, :classes, :objects or :raises, or 1 to 4
adaptiveoffRecord a path in full the first time, then sample it
recording_dir./tmp/runtime_analysisWhere the recording files are written
rotatenoneStart a new file by size, age or number of call trees
retainnoneCap on the total bytes kept
environmentRails.env under RailsWritten to the run manifest beside the recording
labelnoneA name for the run, such as suite, written to the manifest

What is written to disk lists what each tier adds.

A tool answers from what the recording’s tier wrote. Tier 1 records who called what, which is enough for what_calls and who_actually_answered. The classes what_it_receives reports need tier 2. Anything that follows a value from where it was returned to the calls made on it needs the object ids of tier 3: verify_mock, observed_shape and how_to_reach among them. The errors that where_nil_comes_from links to a nil need tier 4.

ra record records at tier 4, so every tool has what it needs. A configure block you write yourself defaults to tier 1, so set c.tier when you add the gem to a project.

A fuller example:

RuntimeAnalysis.configure do |c|
  c.tier = 3
  c.adaptive = true
  c.rotate = { max_bytes: "64MB", max_age: 3600 }
  c.retain = "1GB"
  c.label = "suite"
end

With no rotate set, a file grows for as long as the process runs.