Skip to content

Optimize compiler execution hot paths - #10100

Closed
bartv wants to merge 18 commits into
masterfrom
compiler-perf
Closed

Optimize compiler execution hot paths#10100
bartv wants to merge 18 commits into
masterfrom
compiler-perf

Conversation

@bartv

@bartv bartv commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Status

This PR has been split into individual PRs per optimization category. See below for the full list and benchmark results.

Per-optimization isolated benchmark results (10 runs avg, dedicated benchmark machine)

PR Branch athonet demo cn_infra im_infra systnant Total Status
#10154 entity-attr-cache +10.2% -0.1% -1.7% -1.7% -1.6% +1.2% open
#10155 unwrap-reference -2.4% -1.3% -2.0% -3.3% +0.6% -1.8% open
#10156 type-validation -1.0% -0.3% -1.2% -3.4% -0.9% -1.5% merged
#10157 index-lookup +10.0% +1.7% -0.6% +0.1% -0.5% +2.3% open
#10158 fstring-singleton +0.1% -0.1% -0.1% -0.1% +0.4% +0.0% merged
#10159 plugin-fastpaths -0.7% -0.3% +0.0% -1.1% +1.1% -0.3% open
#10160 check-args-cache +3.1% +2.1% +2.0% +0.3% +0.9% +1.7% open
#10161 logging-guards +3.5% +1.6% -1.5% -1.6% +0.2% +0.2% merged
#10162 gpp-dedup -1.6% -0.2% +0.4% -7.4% -15.9% -4.8% merged
#10163 freeze-dep-cache +3.0% +2.4% +0.6% -1.5% -6.4% -0.4% merged
#10164 resolve-proxies +0.8% +1.4% +0.4% -1.1% +3.7% +0.9% merged

