Skip to content

[Repo Assist] perf: cache GetParameters/PropertyType/FieldType/EventHandlerType in target-model wrappers - #513

Merged
dsyme merged 2 commits into
masterfrom
repo-assist/perf-cache-GetParameters-20260501-3f57ea6b8fa06ce7
May 1, 2026
Merged

dsyme merged 2 commits into
masterfrom
repo-assist/perf-cache-GetParameters-20260501-3f57ea6b8fa06ce7

Conversation

@github-actions

@github-actions github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

🤖 This is an automated PR from Repo Assist.

Summary

Caches computed values in the target-model member wrappers (txILConstructorDef, txILMethodDef, txILPropertyDef, txILEventDef, txILFieldDef) inside TargetTypeDefinition to avoid repeated type-resolution and array allocations on every call.

What was happening

Each wrapper function creates a new XxxInfo() object expression. Before this change, several frequently-called members recomputed their results from scratch on every access:

Wrapper Member Was doing
txILConstructorDef GetParameters() Array.map (txILParameter ...) every call
txILMethodDef GetParameters() Array.map (txILParameter ...) every call
txILPropertyDef PropertyType txILType resolution every call
txILPropertyDef GetIndexParameters() Array.map (txILParameter ...) every call
txILEventDef EventHandlerType txILType resolution every call
txILFieldDef FieldType txILType resolution every call

Why this matters

TargetTypeDefinition already caches its member wrapper arrays via lazy (ctorDefs, methDefs, fieldDefs, propDefs, eventDefs). This means the same wrapper objects are reused across calls. Without result caching inside the wrappers, every access to GetParameters(), PropertyType, etc. allocates a new array and re-runs IL type resolution — which the F# compiler does repeatedly during type inference against a generative type provider's generated assembly.

Fix

Added lazy bindings before each object expression to cache the computed results:

// txILMethodDef (before)
override __.GetParameters() = inp.Parameters |> Array.map (txILParameter (gps, gps2))

// txILMethodDef (after)
let parametersCache = lazy (inp.Parameters |> Array.map (txILParameter (gps, gps2)))
// ...
override __.GetParameters() = parametersCache.Value
```

Same pattern applied to `txILConstructorDef`, `txILPropertyDef`, `txILEventDef`, and `txILFieldDef`.

Note: This is independent of and complementary to PR #509 (which optimises `Array.filter (canBindXxx)` on the bind-all path).

## Test Status

```
Passed! - Failed: 0, Passed: 157, Skipped: 0, Total: 157

All 157 tests pass.

Generated by 🌈 Repo Assist, see workflow run.

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a

…target-model wrappers

The tx* wrapper functions for TargetTypeDefinition member wrappers
(txILConstructorDef, txILMethodDef, txILPropertyDef, txILEventDef,
txILFieldDef) previously recomputed type-resolved values on every call:

- GetParameters() re-ran Array.map over txILParameter on every call
- PropertyType re-ran txILType on every call
- GetIndexParameters() re-ran Array.map over txILParameter on every call
- EventHandlerType re-ran txILType on every call
- FieldType re-ran txILType on every call

Since TargetTypeDefinition caches its member wrappers in lazy arrays
(ctorDefs, methDefs, fieldDefs, propDefs, eventDefs), the same wrapper
objects are reused across calls. Caching these derived values with lazy
eliminates redundant type-resolution and array allocation on repeated
access — which occurs frequently when the F# compiler type-checks a
generative type provider's generated assembly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dsyme
dsyme marked this pull request as ready for review May 1, 2026 16:24
@dsyme
dsyme merged commit 2505f60 into master May 1, 2026
2 checks passed
dsyme added a commit that referenced this pull request Jul 29, 2026
…er body (issue #341) (#520)

## Problem

Issue #341 reported that generative compilation
(`AssemblyCompiler.Compile`) is still very slow for large providers
(SwaggerProvider + Stripe schema), even after the memoization work in
#443/#457/#471/#493/#497/#502/#509/#513.

Profiling the benchmark suite with dotnet-trace showed that with those
caches in place, **~80% of `Compile` time was FSharp.Core quotation
deserialization** (`PatternsModule.deserialize` → `instMeth` →
`GetGenericArguments`/`MakeGenericType`), attributed to the
`CodeGenerator` constructor.

Root cause: `CodeGenerator` defines ~63 active patterns from quotation
literals — `let (|Addition|_|) = (|SpecificCall|_|) <@ (+) @>`, `<@ max
@>`, `<@ sqrt @>`, etc. Evaluating a quotation literal deserializes it
(reflection-binding every method it mentions), and a `CodeGenerator` is
constructed **once per emitted member body**. A provider with 10,000
members therefore deserialized those quotations ~630,000 times.
`lessThan` was worse still: it deserialized `<@@ (<) @@>` on every call.

## Fix

* New `CodeGenPatterns(convTypeToTgt)` type holding all these patterns
as `member val` properties, so they are computed once at construction (a
plain property getter would re-evaluate per access).
* `AssemblyCompiler.Compile` constructs one instance and passes it to
every `CodeGenerator`; the constructor now just re-binds the local
active patterns from it, so the large `emitExpr` match code is
untouched.
* `lessThan` uses a precomputed generic method definition.

## Results

Quick benchmark (`benchmarks -- --quick`, ms/iter, before → after):

| Scenario | Before | After | Speedup |
|---|---|---|---|
| Methods 50×20 | 880 | 86 | 10× |
| Methods 200×20 | 2893 | 180 | 16× |
| Methods 1000×10 | 6892 | 535 | 13× |
| Props 500×10 | 8074 | 1259 | 6.4× |
| ComplexBody 300×10 | 2528 | 158 | 16× |
| Nested 100×4×5 | 1668 | 127 | 13× |

End-to-end with the exact repro from #341 (SwaggerProvider built from
source against this branch, Stripe `spec3.sdk.json`, `dotnet fsi`):
**62–72 s → 19–20 s** wall clock; the remainder is dominated by schema
download, OpenApi parsing and fsi startup rather than the TPSDK.

Re-profiling after the change confirms the SDK-side deserialization is
gone; the remaining `deserialize` samples come from the provider
author's own `<@@ ... @@>` literals in `invokeCode` (once per member,
inherent to user code).

## Testing

* Full test suite passes: 160/160.
* Benchmark suite builds and runs (numbers above).
* SwaggerProvider builds unmodified against the patched
`ProvidedTypes.fs` and the generated Stripe client works
(`Stripe.Client()` constructed successfully).

Fixes #341 (the remaining slowness reported there after the earlier
rounds of caching).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Don Syme <dsyme@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant