runtimeanalyz.ing

← All capabilities

break_cycle

CLI · MCP Pro

Cut a dependency cycle. The files or namespaces that form a dependency cycle at runtime. Each cycle is marked with its lowest-traffic edge, which is the cheapest one to cut.

Reporting::Report reporting
progress
3,418 calls
report_row
12 calls
Enrollment enrollment
Enrollment enrollment
def report_row = Reporting::Row
  .new(student.name, progress)
Row.new ( name, String progress Float )
12 calls · cut here
Reporting::Row reporting
Reporting reads enrollments thousands of times, and that is the intended direction of the dependency. One method with one caller calls in the opposite direction, which makes the cycle. Removing that one call removes the cycle.

The problem

The acyclic dependencies principle says that dependencies between parts of an app should run one way. Once two namespaces depend on each other they behave as one namespace, and you can’t load, test or extract either without the other.

In the example app Reporting reads from Enrollment. A later change needed a report row for an enrollment, and the method that builds it was put on Enrollment.

app/reporting/reporting/report.rb:7:

def rows
  enrollments.map(&:report_row)
end

app/models/enrollment.rb:51:

def report_row
  Reporting::Row.new(student.name, progress)
end

The dependency now runs Reporting → enrollment → Reporting. Ruby raises no error, because constants resolve when the line runs and both files load. A static tool can miss the cycle, because the edge that closes it can be a send, a callback or a constant looked up by name, and none of those appear in the source as a reference.

The fix

ra break_cycle lists the cycles and the edge to cut in each. The Reporting cycle shown above is the second one in this report:

$ ra break_cycle

break_cycle
· 1 cycle at the namespaces level (a namespace is a class's leading module, or for a top-level class the file's first path segment after app/<layer>/)
tangle · 12 members and 24 edges in one component: Billing, Finance, Reporting, book, curriculum, email_notifier, enrollment, invoice, invoice_line, invoice_mailer, progress, student
the shortest cycles inside it, cheapest to cut first:
  … the Billing ⇄ invoice ⇄ invoice_line cycle
⚠ cycle · Reporting → enrollment → Reporting
  Enrollment#report_row → Reporting::Row#initialize   3 calls
  Reporting::Report#rows → Enrollment#report_row   2 calls   ← cut
  rows is only reached from an entry point
  → cut Reporting::Report#rows → Enrollment#report_row, the cheapest edge closing the cycle   ADP
  … 4 more cycles in the tangle

The tangle line says the twelve namespaces can all reach each other, so they form one component. The report then lists the shortest cycles inside it, cheapest to cut first. The Reporting cycle is two edges long, and removing either edge breaks it. The tool marks the edge with fewer calls, Report#rows calling report_row. Whichever edge you remove, the fix is to build the row on the side that already depends on the other, and the example app has that method in app/reporting/reporting/row.rb:3:

module Reporting
  class Report
    def rows = enrollments.map { |enrollment| Row.for(enrollment) }
  end

  class Row
    def self.for(enrollment) = new(enrollment.student.name, enrollment.progress)
  end
end

Once you delete Enrollment#report_row, Enrollment no longer refers to Reporting.

How it works

  1. Every recorded call is an edge from one method to another, and the recording holds each method’s file and namespace. The tool groups the calls at the level you ask for, files or namespaces, which gives a directed graph with a recorded call count on every edge.
  2. Any group of namespaces that can each reach the others is a strongly connected component. Every record says which side made the call, so the direction of each edge is known. Within a component the report lists the shortest cycles first.
  3. Within a cycle, each edge is weighed by its traffic and by how many distinct callers it takes to remove. The cheapest edge to cut is marked, along with the methods that make the call.

A namespace is a class’s leading module. A top-level class has none, so the tool uses the file’s first path segment after app/<layer>/, which makes each top-level model its own namespace. That is why the example app’s component has twelve members: book, curriculum, enrollment and student are models in one app/models directory that call each other often, and each counts as a namespace. --level files gives the same report by file.

Run it on your app

The output on this page comes from our example Rails app, and it is regenerated whenever a tool changes. These are the commands behind it. On your own app, run ra record first, then put your own class, method or file in their place.

ra break_cycle
ra break_cycle --level files

The cycle is app/reporting/reporting/report.rb:7 and app/models/enrollment.rb:51.

Reading the result

  • A cycle is built from recorded calls. A constant that is referenced but has no method called on it, such as a class named in a rescue clause, produces no recorded call, so it does not close a cycle in this report.
  • The marked edge has the least traffic, which makes it the least work to cut. The better design is sometimes to reverse a busier edge, and the call counts do not tell you when that is the case.
  • Each top-level class is its own namespace, so unnamespaced models in one directory are reported as cycles between files. If those cycles are noise, nest the models in modules or read the file-level report.

Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.

stabilize_dep

CLI · MCP Pro

Dependencies that run against stability. A stable namespace, one with many callers and few dependencies, calling into a less stable one. Instability is computed from recorded calls, and the report lists the call sites to move or to put behind an abstraction.

See more →

relocate_file

CLI · MCP Pro

Which file should move where. Two files that call each other often and are directories apart. The report names the folder they belong in, weighted by recorded call traffic and the distance between them.

See more →

check_diff

CLI · CI · MCP Pro

Check the methods a diff changed. Given a base ref, it reports the findings the changed methods introduced: a stub that no longer matches the real method, a dependency cycle, feature envy.

See more →