Skip to content

perf(compile): fix quadratic and redundant-resolve costs in placement - #2878

Open
Sergei Kozyrev (mukkumayc) wants to merge 3 commits into
microsoft:mainfrom
mukkumayc:perf/placement-large-tree-scaling
Open

perf(compile): fix quadratic and redundant-resolve costs in placement#2878
Sergei Kozyrev (mukkumayc) wants to merge 3 commits into
microsoft:mainfrom
mukkumayc:perf/placement-large-tree-scaling

Conversation

@mukkumayc

@mukkumayc Sergei Kozyrev (mukkumayc) commented Sep 7, 2026

Copy link
Copy Markdown

Motivation

I hit a real performance problem running apm compile on a large internal monorepo, roughly 159,000 directories and 294,000 files, with several scoped applyTo patterns. Compile time was bad enough that I dug into ContextOptimizer and found the causes below.

I mostly write JS, not Python, so I used Claude Code to implement this fix. I reviewed the diff and verified it with a before after benchmark, but flagging this for transparency.

Summary

ContextOptimizer had several costs that only show up once a project has multiple scoped (non-universal) applyTo patterns, each matching a large, overlapping set of directories, the shape of a real monorepo with per-module conventions. Four issues stood out:

  • _file_matches_pattern and _safe_recursive_glob resolved every file path independently per pattern instead of reusing a memoized relative path.
  • _calculate_distribution_score recomputed the depth mean inside its variance loop (O(n²)) and rescanned _directory_cache for a count that's just its length.
  • _is_hierarchically_covered, _find_minimal_coverage_placement, and _is_instruction_relevant resolved paths that were already canonical _directory_cache keys.
  • _optimize_single_point_placement rescanned every matching directory per candidate instead of short-circuiting on the first miss.

No behavioral changes. Only removing provably redundant work.

Benchmark

Before/after comparison on synthetic monorepo-shaped projects: https://github.com/mukkumayc/apm-placement-benchmark

directories before after speedup
~6 0.28s 0.27s 1.0x
~1000 2.90s 0.58s 5.0x
~2000 8.39s 0.92s 9.1x
~10000 158.00s 3.61s 43.8x

Test plan

  • Full existing unit/integration test suite passes unchanged

@mukkumayc

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The newly introduced memoization caches can grow unbounded across repeated compiles because they are not cleared when the long-lived ContextOptimizer instance is reused.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Improves apm compile placement performance by removing redundant path normalization and repeated scans inside ContextOptimizer, targeting large monorepos with multiple overlapping scoped applyTo patterns.

Changes:

  • Memoizes per-file relative-path strings and uses cached Set[str] membership for ** glob matching.
  • Fixes an O(n^2) mean/variance recomputation in distribution scoring and removes redundant directory-cache rescans.
  • Short-circuits hierarchical coverage checks to avoid full rescans per candidate placement.
File summaries
File Description
src/apm_cli/compilation/context_optimizer.py Adds memoized relpath + resolved-dir caches and refactors matching/coverage calculations to reduce redundant work and quadratic behavior in placement optimization.
Review details

Suppressed comments (1)

src/apm_cli/compilation/context_optimizer.py:168

  • _resolved_dir_cache is introduced as an unbounded memoization dict but is not cleared between compile runs. Since ContextOptimizer is reused across compiles, this cache can grow without bound and retain resolved Paths long-term; it should be cleared when starting a new placement optimization (similar to _glob_cache).
        self._inheritance_cache: builtins.dict[Path, builtins.list[Path]] = {}  # (#171)
        self._resolved_dir_cache: builtins.dict[Path, Path] = {}
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/apm_cli/compilation/context_optimizer.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes appear to be performance-only refactors with cache invalidation handled per run, and the only identified issue is a small maintainability cleanup (dead try/except) with a straightforward fix.

Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/apm_cli/compilation/context_optimizer.py Outdated
ContextOptimizer had several costs that only show up once a project
has multiple scoped applyTo patterns each matching large,
overlapping directory sets:

- _file_matches_pattern and _safe_recursive_glob resolved every file
  path independently per pattern instead of reusing a memoized
  relative path.
- _calculate_distribution_score recomputed the depth mean inside its
  variance loop (O(n^2)) and rescanned _directory_cache for a count
  that's just its length.
- _is_hierarchically_covered, _find_minimal_coverage_placement, and
  _is_instruction_relevant resolved paths that were already canonical
  _directory_cache keys.
- _optimize_single_point_placement rescanned every matching directory
  per candidate instead of short-circuiting on the first miss.

No behavioral changes; all existing tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Match the existing pattern for _glob_cache/_glob_set_cache: reset
per-instance path caches at the start of optimize_instruction_placement
so repeated calls on the same ContextOptimizer stay consistent with
the rest of the scan-dependent cache resets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- _file_matches_pattern's non-** branch no longer wraps _portable_rel_path
  in a try/except ValueError -- the helper already swallows it internally
  and falls back to portable_relpath (which never raises).
- _single_placement_covers_all now delegates to _is_hierarchically_covered
  instead of re-deriving the same relative_to/ValueError check, keeping
  one source of truth for the mandatory-coverage constraint.
- Fix test_single_dir_returns_that_dir (both duplicated copies) to pass an
  already-resolved directory, matching its sibling tests and the
  already-canonical-paths invariant the placement code now relies on --
  it previously only passed because pytest's tmp_path happens to already
  be canonical on Linux.

Re-benchmarked against apm-placement-benchmark's proj_1000/2000/10000
fixtures after these changes: speedups unchanged from the perf branch's
prior numbers (up to 43.7x on proj_10000), confirming no regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mukkumayc
Sergei Kozyrev (mukkumayc) force-pushed the perf/placement-large-tree-scaling branch from 71f6840 to fb19e23 Compare September 8, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants