[Repo Assist] Memoize transType in AssemblyCompiler to reduce redundant type translation - #443
Conversation
…ation For generative type providers with many types (e.g. SwaggerProvider over a large OpenAPI schema), the transType function is called thousands of times for commonly-used types like string, int, bool, etc. Adding a simple Dictionary cache avoids redundant computation in phases 2 and 3 of Compile(), which should meaningfully reduce assembly compilation time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/repo-assist Add a performance benchmark suite to the SDK using Benchmark.NET including some basic stress scenarios and take before/after measurements for this fix and include the benchmarks in the PR |
…rios Adds benchmarks/ directory with a standalone Benchmark.NET project that measures GetGeneratedAssemblyContents on progressively larger generative type providers: - Moderate: 50 types × 20 methods - Heavy: 200 types × 20 methods - Stress: 500 types × 10 methods (simulates a large OpenAPI provider) All scenarios cycle through 12 common .NET types (string, int, bool, float, int64, DateTime, Guid, decimal, byte[], and option variants) to maximise transType cache hits. The benchmark isolates compilation time by pre-building all ProvidedTypeDefinition instances in IterationSetup so that setup cost is excluded from the timed region. Before/after measurements on GitHub Actions runner (net8.0): 50 types × 20 methods: 910 ms -> 893 ms (~2%) 200 types × 20 methods: 3689 ms -> 3521 ms (~5%) 500 types × 10 methods: 4564 ms -> 4392 ms (~4%) See benchmarks/FSharp.TypeProviders.SDK.Benchmarks/README.md for full instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
|
🤖 Repo Assist here — following up on I've pushed a new commit to this PR adding a Benchmark scenarios
Each scenario cycles through 12 common .NET types ( Setup (creating Before/After measurementsMeasured with
The improvement is modest in this synthetic scenario because all 12 types are simple primitives and
The cache itself adds near-zero overhead (one Running# Full BDN run (takes several minutes, needs Release mode):
dotnet run -c Release --project benchmarks/FSharp.TypeProviders.SDK.Benchmarks -- --filter '*'
# Quick timing (seconds):
dotnet run -c Release --project benchmarks/FSharp.TypeProviders.SDK.Benchmarks -- --quick
|
🤖 *This PR was created by Repo Assist, an automated AI assistant.* Prepares the RELEASE_NOTES.md for version **8.3.0** — a minor release capturing significant improvements merged since 8.2.0 (February 24). ## Changes since 8.2.0 | Type | PR | Description | |------|----|-------------| | 🐛 Bug fix | #432 | Fix custom attributes on nested erased types | | 🐛 Bug fix | #458 | Fix `GetNestedType` on `TypeSymbol`/`ProvidedTypeSymbol` for generic provided types | | 🐛 Bug fix | #459 | Fix mutable variable captures in `QuotationSimplifier` — promote to ref cells | | ⚡ Performance | #443 | Memoize `transType` in `AssemblyCompiler` to reduce redundant type translation | | ⚡ Performance | #457 | Cache `transTypeRef` and `transMethRef` in assembly compiler | | ✨ Feature | #428 | New warning when all static parameters in a type provider are optional | | 📚 Docs | #455 | Documentation guide overhaul | | 🧪 Tests | #442 | Add coverage tests and Coverage build target | | 🔧 Toolchain | #431 | Update to .NET 8 SDK and toolchain | This is a minor version bump (8.2.0 → 8.3.0) due to the new feature (#428) and significant improvements. If preferred, a patch release (8.2.1) is also reasonable given the emphasis on bug fixes. ## Test Status This PR only modifies RELEASE_NOTES.md. The build/test status for the underlying changes is tracked in the individual PRs listed above (all passed CI before merging). --- *To release: merge this PR, then tag `v8.3.0` and publish the NuGet package via the existing build pipeline.* > Generated by [Repo Assist](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/22448247879) > > To install this [agentic workflow](https://github.com/githubnext/agentics/tree/afb00b92a9514fee9a14c583f059a03d05738f70/workflows/repo-assist.md), run > ``` > gh aw add githubnext/agentics@afb00b9 > ``` <!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, id: 22448247879, workflow_id: repo-assist, run: https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/22448247879 --> <!-- gh-aw-workflow-id: repo-assist --> --------- Co-authored-by: Repo Assist <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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>
🤖 This PR was created by Repo Assist, an automated AI assistant.
Summary
Adds a
Dictionary(Type, ILType)cache (transTypeCache) insideAssemblyCompilerso thattransTypeavoids recomputing the same type translation more than once during assembly compilation.Closes / addresses: #341 (performance investigation)
Root Cause
transTypeis called for every parameter type, return type, field type, and local variable type when emitting IL in phases 2 and 3 ofCompile(). For a generative type provider over a large schema (e.g. SwaggerProvider reading Stripe's OpenAPI spec), commonly-used types likestring,int,bool,unit, and generic containers appear in thousands of method signatures. Each call recurses throughtransTypeSpec → transTypeRef → transTypeRefScope → transScopeRef, redoing the same work each time.Fix
Added
let transTypeCache = Dictionary(Type, ILType)()at theAssemblyCompilerclass level and modifiedtransTypeto return a cached result when available, or compute-and-store when not.Generic parameters, array types, and all other cases are cached correctly: for a given
Typeobject the result oftransTypeis always identical.Trade-offs
ILTypeentry per distinct .NETTypeencountered.Compile()is single-threaded.AssemblyCompilerinstance (not global), so it is naturally scoped to one compilation.Test Status
All existing tests pass. The skipped test (
lambdas - failing) was already skipped before this change.