Performance

Find the code that slows down as your data grows.

Repowise reads your code for shapes that get slower as data grows, such as a query inside a loop or a blocking call in async code. It follows the call graph across files and ranks code that runs per request first.

Runs on your source during every index. Nothing to deploy.

At a glance

22
performance detectors
Loops, async code, repeated setup, data structures and SQL.
3 hops
across the call graph
The loop and the I/O call can sit in different functions and files.
4
opportunity states
Plan ready, advisory, investigate or expected, so each finding says what happens next.
Request
paths ranked first
Then scheduled jobs, then code with no role evidence.

Sound familiar?

Fast in staging. Slow for your biggest customer.

  • 01

    The query and the loop that calls it are in different files, so review never sees both.

  • 02

    The profiler shows a hot function, not the loop three calls up that drives it.

  • 03

    Two hundred warnings, and nobody knows which ones run on every request.

  • 04

    The endpoint was fine until the table passed a million rows.

What makes it different

What it finds, with the line to change.

  1. N+1 and I/O in loops

    A database, network, file or subprocess call per iteration.

    Including nested loops with I/O, and any Spring Data or MyBatis call inside a Java loop.

  2. ORM lazy loads

    Relations loaded one row at a time.

    Django and SQLAlchemy lazy loads inside a loop, where the loop's rows resolve to one model.

  3. Concurrency

    Blocking calls where they stall everything.

    Blocking I/O in async code, serial awaits in a loop, I/O under a lock, goroutines started in an unbounded loop.

  4. Repeated setup

    Work that belongs outside the loop.

    Regex compiles, JSON parses, client construction and Go defers on every pass.

  5. Quadratic data work

    Loops that cost the square of the input.

    Nested loops over the same data, list membership tests, inserts at the front and repeated string building.

  6. Over-fetch

    Reading everything to keep a little.

    Unbounded reads reduced in memory, and SQL joins with no join condition.

In depth

From a slow shape to the first edit.

Across files

The loop and the query can be three calls apart.

A database call inside a loop is easy to spot when both sit in one function. Usually they do not: the handler loops, a service method enriches each row, and a repository function runs the query. repowise follows the resolved call graph up to three hops, attaches the caller-to-sink path, and says whether the loop grows with the data or is bounded.

What the loop walks
Rows from a query, a caller's collection, a fixed set.
Grows or bounded
Read for Python, TypeScript, JavaScript, Go, Java, C#, Ruby, Rust, Kotlin, C++ and Pascal.
Batchable or not
A loop that feeds each result into the next call gets no batch advice.
One shared fix
When every site reaches one sink, the plan adds the bulk call first.
performance · n+1 · traced across files
  1. list_orders routes/orders.py:31

    GET /orders · per request

  2. enrich_orders services/orders.py:88

    for order in rows (grows with the data)

  3. get_customer db/customers.py:14

    SELECT ... WHERE id = ? (database)

State
plan ready · one shared fix for 2 call sites
First step
Add get_customers(ids) and call it once before the loop

enrich_orders · services/orders.py:88

One query per order, on every request. Kept apart from the defect score.

Illustrative finding, traced through the call graph

On a real repository

Two queries per id, inside one loop, in eladmin.

On eladmin, an open-source Spring Boot admin system, repowise reports an N+1 in S3StorageServiceImpl.deleteAll. The method loops over the ids it is given and, for each one, calls s3StorageRepository.selectFilePathById and later s3StorageRepository.deleteById, both Spring Data repository calls, so deleting 500 files runs 1,000 queries. The finding names the loop line, both sink calls and the fact that the loop walks a parameter. The batch form loads every path in one query and deletes in one statement.

Exact location
Method at line 77, loop at line 86.
Both sinks
Two repository calls, grouped into one opportunity.
What it walks
A parameter: the list of ids the caller passes.
Opportunity id
Opened by an agent with get_health(opportunity_id=...).
performance finding, eladmin

io_in_loop

boundary dbseverity medium
for (Long id : ids) {    String filePath = s3StorageRepository.selectFilePathById(id);    ...    s3StorageRepository.deleteById(id);}
Method
S3StorageServiceImpl.deleteAll line 77
Loop
line 86, iterates a parameter
Sinks
selectFilePathById, deleteById
Opportunity
one id groups both sink calls

Reason, as reported

a database call runs once per loop iteration (N+1 query)

Real finding, repowise 0.59 on elunez/eladmin, from repowise health --format json

Ranking

What runs per request is fixed first.

Each opportunity records what runs it: a request handler, an event consumer, a scheduled job, startup, a CLI command, UI or test code. Loops on a request path rank above the rest, then scheduled jobs. Code that runs once per deploy, a loop with a fixed size and a function switched off by a constant flag are counted by reason and kept out of the default queue.

