diff --git a/docs/docs/how-to/parallelization.md b/docs/docs/how-to/parallelization.md index d52c0cec151..4be03f4f820 100644 --- a/docs/docs/how-to/parallelization.md +++ b/docs/docs/how-to/parallelization.md @@ -121,7 +121,7 @@ public class BuildProjectModule : Module public record MyParallelLimit : IParallelLimit { - public int Limit => 2; + public static int Limit => 2; } ``` diff --git a/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs b/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs index ade3becff21..08f697750b3 100644 --- a/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs +++ b/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs @@ -1,27 +1,56 @@ -using ModularPipelines.Interfaces; +using ModularPipelines.Helpers; +using ModularPipelines.Interfaces; +using Semaphores; namespace ModularPipelines.Attributes; +/// +/// Specifies a parallel execution limit for a module using a strongly-typed limit class. +/// +/// The type implementing . [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] public sealed class ParallelLimiterAttribute : ParallelLimiterAttribute - where TParallelLimit : IParallelLimit, new() + where TParallelLimit : IParallelLimit { public ParallelLimiterAttribute() : base(typeof(TParallelLimit)) { } + + /// + internal override AsyncSemaphore GetLock(IParallelLimitProvider provider) + { + return provider.GetLock(); + } } -public class ParallelLimiterAttribute : Attribute +/// +/// Base attribute for specifying parallel execution limits. +/// +public abstract class ParallelLimiterAttribute : Attribute { + /// + /// Gets the type implementing . + /// public Type Type { get; } - public ParallelLimiterAttribute(Type type) + /// + /// Initializes a new instance of the class. + /// + /// The type implementing . + protected ParallelLimiterAttribute(Type type) { if (!type.IsAssignableTo(typeof(IParallelLimit))) { - throw new Exception("Type must be of IParallelLimit"); + throw new ArgumentException("Type must implement IParallelLimit", nameof(type)); } Type = type; } + + /// + /// Gets the semaphore lock from the provider without reflection. + /// + /// The parallel limit provider. + /// The semaphore for this limit type. + internal abstract AsyncSemaphore GetLock(IParallelLimitProvider provider); } \ No newline at end of file diff --git a/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs b/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs new file mode 100644 index 00000000000..84ccd2d4b9d --- /dev/null +++ b/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs @@ -0,0 +1,68 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine.Execution; + +/// +/// Factory for creating ModuleExecutionContext instances without reflection. +/// +/// +/// Replaces Activator.CreateInstance with compiled expression trees for better performance. +/// +internal static class ExecutionContextFactory +{ + /// + /// Delegate signature for creating a ModuleExecutionContext. + /// + internal delegate ModuleExecutionContext CreateContextDelegate(IModule module, Type moduleType); + + private static readonly ConcurrentDictionary ContextFactoryCache = new(); + + /// + /// Creates a ModuleExecutionContext for the specified module. + /// + /// The module instance. + /// The type of the module. + /// A typed ModuleExecutionContext. + public static ModuleExecutionContext Create(IModule module, Type moduleType) + { + var resultType = module.ResultType; + var factory = ContextFactoryCache.GetOrAdd(resultType, CreateFactory); + return factory(module, moduleType); + } + + private static CreateContextDelegate CreateFactory(Type resultType) + { + // Parameters + var moduleParam = Expression.Parameter(typeof(IModule), "module"); + var moduleTypeParam = Expression.Parameter(typeof(Type), "moduleType"); + + // Get the generic types + var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); + var typedModuleType = typeof(Module<>).MakeGenericType(resultType); + + // Find the constructor: ModuleExecutionContext(Module module, Type moduleType) + var constructor = contextType.GetConstructor(new[] { typedModuleType, typeof(Type) }) + ?? throw new InvalidOperationException( + $"Could not find constructor for {contextType.Name} with (Module<{resultType.Name}>, Type) parameters."); + + // Cast module to Module + var castModule = Expression.Convert(moduleParam, typedModuleType); + + // Create new ModuleExecutionContext((Module)module, moduleType) + var newContext = Expression.New(constructor, castModule, moduleTypeParam); + + // Cast to base type + var castToBase = Expression.Convert(newContext, typeof(ModuleExecutionContext)); + + // Create and compile the lambda + var lambda = Expression.Lambda( + castToBase, + moduleParam, + moduleTypeParam); + + return lambda.Compile(); + } +} diff --git a/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs b/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs new file mode 100644 index 00000000000..3adcec6b793 --- /dev/null +++ b/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs @@ -0,0 +1,113 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine.Execution; + +/// +/// Factory for creating cached delegates to execute modules without runtime reflection. +/// +/// +/// This class replaces the reflection-heavy pattern of using MakeGenericMethod and GetProperty("Result") +/// with compiled expression trees that are cached per result type. +/// +internal static class ModuleExecutionDelegateFactory +{ + /// + /// Delegate signature for executing a module and returning its result. + /// + internal delegate Task ExecuteModuleDelegate( + IModuleExecutionPipeline pipeline, + IModule module, + ModuleExecutionContext executionContext, + IModuleContext moduleContext, + CancellationToken cancellationToken); + + private static readonly ConcurrentDictionary ExecutorCache = new(); + + /// + /// Gets a cached delegate for executing a module with the specified result type. + /// + /// The result type of the module (T in Module<T>). + /// A delegate that executes the module and returns its result. + public static ExecuteModuleDelegate GetExecutor(Type resultType) + { + return ExecutorCache.GetOrAdd(resultType, CreateExecutor); + } + + private static ExecuteModuleDelegate CreateExecutor(Type resultType) + { + // Parameters for the delegate + var pipelineParam = Expression.Parameter(typeof(IModuleExecutionPipeline), "pipeline"); + var moduleParam = Expression.Parameter(typeof(IModule), "module"); + var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext"); + var moduleContextParam = Expression.Parameter(typeof(IModuleContext), "moduleContext"); + var cancellationTokenParam = Expression.Parameter(typeof(CancellationToken), "cancellationToken"); + + // Get the generic types + var moduleType = typeof(Module<>).MakeGenericType(resultType); + var executionContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); + var moduleResultType = typeof(ModuleResult<>).MakeGenericType(resultType); + var taskType = typeof(Task<>).MakeGenericType(moduleResultType); + + // Cast module to Module + var castModule = Expression.Convert(moduleParam, moduleType); + + // Cast executionContext to ModuleExecutionContext + var castContext = Expression.Convert(contextParam, executionContextType); + + // Get the ExecuteAsync method + var executeMethod = typeof(IModuleExecutionPipeline) + .GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))! + .MakeGenericMethod(resultType); + + // Call pipeline.ExecuteAsync(module, executionContext, moduleContext, cancellationToken) + var callExecute = Expression.Call( + pipelineParam, + executeMethod, + castModule, + castContext, + moduleContextParam, + cancellationTokenParam); + + // We need to create an async wrapper that awaits the task and casts the result to IModuleResult + // Since Expression trees can't directly represent async/await, we'll use a helper method + var helperMethod = typeof(ModuleExecutionDelegateFactory) + .GetMethod(nameof(ExecuteAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(resultType); + + var callHelper = Expression.Call( + helperMethod, + pipelineParam, + castModule, + castContext, + moduleContextParam, + cancellationTokenParam); + + // Create and compile the lambda + var lambda = Expression.Lambda( + callHelper, + pipelineParam, + moduleParam, + contextParam, + moduleContextParam, + cancellationTokenParam); + + return lambda.Compile(); + } + + private static async Task ExecuteAndCastAsync( + IModuleExecutionPipeline pipeline, + Module module, + ModuleExecutionContext executionContext, + IModuleContext moduleContext, + CancellationToken cancellationToken) + { + var result = await pipeline.ExecuteAsync(module, executionContext, moduleContext, cancellationToken) + .ConfigureAwait(false); + return result; + } +} diff --git a/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs new file mode 100644 index 00000000000..b6f04651cbd --- /dev/null +++ b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs @@ -0,0 +1,110 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine.Execution; + +/// +/// Factory for creating ModuleResult instances without reflection. +/// +/// +/// Replaces reflection-based constructor invocation with compiled expression trees. +/// +internal static class ModuleResultFactory +{ + /// + /// Delegate for creating a ModuleResult with a null value (for skipped modules). + /// + internal delegate IModuleResult CreateSkippedResultDelegate(ModuleExecutionContext executionContext); + + /// + /// Delegate for creating a ModuleResult with an exception. + /// + internal delegate IModuleResult CreateExceptionResultDelegate(Exception exception, ModuleExecutionContext executionContext); + + private static readonly ConcurrentDictionary SkippedResultCache = new(); + private static readonly ConcurrentDictionary ExceptionResultCache = new(); + + /// + /// Creates a skipped ModuleResult for the specified result type. + /// + public static IModuleResult CreateSkipped(Type resultType, ModuleExecutionContext executionContext) + { + var factory = SkippedResultCache.GetOrAdd(resultType, CreateSkippedFactory); + return factory(executionContext); + } + + /// + /// Creates an exception ModuleResult for the specified result type. + /// + public static IModuleResult CreateException(Type resultType, Exception exception, ModuleExecutionContext executionContext) + { + var factory = ExceptionResultCache.GetOrAdd(resultType, CreateExceptionFactory); + return factory(exception, executionContext); + } + + private static CreateSkippedResultDelegate CreateSkippedFactory(Type resultType) + { + var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext"); + var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType); + var typedContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); + + // Cast to typed context + var castContext = Expression.Convert(contextParam, typedContextType); + + // Find the internal constructor: ModuleResult(T? value, ModuleExecutionContext context) + var constructor = resultGenericType.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + null, + new[] { resultType, typeof(ModuleExecutionContext) }, + null); + + if (constructor == null) + { + throw new InvalidOperationException( + $"Could not find internal constructor for ModuleResult<{resultType.Name}>(T?, ModuleExecutionContext)"); + } + + // Create: new ModuleResult(default(T), executionContext) + var defaultValue = Expression.Default(resultType); + var newResult = Expression.New(constructor, defaultValue, contextParam); + + // Cast to IModuleResult + var castToInterface = Expression.Convert(newResult, typeof(IModuleResult)); + + var lambda = Expression.Lambda(castToInterface, contextParam); + return lambda.Compile(); + } + + private static CreateExceptionResultDelegate CreateExceptionFactory(Type resultType) + { + var exceptionParam = Expression.Parameter(typeof(Exception), "exception"); + var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext"); + var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType); + + // Find the internal constructor: ModuleResult(Exception exception, ModuleExecutionContext context) + // Note: The constructor takes the base class ModuleExecutionContext, not ModuleExecutionContext + var constructor = resultGenericType.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + null, + new[] { typeof(Exception), typeof(ModuleExecutionContext) }, + null); + + if (constructor == null) + { + throw new InvalidOperationException( + $"Could not find internal constructor for ModuleResult<{resultType.Name}>(Exception, ModuleExecutionContext)"); + } + + // Create: new ModuleResult(exception, executionContext) + var newResult = Expression.New(constructor, exceptionParam, contextParam); + + // Cast to IModuleResult + var castToInterface = Expression.Convert(newResult, typeof(IModuleResult)); + + var lambda = Expression.Lambda(castToInterface, exceptionParam, contextParam); + return lambda.Compile(); + } +} diff --git a/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs b/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs index 0b5ffd7df35..421757ddac3 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs @@ -27,20 +27,13 @@ public void RegisterTerminatedResult(IModule module, Type moduleType, Exception { var resultType = module.ResultType; - // Create execution context with PipelineTerminated status - var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); - var executionContext = (ModuleExecutionContext)Activator.CreateInstance(contextType, module, moduleType)!; + // Create execution context with PipelineTerminated status using compiled delegate factory + var executionContext = ExecutionContextFactory.Create(module, moduleType); executionContext.Status = Enums.Status.PipelineTerminated; executionContext.Exception = exception; - // Create ModuleResult with the exception - var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType); - var result = (IModuleResult)Activator.CreateInstance( - resultGenericType, - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, - null, - new object[] { exception, executionContext }, - null)!; + // Create ModuleResult with the exception using compiled delegate factory + var result = ModuleResultFactory.CreateException(resultType, exception, executionContext); _resultRegistry.RegisterResult(moduleType, result); } diff --git a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs index 9a0afcb4f08..00c920f17e4 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs @@ -147,18 +147,20 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr var logger = GetOrCreateLogger(moduleType, scopedServiceProvider); var moduleContext = new ModuleContext(pipelineContext, module, executionContext, logger); - // Set up logging - use try/finally to ensure cleanup of AsyncLocal context - // Assignment MUST be inside try block to guarantee cleanup even if an exception + // Set up logging and module type context - use try/finally to ensure cleanup of AsyncLocal context + // Assignments MUST be inside try block to guarantee cleanup even if an exception // occurs immediately after assignment try { ModuleLogger.Values.Value = logger; + ModuleLogger.CurrentModuleType.Value = moduleType; await ExecuteModuleLifecycle(moduleState, scopedServiceProvider, pipelineContext, executionContext, moduleContext, cancellationToken).ConfigureAwait(false); } finally { // Clear AsyncLocal to prevent potential leaks in edge cases (thread pool reuse, long-running contexts) ModuleLogger.Values.Value = null; + ModuleLogger.CurrentModuleType.Value = null; } } @@ -259,9 +261,8 @@ private async Task ExecuteModuleLifecycle( private ModuleExecutionContext CreateExecutionContext(IModule module, Type moduleType) { - var resultType = module.ResultType; - var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); - return (ModuleExecutionContext)Activator.CreateInstance(contextType, module, moduleType)!; + // Use compiled delegate factory instead of Activator.CreateInstance + return ExecutionContextFactory.Create(module, moduleType); } private async Task ExecuteTypedModule( @@ -270,26 +271,10 @@ private async Task ExecuteTypedModule( IModuleContext moduleContext, CancellationToken cancellationToken) { - var resultType = module.ResultType; - - var executeMethodInfo = typeof(IModuleExecutionPipeline).GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync)) - ?? throw new InvalidOperationException($"Method '{nameof(IModuleExecutionPipeline.ExecuteAsync)}' not found on type '{nameof(IModuleExecutionPipeline)}'."); - - var executeMethod = executeMethodInfo.MakeGenericMethod(resultType); - - var invokeResult = executeMethod.Invoke(_executionPipeline, new object[] { module, executionContext, moduleContext, cancellationToken }) - ?? throw new InvalidOperationException($"Invocation of '{nameof(IModuleExecutionPipeline.ExecuteAsync)}' returned null."); - - var task = (Task)invokeResult; - await task.ConfigureAwait(false); - - var resultProperty = task.GetType().GetProperty("Result") - ?? throw new InvalidOperationException($"Property 'Result' not found on task type '{task.GetType().Name}'."); - - var resultValue = resultProperty.GetValue(task) - ?? throw new InvalidOperationException($"Property 'Result' returned null for task type '{task.GetType().Name}'."); - - return (IModuleResult)resultValue; + // Use compiled delegate instead of MakeGenericMethod + Invoke + GetProperty("Result") + var executor = ModuleExecutionDelegateFactory.GetExecutor(module.ResultType); + return await executor(_executionPipeline, module, executionContext, moduleContext, cancellationToken) + .ConfigureAwait(false); } private IModuleLogger GetOrCreateLogger(Type moduleType, IServiceProvider scopedServiceProvider) diff --git a/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs b/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs index f1a9c5bfeb9..2101111c9fe 100644 --- a/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs +++ b/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using ModularPipelines.Attributes; using ModularPipelines.Helpers; -using ModularPipelines.Interfaces; using ModularPipelines.Logging; namespace ModularPipelines.Engine.Execution; @@ -26,17 +25,18 @@ public ParallelLimitHandler( /// public async Task AcquireParallelLimitAsync(Type moduleType) { - var parallelLimitAttributeType = - moduleType.GetCustomAttributes().FirstOrDefault()?.Type; + var parallelLimiterAttribute = + moduleType.GetCustomAttributes().FirstOrDefault(); - if (parallelLimitAttributeType != null) + if (parallelLimiterAttribute != null) { _logger.LogDebug( "Module {ModuleName} acquiring parallel limit from {LimiterType}", MarkupFormatter.FormatModuleName(moduleType.Name), - parallelLimitAttributeType.Name); + parallelLimiterAttribute.Type.Name); - return await _parallelLimitProvider.GetLock(parallelLimitAttributeType).WaitAsync().ConfigureAwait(false); + // Use the attribute's GetLock method to avoid reflection on IParallelLimit + return await parallelLimiterAttribute.GetLock(_parallelLimitProvider).WaitAsync().ConfigureAwait(false); } return NoOpDisposable.Instance; diff --git a/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs b/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs new file mode 100644 index 00000000000..290fb3634fe --- /dev/null +++ b/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs @@ -0,0 +1,72 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine.Execution; + +/// +/// Factory for creating cached delegates to call IModuleResultRepository methods without reflection. +/// +internal static class ResultRepositoryDelegateFactory +{ + /// + /// Delegate for calling GetResultAsync on a repository. + /// + internal delegate Task GetResultDelegate( + IModuleResultRepository repository, + IModule module, + IPipelineContext context); + + private static readonly ConcurrentDictionary GetResultCache = new(); + + /// + /// Gets a cached delegate for calling GetResultAsync with the specified result type. + /// + public static GetResultDelegate GetResultDelegateFor(Type resultType) + { + return GetResultCache.GetOrAdd(resultType, CreateGetResultDelegate); + } + + private static GetResultDelegate CreateGetResultDelegate(Type resultType) + { + var repositoryParam = Expression.Parameter(typeof(IModuleResultRepository), "repository"); + var moduleParam = Expression.Parameter(typeof(IModule), "module"); + var contextParam = Expression.Parameter(typeof(IPipelineContext), "context"); + + // Get types + var moduleType = typeof(Module<>).MakeGenericType(resultType); + + // Cast module to Module + var castModule = Expression.Convert(moduleParam, moduleType); + + // Get the GetResultAsync method + var method = typeof(IModuleResultRepository) + .GetMethod(nameof(IModuleResultRepository.GetResultAsync))! + .MakeGenericMethod(resultType); + + // Call: repository.GetResultAsync((Module)module, context) + var callMethod = Expression.Call(repositoryParam, method, castModule, contextParam); + + // We need an async helper since expression trees can't represent async + var helperMethod = typeof(ResultRepositoryDelegateFactory) + .GetMethod(nameof(GetResultAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(resultType); + + var callHelper = Expression.Call(helperMethod, repositoryParam, castModule, contextParam); + + var lambda = Expression.Lambda(callHelper, repositoryParam, moduleParam, contextParam); + return lambda.Compile(); + } + + private static async Task GetResultAndCastAsync( + IModuleResultRepository repository, + Module module, + IPipelineContext context) + { + var result = await repository.GetResultAsync(module, context).ConfigureAwait(false); + return result; + } +} diff --git a/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs b/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs index 0e777620fc6..a5e80b3e9a6 100644 --- a/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs +++ b/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs @@ -1,6 +1,6 @@ -using System.Reflection; using Microsoft.Extensions.Logging; using ModularPipelines.Context; +using ModularPipelines.Engine.Execution; using ModularPipelines.Enums; using ModularPipelines.Helpers; using ModularPipelines.Models; @@ -46,7 +46,7 @@ public async Task RegisterIgnoredModuleResultsAsync(IReadOnlyList // For ignored modules, always check for historical data if a repository is configured if (_resultRepository.GetType() != typeof(NoOpModuleResultRepository)) { - var historicalResult = await TryGetHistoricalResultAsync(module, moduleType, resultType, pipelineContext).ConfigureAwait(false); + var historicalResult = await TryGetHistoricalResultAsync(module, resultType, pipelineContext).ConfigureAwait(false); if (historicalResult != null) { // Update the status to UsedHistory since we're using a cached result @@ -65,67 +65,35 @@ public async Task RegisterIgnoredModuleResultsAsync(IReadOnlyList _logger.LogDebug("Registering skipped result for ignored module {ModuleName}", MarkupFormatter.FormatModuleName(moduleType.Name)); - // Create execution context with Skipped status - var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType); - var executionContext = (ModuleExecutionContext) Activator.CreateInstance(contextType, module, moduleType)!; + // Create execution context with Skipped status using compiled delegate factory + var executionContext = ExecutionContextFactory.Create(module, moduleType); executionContext.Status = Status.Skipped; executionContext.SkipResult = ignoredModule.SkipDecision; - // Create ModuleResult with the skipped status using the value constructor (T?, ModuleExecutionContext) - var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType); - var constructor = resultGenericType.GetConstructor( - BindingFlags.NonPublic | BindingFlags.Instance, - null, - new[] { resultType, typeof(ModuleExecutionContext) }, - null); - - if (constructor == null) - { - _logger.LogWarning("Could not find constructor for ModuleResult<{ResultType}>", resultType.Name); - continue; - } - - var result = (IModuleResult) constructor.Invoke(new object?[] { null, executionContext })!; + // Create ModuleResult with the skipped status using compiled delegate factory + var result = ModuleResultFactory.CreateSkipped(resultType, executionContext); _resultRegistry.RegisterResult(moduleType, result); } } /// - /// Attempts to get a historical result for a module using reflection to call the generic GetResultAsync method. + /// Attempts to get a historical result for a module using compiled delegates to call the generic GetResultAsync method. /// private async Task TryGetHistoricalResultAsync( IModule module, - Type moduleType, Type resultType, IPipelineContext pipelineContext) { try { - // Get the generic GetResultAsync method - var getResultAsyncMethod = typeof(IModuleResultRepository) - .GetMethod(nameof(IModuleResultRepository.GetResultAsync))! - .MakeGenericMethod(resultType); - - // Invoke the method: Task?> GetResultAsync(Module module, IPipelineHookContext pipelineContext) - var task = (Task?) getResultAsyncMethod.Invoke(_resultRepository, new object[] { module, pipelineContext }); - - if (task == null) - { - return null; - } - - await task.ConfigureAwait(false); - - // Get the Result property from the completed Task?> - var resultProperty = task.GetType().GetProperty("Result"); - var historicalResult = resultProperty?.GetValue(task) as IModuleResult; - - return historicalResult; + // Use compiled delegate instead of MakeGenericMethod + Invoke + GetProperty("Result") + var getResultDelegate = ResultRepositoryDelegateFactory.GetResultDelegateFor(resultType); + return await getResultDelegate(_resultRepository, module, pipelineContext).ConfigureAwait(false); } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to get historical result for module {ModuleName}", moduleType.Name); + _logger.LogWarning(ex, "Failed to get historical result for module {ModuleName}", module.GetType().Name); return null; } } diff --git a/src/ModularPipelines/Engine/OptionsProvider.cs b/src/ModularPipelines/Engine/OptionsProvider.cs index d2ef3b9ca49..9b278846c6a 100644 --- a/src/ModularPipelines/Engine/OptionsProvider.cs +++ b/src/ModularPipelines/Engine/OptionsProvider.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Collections.Concurrent; +using System.Linq.Expressions; using Microsoft.Extensions.Options; using ModularPipelines.DependencyInjection; @@ -6,6 +7,12 @@ namespace ModularPipelines.Engine; internal class OptionsProvider : IOptionsProvider { + /// + /// Cache of compiled property accessors for IOptions<T>.Value. + /// Avoids repeated reflection for property access. + /// + private static readonly ConcurrentDictionary> ValueGetterCache = new(); + private readonly IPipelineServiceContainerWrapper _pipelineServiceContainerWrapper; private readonly IServiceProvider _serviceProvider; @@ -44,10 +51,26 @@ public OptionsProvider(IPipelineServiceContainerWrapper pipelineServiceContainer continue; } - var valueProperty = option.GetType().GetProperty("Value", BindingFlags.Public | BindingFlags.Instance) - ?? throw new InvalidOperationException($"Property 'Value' not found on type '{option.GetType().Name}'."); - - yield return valueProperty.GetValue(option); + // Use cached compiled delegate instead of reflection + var getter = ValueGetterCache.GetOrAdd(option.GetType(), CreateValueGetter); + yield return getter(option); } } + + /// + /// Creates a compiled delegate to access the Value property of an IOptions<T> instance. + /// + private static Func CreateValueGetter(Type optionsType) + { + var valueProperty = optionsType.GetProperty("Value") + ?? throw new InvalidOperationException($"Property 'Value' not found on type '{optionsType.Name}'."); + + // Build: (object obj) => (object?)((OptionsType)obj).Value + var param = Expression.Parameter(typeof(object), "obj"); + var cast = Expression.Convert(param, optionsType); + var propertyAccess = Expression.Property(cast, valueProperty); + var convertToObject = Expression.Convert(propertyAccess, typeof(object)); + + return Expression.Lambda>(convertToObject, param).Compile(); + } } \ No newline at end of file diff --git a/src/ModularPipelines/Helpers/IParallelLimitProvider.cs b/src/ModularPipelines/Helpers/IParallelLimitProvider.cs index 0c48ee05109..95bee9db13b 100644 --- a/src/ModularPipelines/Helpers/IParallelLimitProvider.cs +++ b/src/ModularPipelines/Helpers/IParallelLimitProvider.cs @@ -1,11 +1,16 @@ using ModularPipelines.Enums; +using ModularPipelines.Interfaces; using Semaphores; namespace ModularPipelines.Helpers; internal interface IParallelLimitProvider { - AsyncSemaphore GetLock(Type parallelLimitType); + /// + /// Gets a semaphore lock for the specified parallel limit type. + /// Uses static abstract interface member to avoid reflection. + /// + AsyncSemaphore GetLock() where TParallelLimit : IParallelLimit; int GetMaxDegreeOfParallelism(); diff --git a/src/ModularPipelines/Helpers/ParallelLimitProvider.cs b/src/ModularPipelines/Helpers/ParallelLimitProvider.cs index d4a7f7df876..866834d8d15 100644 --- a/src/ModularPipelines/Helpers/ParallelLimitProvider.cs +++ b/src/ModularPipelines/Helpers/ParallelLimitProvider.cs @@ -30,24 +30,21 @@ public ParallelLimitProvider(IOptions pipelineOptions) : null); } - public AsyncSemaphore GetLock(Type parallelLimitType) + /// + /// Gets a semaphore lock for the specified parallel limit type. + /// Uses static abstract interface member to avoid reflection. + /// + public AsyncSemaphore GetLock() where TParallelLimit : IParallelLimit { - var parallelLimit = Activator.CreateInstance(parallelLimitType) as IParallelLimit; - if (parallelLimit is null) - { - throw new ArgumentException( - $"Type '{parallelLimitType.FullName}' must implement {nameof(IParallelLimit)} and have a parameterless constructor.", - nameof(parallelLimitType)); - } + var limit = TParallelLimit.Limit; - if (parallelLimit.Limit <= 0) + if (limit <= 0) { throw new ArgumentException( - $"Parallel limit for type '{parallelLimitType.FullName}' must be a positive integer, but was {parallelLimit.Limit}.", - nameof(parallelLimitType)); + $"Parallel limit for type '{typeof(TParallelLimit).FullName}' must be a positive integer, but was {limit}."); } - return _locks.GetOrAdd(parallelLimit.GetType(), _ => new AsyncSemaphore(parallelLimit.Limit)); + return _locks.GetOrAdd(typeof(TParallelLimit), _ => new AsyncSemaphore(limit)); } public int GetMaxDegreeOfParallelism() diff --git a/src/ModularPipelines/Interfaces/IParallelLimit.cs b/src/ModularPipelines/Interfaces/IParallelLimit.cs index f0865cd85d6..755942d01be 100644 --- a/src/ModularPipelines/Interfaces/IParallelLimit.cs +++ b/src/ModularPipelines/Interfaces/IParallelLimit.cs @@ -3,10 +3,23 @@ /// /// Defines a limit for parallel execution. /// +/// +/// Implementations must provide a static property. +/// This is a breaking change from the previous instance-based design, +/// enabling compile-time resolution without reflection. +/// +/// +/// +/// public class MyDatabaseLimit : IParallelLimit +/// { +/// public static int Limit => 5; +/// } +/// +/// public interface IParallelLimit { /// /// Gets the maximum number of operations that can execute in parallel. /// - int Limit { get; } + static abstract int Limit { get; } } \ No newline at end of file diff --git a/src/ModularPipelines/Logging/ModuleLogger.cs b/src/ModularPipelines/Logging/ModuleLogger.cs index 03b67738122..c17493e6ea6 100644 --- a/src/ModularPipelines/Logging/ModuleLogger.cs +++ b/src/ModularPipelines/Logging/ModuleLogger.cs @@ -9,7 +9,7 @@ namespace ModularPipelines.Logging; /// Base class for module-specific loggers with ambient context support. /// /// -/// This class uses AsyncLocal to provide ambient context for the current module's logger. +/// This class uses AsyncLocal to provide ambient context for the current module's logger and type. /// This allows File/Folder helpers and other utilities to access the logger without explicit parameter passing. /// AsyncLocal is thread-safe and flows with async/await contexts, making it ideal for async module execution. /// @@ -25,6 +25,12 @@ internal abstract class ModuleLogger : IModuleLogger, IConsoleWriter /// internal static readonly AsyncLocal Values = new(); + /// + /// Ambient context storage for the current module's type. + /// Enables fast module type detection without stack trace inspection. + /// + internal static readonly AsyncLocal CurrentModuleType = new(); + /// /// Gets the current logger from ambient context, or a null logger if none is set. /// diff --git a/src/ModularPipelines/Logging/ModuleLoggerProvider.cs b/src/ModularPipelines/Logging/ModuleLoggerProvider.cs index 76d484402a0..3fa69d3646d 100644 --- a/src/ModularPipelines/Logging/ModuleLoggerProvider.cs +++ b/src/ModularPipelines/Logging/ModuleLoggerProvider.cs @@ -42,11 +42,20 @@ public IModuleLogger GetLogger() return _moduleLogger; } + // Fast path: check if logger is already set in AsyncLocal if (ModuleLogger.Values.Value != null) { return _moduleLogger = ModuleLogger.Values.Value; } + // Fast path: check if module type is set in AsyncLocal (avoids stack trace inspection) + var moduleType = ModuleLogger.CurrentModuleType.Value; + if (moduleType != null) + { + return MakeLogger(moduleType); + } + + // Fallback: use stack trace inspection (for edge cases where AsyncLocal context is lost) var detectedType = _stackTraceDetector.DetectModuleType(); if (detectedType == null) diff --git a/test/ModularPipelines.UnitTests/ParallelLimiterTests.cs b/test/ModularPipelines.UnitTests/ParallelLimiterTests.cs index 26132fa134a..442f35d3f43 100644 --- a/test/ModularPipelines.UnitTests/ParallelLimiterTests.cs +++ b/test/ModularPipelines.UnitTests/ParallelLimiterTests.cs @@ -168,6 +168,6 @@ await TestPipelineHostBuilder.Create(new TestHostSettings(), timeProvider) private record MyParallelLimit : IParallelLimit { - public int Limit => 3; + public static int Limit => 3; } } \ No newline at end of file