
Performance Review
- 68 installs
- 325 repo stars
- Updated August 2, 2026
- athola/claude-night-market
Enrich the Tier-1 performance-review skill with optional Gauntlet tree-sitter and graph analysis when the plugin is installed.
About
This Gauntlet integration skill extends the parent performance-review capability in Claude Night Market for builders who want deeper static analysis without breaking environments that lack Gauntlet. At load time it probes for tree-sitter parsing and graph storage modules, storing sentinels so Tier-2 and Tier-3 helpers return no extra findings when dependencies are missing—mirroring proven optional-plugin patterns elsewhere in the repo. Tier 1 remains a complete performance review path; optional tiers add structure-aware and graph-scoped signals useful before merge or when investigating regressions. Solo maintainers shipping backend or API services benefit when they already run Gauntlet in CI or locally and want one skill surface for review depth. It is integration documentation and behavior for `performance_review.py`, not a standalone review from scratch. Pair it with your normal ship checklist and treat Gauntlet as an accelerator, not a hard gate unless you enforce it in CI.
- Documents Tier-2 tree-sitter parsing via optional `gauntlet.treesitter_parser` import
- Documents Tier-3 graph-backed findings via optional `gauntlet.graph.GraphStore`
- Dual try-import sentinels (`ImportError`, `ModuleNotFoundError`) with early-return empty findings when Gauntlet is absen
- Tier-1 performance review works standalone; Tiers 2/3 activate only when Gauntlet is installed
- Follows the same optional-import contract used in leyline tokens and pensive blast-radius hooks
Performance Review by the numbers
- 68 all-time installs (skills.sh)
- Ranked #525 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/athola/claude-night-market --skill performance-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 325 |
| Last updated | August 2, 2026 |
| Repository | athola/claude-night-market ↗ |
What it does
Enrich the Tier-1 performance-review skill with optional Gauntlet tree-sitter and graph analysis when the plugin is installed.
Files
Table of Contents
- Quick Start
- When to Use
- When NOT to Use
- Required TodoWrite Items
- Workflow
- Tiered Analysis
- Output Format
- Cross-Plugin Dependencies
- Supporting Modules
Performance Review
Static-analysis review of time and space complexity hotspots.
The skill runs in three escalating tiers. Tier 1 uses Python's stdlib ast and always runs. Tier 2 uses gauntlet's tree-sitter parser to extend detection across languages when gauntlet is installed. Tier 3 uses the gauntlet code graph to upgrade severity when hotspots reach other hotspots transitively. If gauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still produces useful findings on Python source.
Quick Start
/performance-review # scan changed files
/performance-review path/to/file.py # scan one file
/performance-review --tier 1 # force Tier 1 onlyProgrammatic use:
from pensive.skills.performance_review import PerformanceReviewSkill
skill = PerformanceReviewSkill()
result = skill.analyze(context, "src/module.py")
for f in result.issues:
print(f"[{f.severity}] {f.file}:{f.line} {f.message}")When to Use
- Pre-merge review of code that runs on user-scaled inputs.
- Triage of a function that "feels slow" before reaching for a
profiler.
- Audit a refactor for newly introduced O(n²) patterns.
- Guardrail for AI-generated code where nested-loop hot spots
are common.
When NOT to Use
- The target needs runtime measurement (memory profile, CPU
time on real data). Use Skill(parseltongue:python-performance) instead: that skill drives cProfile, py-spy, and benchmarks.
- General refactoring guidance not focused on hotspots: use
Skill(pensive:code-refinement) whose algorithm-efficiency module covers broader optimization patterns. This skill detects; that skill teaches.
- Architecture-level performance (sharding, caching layers,
queue placement): use Skill(pensive:architecture-review).
Required TodoWrite Items
1. perf-review:context-established 2. perf-review:scan-complete 3. perf-review:findings-categorized 4. perf-review:integration-checked 5. perf-review:report-generated 6. perf-review:findings-verified
Workflow
Step 1: Context (perf-review:context-established)
- Identify target files. If invoked with no argument, use
git diff --name-only. If invoked with a path, scope to that.
- Note language(s) involved. Tier 1 covers Python; non-Python
files need gauntlet for Tier 2 coverage.
Step 2: Tier 1 AST scan (perf-review:scan-complete)
Load modules/time-complexity.md for the time-side patterns and modules/space-complexity.md for space-side. Each module documents the AST shape of every detector.
For each Python target file, call:
from pensive.skills.performance_review import PerformanceReviewSkill
result = PerformanceReviewSkill().analyze(context, path)The visitor walks the AST once and emits ReviewFinding records.
Step 3: Categorize and rank (perf-review:findings-categorized)
Group findings by severity:
- HIGH: O(n²) or worse on input-sized iterables (T1, T2).
- MEDIUM: Unbounded allocation or per-iteration overhead
(T3, T4, S1, S3).
- LOW: Style-level inefficiencies (T5, T6, S2).
- CRITICAL: Reserved for Tier-3 transitive upgrades.
Within a severity, sort by file then line. Suppress findings the user has explicitly marked acceptable (TODO/comment markers) at module-load time of the target.
Step 4: Tier 2/3 enrichment (perf-review:integration-checked)
Load modules/gauntlet-integration.md for the contract.
If gauntlet is installed, run Tier 2 on non-Python files that were skipped at Step 2. If a .gauntlet/graph.db exists in the working tree, run Tier 3 to upgrade severities based on transitive hotspot reachability.
If gauntlet is missing, this step is a no-op and the report notes "Tier 2/3 not available: install gauntlet for multi-language and call-chain coverage."
Step 5: Report (perf-review:report-generated)
Emit a markdown report:
## Performance Review: <target>
### HIGH (<count>)
- src/foo.py:42: Nested loop over the same iterable 'items'.
Suggestion: sort + two pointers, or hash-set membership.
### MEDIUM (<count>)
- ...
### LOW (<count>)
- ...
Tier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)The report is informational. Apply fixes via Skill(pensive:code-refinement) or hand-merge.
Tiered Analysis
| Tier | Source | When it runs | What it covers |
|---|---|---|---|
| 1 | stdlib ast | Always (Python source only) | T1-T6, S1-S3 |
| 2 | gauntlet.treesitter_parser | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |
| 3 | gauntlet.graph.GraphStore | When .gauntlet/graph.db exists | Severity upgrade via transitive call chains |
Output Format
Findings use the shared ReviewFinding dataclass from pensive.skills.base:
ReviewFinding(
file="src/module.py",
line=42,
severity="HIGH", # LOW | MEDIUM | HIGH | CRITICAL
category="time", # time | space
message="Nested loop over the same iterable 'items'.",
suggestion="Sort + two pointers, or hash-set membership.",
anchor="verbatim source text at file:line",
code_snippet="",
)This shape matches every other pensive review skill, so the findings can flow into Skill(pensive:unified-review) without translation.
Cross-Plugin Dependencies
| Dependency | Required? | Effect when missing |
|---|---|---|
gauntlet.treesitter_parser | Optional | Tier 2 returns []; Python coverage unchanged |
gauntlet.graph.GraphStore | Optional | Tier 3 returns []; severities are not upgraded |
The optional-import contract follows the precedent in plugins/leyline/src/leyline/tokens.py:25-32 and plugins/gauntlet/hooks/pr_blast_radius.py:52-56: try-import to module-level sentinels, then early-return on None inside each tier helper. See modules/gauntlet-integration.md for the exact code shape.
Supporting Modules
modules/time-complexity.md: T1-T6 detector patterns and AST
shapes.
modules/space-complexity.md: S1-S3 detector patterns.modules/gauntlet-integration.md: Tier 2/3 contract,
fallback semantics, examples.
modules/kuva-visualization.md: Rendering benchmark data as
charts with kuva (criterion, pytest-benchmark, ad-hoc tables). Covers when chart evidence satisfies proof-of-work requirements.
Verification
A perf-review finding is only useful if the caller can confirm it is real. Use this checklist before treating any finding as worth fixing:
1. Reproduce under a profiler. Run cProfile, py-spy, or the language-specific equivalent on the hotspot. The findings pinpoint AST shapes; the profiler validates the runtime impact. 2. Re-run the failing benchmark. If benches/ exists, the hotspot should show up in numbers, not just AST scans. 3. Compare numbers before and after the proposed fix. The fix is wrong if numbers do not move. Capture both timings as evidence references like [E1] (before) and [E2] (after). When 3+ data points exist, render a kuva chart and attach it to the PR (see modules/kuva-visualization.md). 4. Sample two or three reported hotspots manually. Findings can be true at the AST level and false at the call-graph level when callers short-circuit. Manual sampling catches that.
The Skill(imbue:proof-of-work) discipline applies: claims like "the hotspot is fixed" require evidence, not assertion.
Testing
A test file already lives at plugins/pensive/tests/skills/test_performance_review.py covering the AST-shape detectors. Two rules for changes here:
- Add a new detector with a test. Any new T- or S- pattern
added to the modules ships with a test that has the smallest AST sample exercising it.
- Add a regression test for any false positive removed. When
the skill stops firing on a shape that used to look hot, the reason should appear as a test case so the regression is discoverable later.
The Iron Law applies: a new detector without a failing test first is a request to skip TDD on a code-analysis component, which is exactly the place where TDD pays off most.
Verify Findings Are Grounded (perf-review:findings-verified)
Every finding must cite a real location and a verbatim anchor. Write findings to .review/findings.json and confirm each citation resolves:
python plugins/imbue/scripts/citation_verifier.py \
--findings .review/findings.json --repo-root .Drop or label UNVERIFIED any finding the verifier fails (exit 1); only verified findings enter the report. See Skill(imbue:review-core) Step 5 and Skill(imbue:structured-output) for the schema.
Exit Criteria
- [ ] A perf-review report file exists for the requested target.
- [ ] Every finding carries a severity label and a concrete
suggestion the caller can act on.
- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)
detectors have been run; tier coverage is reported.
- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)
contracts honor the optional-import sentinel: missing modules return [] rather than raising.
- [ ] Each new detector ships with a smallest-AST test that
fails before the detector exists; each removed false positive ships with a regression test.
- [ ] Findings flow into
Skill(pensive:unified-review)without
translation when invoked from the unified entry point.
- [ ] Every reported finding carries a
Location+ verbatimAnchor
confirmed by citation_verifier.py (exit 0), or unverified findings were dropped or labeled UNVERIFIED
Gauntlet Integration
Performance review is a Tier-1 skill out of the box. Tiers 2 and 3 enrich the analysis when gauntlet is installed.
Optional-import contract
At module load time, performance_review.py runs two try-imports to module-level sentinels:
try:
from gauntlet.treesitter_parser import parse_file as _gt_parse
except (ImportError, ModuleNotFoundError):
_gt_parse = None
try:
from gauntlet.graph import GraphStore as _GraphStore
except (ImportError, ModuleNotFoundError):
_GraphStore = NoneThe dual-exception catch matches the precedent in plugins/leyline/src/leyline/tokens.py:25-32. It survives the case where the import fails for a reason other than the module being absent (e.g., a transitive ImportError deep inside gauntlet's own stack).
Each tier helper checks its sentinel and early-returns:
def _tier2_findings(self, context, file_path):
if _gt_parse is None:
return []
...
def _tier3_findings(self, context, existing, file_path):
if _GraphStore is None:
return []
...This is the same pattern proven in plugins/pensive/hooks/pr_blast_radius.py:52-56, where gauntlet's blast-radius graph is consulted only when the plugin is installed.
Tier 2: Tree-sitter coverage
When _gt_parse is set, _tier2_findings invokes parse_file(path) and receives (nodes, edges) describing the target file's AST in gauntlet's neutral graph format.
Languages currently parsed: Python, JavaScript, TypeScript, Go, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per gauntlet's _EXT_TO_LANG map).
The patterns translated to Tier 2 are the language-agnostic ones:
- T1 (nested loop over same iterable): present in every
imperative language.
- T2 (membership in list): adapts to language idioms (e.g.,
Array.includes in JS, slices.Contains in Go).
- S1 (append in nested loops):
arr.push(...)in JS,
append(slice, ...) in Go.
Patterns that do NOT translate (skipped at Tier 2):
- T3 (
re.compilein a loop): Python-specific call shape. - T6 (list comprehension passed to a reducer): Python-specific
syntax.
- T4 (string
+=): many languages have language-level string
builders that handle this; the cost model differs.
Tier 3: Transitive call analysis
When both _GraphStore is set AND a .gauntlet/graph.db file exists in the working tree, _tier3_findings opens the graph and queries impact_radius() for each existing finding's function.
If a function reachable from a Tier-1/2 hotspot is itself a hotspot, the original finding's severity is upgraded one step:
| Original | Upgraded |
|---|---|
| LOW | MEDIUM |
| MEDIUM | HIGH |
| HIGH | CRITICAL |
This catches cases where the surface code looks fine but the helper it calls is the actual bottleneck.
The graph file is built by gauntlet's own command:
/gauntlet-graph build .When the graph does not exist, Tier 3 returns []. Building the graph is a one-time cost; it speeds up every subsequent review.
Failure modes and fallbacks
| Condition | Tier 2 | Tier 3 | User-visible effect |
|---|---|---|---|
| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |
| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |
| Both installed | full enrichment | severity upgrades active | Maximum coverage |
In every case, Tier 1 still runs. The skill never fails because gauntlet is missing. This is a deliberate choice: pensive must not require an optional plugin to deliver core value.
Verification
The fallback contract is exercised by three tests in plugins/pensive/tests/skills/test_performance_review.py:
test_tier2_returns_empty_when_gauntlet_missing: stubs
_gt_parse to None and asserts _tier2_findings returns [].
test_tier3_returns_empty_when_graphstore_missing: same for
_tier3_findings.
test_full_analyze_with_gauntlet_blocked_returns_tier1_only:
stubs both sentinels and asserts the full analyze() still produces Tier-1 findings (T1 fires on a nested-loop snippet).
Run them with:
cd plugins/pensive
uv run pytest tests/skills/test_performance_review.py -v --no-covVisualizing Performance Findings with kuva
When a performance review produces before/after benchmark data, render it as a chart. Text comparisons like "380ms → 60ms" are correct but hard to scan across multiple hotspots. A scatter or bar chart makes regressions and wins immediately visible.
kuva is a Rust scientific plotting library (and CLI binary) that renders directly from TSV/CSV input to SVG, PNG, or the terminal. Install once; pipe benchmark data in without modifying project source.
Install
cargo install kuva --features cliRendering a before/after benchmark comparison
criterion (Rust)
criterion writes per-benchmark timing samples to target/criterion/<name>/new/estimates.json. Extract the mean and pipe to kuva:
# Collect before/after means for all criterion benchmarks
python3 - <<'EOF'
import json, pathlib, sys
rows = ["benchmark\tstage\tns"]
for est in pathlib.Path("target/criterion").rglob("estimates.json"):
bench = est.parts[-3]
data = json.loads(est.read_text())
mean_ns = data["mean"]["point_estimate"]
# Distinguish before/after by tag; adjust to your workflow.
rows.append(f"{bench}\tafter\t{mean_ns:.1f}")
print("\n".join(rows))
EOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \
--title "Before vs After" --terminalFor a paired comparison where you have both runs saved:
# before.tsv and after.tsv each contain: benchmark<TAB>ns
kuva scatter before.tsv after.tsv \
--x ns --y ns --color-by stage \
--title "Hotspot timing (lower is better)" \
-o perf-comparison.svgpytest-benchmark (Python)
pytest --benchmark-json=bench.json tests/
# Convert to TSV
python3 -c "
import json, sys
d = json.load(open('bench.json'))
print('name\tns')
for b in d['benchmarks']:
print(b['name'] + '\t' + str(b['stats']['mean'] * 1e9))
" | kuva bar /dev/stdin --x name --y ns \
--title "Benchmark means (ns)" -o bench.svgAd-hoc timing table
If you are capturing timings manually (e.g., from production traces as in the mlock war story):
stage p50_ms p99_ms
before_mlock 180 380
after_mlock 35 60kuva bar timings.tsv --x stage --y p99_ms \
--title "p99 barge-in latency (ms)" -o latency.svgTerminal output (no file required)
For quick CI feedback without writing an SVG artifact, add --terminal to any kuva command. The chart renders as Unicode block characters directly in the shell, visible in CI logs.
kuva bar timings.tsv --x stage --y p99_ms --terminalWhen to attach a chart as evidence
The Skill(imbue:proof-of-work) discipline requires evidence references [E1]/[E2] for before/after claims. A kuva-rendered SVG in the PR description or comments is a valid [E2] when it shows the post-fix benchmark result alongside the pre-fix baseline.
Minimum evidence bar:
| Claim | Required chart type |
|---|---|
| "Latency improved by X" | Bar or scatter with before/after |
| "Throughput doubled" | Line or bar over input size range |
| "Memory usage flat" | Line over time or input size |
| "O(n log n) vs O(n²)" | Log-log scatter showing slope change |
When NOT to use kuva
- The project already has matplotlib/plotly in its dev dependencies;
consistency matters more than zero-dep.
- The hotspot is trivial (single function, clear before/after number
in a two-column table). Charts are for 3+ data points.
- CI environment has no Rust toolchain and adding one is not
worth it; fall back to a numeric table in the PR comment.
Space Complexity Detectors
Three AST patterns that signal likely space-complexity hotspots. Each detector cites the AST node it matches, the heuristic, and a concrete fix.
S1: Unbounded .append() inside nested loops (MEDIUM)
AST shape: ast.Call whose func is ast.Attribute named append, found while the loop stack has depth >= 2.
Why it matters: A single-loop accumulator is bounded by the input size, which is usually fine. A nested-loop accumulator grows multiplicatively (n×m or n²) and is the typical "result explosion" pattern that drives memory exhaustion.
Note: The detector deliberately does not flag single-loop appends. They are common, expected, and rarely a hotspot. If single-loop accumulation becomes a problem, that is a runtime profiling concern handled by Skill(parseltongue:python-performance).
Fix: If the consumer can iterate, yield instead of materialize:
def all_pairs(xs):
for x in xs:
for y in xs:
yield (x, y) # streaming, O(1) spaceWhen the full list is genuinely needed, document the size bound:
# Bounded: |xs| <= 100, so output <= 10000 pairs.
out = [(x, y) for x in xs for y in xs]S2: List wrapping a generator inside a reducer (LOW)
AST shape: ast.Call to one of sum, max, min, any, all, sorted, set, frozenset, where the first arg is itself an ast.Call to list, dict, tuple, or set with an ast.GeneratorExp as its first argument.
Why it matters: max(list(g)) allocates the full list, then walks it. The wrapper is redundant: reducers accept generators directly.
Fix:
# Before
return max(list(x * 2 for x in xs))
# After
return max(x * 2 for x in xs)For sorted / set the wrapper is sometimes intentional (to force evaluation), but it's still cheaper to let sorted / set consume the generator directly.
S3: Per-iteration allocation inside a loop (MEDIUM)
AST shape: ast.Call inside a loop body where either:
- The
funcis anast.Attributewith namecopy, or - The
funcis anast.Nameofdict,list, ortuple
with a non-comprehension first argument (the comprehension case is a builder, not a copy).
Why it matters: base.copy() per iteration allocates a new container N times. If only one or two fields change per iteration, a single allocation outside the loop with selective mutation costs less.
Fix: Hoist when possible.
# Before
for x in items:
snapshot = base.copy()
snapshot["key"] = x
out.append(snapshot)
# After (when downstream tolerates shared dict identity):
shared = {**base}
for x in items:
shared["key"] = x
out.append(dict(shared)) # explicit copy at the boundaryWhen the snapshots must be independent, keep .copy() but move it outside the loop if possible, or use copy.deepcopy once and patch.
What is NOT in this module
- S4 (closure capture) was scoped in the plan but deferred:
reliable detection requires control-flow analysis beyond single-pass AST. Revisit when gauntlet's graph integration matures.
- Numerical-stability concerns (precision loss, overflow):
use Skill(pensive:math-review).
- String-builder patterns: covered by T4 in
time-complexity.md since the dominant cost is time (quadratic concat), not space.
Test references
plugins/pensive/tests/skills/test_performance_review.py:
test_s1_unbounded_append_in_looptest_s2_list_wrapping_generator_in_reducertest_s3_copy_inside_loop
Each test feeds a synthetic snippet through the visitor and asserts the expected severity and line.
Time Complexity Detectors
Six AST patterns that signal likely time-complexity hotspots. Each detector cites the AST node it matches, the heuristic, and a concrete fix.
T1: Nested loop over the same iterable (HIGH)
AST shape: ast.For whose iter is ast.Name, where the same Name.id already appears in an enclosing ast.For's iter on the loop stack.
Why it matters: for x in items: for y in items: ... is O(n²) and rarely intentional. When items is large, this becomes the hot spot.
Fix:
- If pairwise comparison is needed, sort once and use two
pointers (O(n log n)).
- If membership is needed, build a set once outside the outer
loop.
- If the nested work is independent, consider
itertools.product for clarity (same complexity but signals intent).
T2: List in lookup inside a loop (HIGH)
AST shape: ast.Compare with ast.In op, right-hand operand ast.Name, found while the loop stack is non-empty.
Why it matters: if x in ys is O(n) when ys is a list, making the enclosing loop O(n²). Static analysis can't prove the variable's type, so the detector flags every in <Name> inside a loop with a conditional suggestion.
Fix: If ys is a list and won't mutate during the loop:
ys_set = set(ys)
for x in xs:
if x in ys_set: # O(1) per lookup
...T3: re.compile() inside a loop body (MEDIUM)
AST shape: ast.Call whose func is the attribute access re.compile, found while the loop stack is non-empty.
Why it matters: Python's regex engine caches compiled patterns internally, but the cache is bounded and not guaranteed for every pattern. Hoisting the compile is cheap and explicit.
Fix:
_PAT = re.compile(r"\d+")
def matches(items):
return [s for s in items if _PAT.search(s)]T4: String += accumulator in a loop (MEDIUM)
AST shape: ast.AugAssign with ast.Add op, target an ast.Name previously bound to a string literal in the same function, occurring inside a loop.
Why it matters: Each += allocates a new string and copies the prefix. For long iterations this becomes O(n²) on total size.
Fix:
parts = []
for r in rows:
parts.append(",".join(r) + "\n")
return "".join(parts)io.StringIO is also acceptable.
T5: Recursive function without memoization (LOW)
AST shape: ast.FunctionDef (or AsyncFunctionDef) whose body contains ast.Call to the function's own name, with no @functools.cache, @functools.lru_cache, or @cache decorator on the def.
Why it matters: Naive recursion (e.g., textbook fib(n) = fib(n-1) + fib(n-2)) has exponential repeat work. Memoization makes the same recurrence linear.
Fix:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)If the recursion is intentionally non-memoized (e.g., side effects on each call), suppress with a comment marker:
# perf-review: intentional, side-effects on each call
def walk(node):
...T6: List comprehension passed to a reducer (LOW)
AST shape: ast.Call to one of sum, max, min, any, all, sorted, set, frozenset, with first arg ast.ListComp.
Why it matters: The list materializes the entire result in memory, then the reducer walks it. A generator expression skips the intermediate.
Fix: Drop the brackets.
# Before
return sum([x * 2 for x in xs])
# After
return sum(x * 2 for x in xs)For sorted / set / frozenset the materialization is unavoidable, so the detector still flags them but a fix is optional and may be cosmetic.
Test references
Tests for each detector are at plugins/pensive/tests/skills/test_performance_review.py. Each detector is paired with at least one BDD-style scenario test (test_t1_*, test_t2_*, ...). New detectors should ship with a failing test first per the Iron Law.