Request first
A handler that loops over rows on every call leads.
Cold paths counted
Migrations and startup are listed, not ranked.
Proof of growth
A loop whose size nothing measured never leads the queue.
In Fix first
Plan-ready causes compete with refactoring on value.
repowise health
$ repowise healthFix first (3 of 41 eligible items) 1. Extract the retry branch from parse_config   now · effort M · +1.4    src/config/loader.py:212    CCN 47 and nesting 6 in a file changed 19 times this quarter.    verify: pytest tests/unit/test_loader.py 2. Batch the customer lookup in enrich_orders   now · effort S    services/orders.py:88 · N+1, runs per request 3. Split OrderService into 3 classes            next · effort L · +0.9    services/orders.py · LCOM4 3, 41 methods Code health 7.4/10 [Good] · Hotspot 6.1 · Worst 2.3 (src/config/loader.py)
Illustrative repository, repowise health

In plain words

A finding means this shape gets slower as the input grows.

An N+1 query is one query to load a list, then one more query for every item in it: fine with ten rows, slow with ten thousand. Repowise finds that shape and its relatives in the parse tree and the resolved call graph, so a finding says the work grows with the data, which a test on small fixtures never shows. When it cannot tell whether a loop grows, or which library a call belongs to, it does not report it.

How to read it

What state an opportunity is in

Plan ready
A fix strategy whose transformation is proven safe, with steps for each call site.
Advisory
A strategy with a prerequisite to confirm first, such as bounded concurrency before parallelizing awaits.
Investigate
The finding is real and the fix is a design choice; a person picks it.
Expected
The repetition is real and fine: a bounded loop, a cold path, or a function switched off by a flag.

How it works

From source to a ranked opportunity.

Each step uses data the index already has, so performance analysis runs with every index and update.
  1. 01 / Parse

    Find loops and what they walk

    Rows from a query, a caller's collection, a fixed set, a directory listing.
  2. 02 / Classify

    Name the I/O by its library

    A call is database, network, file system or subprocess by where its receiver was imported from.
  3. 03 / Trace

    Follow calls up to three hops

    The loop and the sink can sit in different files; the finding carries the path.
  4. 04 / Rank

    Group causes, request paths first

    Findings that share one fix become one opportunity, ordered by what runs them.

Questions

What people ask before they try it.

Can static analysis find N+1 queries?

Yes, when the loop and the query can be connected in the code. repowise recognises a database, network, file system or subprocess call by the library its receiver was imported from, finds it inside a loop, and follows the resolved call graph up to three hops, so the loop in a service and the query in a repository class are reported together with the path between them. In Java, any call on a Spring Data repository or a MyBatis mapper inside a for loop counts as a database call, whatever the method is named.

Which languages does performance analysis support?

Python, TypeScript and JavaScript (including Vue and Svelte scripts), Java, Go, C# (including Razor), Rust, Kotlin, Scala, Ruby, C++, Dart, Pascal and Apex. Detectors differ by language where the language makes a shape cheap: TypeScript and JavaScript never get the string-concatenation finding, because JavaScript engines make repeated += linear. Request, event, job and CLI roles are read for Python and TypeScript or JavaScript.

Does it replace a profiler or APM?

It covers the step before them. A profiler measures what was slow on the traffic you had; repowise reads the code and finds the shapes that get slower as data grows, on a branch, in CI or on a repository you have never run. A finding names the file, the loop, what the loop walks and the first edit. Keep your profiler for timings on production traffic.

Does a performance finding lower my code health score?

No. Performance has its own score beside defect risk and maintainability, and its 22 detectors move only that one. Performance work still appears in the fix-first queue, where it competes with refactoring on value.

How does it avoid false positives?

It is built for precision. A loop whose size the code does not show is never treated as growing; a statement that runs a fixed number of times, such as schema setup, is not an N+1; an await covered by an ESLint no-await-in-loop suppression is not reported; a call through a library repowise does not model is not classified as I/O. Opportunities whose repetition is fine are marked expected, with the reason: a bounded loop, a cold path, or a function switched off by a constant flag.

How does my agent get performance findings?

get_health(include=["performance"]) returns the performance summary and the open opportunities; get_health(opportunity_id="perf...") opens one with its steps, call sites, the caller-to-sink path and the tests to run. Filters narrow by context (production by default), confidence, actionability and sort order, so an agent can ask for the causes worth fixing now and nothing else.

Last reviewed: October 2026

Get started

Find the loops that will hurt at scale.

Index a public repository free and open the Performance tab of its code health page. For a team rollout across private repositories, book a demo.