Performance pass
Measure, then fix the cause. Don't micro-optimize what isn't hot. The big wins in a Phoenix/Ecto app are almost always N+1 queries, unbounded result sets, and missing indexes — in that order.
The usual suspects (emisar-specific)
- N+1 queries. An association loaded per-row in a loop. Symptom: the same
SELECTrepeated in the log.- Fix: declare the association in the Query module's
preloads/0and pass:preloadtoRepo.fetch/list(IL-10) — neverRepo.preloadinside anEnum.map. Detect:rg -n 'Repo\.preload|\.\w+\b' ...for context calls or preloads insideEnum./comprehensions.
- Fix: declare the association in the Query module's
- Unbounded lists. Loading every row, or assigning a big list to the socket.
- Lists that can grow (runs, audit events, runners): page with
Repo.list/3(keyset viacursor_fields), and render withstream/3in LiveView, neverassign(socket, :items, all)(IL-18).
- Lists that can grow (runs, audit events, runners): page with
- Missing indexes. Every
Query.by_*filter, every FK, and thenot_deletedsoft-delete predicate should be backed by an index. Cross-check the Query helpers against the migration; a partial indexwhere deleted_at is nullmatchesnot_deleted/1. - Slow query / wrong plan. Confirm with
Repo.explain(:all, query)iniex -S mix(verify the call exists/shape first) — look forSeq Scanon a large table where an index should apply. - Over-preloading. The opposite problem: loading associations a screen doesn't use. Preload only what the caller renders.
How to run it
cd portal
# repeated identical SELECTs in dev = N+1 (queries log by default in dev)
# profile a specific read in iex:
echo 'Emisar.Repo.explain(:all, Emisar.Runs.ActionRun.Query.all())' | iex -S mix
Read the LiveView's mount/handle_* for list assigns; read the context read path
for preload shape; read the migration for indexes.
Output
Findings ordered by impact: issue · where · cost · fix. Fix the unambiguous ones
(add a preload to preloads/0, switch an assign to a stream, add a missing index in
a NEW migration when the original already ran — IL-11). Leave a judgment call (is
this list big enough to page?) as a flagged question, not a silent change. Re-measure
after fixing.