From f24a5daa2cc6117a73eac96f2a45d738335d063a Mon Sep 17 00:00:00 2001 From: Marty T <120425148+tippmar-nr@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:23:39 -0500 Subject: [PATCH] feat: New Garbage Collection Metrics Sampler for .NET 6+ (#2838) --- .../Core/AgentHealth/AgentHealthReporter.cs | 11 + src/Agent/NewRelic/Agent/Core/AgentManager.cs | 4 +- .../Core/Config/BootstrapConfiguration.cs | 36 ++++ .../Configuration/DefaultConfiguration.cs | 6 +- .../Configuration/ReportedConfiguration.cs | 2 + .../Core/DependencyInjection/AgentServices.cs | 29 ++- .../Agent/Core/Metrics/MetricNames.cs | 15 ++ .../Agent/Core/Samplers/GCSampleType.cs | 90 +++++++++ .../Agent/Core/Samplers/GCSamplerV2.cs | 75 +++++++ .../Samplers/GCSamplerV2ReflectionHelper.cs | 112 +++++++++++ .../NewRelic/Agent/Core/Samplers/GcSampler.cs | 24 +-- .../Agent/Core/Samplers/ImmutableGCSample.cs | 67 +++++++ .../Transformers/GCSampleTransformerV2.cs | 129 ++++++++++++ .../Configuration/IConfiguration.cs | 2 + .../Reflection/VisibilityBypasser.cs | 34 ++++ .../NewRelicConfigModifier.cs | 9 + .../AgentMetrics/DotNetPerfMetricsTests.cs | 61 +++++- .../CompositeTests/CompositeTestAgent.cs | 6 +- .../AgentHealth/AgentHealthReporterTests.cs | 9 + .../Config/BootstrapConfigurationTests.cs | 67 +++++++ .../DataTransport/AgentSettingsTests.cs | 3 +- .../DataTransport/ConnectModelTests.cs | 3 +- .../ExhaustiveTestConfiguration.cs | 4 +- .../DependencyInjection/AgentServicesTests.cs | 65 +++++- .../Core.UnitTest/Metrics/MetricNamesTests.cs | 14 ++ .../Samplers/GCSamplerV2Tests.cs | 105 ++++++++++ .../Samplers/ImmutableGCSampleTests.cs | 67 +++++++ .../GCSampleTransformerV2Tests.cs | 188 ++++++++++++++++++ .../GCStatsSampleTransformerTests.cs | 7 +- .../Transformers/MetricTestHelpers.cs | 17 +- .../Reflection/VisibilityBypasserTests.cs | 67 ++++++- 31 files changed, 1272 insertions(+), 56 deletions(-) create mode 100644 src/Agent/NewRelic/Agent/Core/Samplers/GCSampleType.cs create mode 100644 src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2.cs create mode 100644 src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2ReflectionHelper.cs create mode 100644 src/Agent/NewRelic/Agent/Core/Samplers/ImmutableGCSample.cs create mode 100644 src/Agent/NewRelic/Agent/Core/Transformers/GCSampleTransformerV2.cs create mode 100644 tests/Agent/UnitTests/Core.UnitTest/Samplers/GCSamplerV2Tests.cs create mode 100644 tests/Agent/UnitTests/Core.UnitTest/Samplers/ImmutableGCSampleTests.cs create mode 100644 tests/Agent/UnitTests/Core.UnitTest/Transformers/GCSampleTransformerV2Tests.cs diff --git a/src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs b/src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs index 15dc94c244..53fce6b123 100644 --- a/src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs +++ b/src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs @@ -683,6 +683,7 @@ private void CollectOneTimeMetrics() ReportInfiniteTracingOneTimeMetrics(); ReportIfLoggingDisabled(); ReportIfInstrumentationIsDisabled(); + ReportIfGCSamplerV2IsEnabled(); } public void CollectMetrics() @@ -838,5 +839,15 @@ private void ReportIfInstrumentationIsDisabled() ReportSupportabilityGaugeMetric(MetricNames.SupportabilityIgnoredInstrumentation, ignoredCount); } } + + private void ReportIfGCSamplerV2IsEnabled() + { + if (_configuration.GCSamplerV2Enabled) + { + ReportSupportabilityCountMetric(MetricNames.SupportabilityGCSamplerV2Enabled); + } + + } + } } diff --git a/src/Agent/NewRelic/Agent/Core/AgentManager.cs b/src/Agent/NewRelic/Agent/Core/AgentManager.cs index 718fc0896c..00d9f31666 100644 --- a/src/Agent/NewRelic/Agent/Core/AgentManager.cs +++ b/src/Agent/NewRelic/Agent/Core/AgentManager.cs @@ -115,7 +115,7 @@ private AgentManager() } _container = AgentServices.GetContainer(); - AgentServices.RegisterServices(_container, bootstrapConfig.ServerlessModeEnabled); + AgentServices.RegisterServices(_container, bootstrapConfig.ServerlessModeEnabled, bootstrapConfig.GCSamplerV2Enabled); // Resolve IConfigurationService (so that it starts listening to config change events) and then publish the serialized event _container.Resolve(); @@ -162,7 +162,7 @@ private AgentManager() Log.Info("The New Relic agent is operating in serverless mode."); } - AgentServices.StartServices(_container, bootstrapConfig.ServerlessModeEnabled); + AgentServices.StartServices(_container, bootstrapConfig.ServerlessModeEnabled, bootstrapConfig.GCSamplerV2Enabled); // Setup the internal API first so that AgentApi can use it. InternalApi.SetAgentApiImplementation(agentApi); diff --git a/src/Agent/NewRelic/Agent/Core/Config/BootstrapConfiguration.cs b/src/Agent/NewRelic/Agent/Core/Config/BootstrapConfiguration.cs index 627945ef39..677896d980 100644 --- a/src/Agent/NewRelic/Agent/Core/Config/BootstrapConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Core/Config/BootstrapConfiguration.cs @@ -2,11 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using NewRelic.Agent.Core.Configuration; using NewRelic.Agent.Core.Utilities; using NewRelic.Agent.Extensions.Logging; using NewRelic.Agent.Core.SharedInterfaces; +using NewRelic.Agent.Extensions.SystemExtensions.Collections.Generic; namespace NewRelic.Agent.Core.Config { @@ -21,6 +24,7 @@ public interface IBootstrapConfiguration string ServerlessFunctionName { get; } string ServerlessFunctionVersion { get; } bool AzureFunctionModeDetected { get; } + bool GCSamplerV2Enabled { get; } } /// @@ -64,6 +68,7 @@ public BootstrapConfiguration(configuration localConfiguration, string configura public BootstrapConfiguration(configuration localConfiguration, string configurationFileName, Func> getWebConfigSettingWithProvenance, IConfigurationManagerStatic configurationManagerStatic, IProcessStatic processStatic, Predicate checkDirectoryExists, Func getFullPath) { ServerlessModeEnabled = CheckServerlessModeEnabled(localConfiguration); + GCSamplerV2Enabled = CheckGCSamplerV2Enabled(TryGetAppSettingAsBoolWithDefault(localConfiguration, "GCSamplerV2Enabled", false)); DebugStartupDelaySeconds = localConfiguration.debugStartupDelaySeconds; ConfigurationFileName = configurationFileName; LogConfig = new BootstrapLogConfig(localConfiguration.log, processStatic, checkDirectoryExists, getFullPath); @@ -133,6 +138,8 @@ public string AgentEnabledAt public bool AzureFunctionModeDetected => ConfigLoaderHelpers.GetEnvironmentVar("FUNCTIONS_WORKER_RUNTIME") != null; + public bool GCSamplerV2Enabled { get; private set;} + private bool CheckServerlessModeEnabled(configuration localConfiguration) { // We may need these later even if we don't use it now. @@ -154,6 +161,11 @@ private bool CheckServerlessModeEnabled(configuration localConfiguration) return localConfiguration.serverlessModeEnabled; } + private bool CheckGCSamplerV2Enabled(bool localConfigurationGcSamplerV2Enabled) + { + return localConfigurationGcSamplerV2Enabled || (ConfigLoaderHelpers.GetEnvironmentVar("NEW_RELIC_GC_SAMPLER_V2_ENABLED").TryToBoolean(out var enabledViaEnvVariable) && enabledViaEnvVariable); + } + private void SetAgentEnabledValues() { _agentEnabledWithProvenance = TryGetAgentEnabledFromWebConfig(); @@ -204,6 +216,30 @@ private ValueWithProvenance TryGetAgentEnabledSetting(Func TransformAppSettings(configuration localConfiguration) + { + if (localConfiguration.appSettings == null) + return new Dictionary(); + + return localConfiguration.appSettings + .Where(setting => setting != null) + .Select(setting => new KeyValuePair(setting.key, setting.value)) + .ToDictionary(IEnumerableExtensions.DuplicateKeyBehavior.KeepFirst); + } + + private bool TryGetAppSettingAsBoolWithDefault(configuration localConfiguration, string key, bool defaultValue) + { + var value = TransformAppSettings(localConfiguration).GetValueOrDefault(key); + + bool parsedBool; + var parsedSuccessfully = bool.TryParse(value, out parsedBool); + if (!parsedSuccessfully) + return defaultValue; + + return parsedBool; + } + + private class BootstrapLogConfig : ILogConfig { private readonly string _directoryFromLocalConfig; diff --git a/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs b/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs index 0db186bbfb..e87e7b1c80 100644 --- a/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs @@ -1912,7 +1912,6 @@ public bool UtilizationDetectAzureFunction } } - public int? UtilizationLogicalProcessors { get @@ -2163,7 +2162,8 @@ public string AzureFunctionResourceIdWithFunctionName(string functionName) return string.Empty; } - return $"{AzureFunctionResourceId}/functions/{functionName}"; } + return $"{AzureFunctionResourceId}/functions/{functionName}"; + } public string AzureFunctionResourceGroupName { @@ -2466,6 +2466,8 @@ public TimeSpan StackExchangeRedisCleanupCycle } } + public bool GCSamplerV2Enabled => _bootstrapConfiguration.GCSamplerV2Enabled; + #endregion #region Helpers diff --git a/src/Agent/NewRelic/Agent/Core/Configuration/ReportedConfiguration.cs b/src/Agent/NewRelic/Agent/Core/Configuration/ReportedConfiguration.cs index ec4c150f0a..c60251e4a8 100644 --- a/src/Agent/NewRelic/Agent/Core/Configuration/ReportedConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Core/Configuration/ReportedConfiguration.cs @@ -711,6 +711,8 @@ public ReportedConfiguration(IConfiguration configuration) public string AzureFunctionResourceIdWithFunctionName(string functionName) => _configuration.AzureFunctionResourceIdWithFunctionName(functionName); + [JsonProperty("gc_sampler_v2.enabled")] + public bool GCSamplerV2Enabled => _configuration.GCSamplerV2Enabled; public IReadOnlyDictionary GetAppSettings() { diff --git a/src/Agent/NewRelic/Agent/Core/DependencyInjection/AgentServices.cs b/src/Agent/NewRelic/Agent/Core/DependencyInjection/AgentServices.cs index a5a6bc9f69..0b424e3434 100644 --- a/src/Agent/NewRelic/Agent/Core/DependencyInjection/AgentServices.cs +++ b/src/Agent/NewRelic/Agent/Core/DependencyInjection/AgentServices.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +#if NETFRAMEWORK using System.Threading; +#endif using NewRelic.Agent.Api; using NewRelic.Agent.Configuration; using NewRelic.Agent.Core.AgentHealth; @@ -58,7 +60,8 @@ public static IContainer GetContainer() /// /// /// - public static void RegisterServices(IContainer container, bool serverlessModeEnabled) + /// + public static void RegisterServices(IContainer container, bool serverlessModeEnabled, bool gcSamplerV2Enabled) { // we register this factory instead of just loading the storage contexts here because deferring the logic gives us a logger container.RegisterFactory>(ExtensionsLoader.LoadContextStorageFactories); @@ -91,9 +94,18 @@ public static void RegisterServices(IContainer container, bool serverlessModeEna container.Register(); container.Register(); #else - container.RegisterInstance>>>(() => new GCEventsListener()); - container.RegisterInstance>(GCSamplerNetCore.FXsamplerIsApplicableToFrameworkDefault); - container.Register(); + if (gcSamplerV2Enabled) + { + container.Register(); + container.Register(); + container.Register(); + } + else + { + container.RegisterInstance>>>(() => new GCEventsListener()); + container.RegisterInstance>(GCSamplerNetCore.FXsamplerIsApplicableToFrameworkDefault); + container.Register(); + } #endif container.Register(); @@ -225,7 +237,7 @@ public static void RegisterServices(IContainer container, bool serverlessModeEna /// /// Starts all of the services needed by resolving them. /// - public static void StartServices(IContainer container, bool serverlessModeEnabled) + public static void StartServices(IContainer container, bool serverlessModeEnabled, bool gcSamplerV2Enabled) { if (!serverlessModeEnabled) container.Resolve(); @@ -242,7 +254,12 @@ public static void StartServices(IContainer container, bool serverlessModeEnable samplerStartThread.Start(); #else if (!serverlessModeEnabled) - container.Resolve().Start(); + { + if (!gcSamplerV2Enabled) + container.Resolve().Start(); + else + container.Resolve().Start(); + } #endif if (!serverlessModeEnabled) { diff --git a/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs b/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs index 43851cf6f8..2ea94148d3 100644 --- a/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs +++ b/src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs @@ -837,6 +837,7 @@ public static string GetSupportabilityInstallType(string installType) public const string SupportabilityLoggingFatalError = "Supportability/DotNET/AgentLogging/DisabledDueToError"; public const string SupportabilityIgnoredInstrumentation = SupportabilityDotnetPs + "IgnoredInstrumentation"; + public const string SupportabilityGCSamplerV2Enabled = SupportabilityDotnetPs + "GCSamplerV2/Enabled"; #endregion Supportability @@ -1034,6 +1035,20 @@ public static string GetThreadpoolThroughputStatsName(ThreadpoolThroughputStatsT { GCSampleType.LOHSize , "GC/LOH/Size" }, { GCSampleType.LOHSurvived, "GC/LOH/Survived" }, + + { GCSampleType.LOHCollectionCount, "GC/LOH/Collections" }, + { GCSampleType.POHCollectionCount, "GC/POH/Collections" }, + + { GCSampleType.TotalHeapMemory, "GC/Heap/Total" }, + { GCSampleType.TotalCommittedMemory, "GC/Heap/Committed" }, + { GCSampleType.TotalAllocatedMemory, "GC/Heap/Allocated" }, + + { GCSampleType.Gen0FragmentationSize, "GC/Gen0/Fragmentation" }, + { GCSampleType.Gen1FragmentationSize, "GC/Gen1/Fragmentation" }, + { GCSampleType.Gen2FragmentationSize, "GC/Gen2/Fragmentation" }, + { GCSampleType.LOHFragmentationSize, "GC/LOH/Fragmentation" }, + { GCSampleType.POHFragmentationSize, "GC/POH/Fragmentation" }, + { GCSampleType.POHSize, "GC/POH/Size" } }; public static string GetGCMetricName(GCSampleType sampleType) diff --git a/src/Agent/NewRelic/Agent/Core/Samplers/GCSampleType.cs b/src/Agent/NewRelic/Agent/Core/Samplers/GCSampleType.cs new file mode 100644 index 0000000000..26b9701f80 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Samplers/GCSampleType.cs @@ -0,0 +1,90 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NewRelic.Agent.Core.Samplers +{ + public enum GCSampleType + { + /// + /// Gen 0 heap size as of the current sample + /// + Gen0Size, + Gen0Promoted, + /// + /// Gen 1 heap size as of the current sample + /// + Gen1Size, + Gen1Promoted, + /// + /// Gen 2 heap size as of the current sample + /// + Gen2Size, + Gen2Survived, + /// + /// Large object heap size as of the current sample + /// + LOHSize, + LOHSurvived, + HandlesCount, + InducedCount, + PercentTimeInGc, + /// + /// Gen 0 heap collection count since the last sample + /// + Gen0CollectionCount, + /// + /// Gen 1 heap collection count since the last sample + /// + Gen1CollectionCount, + /// + /// Gen 2 heap collection count since the last sample + /// + Gen2CollectionCount, + + // the following are supported by GCSamplerV2 only + /// + /// Pinned object heap size + /// + POHSize, + /// + /// Large object heap collection count since the last sample + /// + LOHCollectionCount, + /// + /// Pinned object heap collection count since the last sample + /// + POHCollectionCount, + /// + /// Total heap memory in use as of the current sample + /// + TotalHeapMemory, + /// + /// Total committed memory in use as of the current sample + /// + TotalCommittedMemory, + /// + /// Total heap memory allocated since the last sample + /// + TotalAllocatedMemory, + /// + /// Fragmentation of the Gen 0 heap as of the current sample + /// + Gen0FragmentationSize, + /// + /// Fragmentation of the Gen 1 heap as of the current sample + /// + Gen1FragmentationSize, + /// + /// Fragmentation of the Gen 2 heap as of the current sample + /// + Gen2FragmentationSize, + /// + /// Fragmentation of the Large Object heap as of the current sample + /// + LOHFragmentationSize, + /// + /// Fragmentation of the Pinned Object heap as of the current sample + /// + POHFragmentationSize, + } +} diff --git a/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2.cs b/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2.cs new file mode 100644 index 0000000000..70921a8bea --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2.cs @@ -0,0 +1,75 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#if NETSTANDARD + +using System; +using NewRelic.Agent.Core.Time; +using NewRelic.Agent.Core.Transformers; +using NewRelic.Agent.Extensions.Logging; + +namespace NewRelic.Agent.Core.Samplers +{ + public class GCSamplerV2 : AbstractSampler + { + private readonly IGCSampleTransformerV2 _transformer; + private DateTime _lastSampleTime; + + private IGCSamplerV2ReflectionHelper _gCSamplerV2ReflectionHelper; + private bool _hasGCOccurred; + + private const int GCSamplerV2IntervalSeconds = 60; + + public GCSamplerV2(IScheduler scheduler, IGCSampleTransformerV2 transformer, IGCSamplerV2ReflectionHelper gCSamplerV2ReflectionHelper) + : base(scheduler, TimeSpan.FromSeconds(GCSamplerV2IntervalSeconds)) + { + _transformer = transformer; + _gCSamplerV2ReflectionHelper = gCSamplerV2ReflectionHelper; + _lastSampleTime = DateTime.UtcNow; + } + + public override void Sample() + { + if (_gCSamplerV2ReflectionHelper.ReflectionFailed) + { + Stop(); + Log.Error($"Unable to get GC sample due to reflection error. No GC metrics will be reported."); + return; + } + + _hasGCOccurred |= _gCSamplerV2ReflectionHelper.HasGCOccurred; + + if (!_hasGCOccurred) // don't do anything until at least one GC has completed + return; + + dynamic gcMemoryInfo = _gCSamplerV2ReflectionHelper.GCGetMemoryInfo_Invoker(0); // GCKind.Any + dynamic generationInfo = _gCSamplerV2ReflectionHelper.GetGenerationInfo(gcMemoryInfo); + + var genInfoLength = generationInfo.Length; + var heapSizesBytes = new long[genInfoLength]; + var fragmentationSizesBytes = new long[genInfoLength]; + var collectionCounts = new int[genInfoLength]; + + var index = 0; + foreach (var generation in generationInfo) + { + var generationIndex = index++; + heapSizesBytes[generationIndex] = generation.SizeAfterBytes; + fragmentationSizesBytes[generationIndex] = generation.FragmentationAfterBytes; + + collectionCounts[generationIndex] = GC.CollectionCount(generationIndex); + } + + var totalMemoryBytes = GC.GetTotalMemory(false); + var totalAllocatedBytes = (long)_gCSamplerV2ReflectionHelper.GCGetTotalAllocatedBytes_Invoker(false); + var totalCommittedBytes = gcMemoryInfo.TotalCommittedBytes; + + var currentSampleTime = DateTime.UtcNow; + + var sample = new ImmutableGCSample(currentSampleTime, _lastSampleTime, totalMemoryBytes, totalAllocatedBytes, totalCommittedBytes, heapSizesBytes, collectionCounts, fragmentationSizesBytes); + _transformer.Transform(sample); + _lastSampleTime = currentSampleTime; + } + } +} +#endif diff --git a/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2ReflectionHelper.cs b/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2ReflectionHelper.cs new file mode 100644 index 0000000000..9352304e60 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Samplers/GCSamplerV2ReflectionHelper.cs @@ -0,0 +1,112 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Linq.Expressions; +using System.Reflection; +using NewRelic.Agent.Extensions.Logging; +using NewRelic.Reflection; + +namespace NewRelic.Agent.Core.Samplers +{ + // to allow for unit testing + public interface IGCSamplerV2ReflectionHelper + { + Func GetGenerationInfo { get; } + bool ReflectionFailed { get; } + Func GCGetMemoryInfo_Invoker { get; } + Func GCGetTotalAllocatedBytes_Invoker { get; } + bool HasGCOccurred { get; } + } + + public class GCSamplerV2ReflectionHelper : IGCSamplerV2ReflectionHelper + { + public Func GetGenerationInfo { get; private set; } + public bool ReflectionFailed { get; private set; } + public Func GCGetMemoryInfo_Invoker { get; private set; } + public Func GCGetTotalAllocatedBytes_Invoker { get; private set; } + + public GCSamplerV2ReflectionHelper() + { + try + { + var assembly = Assembly.Load("System.Runtime"); + var gcType = assembly.GetType("System.GC"); + var paramType = assembly.GetType("System.GCKind"); + var returnType = assembly.GetType("System.GCMemoryInfo"); + + if (!VisibilityBypasser.Instance.TryGenerateOneParameterStaticMethodCaller(gcType, "GetGCMemoryInfo", paramType, returnType, out var accessor)) + { + ReflectionFailed = true; + } + else + GCGetMemoryInfo_Invoker = accessor; + + if (!ReflectionFailed) + { + paramType = typeof(bool); + returnType = typeof(long); + if (!VisibilityBypasser.Instance.TryGenerateOneParameterStaticMethodCaller(gcType, "GetTotalAllocatedBytes", paramType, returnType, out var accessor1)) + { + ReflectionFailed = true; + } + else + GCGetTotalAllocatedBytes_Invoker = accessor1; + } + + if (!ReflectionFailed) + GetGenerationInfo = GCMemoryInfoHelper.GenerateGetMemoryInfoMethod(); + } + catch (Exception e) + { + Log.Warn(e, $"Failed to initialize GCSamplerV2ReflectionHelper."); + ReflectionFailed = true; + } + } + + public bool HasGCOccurred => GC.CollectionCount(0) > 0; + } + + internal static class GCMemoryInfoHelper + { + /// + /// Generate a function that takes a GCMemoryInfo instance as an input parameter and + /// returns an array of GCGenerationInfo instances. + /// + /// Essentially builds the equivalent of + /// object Foo(object input) => ((GCMemoryInfo)input).GenerationInfo.ToArray(); + /// + public static Func GenerateGetMemoryInfoMethod() + { + var assembly = Assembly.Load("System.Runtime"); + var gcMemoryInfoType = assembly.GetType("System.GCMemoryInfo"); + + // Define a parameter expression for the input object + var inputParameter = Expression.Parameter(typeof(object), "input"); + + // Cast the input parameter to GCMemoryInfo + var gcMemoryInfoParameter = Expression.Convert(inputParameter, gcMemoryInfoType); + + // Get the GenerationInfo property + var generationInfoProperty = gcMemoryInfoType.GetProperty("GenerationInfo"); + + // Access the GenerationInfo property + var accessGenerationInfo = Expression.Property(gcMemoryInfoParameter, generationInfoProperty); + + // Get the ReadOnlySpan type using the full type name + var readOnlySpanType = assembly.GetType("System.ReadOnlySpan`1[[System.GCGenerationInfo, System.Private.CoreLib]]"); + + // Get the ToArray method of ReadOnlySpan + var toArrayMethod = readOnlySpanType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance); + + // Call ToArray() on GenerationInfo + var callToArray = Expression.Call(accessGenerationInfo, toArrayMethod); + + // Create a lambda expression + var lambda = Expression.Lambda>(Expression.Convert(callToArray, typeof(object)), inputParameter); + + // Compile the lambda expression into a delegate + return lambda.Compile(); + } + } +} diff --git a/src/Agent/NewRelic/Agent/Core/Samplers/GcSampler.cs b/src/Agent/NewRelic/Agent/Core/Samplers/GcSampler.cs index 5e6e5e8760..10305cd719 100644 --- a/src/Agent/NewRelic/Agent/Core/Samplers/GcSampler.cs +++ b/src/Agent/NewRelic/Agent/Core/Samplers/GcSampler.cs @@ -1,6 +1,8 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK + using System; using System.Collections.Generic; using System.Linq; @@ -12,26 +14,6 @@ namespace NewRelic.Agent.Core.Samplers { - public enum GCSampleType - { - Gen0Size, - Gen0Promoted, - Gen1Size, - Gen1Promoted, - Gen2Size, - Gen2Survived, - LOHSize, - LOHSurvived, - HandlesCount, - InducedCount, - PercentTimeInGc, - Gen0CollectionCount, - Gen1CollectionCount, - Gen2CollectionCount - } - -#if NETFRAMEWORK - public class GcSampler : AbstractSampler { private const string GCPerfCounterCategoryName = ".NET CLR Memory"; @@ -345,5 +327,5 @@ public override void Sample() } } } -#endif } +#endif diff --git a/src/Agent/NewRelic/Agent/Core/Samplers/ImmutableGCSample.cs b/src/Agent/NewRelic/Agent/Core/Samplers/ImmutableGCSample.cs new file mode 100644 index 0000000000..eb7aed3a3b --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Samplers/ImmutableGCSample.cs @@ -0,0 +1,67 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace NewRelic.Agent.Core.Samplers +{ + public class ImmutableGCSample + { + public readonly DateTime LastSampleTime; + public readonly DateTime CurrentSampleTime; + + public readonly long TotalMemoryBytes; // In-use memory on the GC heap as of current GC + public readonly long TotalAllocatedBytes; // total memory allocated on GC heap since process start + public readonly long TotalCommittedBytes;// committed virtual memory as of current GC + + public readonly long[] GCHeapSizesBytes; // heap sizes as of current GC + public readonly int[] GCCollectionCounts; // number of collections since last sample + public readonly long[] GCFragmentationSizesBytes; // heap fragmentation size as of current GC + + public ImmutableGCSample() + { + LastSampleTime = CurrentSampleTime = DateTime.MinValue; + GCHeapSizesBytes = new long[5]; + GCCollectionCounts = new int[5]; + GCFragmentationSizesBytes = new long[5]; + } + + public ImmutableGCSample(DateTime lastSampleTime, DateTime currentSampleTime, long totalMemoryBytes, long totalAllocatedBytes, long totalCommittedBytes, long[] heapSizesBytes, int[] rawCollectionCounts, long[] fragmentationSizesBytes) + { + LastSampleTime = lastSampleTime; + CurrentSampleTime = currentSampleTime; + + TotalMemoryBytes = totalMemoryBytes; + + TotalAllocatedBytes = totalAllocatedBytes; + TotalCommittedBytes = totalCommittedBytes; + + GCHeapSizesBytes = heapSizesBytes; + GCFragmentationSizesBytes = fragmentationSizesBytes; + + // should always be 5, but handle smaller just in case + var collectionLength = rawCollectionCounts.Length; + GCCollectionCounts = new int[5]; // we always report 5 samples + + // Gen 0 + GCCollectionCounts[0] = rawCollectionCounts[0] - rawCollectionCounts[1]; + // Gen 1 + GCCollectionCounts[1] = rawCollectionCounts[1] - rawCollectionCounts[2]; + + // Gen 2 + if (collectionLength > 3) + GCCollectionCounts[2] = rawCollectionCounts[2] - rawCollectionCounts[3]; + else + GCCollectionCounts[2] = rawCollectionCounts[2]; + + // LOH & POH + if (collectionLength == 4) + GCCollectionCounts[3] = rawCollectionCounts[3]; + if (collectionLength > 4) + { + GCCollectionCounts[3] = rawCollectionCounts[3] - rawCollectionCounts[4]; + GCCollectionCounts[4] = rawCollectionCounts[4]; + } + } + } +} diff --git a/src/Agent/NewRelic/Agent/Core/Transformers/GCSampleTransformerV2.cs b/src/Agent/NewRelic/Agent/Core/Transformers/GCSampleTransformerV2.cs new file mode 100644 index 0000000000..4159951a9d --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Transformers/GCSampleTransformerV2.cs @@ -0,0 +1,129 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using NewRelic.Agent.Core.Aggregators; +using NewRelic.Agent.Core.Samplers; +using NewRelic.Agent.Core.WireModels; + +namespace NewRelic.Agent.Core.Transformers +{ + public interface IGCSampleTransformerV2 + { + void Transform(ImmutableGCSample sample); + } + + public class GCSampleTransformerV2 : IGCSampleTransformerV2 + { + private readonly IMetricBuilder _metricBuilder; + private readonly IMetricAggregator _metricAggregator; + + // public for testing purposes only + public ImmutableGCSample PreviousSample { get; private set; } + public ImmutableGCSample CurrentSample {get; private set;} = new(); + + public GCSampleTransformerV2(IMetricBuilder metricBuilder, IMetricAggregator metricAggregator) + { + _metricBuilder = metricBuilder; + _metricAggregator = metricAggregator; + } + + public void Transform(ImmutableGCSample sample) + { + PreviousSample = CurrentSample; + CurrentSample = sample; + + var metrics = BuildMetrics(); + RecordMetrics(metrics); + + } + + private List BuildMetrics() + { + var metrics = new List + { + CreateMetric_ByteData(GCSampleType.Gen0Size, CurrentSample.GCHeapSizesBytes[0]), + CreateMetric_Count(GCSampleType.Gen0CollectionCount, PreviousSample.GCCollectionCounts[0], CurrentSample.GCCollectionCounts[0]), + CreateMetric_ByteData(GCSampleType.Gen0FragmentationSize, CurrentSample.GCFragmentationSizesBytes[0]), + + CreateMetric_ByteData(GCSampleType.Gen1Size, CurrentSample.GCHeapSizesBytes[1]), + CreateMetric_Count(GCSampleType.Gen1CollectionCount, PreviousSample.GCCollectionCounts[1], CurrentSample.GCCollectionCounts[1]), + CreateMetric_ByteData(GCSampleType.Gen1FragmentationSize, CurrentSample.GCFragmentationSizesBytes[1]), + + CreateMetric_ByteData(GCSampleType.Gen2Size, CurrentSample.GCHeapSizesBytes[2]), + CreateMetric_Count(GCSampleType.Gen2CollectionCount, PreviousSample.GCCollectionCounts[2], CurrentSample.GCCollectionCounts[2]), + CreateMetric_ByteData(GCSampleType.Gen2FragmentationSize, CurrentSample.GCFragmentationSizesBytes[2]), + + CreateMetric_ByteData(GCSampleType.LOHSize, CurrentSample.GCHeapSizesBytes[3]), + CreateMetric_Count(GCSampleType.LOHCollectionCount, PreviousSample.GCCollectionCounts[3], CurrentSample.GCCollectionCounts[3]), + CreateMetric_ByteData(GCSampleType.LOHFragmentationSize, CurrentSample.GCFragmentationSizesBytes[3]), + + CreateMetric_ByteData(GCSampleType.POHSize, CurrentSample.GCHeapSizesBytes[4]), + CreateMetric_Count(GCSampleType.POHCollectionCount, PreviousSample.GCCollectionCounts[4], CurrentSample.GCCollectionCounts[4]), + CreateMetric_ByteData(GCSampleType.POHFragmentationSize, CurrentSample.GCFragmentationSizesBytes[4]), + + CreateMetric_ByteData(GCSampleType.TotalHeapMemory, CurrentSample.TotalMemoryBytes), + CreateMetric_ByteData(GCSampleType.TotalCommittedMemory, CurrentSample.TotalCommittedBytes), + + CreateMetric_ByteDataDelta(GCSampleType.TotalAllocatedMemory, PreviousSample.TotalAllocatedBytes, CurrentSample.TotalAllocatedBytes), + }; + + return metrics; + } + + private void RecordMetrics(List metrics) + { + foreach (var metric in metrics) + { + _metricAggregator.Collect(metric); + } + } + + /// + /// Create a byte data metric representing the current value + /// + /// + /// + /// + private MetricWireModel CreateMetric_ByteData(GCSampleType sampleType, long currentValueBytes) + { + return _metricBuilder.TryBuildGCBytesMetric(sampleType, currentValueBytes); + } + + /// + /// Create a byte data metric that is the difference between the current value and previous value + /// + /// + /// + /// + /// + private MetricWireModel CreateMetric_ByteDataDelta(GCSampleType sampleType, long previousValueBytes, long currentValueBytes) + { + var sampleValueBytes = currentValueBytes - previousValueBytes; + if (sampleValueBytes < 0) + { + sampleValueBytes = 0; + } + return _metricBuilder.TryBuildGCBytesMetric(sampleType, sampleValueBytes); + } + + /// + /// Create a count metric that is the difference between the current value and previous value + /// + /// + /// + /// + /// + private MetricWireModel CreateMetric_Count(GCSampleType sampleType, long previousValue, long currentValue) + { + var sampleValue = currentValue - previousValue; + if (sampleValue < 0) + { + sampleValue = 0; + } + + return _metricBuilder.TryBuildGCCountMetric(sampleType, (int)sampleValue); + } + + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs index 320b7d6dbd..2c70b0d8f7 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs @@ -231,5 +231,7 @@ public interface IConfiguration string AzureFunctionResourceIdWithFunctionName(string functionName); bool UtilizationDetectAzureFunction { get; } + + bool GCSamplerV2Enabled { get; } } } diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Reflection/VisibilityBypasser.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Reflection/VisibilityBypasser.cs index a9c61d1dbb..064c7b3fc5 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Reflection/VisibilityBypasser.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Reflection/VisibilityBypasser.cs @@ -297,6 +297,27 @@ private static Func GenerateMethodCallerInternal(Type ownerType, return GenerateMethodCallerInternal(resultType, methodInfo); } + public bool TryGenerateOneParameterStaticMethodCaller(Type ownerType, string methodName, Type paramType, Type returnType, out Func accessor) + { + try + { + var methodInfo = ownerType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, null, new Type[] { paramType }, null); + if (methodInfo == null) + { + accessor = null; + return false; + } + + accessor = (object param) => methodInfo.Invoke(null, new object[] { param }); + return true; + } + catch + { + accessor = null; + return false; + } + } + private static Func GenerateMethodCallerInternal(Type ownerType, Type resultType, Type parameterType, string methodName) { var methodInfo = GetMethodInfo(ownerType, methodName); @@ -657,6 +678,19 @@ public Func GenerateParameterlessStaticMethodCaller(string ass return (Func)methodInfo.CreateDelegate(typeof(Func)); } + public bool TryGenerateParameterlessStaticMethodCaller(Type ownerType, string methodName, out Func accessor) + { + var methodInfo = ownerType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + if (methodInfo == null) + { + accessor = null; + return false; + } + + accessor = (Func)methodInfo.CreateDelegate(typeof(Func)); + return true; + } + private static PropertyInfo GetPropertyInfo(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs index 4a603a7b1d..230d2fffeb 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs @@ -503,5 +503,14 @@ public NewRelicConfigModifier SetDisableFileSystemWatcher(bool enabled = true) CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(_configFilePath, new[] { "configuration", "service" }, "disableFileSystemWatcher", enabled.ToString().ToLower()); return this; } + + public NewRelicConfigModifier EnableGCSamplerV2(bool enabled) + { + CommonUtils.ModifyOrCreateXmlNodeInNewRelicConfig(_configFilePath, new[] { "configuration" }, "appSettings", string.Empty); + CommonUtils.ModifyOrCreateXmlNodeInNewRelicConfig(_configFilePath, new[] { "configuration", "appSettings" }, "add", string.Empty); + CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(_configFilePath, new[] { "configuration", "appSettings", "add"}, "key", "GCSamplerV2Enabled"); + CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(_configFilePath, new[] { "configuration", "appSettings", "add"}, "value", $"{enabled}"); + return this; + } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AgentMetrics/DotNetPerfMetricsTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AgentMetrics/DotNetPerfMetricsTests.cs index 883ed350df..e482ce98ac 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AgentMetrics/DotNetPerfMetricsTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AgentMetrics/DotNetPerfMetricsTests.cs @@ -16,7 +16,7 @@ namespace NewRelic.Agent.IntegrationTests.AgentMetrics public class DotNetPerfMetricsTestsFW : DotNetPerfMetricsTests { public DotNetPerfMetricsTestsFW(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output) - : base(fixture, output) + : base(fixture, output, false) { } @@ -27,7 +27,7 @@ public DotNetPerfMetricsTestsFW(ConsoleDynamicMethodFixtureFWLatest fixture, ITe public class DotNetPerfMetricsTestsCoreOldest : DotNetPerfMetricsTests { public DotNetPerfMetricsTestsCoreOldest(ConsoleDynamicMethodFixtureCoreOldest fixture, ITestOutputHelper output) - : base(fixture, output) + : base(fixture, output, false) { } @@ -38,13 +38,34 @@ public DotNetPerfMetricsTestsCoreOldest(ConsoleDynamicMethodFixtureCoreOldest fi public class DotNetPerfMetricsTestsCoreLatest : DotNetPerfMetricsTests { public DotNetPerfMetricsTestsCoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output) - : base(fixture, output) + : base(fixture, output, false) { } protected override string[] ExpectedMetricNames_GC => ExpectedMetricNames_GC_NetCore; } + [NetCoreTest] + public class DotNetPerfMetricsTestsGCSamplerV2CoreOldest : DotNetPerfMetricsTests + { + public DotNetPerfMetricsTestsGCSamplerV2CoreOldest(ConsoleDynamicMethodFixtureCoreOldest fixture, ITestOutputHelper output) + : base(fixture, output, true) + { + } + + protected override string[] ExpectedMetricNames_GC => ExpectedMetricNames_GC_V2; + } + + [NetCoreTest] + public class DotNetPerfMetricsTestsGCSamplerV2CoreLatest : DotNetPerfMetricsTests + { + public DotNetPerfMetricsTestsGCSamplerV2CoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output) + : base(fixture, output, true) + { + } + + protected override string[] ExpectedMetricNames_GC => ExpectedMetricNames_GC_V2; + } public abstract class DotNetPerfMetricsTests : NewRelicIntegrationTest where TFixture : ConsoleDynamicMethodFixture { @@ -58,8 +79,9 @@ public abstract class DotNetPerfMetricsTests : NewRelicIntegrationTest protected const string METRICNAME_THREADPOOL_COMPLETION_INUSE = "Threadpool/Completion/InUse"; protected readonly TFixture Fixture; + private readonly bool _gcSamplerV2Enabled; - protected abstract string[] ExpectedMetricNames_GC { get; } + protected abstract string[] ExpectedMetricNames_GC { get;} protected string[] ExpectedMetricNames_GC_NetFramework => new string[] { "GC/Gen0/Size", @@ -91,6 +113,29 @@ public abstract class DotNetPerfMetricsTests : NewRelicIntegrationTest "GC/Gen1/Collections", "GC/Gen2/Collections" }; + + protected string[] ExpectedMetricNames_GC_V2 => new string[] + { + "GC/Gen0/Size", + "GC/Gen0/Fragmentation", + "GC/Gen1/Size", + "GC/Gen1/Fragmentation", + "GC/Gen2/Size", + "GC/Gen2/Fragmentation", + "GC/LOH/Size", + "GC/LOH/Fragmentation", + "GC/POH/Size", + "GC/POH/Fragmentation", + "GC/Gen0/Collections", + "GC/Gen1/Collections", + "GC/Gen2/Collections", + "GC/LOH/Collections", + "GC/POH/Collections", + "GC/Heap/Total", + "GC/Heap/Committed", + "GC/Heap/Allocated" + }; + protected string[] ExpectedMetricNames_Memory => new string[] { "Memory/Physical", @@ -113,9 +158,12 @@ public abstract class DotNetPerfMetricsTests : NewRelicIntegrationTest "Threadpool/Throughput/QueueLength" }; - public DotNetPerfMetricsTests(TFixture fixture, ITestOutputHelper output) : base(fixture) + public DotNetPerfMetricsTests(TFixture fixture, ITestOutputHelper output, bool gcSamplerV2Enabled) : base(fixture) { Fixture = fixture; + + _gcSamplerV2Enabled = gcSamplerV2Enabled; + Fixture.TestLogger = output; Fixture.AddCommand($"PerformanceMetrics Test {THREADPOOL_WORKER_MAX} {THREADPOOL_COMPLETION_MAX}"); @@ -127,6 +175,9 @@ public DotNetPerfMetricsTests(TFixture fixture, ITestOutputHelper output) : base Fixture.RemoteApplication.NewRelicConfig.SetLogLevel("finest"); Fixture.RemoteApplication.AddAppSetting("NewRelic.EventListenerSamplersEnabled", "true"); Fixture.RemoteApplication.NewRelicConfig.ConfigureFasterMetricsHarvestCycle(10); + + if (_gcSamplerV2Enabled) + Fixture.RemoteApplication.NewRelicConfig.EnableGCSamplerV2(true); } ); diff --git a/tests/Agent/UnitTests/CompositeTests/CompositeTestAgent.cs b/tests/Agent/UnitTests/CompositeTests/CompositeTestAgent.cs index 8a9dc96cd3..934f18bbe5 100644 --- a/tests/Agent/UnitTests/CompositeTests/CompositeTestAgent.cs +++ b/tests/Agent/UnitTests/CompositeTests/CompositeTestAgent.cs @@ -133,7 +133,7 @@ public CompositeTestAgent(bool enableServerlessMode = false) : this(shouldAllowT { } - public CompositeTestAgent(bool shouldAllowThreads, bool includeAsyncLocalStorage, bool enableServerlessMode = false) + public CompositeTestAgent(bool shouldAllowThreads, bool includeAsyncLocalStorage, bool enableServerlessMode = false, bool enableGCSamplerV2 = false) { Log.Initialize(new Logger()); @@ -179,7 +179,7 @@ public CompositeTestAgent(bool shouldAllowThreads, bool includeAsyncLocalStorage // Construct services _container = AgentServices.GetContainer(); - AgentServices.RegisterServices(_container, enableServerlessMode); + AgentServices.RegisterServices(_container, enableServerlessMode, enableGCSamplerV2); // Replace existing registrations with mocks before resolving any services _container.ReplaceInstanceRegistration(mockEnvironment); @@ -220,7 +220,7 @@ public CompositeTestAgent(bool shouldAllowThreads, bool includeAsyncLocalStorage InstrumentationService = _container.Resolve(); InstrumentationWatcher = _container.Resolve(); - AgentServices.StartServices(_container, false); + AgentServices.StartServices(_container, false, enableGCSamplerV2); DisableAgentInitializer(); InternalApi.SetAgentApiImplementation(_container.Resolve()); diff --git a/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs b/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs index 37d91e2fd3..b25163cee6 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/AgentHealth/AgentHealthReporterTests.cs @@ -60,6 +60,8 @@ private IConfiguration GetDefaultConfiguration() Mock.Arrange(() => configuration.InfiniteTracingCompression).Returns(true); Mock.Arrange(() => configuration.LoggingEnabled).Returns(() => _enableLogging); Mock.Arrange(() => configuration.IgnoredInstrumentation).Returns(() => _ignoredInstrumentation); + Mock.Arrange(() => configuration.GCSamplerV2Enabled).Returns(true); + return configuration; } @@ -522,5 +524,12 @@ public void IgnoredInstrumentationSupportabiltyMetricMissing() Assert.That(_publishedMetrics.Any(x => x.MetricNameModel.Name == "Supportability/Dotnet/IgnoredInstrumentation"), Is.False); } + + [Test] + public void GCSamplerV2EnabledSupportabiliityMetricPresent() + { + _agentHealthReporter.CollectMetrics(); + Assert.That(_publishedMetrics.Any(x => x.MetricNameModel.Name == "Supportability/Dotnet/GCSamplerV2/Enabled"), Is.True); + } } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/Config/BootstrapConfigurationTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Config/BootstrapConfigurationTests.cs index f737daa7f5..20a8bcdda1 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Config/BootstrapConfigurationTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Config/BootstrapConfigurationTests.cs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.IO; using NewRelic.Agent.Core.Configuration; using NewRelic.Agent.Core.SharedInterfaces; using NUnit.Framework; using Telerik.JustMock; +using Telerik.JustMock.Helpers; namespace NewRelic.Agent.Core.Config { @@ -35,6 +37,7 @@ public void TestDefaultBootstrapConfiguration() Assert.That(config.ServerlessModeEnabled, Is.False); Assert.That(config.ServerlessFunctionName, Is.Null); Assert.That(config.ServerlessFunctionVersion, Is.Null); + Assert.That(config.GCSamplerV2Enabled, Is.False); }); } @@ -152,6 +155,54 @@ public void DoesNotThrowWhenExceptionOccursWhileReadingAppSettings() Assert.That(config.AgentEnabled, Is.True); } + [Test] + public void GCSamplerV2_DisabledByDefault() + { + var config = CreateBootstrapConfiguration(); + + Assert.That(config.GCSamplerV2Enabled, Is.False); + } + [Test] + public void GCSamplerV2_EnabledViaLocalConfig() + { + _localConfiguration.appSettings.Add(new configurationAdd { key = "GCSamplerV2Enabled", value = "true" }); + + var config = CreateBootstrapConfiguration(); + + Assert.Multiple(() => + { + Assert.That(config.GCSamplerV2Enabled, Is.True); + }); + } + [Test] + public void GCSamplerV2_EnabledViaEnvironmentVariable() + { + _originalEnvironment = ConfigLoaderHelpers.EnvironmentVariableProxy; + try + { + + var environmentMock = Mock.Create(); + Mock.Arrange(() => environmentMock.GetEnvironmentVariable(Arg.IsAny())).Returns(MockGetEnvironmentVar); + ConfigLoaderHelpers.EnvironmentVariableProxy = environmentMock; + + _localConfiguration.appSettings.Add(new configurationAdd { key = "GCSamplerV2Enabled", value = "false" }); + + SetEnvironmentVar("NEW_RELIC_GC_SAMPLER_V2_ENABLED", "1"); + + var config = CreateBootstrapConfiguration(); + + Assert.Multiple(() => + { + Assert.That(config.GCSamplerV2Enabled, Is.True); + }); + + } + finally + { + ConfigLoaderHelpers.EnvironmentVariableProxy = _originalEnvironment; + } + } + private BootstrapConfiguration CreateBootstrapConfiguration() { return new BootstrapConfiguration(_localConfiguration, TestFileName, _ => _webConfigValueWithProvenance, _configurationManagerStatic, new ProcessStatic(), Directory.Exists, Path.GetFullPath); @@ -163,5 +214,21 @@ private BootstrapConfiguration CreateBootstrapConfiguration() private IConfigurationManagerStatic _configurationManagerStatic; private const string TestWebConfigProvenance = "web.config"; private const string TestAppSettingProvenance = "app setting"; + + private IEnvironment _originalEnvironment; + private Dictionary _envVars = new Dictionary(); + private void SetEnvironmentVar(string name, string value) + { + _envVars[name] = value; + } + + private void ClearEnvironmentVars() => _envVars.Clear(); + + private string MockGetEnvironmentVar(string name) + { + if (_envVars.TryGetValue(name, out var value)) return value; + return null; + } + } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/AgentSettingsTests.cs b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/AgentSettingsTests.cs index af0e69da57..05d96011d0 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/AgentSettingsTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/AgentSettingsTests.cs @@ -341,7 +341,8 @@ public void serializes_correctly() "agent.disable_file_system_watcher": false, "ai_monitoring.enabled": true, "ai_monitoring.streaming.enabled": true, - "ai_monitoring.record_content.enabled": true + "ai_monitoring.record_content.enabled": true, + "gc_sampler_v2.enabled": true } """; diff --git a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ConnectModelTests.cs b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ConnectModelTests.cs index 5ae72cf47c..74a4d8a364 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ConnectModelTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ConnectModelTests.cs @@ -412,7 +412,8 @@ public void serializes_correctly() "agent.disable_file_system_watcher": false, "ai_monitoring.enabled": true, "ai_monitoring.streaming.enabled": true, - "ai_monitoring.record_content.enabled": true + "ai_monitoring.record_content.enabled": true, + "gc_sampler_v2.enabled": true }, "metadata": { "hello": "there" diff --git a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ExhaustiveTestConfiguration.cs b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ExhaustiveTestConfiguration.cs index 90345085d5..3cf8cab6ca 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ExhaustiveTestConfiguration.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/DataTransport/ExhaustiveTestConfiguration.cs @@ -378,7 +378,6 @@ public class ExhaustiveTestConfiguration : IConfiguration public bool UtilizationDetectKubernetes => true; public bool UtilizationDetectAzureFunction => true; - public int? UtilizationLogicalProcessors => 22; public int? UtilizationTotalRamMib => 33; @@ -493,5 +492,8 @@ public IReadOnlyDictionary GetAppSettings() public string AzureFunctionResourceIdWithFunctionName(string functionName) => $"AzureFunctionResourceId/{functionName}"; public string LoggingLevel => "info"; + + public bool GCSamplerV2Enabled => true; + } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/DependencyInjection/AgentServicesTests.cs b/tests/Agent/UnitTests/Core.UnitTest/DependencyInjection/AgentServicesTests.cs index aeadafac2d..b4783bbcf2 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/DependencyInjection/AgentServicesTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/DependencyInjection/AgentServicesTests.cs @@ -1,6 +1,13 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +#if NET +using System; +using System.Collections.Generic; +using NewRelic.Agent.Core.Samplers; +using NewRelic.Agent.Core.Transformers; +#endif + using Autofac.Core.Registration; using NewRelic.Agent.Configuration; using NewRelic.Agent.Core.Commands; @@ -26,7 +33,7 @@ public void ConfigurationServiceCanFullyResolve() using (new ConfigurationAutoResponder(configuration)) using (var container = AgentServices.GetContainer()) { - AgentServices.RegisterServices(container, false); + AgentServices.RegisterServices(container, false, false); Assert.DoesNotThrow(() => container.Resolve()); } } @@ -44,7 +51,7 @@ public void AllServicesCanFullyResolve() using (new ConfigurationAutoResponder(configuration)) using (var container = AgentServices.GetContainer()) { - AgentServices.RegisterServices(container, false); + AgentServices.RegisterServices(container, false, false); container.ReplaceInstanceRegistration(configurationService); #if NET @@ -52,7 +59,7 @@ public void AllServicesCanFullyResolve() #endif Assert.DoesNotThrow(() => container.Resolve()); - Assert.DoesNotThrow(() => AgentServices.StartServices(container, false)); + Assert.DoesNotThrow(() => AgentServices.StartServices(container, false, false)); } } @@ -72,7 +79,7 @@ public void CorrectServicesAreRegistered_BasedOnServerlessMode(bool serverlessMo using (new ConfigurationAutoResponder(configuration)) using (var container = AgentServices.GetContainer()) { - AgentServices.RegisterServices(container, serverlessModeEnabled); + AgentServices.RegisterServices(container, serverlessModeEnabled, false); container.ReplaceInstanceRegistration(configurationService); #if NET @@ -80,7 +87,7 @@ public void CorrectServicesAreRegistered_BasedOnServerlessMode(bool serverlessMo #endif // Assert Assert.DoesNotThrow(() => container.Resolve()); - Assert.DoesNotThrow(() => AgentServices.StartServices(container, true)); + Assert.DoesNotThrow(() => AgentServices.StartServices(container, true, false)); // ensure dependent services are registered if (serverlessModeEnabled) @@ -112,5 +119,53 @@ public void CorrectServicesAreRegistered_BasedOnServerlessMode(bool serverlessMo } } } + +#if NET + [TestCase(true)] + [TestCase(false)] + public void CorrectServicesAreRegistered_BasedOnGCSamplerV2EnabledMode(bool gcSamplerV2Enabled) + { + // Arrange + var configuration = Mock.Create(); + Mock.Arrange(() => configuration.AutoStartAgent).Returns(false); + Mock.Arrange(() => configuration.NewRelicConfigFilePath).Returns("c:\\"); + var configurationService = Mock.Create(); + Mock.Arrange(() => configurationService.Configuration).Returns(configuration); + + // Act + using (new ConfigurationAutoResponder(configuration)) + using (var container = AgentServices.GetContainer()) + { + AgentServices.RegisterServices(container, false, gcSamplerV2Enabled); + + container.ReplaceInstanceRegistration(configurationService); + container.ReplaceRegistrations(); // creates a new scope, registering the replacement instances from all .ReplaceRegistration() calls above + // Assert + Assert.DoesNotThrow(() => container.Resolve()); + Assert.DoesNotThrow(() => AgentServices.StartServices(container, false, gcSamplerV2Enabled)); + + // ensure dependent services are registered + if (gcSamplerV2Enabled) + { + Assert.DoesNotThrow(() => container.Resolve()); + Assert.DoesNotThrow(() => container.Resolve()); + + Assert.Throws(() => container.Resolve>>>()); + Assert.Throws(() => container.Resolve>()); + Assert.Throws(() => container.Resolve()); + + } + else + { + Assert.DoesNotThrow(() => container.Resolve>>>()); + Assert.DoesNotThrow(() => container.Resolve>()); + Assert.DoesNotThrow(() => container.Resolve()); + + Assert.Throws(() => container.Resolve()); + Assert.Throws(() => container.Resolve()); + } + } + } +#endif } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/Metrics/MetricNamesTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Metrics/MetricNamesTests.cs index 22cd05d110..c1412d1f2b 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Metrics/MetricNamesTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Metrics/MetricNamesTests.cs @@ -328,6 +328,20 @@ public static void MetricNamesTest_GetGCMetricName() { GCSampleType.LOHSize, "GC/LOH/Size" }, { GCSampleType.LOHSurvived, "GC/LOH/Survived" }, + + { GCSampleType.LOHCollectionCount, "GC/LOH/Collections" }, + { GCSampleType.POHCollectionCount, "GC/POH/Collections" }, + + { GCSampleType.TotalHeapMemory, "GC/Heap/Total" }, + { GCSampleType.TotalCommittedMemory, "GC/Heap/Committed" }, + { GCSampleType.TotalAllocatedMemory, "GC/Heap/Allocated" }, + + { GCSampleType.Gen0FragmentationSize, "GC/Gen0/Fragmentation" }, + { GCSampleType.Gen1FragmentationSize, "GC/Gen1/Fragmentation" }, + { GCSampleType.Gen2FragmentationSize, "GC/Gen2/Fragmentation" }, + { GCSampleType.LOHFragmentationSize, "GC/LOH/Fragmentation" }, + { GCSampleType.POHFragmentationSize, "GC/POH/Fragmentation" }, + { GCSampleType.POHSize, "GC/POH/Size" } }; //Ensure that we have covered all sample types with our tests diff --git a/tests/Agent/UnitTests/Core.UnitTest/Samplers/GCSamplerV2Tests.cs b/tests/Agent/UnitTests/Core.UnitTest/Samplers/GCSamplerV2Tests.cs new file mode 100644 index 0000000000..f81d1560f8 --- /dev/null +++ b/tests/Agent/UnitTests/Core.UnitTest/Samplers/GCSamplerV2Tests.cs @@ -0,0 +1,105 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET + +using System; +using NewRelic.Agent.Core.Time; +using NewRelic.Agent.Core.Transformers; +using NUnit.Framework; +using Telerik.JustMock; + +namespace NewRelic.Agent.Core.Samplers +{ + [TestFixture] + public class GCSamplerV2Tests + { + private IScheduler _scheduler; + private IGCSampleTransformerV2 _transformer; + private IGCSamplerV2ReflectionHelper _reflectionHelper; + private GCSamplerV2 _gcSamplerV2; + + [SetUp] + public void SetUp() + { + _scheduler = Mock.Create(); + _transformer = Mock.Create(); + _reflectionHelper = Mock.Create(); + + _gcSamplerV2 = new GCSamplerV2(_scheduler, _transformer, _reflectionHelper); + } + + [TearDown] + public void TearDown() + { + _gcSamplerV2.Dispose(); + } + + [Test] + public void Sample_ShouldStop_WhenReflectionFails() + { + // Arrange + Mock.Arrange(() => _reflectionHelper.ReflectionFailed).Returns(true); + + // Act + _gcSamplerV2.Sample(); + + // Assert + Mock.Assert(() => _scheduler.StopExecuting(Arg.IsAny(), Arg.IsAny()), Occurs.Once()); + } + + [Test] + public void Sample_ShouldNotTransform_WhenNoGCOccurred() + { + // Arrange + Mock.Arrange(() => _reflectionHelper.ReflectionFailed).Returns(false); + Mock.Arrange(() => _reflectionHelper.HasGCOccurred).Returns(false); + + // Act + _gcSamplerV2.Sample(); + + // Assert + Mock.Assert(() => _transformer.Transform(Arg.IsAny()), Occurs.Never()); + } + + [Test] + public void Sample_Transforms_WhenGCHasOccurred() + { + // Arrange + Mock.Arrange(() => _reflectionHelper.ReflectionFailed).Returns(false); + Mock.Arrange(() => _reflectionHelper.HasGCOccurred).Returns(true); + + var gcMemoryInfo = new GCMemoryInfo { TotalCommittedBytes = 4096L }; + var generationInfo = new[] + { + new GenerationInfo { SizeAfterBytes = 100L, FragmentationAfterBytes = 10L }, + new GenerationInfo { SizeAfterBytes = 200L, FragmentationAfterBytes = 20L }, + new GenerationInfo { SizeAfterBytes = 300L, FragmentationAfterBytes = 30L }, + new GenerationInfo { SizeAfterBytes = 400L, FragmentationAfterBytes = 40L }, + new GenerationInfo { SizeAfterBytes = 500L, FragmentationAfterBytes = 50L } + }; + + Mock.Arrange(() => _reflectionHelper.GCGetMemoryInfo_Invoker(Arg.IsAny())).Returns(gcMemoryInfo); + Mock.Arrange(() => _reflectionHelper.GetGenerationInfo(Arg.IsAny())).Returns(generationInfo); + Mock.Arrange(() => _reflectionHelper.GCGetTotalAllocatedBytes_Invoker(Arg.IsAny())).Returns(2048L); + + // Act + _gcSamplerV2.Sample(); + + // Assert + Mock.Assert(() => _transformer.Transform(Arg.IsAny()), Occurs.Once()); + } + + // Mock classes to replace anonymous types + public class GCMemoryInfo + { + public long TotalCommittedBytes { get; set; } + } + + public class GenerationInfo + { + public long SizeAfterBytes { get; set; } + public long FragmentationAfterBytes { get; set; } + } + } +} +#endif diff --git a/tests/Agent/UnitTests/Core.UnitTest/Samplers/ImmutableGCSampleTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Samplers/ImmutableGCSampleTests.cs new file mode 100644 index 0000000000..d31666f83d --- /dev/null +++ b/tests/Agent/UnitTests/Core.UnitTest/Samplers/ImmutableGCSampleTests.cs @@ -0,0 +1,67 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using NewRelic.Agent.Core.Samplers; +using NUnit.Framework; + +namespace NewRelic.Agent.Core.Tests.Samplers +{ + [TestFixture] + public class ImmutableGCSampleTests + { + [Test] + public void Constructor_Default_ShouldInitializeFields() + { + // Act + var sample = new ImmutableGCSample(); + + // Assert + Assert.Multiple(() => + { + Assert.That(sample.LastSampleTime, Is.EqualTo(DateTime.MinValue)); + Assert.That(sample.CurrentSampleTime, Is.EqualTo(DateTime.MinValue)); + Assert.That(sample.GCHeapSizesBytes.Length, Is.EqualTo(5)); + Assert.That(sample.GCCollectionCounts.Length, Is.EqualTo(5)); + Assert.That(sample.GCFragmentationSizesBytes.Length, Is.EqualTo(5)); + }); + } + + [Test] + [TestCase(3, new long[] { 100, 200, 300 }, new[] { 5, 4, 3 }, new long[] { 10, 20, 30 }, new[] { 1, 1, 3, 0, 0 })] + [TestCase(4, new long[] { 100, 200, 300, 400 }, new[] { 5, 4, 3, 2 }, new long[] { 10, 20, 30, 40 }, new[] { 1, 1, 1, 2, 0})] + [TestCase(5, new long[] { 100, 200, 300, 400, 500 }, new[] { 5, 4, 3, 2, 1 }, new long[] { 10, 20, 30, 40, 50 }, new[] { 1, 1, 1, 1, 1 })] + public void Constructor_WithParameters_ShouldInitializeFields(int collectionLength, long[] heapSizesBytes, int[] rawCollectionCounts, long[] fragmentationSizesBytes, int[] expectedCollectionCounts) + { + // Arrange + var lastSampleTime = DateTime.UtcNow.AddMinutes(-1); + var currentSampleTime = DateTime.UtcNow; + var totalMemoryBytes = 1024L; + var totalAllocatedBytes = 2048L; + var totalCommittedBytes = 4096L; + + // Act + var sample = new ImmutableGCSample(lastSampleTime, currentSampleTime, totalMemoryBytes, totalAllocatedBytes, totalCommittedBytes, heapSizesBytes, rawCollectionCounts, fragmentationSizesBytes); + + // Assert + Assert.Multiple(() => + { + Assert.That(sample.LastSampleTime, Is.EqualTo(lastSampleTime)); + Assert.That(sample.CurrentSampleTime, Is.EqualTo(currentSampleTime)); + Assert.That(sample.TotalMemoryBytes, Is.EqualTo(totalMemoryBytes)); + Assert.That(sample.TotalAllocatedBytes, Is.EqualTo(totalAllocatedBytes)); + Assert.That(sample.TotalCommittedBytes, Is.EqualTo(totalCommittedBytes)); + Assert.That(sample.GCHeapSizesBytes, Is.EqualTo(heapSizesBytes)); + Assert.That(sample.GCFragmentationSizesBytes, Is.EqualTo(fragmentationSizesBytes)); + + // Verify GCCollectionCounts + Assert.That(sample.GCCollectionCounts.Length, Is.EqualTo(5)); + Assert.That(sample.GCCollectionCounts[0], Is.EqualTo(expectedCollectionCounts[0])); // Gen 0 + Assert.That(sample.GCCollectionCounts[1], Is.EqualTo(expectedCollectionCounts[1])); // Gen 1 + Assert.That(sample.GCCollectionCounts[2], Is.EqualTo(expectedCollectionCounts[2])); // Gen 2 + Assert.That(sample.GCCollectionCounts[3], Is.EqualTo(expectedCollectionCounts[3])); // LOH + Assert.That(sample.GCCollectionCounts[4], Is.EqualTo(expectedCollectionCounts[4])); // POH + }); + } + } +} diff --git a/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCSampleTransformerV2Tests.cs b/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCSampleTransformerV2Tests.cs new file mode 100644 index 0000000000..b6dc9c96ad --- /dev/null +++ b/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCSampleTransformerV2Tests.cs @@ -0,0 +1,188 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using NewRelic.Agent.Core.Aggregators; +using NewRelic.Agent.Core.Metrics; +using NewRelic.Agent.Core.Samplers; +using NewRelic.Agent.Core.WireModels; +using NUnit.Framework; +using Telerik.JustMock; + +namespace NewRelic.Agent.Core.Transformers +{ + [TestFixture] + public class GCSampleTransformerV2Tests + { + private IMetricBuilder _metricBuilder; + private IMetricAggregator _metricAggregator; + private GCSampleTransformerV2 _transformer; + + [SetUp] + public void SetUp() + { + _metricBuilder = new MetricWireModel.MetricBuilder(new MetricNameService()); + + _metricAggregator = Mock.Create(); + + _transformer = new GCSampleTransformerV2(_metricBuilder, _metricAggregator); + } + + [Test] + public void Transform_ShouldUpdateCurrentAndPreviousSamples() + { + // Arrange + var sample = CreateSample(); + Mock.Arrange(() => _metricAggregator.Collect(Arg.IsAny())); + + // Act + _transformer.Transform(sample); + + // Assert + + Assert.Multiple(() => + { + Assert.That(_transformer.PreviousSample, Is.Not.Null); + Assert.That(_transformer.CurrentSample, Is.EqualTo(sample)); + }); + } + + [Test] + public void Transform_ShouldBuildAndRecordMetrics() + { + // Arrange + var sample = CreateSample(); + + var generatedMetrics = new Dictionary(); + + Mock.Arrange(() => _metricAggregator.Collect(Arg.IsAny())).DoInstead(m => generatedMetrics.Add(m.MetricNameModel.Name, m.DataModel)); + + // Act + _transformer.Transform(sample); + + // Assert + const float bytesPerMb = 1048576f; + Assert.Multiple(() => + { + Assert.That(generatedMetrics, Has.Count.EqualTo(18)); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.TotalAllocatedMemory), sample.TotalAllocatedBytes / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.TotalCommittedMemory), sample.TotalCommittedBytes / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.TotalHeapMemory), sample.TotalMemoryBytes / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen0Size), sample.GCHeapSizesBytes[0] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen1Size), sample.GCHeapSizesBytes[1] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen2Size), sample.GCHeapSizesBytes[2] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.LOHSize), sample.GCHeapSizesBytes[3] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.POHSize), sample.GCHeapSizesBytes[4] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen0FragmentationSize), sample.GCFragmentationSizesBytes[0] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen1FragmentationSize), sample.GCFragmentationSizesBytes[1] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen2FragmentationSize), sample.GCFragmentationSizesBytes[2] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.LOHFragmentationSize), sample.GCFragmentationSizesBytes[3] / bytesPerMb); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.POHFragmentationSize), sample.GCFragmentationSizesBytes[4] / bytesPerMb); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen0CollectionCount), sample.GCCollectionCounts[0]); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen1CollectionCount), sample.GCCollectionCounts[1]); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen2CollectionCount), sample.GCCollectionCounts[2]); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.LOHCollectionCount), sample.GCCollectionCounts[3]); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.POHCollectionCount), sample.GCCollectionCounts[4]); + }); + } + + [Test] + public void Transform_ShouldRecordZeroMetric_WhenCurrentValueIsLessThanPreviousValue() + { + // Arrange + var previousSample = CreateSampleWithCollectionCounts([3, 3, 3, 3, 3]); + var currentSample = CreateSampleWithCollectionCounts([1, 1, 1, 1, 1]); + + var generatedMetrics = new Dictionary(); + + Mock.Arrange(() => _metricAggregator.Collect(Arg.IsAny())).DoInstead(m => generatedMetrics.Add(m.MetricNameModel.Name, m.DataModel)); + + // Act + _transformer.Transform(previousSample); + + generatedMetrics.Clear(); + + _transformer.Transform(currentSample); + + // Assert + Assert.Multiple(() => + { + Assert.That(generatedMetrics, Has.Count.EqualTo(18)); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen0CollectionCount), 0); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen1CollectionCount), 0); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.Gen2CollectionCount), 0); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.LOHCollectionCount), 0); + MetricTestHelpers.CompareCountMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.POHCollectionCount), 0); + }); + } + + + private ImmutableGCSample CreateSample() + { + return new ImmutableGCSample( + lastSampleTime: System.DateTime.UtcNow.AddMinutes(-1), + currentSampleTime: System.DateTime.UtcNow, + totalMemoryBytes: 1024L, + totalAllocatedBytes: 2048L, + totalCommittedBytes: 4096L, + heapSizesBytes: [100, 200, 300, 400, 500], + rawCollectionCounts: [5, 4, 3, 2, 1], + fragmentationSizesBytes: [10, 20, 30, 40, 50] + ); + } + + private ImmutableGCSample CreateSampleWithCollectionCounts(int[] collectionCounts) + { + return new ImmutableGCSample( + lastSampleTime: System.DateTime.UtcNow.AddMinutes(-1), + currentSampleTime: System.DateTime.UtcNow, + totalMemoryBytes: 1024L, + totalAllocatedBytes: 2048L, + totalCommittedBytes: 4096L, + heapSizesBytes: [100, 200, 300, 400, 500], + rawCollectionCounts: collectionCounts, + fragmentationSizesBytes: [10, 20, 30, 40, 50] + ); + } + + [Test] + public void Transform_ShouldRecordZeroMetric_WhenCurrentAllocatedMemoryIsLessThanPreviousAllocatedMemory() + { + // Arrange + var previousSample = CreateSampleWithAllocatedBytes(2048L); + var currentSample = CreateSampleWithAllocatedBytes(1024L); + + var generatedMetrics = new Dictionary(); + + Mock.Arrange(() => _metricAggregator.Collect(Arg.IsAny())).DoInstead(m => generatedMetrics.Add(m.MetricNameModel.Name, m.DataModel)); + + // Act + _transformer.Transform(previousSample); + + generatedMetrics.Clear(); + + _transformer.Transform(currentSample); + + // Assert + Assert.Multiple(() => + { + Assert.That(generatedMetrics, Has.Count.EqualTo(18)); + MetricTestHelpers.CompareMetric(generatedMetrics, MetricNames.GetGCMetricName(GCSampleType.TotalAllocatedMemory), 0); + }); + } + + private ImmutableGCSample CreateSampleWithAllocatedBytes(long allocatedBytes) + { + return new ImmutableGCSample( + lastSampleTime: System.DateTime.UtcNow.AddMinutes(-1), + currentSampleTime: System.DateTime.UtcNow, + totalMemoryBytes: 1024L, + totalAllocatedBytes: allocatedBytes, + totalCommittedBytes: 4096L, + heapSizesBytes: [100, 200, 300, 400, 500], + rawCollectionCounts: [5, 4, 3, 2, 1], + fragmentationSizesBytes: [10, 20, 30, 40, 50] + ); + } + } +} diff --git a/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCStatsSampleTransformerTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCStatsSampleTransformerTests.cs index 74e66a7f55..db0320125f 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCStatsSampleTransformerTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Transformers/GCStatsSampleTransformerTests.cs @@ -41,7 +41,12 @@ public void Setup() //Build example sample data var sampleValue = 0f; _sampleData = new Dictionary(); - foreach (var val in Enum.GetValues(typeof(GCSampleType))) + + var values = Enum.GetValues(typeof(GCSampleType)); + // only consider "old" GCSampleType enum members for this test + values = values.Cast().Where(x => x < GCSampleType.POHSize).ToArray(); + + foreach (var val in values) { _sampleData.Add((GCSampleType)val, sampleValue++); } diff --git a/tests/Agent/UnitTests/Core.UnitTest/Transformers/MetricTestHelpers.cs b/tests/Agent/UnitTests/Core.UnitTest/Transformers/MetricTestHelpers.cs index 78ac38e8d2..94c052b9af 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Transformers/MetricTestHelpers.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Transformers/MetricTestHelpers.cs @@ -13,13 +13,18 @@ public static class MetricTestHelpers public static void CompareMetric(Dictionary generatedMetrics, string metricName, float expectedValue) { NrAssert.Multiple( - () => Assert.That(generatedMetrics[metricName].Value0, Is.EqualTo(1)), - () => Assert.That(generatedMetrics[metricName].Value1, Is.EqualTo(expectedValue)), - () => Assert.That(generatedMetrics[metricName].Value2, Is.EqualTo(expectedValue)), - () => Assert.That(generatedMetrics[metricName].Value3, Is.EqualTo(expectedValue)), - () => Assert.That(generatedMetrics[metricName].Value4, Is.EqualTo(expectedValue)), - () => Assert.That(generatedMetrics[metricName].Value5, Is.EqualTo(expectedValue * expectedValue)) + () => Assert.That(generatedMetrics[metricName].Value0, Is.EqualTo(1), message: $"{metricName}.Value0"), + () => Assert.That(generatedMetrics[metricName].Value1, Is.EqualTo(expectedValue), message: $"{metricName}.Value1"), + () => Assert.That(generatedMetrics[metricName].Value2, Is.EqualTo(expectedValue), message: $"{metricName}.Value2"), + () => Assert.That(generatedMetrics[metricName].Value3, Is.EqualTo(expectedValue), message: $"{metricName}.Value3"), + () => Assert.That(generatedMetrics[metricName].Value4, Is.EqualTo(expectedValue), message: $"{metricName}.Value4"), + () => Assert.That(generatedMetrics[metricName].Value5, Is.EqualTo(expectedValue * expectedValue), message: $"{metricName}.Value5") ); } + + public static void CompareCountMetric(Dictionary generatedMetrics, string metricName, float expectedValue) + { + Assert.That(generatedMetrics[metricName].Value0, Is.EqualTo(expectedValue), message: $"{metricName}.Value0"); + } } } diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Reflection/VisibilityBypasserTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Reflection/VisibilityBypasserTests.cs index f25521058f..f9cc2811d4 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Reflection/VisibilityBypasserTests.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Reflection/VisibilityBypasserTests.cs @@ -54,6 +54,8 @@ private class PrivateInner public string GetWritableStringField { get { return _writableStringField; } } private int _writeableIntField = 7; public int GetWriteableIntField { get { return _writeableIntField; } } + + public static int StaticMethodWithOneParameter(int param) { return param;} } public static class PublicStatic @@ -628,7 +630,7 @@ public void test_input_validation() public class StaticMethodTests { [Test] - public void test_static_generator() + public void generate_parameterless_static_method_caller() { var assemblyName = Assembly.GetExecutingAssembly().FullName; var typeName = "NewRelic.Reflection.UnitTests.PublicStatic"; @@ -640,5 +642,66 @@ public void test_static_generator() Assert.Throws(() => VisibilityBypasser.Instance.GenerateParameterlessStaticMethodCaller(assemblyName, typeName, "NoSuchMethod")); } - } + [Test] + public void try_generate_one_parameter_static_method_caller() + { + var methodName = "StaticMethodWithOneParameter"; + var expectedValue = 5; + + var success = VisibilityBypasser.Instance.TryGenerateOneParameterStaticMethodCaller(typeof(PublicOuter), methodName, typeof(int), typeof(int), out var accessor ); + Assert.That(success, Is.True); + + var actualValue = accessor(5); + + Assert.That(actualValue, Is.EqualTo(expectedValue)); + } + + [Test] + public void try_generate_one_parameter_static_method_caller_failure() + { + // Arrange + var ownerType = typeof(PublicStatic); + var methodName = "NonExistentMethod"; + var paramType = typeof(int); + var returnType = typeof(int); + Func accessor; + + // Act + var result = VisibilityBypasser.Instance.TryGenerateOneParameterStaticMethodCaller(ownerType, methodName, paramType, returnType, out accessor); + + // Assert + Assert.That(result, Is.False); + Assert.That(accessor, Is.Null); + } + + + [Test] + public void try_generate_parameterless_static_method_caller() + { + var methodName = "GetANumber"; + var expectedValue = 3; + + var success = VisibilityBypasser.Instance.TryGenerateParameterlessStaticMethodCaller(typeof(PublicStatic), methodName, out var accessor); + Assert.That(success, Is.True); + + var actualValue = accessor(); + + Assert.That(actualValue, Is.EqualTo(expectedValue)); + } + + [Test] + public void try_generate_parameterless_static_method_caller_failure() + { + // Arrange + var ownerType = typeof(PublicStatic); + var methodName = "NonExistentMethod"; + Func accessor; + + // Act + var success = VisibilityBypasser.Instance.TryGenerateParameterlessStaticMethodCaller(ownerType, methodName, out accessor); + + // Assert + Assert.That(success, Is.False); + Assert.That(accessor, Is.Null); + } } }