diff --git a/CHANGELOG.md b/CHANGELOG.md index 699c7cfbb..716b3a83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Breaking Changes: Enhancements: - Minimally improved support for methods having `ref struct` parameter and return types, such as `Span`: Intercepting such methods caused the runtime to throw `InvalidProgramException` and `NullReferenceException` due to forbidden conversions of `ref struct` values when transferring them into & out of `IInvocation` instances. To prevent these exceptions from being thrown, such values now get replaced with `null` in `IInvocation`, and with `default` values in return values and `out` arguments. When proceeding to a target, the target methods likewise receive such nullified values. (@stakx, #665) +- A new `System.Diagnostics.Tracing.EventSource` named _Castle.DynamicProxy_ publishes DynamicProxy events and metrics. Those can be inspected using tools such as [`dotnet-trace`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace) (for events) and [`dotnet-counters`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters) (for metrics). (@stakx, #717) - Dependencies were updated Bugfixes: diff --git a/README.md b/README.md index 52b0b5006..e2667de44 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,13 @@ Symbol | .NET 4.6.2 | .NET Standard 2.0 | . `FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: `FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: `FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: +`FEATURE_EVENTCOUNTER` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: `FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: `FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: * `FEATURE_APPDOMAIN` - enables support for features that make use of an AppDomain in the host. * `FEATURE_ASSEMBLYBUILDER_SAVE` - enabled support for saving the dynamically generated proxy assembly. * `FEATURE_BYREFLIKE` - enables support for by-ref-like (`ref struct`) types such as `Span` and `ReadOnlySpan`. +* `FEATURE_EVENTCOUNTER` - enables support for emitting `EventCounter`-based metrics to an `EventSource` named **Castle.DynamicProxy**. * `FEATURE_SERIALIZATION` - enables support for serialization of dynamic proxies and other types. * `FEATURE_SYSTEM_CONFIGURATION` - enables features that use System.Configuration and the ConfigurationManager. diff --git a/buildscripts/common.props b/buildscripts/common.props index b2989c095..e47bb2231 100644 --- a/buildscripts/common.props +++ b/buildscripts/common.props @@ -70,6 +70,10 @@ $(DefineConstants);FEATURE_BYREFLIKE + + $(DefineConstants);FEATURE_EVENTCOUNTER + + diff --git a/src/Castle.Core/DynamicProxy/DynamicProxyEventSource.cs b/src/Castle.Core/DynamicProxy/DynamicProxyEventSource.cs new file mode 100644 index 000000000..97f50a082 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/DynamicProxyEventSource.cs @@ -0,0 +1,131 @@ +// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable enable + +namespace Castle.DynamicProxy +{ + using System; + using System.Diagnostics.Tracing; +#if FEATURE_EVENTCOUNTER + using System.Threading; +#endif + + [EventSource(Name = "Castle.DynamicProxy")] + internal sealed class DynamicProxyEventSource : EventSource + { + public static DynamicProxyEventSource Log { get; } = new DynamicProxyEventSource(); + +#if FEATURE_EVENTCOUNTER + private int typeCacheHitCount; + private int typeCacheMissCount; + private int typeGeneratedCount; +#endif + + private DynamicProxyEventSource() + { + } + + private enum EventId + { + None, + TypeCacheHit, + TypeCacheMiss, + TypeGenerated, + } + + [NonEvent] + public void TypeCacheHit(Type requested, Type cached) + { +#if FEATURE_EVENTCOUNTER + Interlocked.Increment(ref typeCacheHitCount); +#endif + + if (IsEnabled()) + { + TypeCacheHit(requested.FullName!, cached.FullName!); + } + } + + [Event((int)EventId.TypeCacheHit)] + private void TypeCacheHit(string requested, string cached) + { + WriteEvent((int)EventId.TypeCacheHit, requested, cached); + } + + [NonEvent] + public void TypeCacheMiss(Type requested) + { +#if FEATURE_EVENTCOUNTER + Interlocked.Increment(ref typeCacheMissCount); +#endif + + if (IsEnabled()) + { + TypeCacheMiss(requested.FullName!); + } + } + + [Event((int)EventId.TypeCacheMiss)] + private void TypeCacheMiss(string requested) + { + WriteEvent((int)EventId.TypeCacheMiss, requested); + } + + [NonEvent] + public void TypeGenerated(Type type) + { +#if FEATURE_EVENTCOUNTER + Interlocked.Increment(ref typeGeneratedCount); +#endif + + if (IsEnabled()) + { + TypeGenerated(type.FullName!); + } + } + + [Event((int)EventId.TypeGenerated)] + private void TypeGenerated(string type) + { + WriteEvent((int)EventId.TypeGenerated, type); + } + +#if FEATURE_EVENTCOUNTER + protected override void OnEventCommand(EventCommandEventArgs command) + { + if (command.Command == EventCommand.Enable) + { + _ = new PollingCounter("castle.dynamic_proxy.type_cache_hit.count", this, () => typeCacheHitCount) + { + DisplayName = "Type cache hits", + DisplayUnits = "count", + }; + + _ = new PollingCounter("castle.dynamic_proxy.type_cache_miss.count", this, () => typeCacheMissCount) + { + DisplayName = "Type cache misses", + DisplayUnits = "count", + }; + + _ = new PollingCounter("castle.dynamic_proxy.type_generated.count", this, () => typeGeneratedCount) + { + DisplayName = "Types generated", + DisplayUnits = "count", + }; + } + } +#endif + } +} diff --git a/src/Castle.Core/DynamicProxy/Generators/BaseProxyGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/BaseProxyGenerator.cs index ec1a150ac..9e07acca2 100644 --- a/src/Castle.Core/DynamicProxy/Generators/BaseProxyGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/BaseProxyGenerator.cs @@ -78,6 +78,7 @@ public Type GetProxyType() { notFoundInTypeCache = true; Logger.DebugFormat("No cached proxy type was found for target type {0}.", targetType.FullName); + DynamicProxyEventSource.Log.TypeCacheMiss(requested: targetType); EnsureOptionsOverrideEqualsAndGetHashCode(); @@ -88,6 +89,7 @@ public Type GetProxyType() if (!notFoundInTypeCache) { Logger.DebugFormat("Found cached proxy type {0} for target type {1}.", proxyType.FullName, targetType.FullName); + DynamicProxyEventSource.Log.TypeCacheHit(requested: targetType, cached: proxyType); } return proxyType; diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs index c4b4ba706..4f358d61f 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs @@ -403,6 +403,7 @@ public Type BuildType() } var type = typeBuilder.CreateTypeInfo(); + DynamicProxyEventSource.Log.TypeGenerated(type); return type; }