Note: Most optimizations show their effect only when combined. The athonet_mpn outliers (+10%) are a consistent warm-up artifact (first benchmark after venv rebuild). The gpp-dedup (#10162) is the clear standalone winner.

Individual PRs

Entity/statement optimizations:

Plugin optimizations:

Scheduler optimizations:

Related PRs (separate branches):

🤖 Generated with Claude Code

bartv and others added 3 commits March 10, 2026 08:20
…e validation

- Replace slow runtime_checkable Protocol isinstance check in unwrap_reference()
  with getattr(), avoiding ~60x overhead per call for non-matching types (strings,
  ints, bools). This speeds up type validation by 3.5x.
- Cache entity attribute lookups and default values after normalization, eliminating
  repeated parent-chain walks during instance creation (25% faster Instance.__init__).
- Fast-path eager promise handling for statements with no promises (the common case).

Measured 10% speedup on connect-infra compile (7.54s → 6.78s) and 6% on synthetic
benchmarks (5.91s → 5.56s for 500-device model).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Entity.get_all_attributes() public method instead of accessing
  _all_attributes_cache directly from runtime.py
- Add proper Callable type annotation for getattr result in references.py
- Rename variable to avoid mypy type narrowing issue across branches
- Add changelog entry for the performance improvement

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartv

bartv commented Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

…string formatter

- Add fast-path in String.validate and Bool.validate using exact type
  checks to skip super().validate() for the common case
- Add fast-path in TypeReferenceUnion.validate to skip unwrap_reference()
  for primitive Python types
- Restructure add_to_index: inline index_value_gate, add early break on
  unready attributes, avoid rebuilding filtered dict per call
- Cache frozenset of index attributes to avoid set() creation in
  lookup_index (245K calls/compile)
- Reuse stateless FStringFormatter as module-level singleton instead of
  allocating per f-string resolution (995K allocs/compile)

Profiled with compilerscaling benchmark at 5000 elines:
  master:         122.2s, 234M calls
  compiler-perf:  96.4s, 190M calls (21% faster)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartv
bartv requested a review from sanderr March 10, 2026 16:32

@sanderr sanderr 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.

Only reviewed eager promise changes for now.

Comment on lines +172 to +174
# Fast path: most statements have no eager promises
if not self._own_eager_promises:
return {}

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.

This is the type of construct I really don't like because of how it's coupled: it builds on a non-trivial and undocumented assumption. I do agree that on the critical path, as we are here, performance may be the more important driver. But, in this case I see no reason we can't get performance and readability both. Why don't we just inline it here?

This suggestion is based on the assumption that it's not the list comprehension that is computationally expensive, but the indirection via two method calls. Please verify that assumption.

Additionally, could we try to make _own_eager_promises non-private (own_eager_promises) so that it can be used directly rather than through get_own_eager_promises(). That method was a nice contrast to get_all_eager_promises(), but it's really not a strict requirement. If you make this change, please make sure to document the attribute.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Verified the assumption: the method call indirection is indeed the expensive part (30% overhead in a microbenchmark of 5M calls), not the list comprehension.

Addressed in 1044994:

  • Inlined schedule_eager_promises into _requires_emit_promises
  • Renamed _own_eager_promises to own_eager_promises (public) with a docstring
  • Removed get_own_eager_promises() (was never called externally)

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.

Does the if not self.own_eager_promises still gain us anything (non-negligible)? If not, I'd drop it. If it does, we can drop the conditional expression in the return statement because the else branch will be unreachable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Yes, the guard is still worthwhile: it avoids the list comprehension + dict construction + WrappedValueVariable allocation for the majority of statements that have no eager promises. Dropped the unreachable conditional on the return statement as you suggested — since the early return handles the empty case, promises is always truthy after the comprehension.

Fixed in bdfd904.

Comment thread src/inmanta/ast/statements/__init__.py Outdated
Comment on lines +196 to +197
Uses an ``in`` check instead of try/except KeyError because most statements have no eager
promises, making the common-case miss cheaper (avoids exception allocation overhead).

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.

This is an implementation comment that does not belong in a docstring. Should be a plain comment above the relevant line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Fixed in 1044994 — moved the rationale to a plain comment above the requires.get() call.

Comment thread src/inmanta/ast/statements/__init__.py Outdated
try:
promises = requires[(self, EagerPromise)]
except KeyError:
key = (self, EagerPromise)

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.

Is this variable here to satisfy mypy on the requires[key] access? How does requires.get((self, EagerPromise)) followed by if promises is None compare, performance-wise? It doesn't require the key variable, and reads ever so slightly easier imo, but I have no clue how dict.get() is implemented (I assume efficiently).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Good suggestion — dict.get() is implemented in C and is efficient. Switched to requires.get((self, EagerPromise)) with if promises is None in 1044994. Reads cleaner and avoids the intermediate variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Switched to requires.get() as suggested. However, since requires is typed as dict[object, object], .get() returns object which mypy can't assign to Sequence[EagerPromise]. Used # type: ignore[assignment] for now — do you have a preferred way to handle this? Options would be:

  1. Keep the type: ignore (current)
  2. Use a cast()
  3. Tighten the type of the requires dict (would be a larger change)

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.

This is indeed one of the instances where our type coverage is still lacking. Rather than type ignores, our usual approach for these types of issues is to accept them in the mypy-baseline file. Our makefile contains a target to regenerate it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Good to know — removed the # type: ignore[assignment] and confirmed the existing mypy baseline entry covers it (0 new errors when running through mypy_baseline filter).

Fixed in bdfd904.

bartv and others added 4 commits March 10, 2026 18:44
…es public

- Inline schedule_eager_promises into _requires_emit_promises to remove
  one level of method call indirection on the hot path
- Rename _own_eager_promises to own_eager_promises (public attribute)
  and remove the get_own_eager_promises() accessor
- Move implementation rationale from _fulfill_promises docstring to a
  plain comment
- Use requires.get() instead of separate key variable + in check in
  _fulfill_promises

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dict.get() returns `object` for dict[object, object], causing a mypy
assignment error. Reverted to `in` check + indexing which mypy can
narrow correctly. Also fixed Black formatting issue from the inlined
list comprehension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/inmanta/ast/statements/__init__.py Outdated
Should only be called after normalization.
"""
return self._own_eager_promises
#: Eager promises this statement itself is responsible for. Set during normalization.

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.

Suggested change
#: Eager promises this statement itself is responsible for. Set during normalization.
# All Eager promises this statement itself is responsible for. Set during normalization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude here on behalf of Bart.

Applied the suggestion. Fixed in bdfd904.

- Fix comment wording on own_eager_promises (s/Eager/eager/ kept as
  reviewer suggested capitalized "Eager")
- Remove unreachable conditional in _requires_emit_promises return
  (early return handles empty case, so promises is always truthy)
- Replace type: ignore with mypy baseline for requires.get() assignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartv
bartv requested a review from sanderr March 11, 2026 11:37
@sanderr

sanderr commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Claude through @bartv, I have another potential improvement in mind. Could you estimate and / or verify the impact on your representative model? My suggestion builds on the assumption that real world models have to regularly reschedule plugins through UnsetException. If this is the case, perhaps we can skip validation steps when re-executing. Concretely, can we cache type validation success on the plugin call instance before propagating the UnsetException? What would that gain us?

@bartv

bartv commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Claude through @bartv, I have another potential improvement in mind. Could you estimate and / or verify the impact on your representative model? My suggestion builds on the assumption that real world models have to regularly reschedule plugins through UnsetException. If this is the case, perhaps we can skip validation steps when re-executing. Concretely, can we cache type validation success on the plugin call instance before propagating the UnsetException? What would that gain us?

Problem

When a plugin accesses an unset model attribute, it raises UnsetException and gets rescheduled. On reschedule, check_args() runs again with the exact same arguments — redoing all type validation (expected_type.validate(value)) and domain conversion (DynamicProxy.return_value()). In the pathological case from the logs, systemtenant::empty_source_or_destination was rescheduled 6,892 times from a single call site, each time
re-validating the same arguments.

Why caching is safe

  1. UnsetException is only raised during plugin execution (accessing an entity attribute that isn't set yet), never during check_args() type validation — validate() only does isinstance checks, no attribute access.
  2. The raw args are already cached in FunctionUnit.args/kwargs — they don't change between reschedules.
  3. The DynamicProxy wrappers remain valid — they're thin proxies that read from the underlying entity instance, so they see updated attribute values on reschedule.
  4. The Context object (resolver, queue, result) is the same across reschedules of the same FunctionUnit.

So it seems feasible, I will see if I can something like this to the compiler scaling tests

@bartv

bartv commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Benchmark: plugin check_args caching on reschedule

Prototype for caching validated plugin arguments across reschedules (UnsetException retries). When a plugin is rescheduled, check_args() is skipped entirely and the previously-validated CheckedArgs (including DynamicProxy wrappers) is reused.

Benchmark setup: 5000 elines, 5 plugins (1–3 entity args each), 8 plugin calls/service, 7 reschedules/service = 35,000 reschedules total. Includes fan-out (4 endpoints waiting on same result), typedef validation (role_t, state_t), and list traversal in finalize_service.

Metric No cache Cached Savings
Overall
Total time 16.884s 16.152s 0.732s (4.3%)
Total function calls 44,718,173 41,873,248 2,844,925 (6.4%)
Plugin validation path
check_args calls 80,002 45,002 35,000 (44%)
check_args time 0.437s 0.337s 0.100s (23%)
validate_and_convert calls 200,003 110,003 90,000 (45%)
validate_and_convert time 0.257s 0.132s 0.125s (49%)
convert_and_validate calls 200,003 110,003 90,000 (45%)
convert_and_validate time 0.087s 0.045s 0.042s (48%)
return_value calls 460,003 370,003 90,000 (20%)
return_value time 0.574s 0.376s 0.198s (34%)
Validation path total 1.355s 0.890s 0.465s (34%)

No regression on compilerscaling (1000 elines, 0 reschedules): 5.70s → 5.64s (within noise).

🤖 Generated with Claude Code on behalf of Bart

bartv and others added 2 commits March 13, 2026 18:05
When a plugin is rescheduled after UnsetException, the same arguments are
re-validated by check_args unnecessarily. Cache the validated ProcessedArgs
in a mutable single-element list on FunctionUnit, reusing it on subsequent
calls. The Context insertion is also skipped on cache hit since it was
already inserted into the cached args list.

Profiling shows check_args is only ~0.5% of compile time, so the practical
impact is negligible, but the optimization is correct and avoids redundant
work on the principle of not repeating deterministic computations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add fast paths for primitive types (str, int, float, bool) across several
frequently-called methods in the compiler:

- Type.validate(): Add exact type checks for Integer, Float, and Number
  that skip the super().validate() Reference check (same pattern as Bool/String)
- DynamicProxy.return_value(): Return immutable primitives directly instead
  of calling copy() (copy for immutable types is a no-op with function overhead)
- DynamicProxy.unwrap(): Fast-path for primitives to skip 6 isinstance checks
- validate_and_convert_to_python_domain(): Fast-path for primitives to skip
  NoneValue check, has_custom_to_python(), ProxyContext allocation
- Plugin.call_in_context(): Skip DynamicUnwrapContext/functools.partial
  creation when return value is a primitive
- Scheduler.run(): Guard LOGGER.log(LOG_LEVEL_TRACE) calls with
  isEnabledFor() to avoid argument evaluation when trace logging is disabled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartv

bartv commented Mar 17, 2026

Copy link
Copy Markdown
Contributor Author

Based on profiling of customer cases some more fast-path optimizations have been added. This results in the following benchmarks:

image

count,
now - prev,
)
if LOGGER.isEnabledFor(LOG_LEVEL_TRACE):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Micro benchmarks show that this is still 2x faster. This also shortcuts the len() calls on the 3 sets

@bartv bartv changed the title Optimize compiler execution: cache entity attributes and speed up type validation Optimize compiler execution hot paths Mar 17, 2026
@sanderr

sanderr commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Claude acting on behalf of @sanderr

@bartv Merging this PR closes #10143 (optimize execution hot paths) and #10141 (cache plugin validation success on UnsetException).

The cache was typed as list[object], causing mypy attr-defined errors
when accessing .args/.kwargs on the cached value. Use proper CheckedArgs
type and extract to a local variable for mypy narrowing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bartv and others added 3 commits March 17, 2026 17:10
The zerowaiters partitioning code called get_progress_potential() twice
per element (once per generator expression). Replaced with a single
loop that partitions into has_potential/new_zerowaiters lists, halving
the number of calls (5.7M -> 2.9M on inmanta_infra benchmark).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The has_relation_precedence_rules() method was called 3.3M+ times per
compile via get_progress_potential(), each time evaluating
bool(self.freeze_dependents) on an empty set. Cache the result as a
plain bool flag set once in add_freeze_dependent(). Access it directly
in get_progress_potential() to eliminate the method call overhead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the recursive closure resolve_proxies() inside find_wait_cycle
with an iterative static method _resolve_proxy(). Eliminates 1.8M
recursive call frames on the systemtenant benchmark (4.7M -> 2.9M
primitive calls). Also avoids closure re-creation on each of the 239
find_wait_cycle invocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartv

bartv commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

cProfile analysis: per-optimization function call reductions

Profiled each optimization branch in isolation against master using cProfile on all 5 real-world benchmarks.

Function call count delta vs master

Branch athonet demo cn_infra im_infra systenant
entity-attr-cache -0.1% +1.3% -0.8% -0.8% -0.0%
unwrap-reference -3.5% +1.8% -1.6% -1.1% -0.0%
type-validation -0.4% +1.5% -1.6% -1.0% -0.2%
index-lookup -0.6% +1.5% -0.2% +0.4% +0.2%
fstring-singleton -0.2% +2.7% +1.2% +0.5% +1.0%
plugin-fastpaths -0.3% +1.5% -1.0% -0.9% +0.0%
check-args-cache -0.2% +2.3% +1.2% +0.5% +0.9%
logging-guards -1.0% +2.3% +1.1% +1.4% +0.8%
gpp-dedup -6.4% -3.6% +0.2% -10.5% -22.7%
freeze-dep-cache -4.6% -0.8% +0.6% -4.6% -11.6%
resolve-proxies +1.5% +2.6% +1.2% -0.1% -1.0%

Absolute function call counts (millions)

Branch athonet demo cn_infra im_infra systenant
master 62.5M 22.6M 44.8M 109.0M 113.8M
gpp-dedup 58.5M 21.8M 44.8M 97.5M 88.0M
freeze-dep-cache 59.6M 22.4M 45.0M 103.9M 100.5M
unwrap-reference 60.3M 23.0M 44.0M 107.8M 113.8M

Top eliminated functions (gpp-dedup on systemtenant: 113.8M → 88.0M, -25.8M calls)

Eliminated calls Saved time Function
-13.9M -2.74s has_relation_precedence_rules()
-11.8M -4.91s BaseListVariable.get_progress_potential()
-11.3M -9.21s ListVariable.get_progress_potential()
-6.8M -2.85s scheduler genexpr (zerowaiters partitioning)
-6.7M -0.72s builtins.len
-4.7M -1.71s resolve_proxies()
-2.6M -2.15s OptionVariable.get_progress_potential()
-2.6M -1.13s DelayedResultVariable.get_progress_potential()
-2.0M -1.21s get_waiting_providers()

The cascade: deduplicating the get_progress_potential() calls in the zerowaiters loop halves the calls to the entire get_progress_potentialhas_relation_precedence_rulesresolve_proxies chain, which reduces scheduler iterations, which further reduces all downstream function calls.

Key observations

  1. gpp-dedup (Optimize: avoid duplicate get_progress_potential calls in scheduler #10162) eliminates 25.8M function calls on systemtenant (-22.7%) and 11.5M on inmanta_infra (-10.5%). This is the single most impactful optimization.

  2. freeze-dep-cache (Optimize: cache has_relation_precedence_rules as bool flag #10163) shows similar eliminated functions because it touches the same get_progress_potential code path. The overlap means these two PRs have diminishing returns when combined.

  3. unwrap-reference (Optimize: replace Protocol isinstance in unwrap_reference with getattr #10155) reduces calls by 3.5% on athonet_mpn (2.2M fewer calls) by eliminating the expensive typing.__instancecheck__ path.

  4. Most other optimizations show <1% call count change when isolated — their impact is in reducing per-call overhead (faster execution per call) rather than eliminating calls entirely.

@bartv

bartv commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

Profile comparison: systemtenant full model (1909 services) with dependency manager

Profiled on the benchmark VM with cProfile. Both runs use the systemtenant fix-speculation branch (dependency manager replacing the empty_source_or_destination plugin).

Total: 1100s (master) → 693s (compiler-perf) — 37% faster

Top functions by self time

Function master self perf self Delta master calls perf calls Notes
runtime:782 get_progress_potential 221s 77s -65% 472M 250M Nearly halved calls and time
runtime:686 get_progress_potential 123s 65s -47% 487M 260M Same pattern
scheduler:317 find_wait_cycle 106s 106s 0% 894 894 Unchanged — same speculation count
scheduler:470 <genexpr> 70s gone 271M Eliminated in compiler-perf
attribute:238 has_relation_precedence 64s gone 560M Eliminated in compiler-perf
scheduler:365 run 55s 100s +82% 1 1 More self-time (inlined work from genexpr?)
builtins.len 46s 31s -33% 758M 487M 271M fewer calls
runtime:853 get_progress_potential 44s 14s -68% 88M 44M Halved
scheduler:339 resolve_proxies 43s gone 203M Replaced by _resolve_proxy (29s)
builtins.isinstance 30s 29s -3% 390M 382M Barely changed
deque.append 25s new 277M New in compiler-perf (batch processing?)

Key observations

  1. has_relation_precedence_rules (64s, 560M calls) — completely eliminated. This was a pure overhead check called on every attribute access.

  2. get_progress_potential — halved across all overloads (~389s → ~156s total self). Fewer calls means the scheduler evaluates progress more efficiently.

  3. find_wait_cycle — identical 894 calls / 106s in both. The speculation count hasn't changed — compiler-perf optimizes around it but doesn't reduce speculation itself. The dependency manager already eliminated the checkpoint-specific speculation; these 894 remaining cycles are from other sources (Host, AbstractHost, AffinityRule, Environment).

  4. resolve_proxies_resolve_proxy — refactored from 43s/203M calls to 29s/121M calls.

  5. The scheduler.run self-time went UP (55s → 100s) because work previously in separate functions (<genexpr>, resolve_proxies) was inlined.

Wall-clock benchmarks (full production model, 1909 services)

Configuration Time vs baseline
9.0.0 compiler (old baseline) 4,927s
core master + dep manager 395s -92%
compiler-perf + dep manager 260s -95%

🤖 Generated with Claude Code

inmantaci pushed a commit that referenced this pull request Mar 23, 2026
…o avoid argument evaluation when disabled (PR #10161)

## Summary

Guard `LOGGER.log(LOG_LEVEL_TRACE)` calls in the scheduler main loop with `isEnabledFor()` to avoid argument evaluation (including `len()` calls on queues) when trace logging is disabled.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.89s |   15.41s |  +3.5% |
| connect_demo   |  7.98s |    8.11s |  +1.6% |
| connect_infra  | 16.96s |   16.70s |  -1.5% |
| inmanta_infra  | 15.29s |   15.04s |  -1.6% |
| systemtenant   | 11.75s |   11.77s |  +0.2% |
| **Total**      | 66.87s |   67.03s |  +0.2% |

Impact is within noise when isolated. The `isEnabledFor` guard avoids evaluating `len()` on queues per scheduler iteration.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 23, 2026
…o avoid argument evaluation when disabled (PR #10161)

## Summary

Guard `LOGGER.log(LOG_LEVEL_TRACE)` calls in the scheduler main loop with `isEnabledFor()` to avoid argument evaluation (including `len()` calls on queues) when trace logging is disabled.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.89s |   15.41s |  +3.5% |
| connect_demo   |  7.98s |    8.11s |  +1.6% |
| connect_infra  | 16.96s |   16.70s |  -1.5% |
| inmanta_infra  | 15.29s |   15.04s |  -1.6% |
| systemtenant   | 11.75s |   11.77s |  +0.2% |
| **Total**      | 66.87s |   67.03s |  +0.2% |

Impact is within noise when isolated. The `isEnabledFor` guard avoids evaluating `len()` on queues per scheduler iteration.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 23, 2026
… partitioning loop (PR #10162)

## Summary

The zerowaiters partitioning code called `get_progress_potential()` twice per element (once per generator expression). Replaced with a single loop that computes the value once and partitions into two lists. Halves the call count (5.7M → 2.9M on inmanta_infra).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |   Delta |
| -------------- | -----: | -------: | ------: |
| athonet_mpn    | 15.45s |   15.20s |   -1.6% |
| connect_demo   |  8.20s |    8.18s |   -0.2% |
| connect_infra  | 16.68s |   16.75s |   +0.4% |
| inmanta_infra  | 15.24s |   14.11s |   -7.4% |
| systemtenant   | 11.92s |   10.02s |  -15.9% |
| **Total**      | 67.49s |   64.26s |   -4.8% |

Significant impact on models with heavy speculation (systemtenant -15.9%, inmanta_infra -7.4%). These projects have many `[0:]` relations that trigger the zerowaiters partitioning path frequently.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 23, 2026
… partitioning loop (PR #10162)

## Summary

The zerowaiters partitioning code called `get_progress_potential()` twice per element (once per generator expression). Replaced with a single loop that computes the value once and partitions into two lists. Halves the call count (5.7M → 2.9M on inmanta_infra).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |   Delta |
| -------------- | -----: | -------: | ------: |
| athonet_mpn    | 15.45s |   15.20s |   -1.6% |
| connect_demo   |  8.20s |    8.18s |   -0.2% |
| connect_infra  | 16.68s |   16.75s |   +0.4% |
| inmanta_infra  | 15.24s |   14.11s |   -7.4% |
| systemtenant   | 11.92s |   10.02s |  -15.9% |
| **Total**      | 67.49s |   64.26s |   -4.8% |

Significant impact on models with heavy speculation (systemtenant -15.9%, inmanta_infra -7.4%). These projects have many `[0:]` relations that trigger the zerowaiters partitioning path frequently.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 23, 2026
… allocating per f-string resolution (PR #10158)

## Summary

Reuse stateless `FStringFormatter` as module-level singleton instead of allocating per f-string resolution (995K allocs/compile).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.39s |   14.41s |  +0.1% |
| connect_demo   |  7.72s |    7.71s |  -0.1% |
| connect_infra  | 15.68s |   15.66s |  -0.1% |
| inmanta_infra  | 14.06s |   14.05s |  -0.1% |
| systemtenant   | 10.95s |   10.99s |  +0.4% |
| **Total**      | 62.80s |   62.82s |  +0.0% |

Impact is within noise when isolated. This micro-optimization saves one object allocation per f-string resolution; the real cost is in `vformat()` itself. Impact is only visible in cProfile (0.35s at 5000 elines in the synthetic compilerscaling benchmark).

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 23, 2026
… allocating per f-string resolution (PR #10158)

## Summary

Reuse stateless `FStringFormatter` as module-level singleton instead of allocating per f-string resolution (995K allocs/compile).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.39s |   14.41s |  +0.1% |
| connect_demo   |  7.72s |    7.71s |  -0.1% |
| connect_infra  | 15.68s |   15.66s |  -0.1% |
| inmanta_infra  | 14.06s |   14.05s |  -0.1% |
| systemtenant   | 10.95s |   10.99s |  +0.4% |
| **Total**      | 62.80s |   62.82s |  +0.0% |

Impact is within noise when isolated. This micro-optimization saves one object allocation per f-string resolution; the real cost is in `vformat()` itself. Impact is only visible in cProfile (0.35s at 5000 elines in the synthetic compilerscaling benchmark).

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 24, 2026
…in find_wait_cycle (PR #10164)

## Summary

The `find_wait_cycle` method used a recursive closure `resolve_proxies()` to unwrap `ResultVariableProxy` chains. On a customer project benchmark, this was called 4.7M times (2.9M primitive + 1.8M recursive). Replaced with an iterative static method `_resolve_proxy()`, eliminating recursive call overhead and closure re-creation on each `find_wait_cycle` invocation.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 15.17s |   15.29s |  +0.8% |
| connect_demo   |  7.96s |    8.07s |  +1.4% |
| connect_infra  | 16.51s |   16.58s |  +0.4% |
| inmanta_infra  | 14.98s |   14.82s |  -1.1% |
| systemtenant   | 11.48s |   11.91s |  +3.7% |
| **Total**      | 66.10s |   66.67s |  +0.9% |

Impact is within noise when isolated. This optimization reduces overhead in the `find_wait_cycle` path which is primarily exercised during scheduler speculation. Its effect becomes pronounced when combined with the batch freezing optimization (PR #10152).

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 24, 2026
…in find_wait_cycle (PR #10164)

## Summary

The `find_wait_cycle` method used a recursive closure `resolve_proxies()` to unwrap `ResultVariableProxy` chains. On a customer project benchmark, this was called 4.7M times (2.9M primitive + 1.8M recursive). Replaced with an iterative static method `_resolve_proxy()`, eliminating recursive call overhead and closure re-creation on each `find_wait_cycle` invocation.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 15.17s |   15.29s |  +0.8% |
| connect_demo   |  7.96s |    8.07s |  +1.4% |
| connect_infra  | 16.51s |   16.58s |  +0.4% |
| inmanta_infra  | 14.98s |   14.82s |  -1.1% |
| systemtenant   | 11.48s |   11.91s |  +3.7% |
| **Total**      | 66.10s |   66.67s |  +0.9% |

Impact is within noise when isolated. This optimization reduces overhead in the `find_wait_cycle` path which is primarily exercised during scheduler speculation. Its effect becomes pronounced when combined with the batch freezing optimization (PR #10152).

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 24, 2026
…call overhead in get_progress_potential (PR #10163)

## Summary

`has_relation_precedence_rules()` was called 3.3M+ times per compile via `get_progress_potential()`, each time evaluating `bool(self.freeze_dependents)` on an empty set. Cache the result as a public bool attribute `has_freeze_dependents` set once in `add_freeze_dependent()`. Access it directly in `get_progress_potential()` with an explicit `int()` cast for clarity.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.82s |   15.26s |  +3.0% |
| connect_demo   |  7.95s |    8.14s |  +2.4% |
| connect_infra  | 16.56s |   16.66s |  +0.6% |
| inmanta_infra  | 14.97s |   14.74s |  -1.5% |
| systemtenant   | 12.05s |   11.28s |  -6.4% |
| **Total**      | 66.35s |   66.08s |  -0.4% |

Impact is most visible on systemtenant (-6.4%) which has heavy scheduler speculation. The method call elimination saves ~0.43s per compile on models with 3.3M+ `get_progress_potential` calls.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 24, 2026
…call overhead in get_progress_potential (PR #10163)

## Summary

`has_relation_precedence_rules()` was called 3.3M+ times per compile via `get_progress_potential()`, each time evaluating `bool(self.freeze_dependents)` on an empty set. Cache the result as a public bool attribute `has_freeze_dependents` set once in `add_freeze_dependent()`. Access it directly in `get_progress_potential()` with an explicit `int()` cast for clarity.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.82s |   15.26s |  +3.0% |
| connect_demo   |  7.95s |    8.14s |  +2.4% |
| connect_infra  | 16.56s |   16.66s |  +0.6% |
| inmanta_infra  | 14.97s |   14.74s |  -1.5% |
| systemtenant   | 12.05s |   11.28s |  -6.4% |
| **Total**      | 66.35s |   66.08s |  -0.4% |

Impact is most visible on systemtenant (-6.4%) which has heavy scheduler speculation. The method call elimination saves ~0.43s per compile on models with 3.3M+ `get_progress_potential` calls.

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 25, 2026
…tion for str/int/float/bool (PR #10156)

## Summary

Add `type(value) is str` / `bool` / `int` / `float` fast-paths in `String.validate`, `Bool.validate`, `Integer.validate`, `Float.validate`, `Number.validate`, and `TypeReferenceUnion.validate` to skip full validation for primitive types (1M+ calls/compile).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.38s |   14.23s |  -1.0% |
| connect_demo   |  7.74s |    7.72s |  -0.3% |
| connect_infra  | 15.66s |   15.47s |  -1.2% |
| inmanta_infra  | 14.06s |   13.58s |  -3.4% |
| systemtenant   | 10.92s |   10.82s |  -0.9% |
| **Total**      | 62.76s |   61.82s |  -1.5% |

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 25, 2026
…tion for str/int/float/bool (PR #10156)

## Summary

Add `type(value) is str` / `bool` / `int` / `float` fast-paths in `String.validate`, `Bool.validate`, `Integer.validate`, `Float.validate`, `Number.validate`, and `TypeReferenceUnion.validate` to skip full validation for primitive types (1M+ calls/compile).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.38s |   14.23s |  -1.0% |
| connect_demo   |  7.74s |    7.72s |  -0.3% |
| connect_infra  | 15.66s |   15.47s |  -1.2% |
| inmanta_infra  | 14.06s |   13.58s |  -3.4% |
| systemtenant   | 10.92s |   10.82s |  -0.9% |
| **Total**      | 62.76s |   61.82s |  -1.5% |

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 26, 2026
…h getattr for 60x speedup on non-matching types (PR #10155)

## Summary

Replace slow `runtime_checkable` Protocol `isinstance` check in `unwrap_reference()` with `getattr()`, avoiding ~60× overhead per call for non-matching types (strings, ints, bools). This eliminates `typing.__instancecheck__`, `inspect._shadowed_dict`, and `inspect.getattr_static` from the profile entirely.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.37s |   14.02s |  -2.4% |
| connect_demo   |  7.74s |    7.64s |  -1.3% |
| connect_infra  | 15.66s |   15.35s |  -2.0% |
| inmanta_infra  | 14.03s |   13.56s |  -3.3% |
| systemtenant   | 10.98s |   11.05s |  +0.6% |
| **Total**      | 62.78s |   61.62s |  -1.8% |

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 26, 2026
…h getattr for 60x speedup on non-matching types (PR #10155)

## Summary

Replace slow `runtime_checkable` Protocol `isinstance` check in `unwrap_reference()` with `getattr()`, avoiding ~60× overhead per call for non-matching types (strings, ints, bools). This eliminates `typing.__instancecheck__`, `inspect._shadowed_dict`, and `inspect.getattr_static` from the profile entirely.

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.37s |   14.02s |  -2.4% |
| connect_demo   |  7.74s |    7.64s |  -1.3% |
| connect_infra  | 15.66s |   15.35s |  -2.0% |
| inmanta_infra  | 14.03s |   13.56s |  -3.3% |
| systemtenant   | 10.98s |   11.05s |  +0.6% |
| **Total**      | 62.78s |   61.62s |  -1.8% |

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
inmantaci pushed a commit that referenced this pull request Mar 31, 2026
… on the project. (PR #10223)

Added change entry for all spin-offs of #10100.
inmantaci pushed a commit that referenced this pull request Mar 31, 2026
… on the project. (PR #10223)

Added change entry for all spin-offs of #10100.
inmantaci pushed a commit that referenced this pull request Apr 16, 2026
…chain walks during instance creation (PR #10154)

## Summary

Cache `get_all_attribute_names` and `get_default_values` results to avoid repeated parent-chain MRO walks during instance creation. Add fast-path for `_requires_emit_promises` / `_fulfill_promises` when no eager promises exist (the common case).

Split from #10100.

## Benchmark results (10 runs avg, dedicated benchmark machine)

| Benchmark      | master | with opt |  Delta |
| -------------- | -----: | -------: | -----: |
| athonet_mpn    | 14.40s |   15.87s | +10.2% |
| connect_demo   |  7.78s |    7.77s |  -0.1% |
| connect_infra  | 15.75s |   15.48s |  -1.7% |
| inmanta_infra  | 14.11s |   13.87s |  -1.7% |
| systemtenant   | 11.04s |   10.86s |  -1.6% |
| **Total**      | 63.08s |   63.85s |  +1.2% |

Impact is within noise when isolated. This optimization primarily benefits models with deep entity inheritance hierarchies and many instances. Its effect becomes more pronounced when combined with other optimizations (visible in the combined compiler-perf branch benchmarks).

## Test plan

- [x] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@bartv bartv closed this Jul 6, 2026
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