runtimeanalyz.ing ● early access · sign-up open

← All capabilities

whats_looping

CLI · MCP free

N+1 and hot loops. The same call site hit many times in one run, read straight off the call trees. A query in a loop, or the same question asked over and over.

Per element

Report report.rb:3
def render(curriculum)
  curriculum.books.map do |book|
    "#{book.title} by #{book.author_name}"
  end
end
author_name
212× in one tree
212 Books 212 distinct receivers

Same object, same question

InvoiceLine × 180 invoice_line.rb:2
def total
  subtotal * (1 + invoice.tax_rate)
end
tax_rate
180× in one tree
Invoice 1 receiver
Fix many receiverspreload · one receivermemoise
One call site, hit hundreds of times inside a single call tree. Counting the distinct objects that received the call tells a loop over a collection apart from one object asked the same thing again.

The problem

The curriculum report takes four seconds to render. Nothing in it looks slow:

class Report
  def render(curriculum)
    curriculum.books.map { |book| "#{book.title} by #{book.author_name}" }
  end
end

class Book < ApplicationRecord
  belongs_to :author

  def author_name = author.name
end

author_name loads the author, one query per book, and a curriculum has two hundred books. It’s the classic N+1, and it hides well: each method is one line, the loop is in one file and the query is in another. Profilers show the time in the database, not the loop that caused it.

Hot loops hide the same way without any database at all:

class InvoiceLine
  def total = subtotal * (1 + invoice.tax_rate)
end

class Invoice
  def tax_rate = Finance::Accounts.rate_for(region, issued_on)
end

Every line on an invoice asks the same invoice the same question, and each time it’s worked out again.

The fix

Ask what’s looping:

$ ra whats_looping

app/models/report.rb:3  Book#author_name
  212× in one call tree (Report#render) · 212 distinct receivers
  median 184× per tree · 1,044 production runs
  → per-element work: preload or batch it

app/models/invoice_line.rb:2  Invoice#tax_rate
  180× in one call tree (Invoice#pdf) · 1 distinct receiver
  median 40× per tree · 6,310 production runs
  → the same object asked again: memoise it

The distinct receivers tell the two apart. Two hundred different books means per-element work, so load the authors in one go:

def render(curriculum)
  curriculum.books.includes(:author).map { |book| "#{book.title} by #{book.author_name}" }
end

One invoice asked a hundred and eighty times means the answer can be kept:

def tax_rate = @tax_rate ||= Finance::Accounts.rate_for(region, issued_on)

Run it again after the change: author_name is still called 212 times, and should be, but the report no longer takes four seconds. tax_rate drops off the list.

How it works

  1. Call trees. Every entry point, a spec example or a request, records one call tree, and every call in it names its call site.
  2. Count per tree. Calls are grouped by tree, call site and method. A site hit hundreds of times in a single tree is a loop.
  3. Count receivers. The recording keeps each receiver’s object identity, which within one tree is unique. Many receivers means the loop walks a collection; one receiver means the same object is asked the same thing repeatedly.
  4. Rank. Across every recorded tree, sites are ranked by how many times they repeat and how often their tree runs. Record production and the ranking follows real traffic.

Limits

  • It sees your methods, not your SQL. The loop shows up as 212 calls to Book#author_name. The query is what you find when you open it. A loop entirely inside a gem or core method, with no call to your own code in it, isn’t seen.
  • A loop isn’t always a problem. Some work has to happen per element. The report is a list of leads, ranked, not a list of bugs.
  • Specs make poor evidence here. Test data is small, so a spec with three books won’t look like a loop. Recorded production runs, or a realistic seed, find far more.

where_time_goes

CLI · MCP free

Where the time goes. The most-travelled call chains, ranked by how often they run, so caching and optimisation land where the traffic actually is.

See more →

how_it_got_here

CLI · MCP free

How execution got here. The real call chains that reach a method, in order, taken from recorded runs. The stack trace you'd have got if it had raised, on demand, for every route in.

See more →

what_calls

CLI · LSP · MCP free

Who can call this method. Every caller of a method from real runs, with where and how often. The calls that actually happened, not grep. what_it_calls does the reverse.

See more →