Skip to content

Commit 5702e51

Browse files
thomhurstclaude
andcommitted
perf: Cache MakeGenericMethod calls in delegate factories
Add ConcurrentDictionary caches for MethodInfo instances created via MakeGenericMethod in ModuleExecutionDelegateFactory and ResultRepositoryDelegateFactory. This avoids repeated reflection calls for the same type arguments. Changes: - Cache ExecuteAsync and ExecuteAndCastAsync generic method instances in ModuleExecutionDelegateFactory - Cache GetResultAsync and GetResultAndCastAsync generic method instances in ResultRepositoryDelegateFactory - Cache base method definitions as static readonly fields for reuse Fixes #1474 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9bd172e commit 5702e51

2 files changed

Lines changed: 48 additions & 16 deletions

File tree

src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ internal delegate Task<IModuleResult> ExecuteModuleDelegate(
2828

2929
private static readonly ConcurrentDictionary<Type, ExecuteModuleDelegate> ExecutorCache = new();
3030

31+
/// <summary>
32+
/// Cache for generic MethodInfo instances created via MakeGenericMethod.
33+
/// Key is the result type, value is the specialized MethodInfo.
34+
/// </summary>
35+
private static readonly ConcurrentDictionary<Type, MethodInfo> ExecuteAsyncMethodCache = new();
36+
private static readonly ConcurrentDictionary<Type, MethodInfo> ExecuteAndCastAsyncMethodCache = new();
37+
38+
/// <summary>
39+
/// Base method definitions, cached once on first use.
40+
/// </summary>
41+
private static readonly MethodInfo ExecuteAsyncMethodDefinition =
42+
typeof(IModuleExecutionPipeline).GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))!;
43+
44+
private static readonly MethodInfo ExecuteAndCastAsyncMethodDefinition =
45+
typeof(ModuleExecutionDelegateFactory).GetMethod(nameof(ExecuteAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
46+
3147
/// <summary>
3248
/// Gets a cached delegate for executing a module with the specified result type.
3349
/// </summary>
@@ -59,10 +75,10 @@ private static ExecuteModuleDelegate CreateExecutor(Type resultType)
5975
// Cast executionContext to ModuleExecutionContext<T>
6076
var castContext = Expression.Convert(contextParam, executionContextType);
6177

62-
// Get the ExecuteAsync method
63-
var executeMethod = typeof(IModuleExecutionPipeline)
64-
.GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))!
65-
.MakeGenericMethod(resultType);
78+
// Get the ExecuteAsync method (cached)
79+
var executeMethod = ExecuteAsyncMethodCache.GetOrAdd(
80+
resultType,
81+
static type => ExecuteAsyncMethodDefinition.MakeGenericMethod(type));
6682

6783
// Call pipeline.ExecuteAsync<T>(module, executionContext, moduleContext, cancellationToken)
6884
var callExecute = Expression.Call(
@@ -74,10 +90,10 @@ private static ExecuteModuleDelegate CreateExecutor(Type resultType)
7490
cancellationTokenParam);
7591

7692
// We need to create an async wrapper that awaits the task and casts the result to IModuleResult
77-
// Since Expression trees can't directly represent async/await, we'll use a helper method
78-
var helperMethod = typeof(ModuleExecutionDelegateFactory)
79-
.GetMethod(nameof(ExecuteAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!
80-
.MakeGenericMethod(resultType);
93+
// Since Expression trees can't directly represent async/await, we'll use a helper method (cached)
94+
var helperMethod = ExecuteAndCastAsyncMethodCache.GetOrAdd(
95+
resultType,
96+
static type => ExecuteAndCastAsyncMethodDefinition.MakeGenericMethod(type));
8197

8298
var callHelper = Expression.Call(
8399
helperMethod,

src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ internal static class ResultRepositoryDelegateFactory
2222

2323
private static readonly ConcurrentDictionary<Type, GetResultDelegate> GetResultCache = new();
2424

25+
/// <summary>
26+
/// Cache for generic MethodInfo instances created via MakeGenericMethod.
27+
/// Key is the result type, value is the specialized MethodInfo.
28+
/// </summary>
29+
private static readonly ConcurrentDictionary<Type, MethodInfo> GetResultAsyncMethodCache = new();
30+
private static readonly ConcurrentDictionary<Type, MethodInfo> GetResultAndCastAsyncMethodCache = new();
31+
32+
/// <summary>
33+
/// Base method definitions, cached once on first use.
34+
/// </summary>
35+
private static readonly MethodInfo GetResultAsyncMethodDefinition =
36+
typeof(IModuleResultRepository).GetMethod(nameof(IModuleResultRepository.GetResultAsync))!;
37+
38+
private static readonly MethodInfo GetResultAndCastAsyncMethodDefinition =
39+
typeof(ResultRepositoryDelegateFactory).GetMethod(nameof(GetResultAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
40+
2541
/// <summary>
2642
/// Gets a cached delegate for calling GetResultAsync with the specified result type.
2743
/// </summary>
@@ -42,18 +58,18 @@ private static GetResultDelegate CreateGetResultDelegate(Type resultType)
4258
// Cast module to Module<T>
4359
var castModule = Expression.Convert(moduleParam, moduleType);
4460

45-
// Get the GetResultAsync<T> method
46-
var method = typeof(IModuleResultRepository)
47-
.GetMethod(nameof(IModuleResultRepository.GetResultAsync))!
48-
.MakeGenericMethod(resultType);
61+
// Get the GetResultAsync<T> method (cached)
62+
var method = GetResultAsyncMethodCache.GetOrAdd(
63+
resultType,
64+
static type => GetResultAsyncMethodDefinition.MakeGenericMethod(type));
4965

5066
// Call: repository.GetResultAsync<T>((Module<T>)module, context)
5167
var callMethod = Expression.Call(repositoryParam, method, castModule, contextParam);
5268

53-
// We need an async helper since expression trees can't represent async
54-
var helperMethod = typeof(ResultRepositoryDelegateFactory)
55-
.GetMethod(nameof(GetResultAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!
56-
.MakeGenericMethod(resultType);
69+
// We need an async helper since expression trees can't represent async (cached)
70+
var helperMethod = GetResultAndCastAsyncMethodCache.GetOrAdd(
71+
resultType,
72+
static type => GetResultAndCastAsyncMethodDefinition.MakeGenericMethod(type));
5773

5874
var callHelper = Expression.Call(helperMethod, repositoryParam, castModule, contextParam);
5975

0 commit comments

Comments
 (0)