From 27161e2215062ef56bc9f4bbf88d8c597712c41b Mon Sep 17 00:00:00 2001 From: Alexey Sosnin Date: Tue, 6 Feb 2024 00:26:13 +0300 Subject: [PATCH] feat: add HTTP requests grid --- .config/dotnet-tools.json | 2 +- Directory.Build.props | 2 +- src/DebugHost/Program.cs | 70 ++-- .../Analyzers/AsyncStatusMachineAnalyzer.cs | 9 +- .../Analyzers/HttpRequestAnalyzer.cs | 336 ++++++++++++++++++ .../Analyzers/ServicePointManagerAnalyzer.cs | 14 +- .../Analyzers/TimerQueueTimerAnalyzer.cs | 3 +- src/Heartbeat.Runtime/Domain/Analyzers.cs | 8 +- .../Extensions/ClrObjectExtensions.cs | 4 +- .../Extensions/ClrValueExtensions.cs | 63 ++++ .../Extensions/ClrValueTypeExtensions.cs | 11 +- src/Heartbeat.Runtime/HeapIndex.cs | 6 +- src/Heartbeat.Runtime/LogExtensions.cs | 5 +- src/Heartbeat.Runtime/Models/AsyncRecord.cs | 4 +- .../Proxies/ArrayListProxy.cs | 7 +- src/Heartbeat.Runtime/Proxies/ArrayProxy.cs | 23 +- .../Proxies/AsyncStateMachineBoxProxy.cs | 9 +- .../Proxies/CancellationTokenSourceProxy.cs | 4 +- .../Proxies/ConcurrentDictionaryProxy.cs | 20 +- .../Proxies/ConnectionProxy.cs | 4 +- .../Proxies/DictionaryProxy.cs | 22 +- .../Proxies/HashtableProxy.cs | 24 +- .../Proxies/HttpWebRequestProxy.cs | 4 +- .../Proxies/HttpWebResponseProxy.cs | 4 +- .../Proxies/IPAddressProxy.cs | 5 +- src/Heartbeat.Runtime/Proxies/ListProxy.cs | 6 +- src/Heartbeat.Runtime/Proxies/ProxyBase.cs | 6 +- .../Proxies/ServicePointProxy.cs | 4 +- src/Heartbeat.Runtime/Proxies/TaskProxy.cs | 15 +- src/Heartbeat.Runtime/Proxies/UriProxy.cs | 4 +- .../Proxies/ValueTaskProxy.cs | 14 +- .../Proxies/ValueTypeProxyBase.cs | 20 -- .../Proxies/WebHeaderCollectionProxy.cs | 4 +- src/Heartbeat.Runtime/RuntimeContext.cs | 3 +- src/Heartbeat/AnalyzeCommandHandler.cs | 5 +- src/Heartbeat/AnalyzeCommandOptions.cs | 3 +- src/Heartbeat/ClientApp/api.yml | 80 +++++ src/Heartbeat/ClientApp/src/App.tsx | 2 + .../src/client/api/dump/httpRequests/index.ts | 48 +++ .../ClientApp/src/client/api/dump/index.ts | 7 + .../ClientApp/src/client/kiota-lock.json | 2 +- .../ClientApp/src/client/models/index.ts | 81 +++++ .../components/HttpRequestStatusSelect.tsx | 54 +++ .../src/components/ProgressContainer.tsx | 4 - src/Heartbeat/ClientApp/src/layout/Menu.tsx | 8 + .../src/pages/httpRequests/HttpRequests.tsx | 118 ++++++ .../ClientApp/src/pages/httpRequests/index.ts | 7 + .../EndpointJsonSerializerContext.cs | 1 + .../EndpointRouteBuilderExtensions.cs | 6 +- src/Heartbeat/Endpoints/Models.cs | 5 +- src/Heartbeat/Endpoints/RouteHandlers.cs | 50 ++- src/Heartbeat/Program.cs | 3 - 52 files changed, 1041 insertions(+), 182 deletions(-) create mode 100644 src/Heartbeat.Runtime/Analyzers/HttpRequestAnalyzer.cs create mode 100644 src/Heartbeat.Runtime/Extensions/ClrValueExtensions.cs delete mode 100644 src/Heartbeat.Runtime/Proxies/ValueTypeProxyBase.cs create mode 100644 src/Heartbeat/ClientApp/src/client/api/dump/httpRequests/index.ts create mode 100644 src/Heartbeat/ClientApp/src/components/HttpRequestStatusSelect.tsx create mode 100644 src/Heartbeat/ClientApp/src/pages/httpRequests/HttpRequests.tsx create mode 100644 src/Heartbeat/ClientApp/src/pages/httpRequests/index.ts diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 56219ce..309e693 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "microsoft.openapi.kiota": { - "version": "1.10.1", + "version": "1.11.0", "commands": [ "kiota" ] diff --git a/Directory.Build.props b/Directory.Build.props index 4fe90f8..c90b374 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ MIT Alexey Sosnin ClrMd diagnostics - Diagnostics utility to analyze memory dumps of a .NET application + Diagnostics utility with web UI to analyze .NET application memory dump diff --git a/src/DebugHost/Program.cs b/src/DebugHost/Program.cs index 137aeea..647373d 100644 --- a/src/DebugHost/Program.cs +++ b/src/DebugHost/Program.cs @@ -1,51 +1,79 @@ using Heartbeat.Domain; using Heartbeat.Runtime; -using Heartbeat.Runtime.Domain; +using Heartbeat.Runtime.Analyzers; using Heartbeat.Runtime.Proxies; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; -// foreach (var dumpPath in Directory.GetFiles(@"D:\dumps", "*.dmp")) -// { -// ProcessFile(dumpPath); -// } +foreach (var dumpPath in Directory.GetFiles(@"C:\Users\Ne4to\projects\local\HttpRequestDumpSamples", "*.dmp")) +{ + ProcessFile(dumpPath); +} -ProcessFile(@"D:\dbg\dump_20230507_155200.dmp"); +// ProcessFile(@"D:\dbg\dump_20230507_155200.dmp"); +// ProcessFile(@"D:\dbg\user-management\local\dotnet-04.DMP"); static void ProcessFile(string filePath) -{ +{ + Console.WriteLine(); Console.WriteLine($"Processing dump: {filePath}"); var runtimeContext = new RuntimeContext(filePath); - WriteDictionary(runtimeContext); + // WriteDictionary(runtimeContext); + // WriteWebRequests(runtimeContext); + WriteHttpRequestMessage(runtimeContext); + + static void WriteHttpRequestMessage(RuntimeContext runtimeContext) + { + var analyzer = new HttpRequestAnalyzer(runtimeContext); + foreach (HttpRequestInfo httpRequest in analyzer.EnumerateHttpRequests()) + { + Console.WriteLine($"{httpRequest.HttpMethod} {httpRequest.Url} {httpRequest.StatusCode}"); + Console.WriteLine("\tRequest headers:"); + PrintHeaders(httpRequest.RequestHeaders); + Console.WriteLine("\tResponse headers:"); + PrintHeaders(httpRequest.ResponseHeaders); + } + + static void PrintHeaders(IReadOnlyList headers) + { + foreach (HttpHeader header in headers) + { + Console.WriteLine($"\t\t{header.Name}: {header.Value}"); + } + } + } - void WriteDictionary(RuntimeContext runtimeContext1) + static void WriteDictionary(RuntimeContext runtimeContext) { - var obj = runtimeContext.EnumerateObjects(null) + IClrValue obj = runtimeContext.EnumerateObjects(null) .Where(obj => !obj.IsNull && obj.Type.Name.StartsWith("System.Collections.Generic.Dictionaryt__builder"); - ClrObject taskObject; + IClrValue taskObject; string? typeName = builderValueClass.Type!.Name; if (typeName == "System.Runtime.CompilerServices.AsyncTaskMethodBuilder") { @@ -117,9 +118,7 @@ where clrObject.Type continue; } - - - var uTaskObject = uField.ReadObjectField("m_task"); + IClrValue uTaskObject = uField.ReadObjectField("m_task"); var statusTask = "NULL"; if (!uTaskObject.IsNull) { diff --git a/src/Heartbeat.Runtime/Analyzers/HttpRequestAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/HttpRequestAnalyzer.cs new file mode 100644 index 0000000..c41420b --- /dev/null +++ b/src/Heartbeat.Runtime/Analyzers/HttpRequestAnalyzer.cs @@ -0,0 +1,336 @@ +using Heartbeat.Runtime.Analyzers.Interfaces; +using Heartbeat.Runtime.Domain; +using Heartbeat.Runtime.Extensions; +using Heartbeat.Runtime.Proxies; + +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; + +using System.Text; + +namespace Heartbeat.Runtime.Analyzers; + +public enum HttpRequestStatus +{ + Pending, + Completed +} + +public class HttpRequestAnalyzer(RuntimeContext context) : AnalyzerBase(context), IWithObjectGCStatus +{ + private static readonly string[] _parsedValueFields = ["_parsedValue", "parsedValue", "ParsedValue", "k__BackingField"]; + private static readonly string[] _rawValueFields = ["_rawValue", "rawValue", "RawValue", "k__BackingField"]; + + public ObjectGCStatus? ObjectGcStatus { get; set; } + public HttpRequestStatus? RequestStatus { get; set; } + + public IEnumerable EnumerateHttpRequests() + { + IEnumerable httpRequests = CollectHttpRequests(); + httpRequests = FilterDuplicates(httpRequests); + if (RequestStatus != null) + { + httpRequests = FilterByStatus(httpRequests); + } + + return httpRequests; + } + + private IEnumerable CollectHttpRequests() + { + IEnumerable objectsToPrint = Context.Heap.EnumerateObjects(); + + foreach (ClrObject clrObject in objectsToPrint) + { + if (clrObject.Type?.Name == "System.Net.Http.HttpRequestMessage") + { + // HttpRequestMessage doesn't have reference to HttpResponseMessage + // the same http request can be found by HttpResponseMessage + // these duplicates handled by FilterDuplicates method + yield return BuildRequest(clrObject, null); + } + + if (clrObject.Type?.Name == "System.Net.Http.HttpResponseMessage") + { + var requestMessage = clrObject.ReadObjectField(Context.IsCoreRuntime ? "_requestMessage" : "requestMessage"); + yield return BuildRequest(requestMessage, clrObject); + } + + // TODO handle System.Net.HttpWebRequest for .NET Framework dumps + } + } + + private HttpRequestInfo BuildRequest(ClrObject request, ClrObject? response) + { + string? httpMethod = Context.IsCoreRuntime + ? request.ReadObjectField("_method").ReadStringField("_method") + : request.ReadObjectField("method").ReadStringField("method"); + + if (httpMethod == null) + { + throw new InvalidOperationException("Http Method was not read"); + } + + string? uri = Context.IsCoreRuntime + ? request.ReadObjectField("_requestUri").ReadStringField("_string") + : request.ReadObjectField("requestUri").ReadStringField("m_String"); + + if (uri == null) + { + throw new InvalidOperationException("Url was not read"); + } + + var requestHeaders = EnumerateHeaders(request).ToArray(); + + int? statusCode = null; + HttpHeader[] responseHeaders = Array.Empty(); + if (response is { IsNull: false }) + { + statusCode = Context.IsCoreRuntime + ? response.Value.ReadField("_statusCode") + : response.Value.ReadField("statusCode"); + + responseHeaders = EnumerateHeaders(response.Value).ToArray(); + } + + return new HttpRequestInfo(request, httpMethod, uri, statusCode, requestHeaders, responseHeaders); + } + + private IEnumerable EnumerateHeaders(ClrObject requestOrResponse) + { + if (!requestOrResponse.TryReadAnyObjectField(new[] { "_headers", "headers" }, out var headers)) + { + yield break; + } + + if (headers.IsNull) + { + yield break; + } + + // System.Collections.Generic.Dictionary + IClrValue headerStore = Context.IsCoreRuntime + ? headers.ReadObjectField("_headerStore") + : headers.ReadObjectField("headerStore"); + + // System.Collections.Generic.Dictionary`2[[System.Net.Http.Headers.HeaderDescriptor, System.Net.Http],[System.Net.Http.Headers.HttpHeaders+HeaderStoreItemInfo, System.Net.Http]] + if (headerStore.Type?.Name?.StartsWith("System.Collections.Generic.Dictionary") ?? false) + { + var dictionaryProxy = new DictionaryProxy(Context, headerStore); + foreach (KeyValuePair item in dictionaryProxy.EnumerateItems()) + { + var headerName = item.Key.Value.IsString() + ? item.Key.Value.AsString() + : item.Key.Value.ReadStringField("_headerName"); + + string? headerValue; + if (item.Value.Value.IsString()) + { + headerValue = item.Value.Value.AsString(); + } + else + { + // System.Net.Http.Headers.HttpHeaders+HeaderStoreItemInfo + if (!item.Value.Value.TryReadAnyObjectField(_parsedValueFields, out var parsedValue) || parsedValue.IsNull) + { + item.Value.Value.TryReadAnyObjectField(_rawValueFields, out parsedValue); + } + + if (parsedValue == null) + { + throw new NotSupportedException("unknown version of HeaderStoreItemInfo, parsedValue or rawValue field is not found"); + } + + headerValue = ReadHeaderValue(parsedValue); + if (headerValue == null) + { + throw new InvalidOperationException($"Unexpected storage structure for {headerName} header"); + } + } + + yield return new HttpHeader(headerName, headerValue); + } + } + else if (headerStore.Type?.Name == "System.Net.Http.Headers.HeaderEntry[]") + { + ArrayProxy headerEntryArray = new(Context, headerStore); + foreach (ArrayItem headerEntry in headerEntryArray.EnumerateArrayElements()) + { + string headerName; + string headerValue; + + var key = headerEntry.Value.ReadValueTypeField("Key"); + IClrValue descriptor = key.ReadObjectField("_descriptor"); + if (descriptor.IsNull) + continue; + + if (descriptor.IsString()) + { + headerName = descriptor.AsString(); + } + else if (descriptor.Type?.Name == "System.Net.Http.Headers.KnownHeader") + { + headerName = descriptor.ReadNotNullStringField("k__BackingField"); + } + else + { + throw new NotSupportedException($"Header name of type {descriptor.Type.Name} is not supported"); + } + + var value = headerEntry.Value.ReadObjectField("Value"); + if (value.IsString()) + { + headerValue = value.AsString(); + } + else if (value.Type?.Name == "System.Net.Http.Headers.HttpHeaders+HeaderStoreItemInfo") + { + IClrValue parsedAndInvalidValues = value.ReadObjectField("ParsedAndInvalidValues"); + + if (!parsedAndInvalidValues.IsValid) + { + continue; + } + + if (parsedAndInvalidValues.IsString()) + { + headerValue = parsedAndInvalidValues.AsString(); + } + else if (parsedAndInvalidValues.Type?.Name == "System.Collections.Generic.List") + { + ListProxy listProxy = new(Context, parsedAndInvalidValues); + + StringBuilder headerValueBuilder = new(); + // System.Net.Http.Headers.ProductInfoHeaderValue + foreach (IClrValue clrValue in listProxy.GetItems()) + { + if (clrValue.IsNull) + { + continue; + } + + // System.Net.Http.Headers.ProductHeaderValue + IClrValue productClrValue = clrValue.ReadObjectField("_product"); + string? name = null; + string? version = null; + if (!productClrValue.IsNull) + { + name = productClrValue.ReadStringField("_name"); + version = productClrValue.ReadStringField("_version"); + } + + string? comment = clrValue.ReadStringField("_comment"); + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent + headerValueBuilder.Append($"{name}/{version} {comment}"); + } + + headerValue = headerValueBuilder.ToString(); + } + else if (parsedAndInvalidValues.Type?.Name == "System.Net.Http.Headers.AuthenticationHeaderValue") + { + var scheme = parsedAndInvalidValues.ReadStringField("_scheme"); + var parameter = parsedAndInvalidValues.ReadStringField("_parameter"); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization + headerValue = $"{scheme} {parameter}"; + } + else if (parsedAndInvalidValues.Type?.Name == "System.Net.Http.Headers.EntityTagHeaderValue") + { + bool isWeak = parsedAndInvalidValues.ReadField("_isWeak"); + string? tag = parsedAndInvalidValues.ReadStringField("_tag"); + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag + headerValue = isWeak ? "W/" + tag : tag; + } + else if (parsedAndInvalidValues.Type?.Name == "System.DateTimeOffset") + { + headerValue = parsedAndInvalidValues.AsDateTimeOffset().ToString("R"); + } + else if (parsedAndInvalidValues.Type?.Name == "System.Net.Http.Headers.MediaTypeWithQualityHeaderValue") + { + headerValue = parsedAndInvalidValues.ReadAnyStringField(new[] { "_mediaType", "_value" }); + } + else + { + throw new NotSupportedException($"Header value of type {parsedAndInvalidValues.Type.Name} is not supported"); + } + } + else + { + throw new NotSupportedException($"Header value of type {value.Type.Name} is not supported"); + } + + yield return new HttpHeader(headerName, headerValue); + } + } + else + { + throw new NotSupportedException($"headers of type {headerStore.Type.Name} is not supported"); + } + } + + static string? ReadHeaderValue(IClrValue parsedValue) + { + if (parsedValue.IsNull) + { + return null; + } + + if (parsedValue.Type?.IsString ?? false) + { + return parsedValue.AsString(); + } + + if (parsedValue.Type?.Name == "System.DateTimeOffset") + { + return parsedValue.AsDateTimeOffset().ToString("O"); + } + + if (parsedValue.Type?.Name == "System.Net.Http.Headers.TransferCodingHeaderValue") + { + return parsedValue.ReadStringField("_value"); + } + + if (parsedValue.Type?.Name == "System.Net.Http.Headers.MediaTypeWithQualityHeaderValue") + { + return parsedValue.ReadAnyStringField(new[] { "mediaType", "_mediaType" }); + } + + throw new NotSupportedException($"Header value of type {parsedValue.Type?.Name} is not supported"); + } + + /// + /// Filter duplicates from requests collection + /// + /// + /// Filter out requests found by HttpRequestMessage only. + /// Requests found by HttpResponseMessage+HttpRequestMessage have more filled props. + /// + private static IEnumerable FilterDuplicates(IEnumerable requests) + { + HashSet processedRequests = new(); + + foreach (HttpRequestInfo httpRequest in requests.OrderBy(r => r.StatusCode == null)) + { + if (!processedRequests.Add(httpRequest.Request.Address)) + { + continue; + } + + yield return httpRequest; + } + } + + private IEnumerable FilterByStatus(IEnumerable requests) + { + foreach (HttpRequestInfo request in requests) + { + bool matchFilter = RequestStatus == HttpRequestStatus.Pending && request.StatusCode == null + || RequestStatus == HttpRequestStatus.Completed && request.StatusCode != null; + + if (matchFilter) + { + yield return request; + } + } + } +} \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Analyzers/ServicePointManagerAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/ServicePointManagerAnalyzer.cs index d475c13..fd58a0f 100644 --- a/src/Heartbeat.Runtime/Analyzers/ServicePointManagerAnalyzer.cs +++ b/src/Heartbeat.Runtime/Analyzers/ServicePointManagerAnalyzer.cs @@ -1,10 +1,8 @@ using Heartbeat.Runtime.Analyzers.Interfaces; -using Heartbeat.Runtime.Domain; using Heartbeat.Runtime.Exceptions; -using Heartbeat.Runtime.Extensions; using Heartbeat.Runtime.Proxies; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; namespace Heartbeat.Runtime.Analyzers; @@ -41,10 +39,10 @@ public void Dump(ILogger logger) continue; } - var servicePointTableObject = servicePointTableField.ReadObject(appDomain); + IClrValue servicePointTableObject = servicePointTableField.ReadObject(appDomain); - IReadOnlyCollection> servicePointTableProxy = Context.IsCoreRuntime - ? new ConcurrentDictionaryProxy(Context, servicePointTableObject) as IReadOnlyCollection> + IReadOnlyCollection> servicePointTableProxy = Context.IsCoreRuntime + ? new ConcurrentDictionaryProxy(Context, servicePointTableObject) as IReadOnlyCollection> : new HashtableProxy(Context, servicePointTableObject); foreach (var keyValuePair in servicePointTableProxy) @@ -55,7 +53,7 @@ public void Dump(ILogger logger) } var weakRefValue = Context.GetWeakRefValue(keyValuePair.Value); - var weakRefTarget = heap.GetObject(weakRefValue); + IClrValue weakRefTarget = heap.GetObject(weakRefValue); logger.LogInformation($"{keyValuePair.Key.AsString()}: {weakRefTarget}"); if (!weakRefTarget.IsNull) @@ -67,7 +65,7 @@ public void Dump(ILogger logger) } } - foreach (var spObject in Context.EnumerateObjectsByTypeName("System.Net.ServicePoint", null)) + foreach (IClrValue spObject in Context.EnumerateObjectsByTypeName("System.Net.ServicePoint", null)) { var servicePointProxy = new ServicePointProxy(Context, spObject); var servicePointAnalyzer = new ServicePointAnalyzer(Context, servicePointProxy) diff --git a/src/Heartbeat.Runtime/Analyzers/TimerQueueTimerAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/TimerQueueTimerAnalyzer.cs index 85971b3..9ed3c83 100644 --- a/src/Heartbeat.Runtime/Analyzers/TimerQueueTimerAnalyzer.cs +++ b/src/Heartbeat.Runtime/Analyzers/TimerQueueTimerAnalyzer.cs @@ -2,6 +2,7 @@ using Heartbeat.Runtime.Domain; using Heartbeat.Runtime.Proxies; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; namespace Heartbeat.Runtime.Analyzers; @@ -27,7 +28,7 @@ public IReadOnlyCollection GetTimers(ObjectGCStatus? status { var timerObjectType = Context.Heap.GetObjectType(address); - var state = timerObjectType.GetFieldByName("m_state").ReadObject(address, false); + IClrValue state = timerObjectType.GetFieldByName("m_state").ReadObject(address, false); var dueTime = timerObjectType.GetFieldByName("m_dueTime").Read(address, true); var period = timerObjectType.GetFieldByName("m_period").Read(address, true); var canceled = timerObjectType.GetFieldByName("m_canceled").Read(address, true); diff --git a/src/Heartbeat.Runtime/Domain/Analyzers.cs b/src/Heartbeat.Runtime/Domain/Analyzers.cs index 15d1ff8..cca2fbe 100644 --- a/src/Heartbeat.Runtime/Domain/Analyzers.cs +++ b/src/Heartbeat.Runtime/Domain/Analyzers.cs @@ -1,9 +1,15 @@ -namespace Heartbeat.Domain; +using Microsoft.Diagnostics.Runtime; + +namespace Heartbeat.Domain; public record DumpInfo(string DumpFileName, string DacFileName, bool CanWalkHeap); public record ObjectInfo(Address Address, TypeInfo Type); public record HttpClientInfo(Address Address, TimeSpan Timeout); + +public record struct HttpHeader(string Name, string Value); +public record HttpRequestInfo(ClrObject Request, string HttpMethod, string Url, int? StatusCode, IReadOnlyList RequestHeaders, IReadOnlyList ResponseHeaders); + public record StringDuplicate(string Value, int Count, int FullLength) { public Size WastedMemory { get; } = new((ulong)((Count - 1) * ( diff --git a/src/Heartbeat.Runtime/Extensions/ClrObjectExtensions.cs b/src/Heartbeat.Runtime/Extensions/ClrObjectExtensions.cs index 9eae5c5..2bd7908 100644 --- a/src/Heartbeat.Runtime/Extensions/ClrObjectExtensions.cs +++ b/src/Heartbeat.Runtime/Extensions/ClrObjectExtensions.cs @@ -1,10 +1,10 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Extensions; public static class ClrObjectExtensions { - public static DateTime GetDateTimeFieldValue(this ClrObject clrObject, string fieldName) + public static DateTime GetDateTimeFieldValue(this IClrValue clrObject, string fieldName) { var createTimeField = clrObject.Type.GetFieldByName(fieldName); var dateDataField = createTimeField.Type.GetFieldByName("dateData"); diff --git a/src/Heartbeat.Runtime/Extensions/ClrValueExtensions.cs b/src/Heartbeat.Runtime/Extensions/ClrValueExtensions.cs new file mode 100644 index 0000000..e7716dc --- /dev/null +++ b/src/Heartbeat.Runtime/Extensions/ClrValueExtensions.cs @@ -0,0 +1,63 @@ +using Microsoft.Diagnostics.Runtime.Interfaces; + +using System.Diagnostics.CodeAnalysis; + +namespace Heartbeat.Runtime.Extensions; + +public static class ClrValueExtensions +{ + public static bool TryReadAnyObjectField(this IClrValue clrValue, IEnumerable fieldNames, [NotNullWhen(true)] out IClrValue? result) + { + foreach (string fieldName in fieldNames) + { + if (clrValue.TryReadObjectField(fieldName, out result)) + { + return true; + } + } + + result = null; + return false; + } + + public static bool TryReadAnyStringField(this IClrValue clrValue, IEnumerable fieldNames, out string? result) + { + foreach (string fieldName in fieldNames) + { + if (clrValue.TryReadStringField(fieldName, null, out result)) + { + return true; + } + } + + result = null; + return false; + } + + public static string? ReadAnyStringField(this IClrValue clrValue, IEnumerable fieldNames) + { + if (clrValue.TryReadAnyStringField(fieldNames, out var result)) + { + return result; + } + + throw new InvalidOperationException($"None of string field '{string.Join(',', fieldNames)}' is found in type {clrValue.Type}."); + } + + public static string ReadNotNullStringField(this IClrValue clrValue, string fieldName) + { + return clrValue.ReadStringField(fieldName) ?? throw new InvalidOperationException($"String field {fieldName} is null"); + } + + public static bool IsString(this IClrValue clrValue) + { + return clrValue.Type?.IsString ?? false; + } + + public static DateTimeOffset AsDateTimeOffset(this IClrValue clrValue) + { + return new DateTimeOffset( + clrValue.ReadField("_dateTime"), + TimeSpan.FromMinutes(clrValue.ReadField("_offsetMinutes"))); + } +} \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs index 22cb0f0..782855c 100644 --- a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs +++ b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using System.Globalization; @@ -6,7 +7,7 @@ namespace Heartbeat.Runtime.Extensions { public static class ClrValueTypeExtensions { - public static bool IsDefaultValue(this ClrValueType valueType) + public static bool IsDefaultValue(this IClrValue valueType) { if (valueType.Type == null) { @@ -54,7 +55,7 @@ public static bool IsDefaultValue(this ClrValueType valueType) return true; } - public static string GetValueAsString(this ClrValueType valueType) + public static string GetValueAsString(this IClrValue valueType) { if (valueType.Type == null) { @@ -80,7 +81,7 @@ public static string GetValueAsString(this ClrValueType valueType) return valueType.Type.Name ?? ""; } - private static bool IsValueDefault(ulong objRef, ClrInstanceField field) + private static bool IsValueDefault(ulong objRef, IClrInstanceField field) { return field.ElementType switch { @@ -102,7 +103,7 @@ private static bool IsValueDefault(ulong objRef, ClrInstanceField field) }; } - private static string GetValueAsString(ulong objRef, ClrInstanceField field) + private static string GetValueAsString(ulong objRef, IClrInstanceField field) { return field.ElementType switch { @@ -124,7 +125,7 @@ private static string GetValueAsString(ulong objRef, ClrInstanceField field) }; } - private static bool IsZeroPtr(ulong objRef, ClrInstanceField field) + private static bool IsZeroPtr(ulong objRef, IClrInstanceField field) { return field.Type.Name switch { diff --git a/src/Heartbeat.Runtime/HeapIndex.cs b/src/Heartbeat.Runtime/HeapIndex.cs index 5f4cdfa..6a617e3 100644 --- a/src/Heartbeat.Runtime/HeapIndex.cs +++ b/src/Heartbeat.Runtime/HeapIndex.cs @@ -1,6 +1,8 @@ using Heartbeat.Runtime.Proxies; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; + namespace Heartbeat.Runtime; public sealed class HeapIndex @@ -76,7 +78,7 @@ void EnumerateArrayElements(ulong address) else if (array.Type.ComponentType?.IsValueType ?? false) { // TODO test and compare with WinDbg / dotnet dump - foreach (var arrayElement in ArrayProxy.EnumerateValueTypes(array)) + foreach (IClrValue arrayElement in ArrayProxy.EnumerateValueTypes(array)) { if (arrayElement.IsValid && arrayElement.Type != null) { @@ -91,7 +93,7 @@ void EnumerateArrayElements(ulong address) } } - void EnumerateFields(ClrType type, ulong objectAddress, ulong? parentAddress = null) + void EnumerateFields(IClrType type, ulong objectAddress, ulong? parentAddress = null) { foreach (var instanceField in type.Fields) { diff --git a/src/Heartbeat.Runtime/LogExtensions.cs b/src/Heartbeat.Runtime/LogExtensions.cs index 6b98493..3204af0 100644 --- a/src/Heartbeat.Runtime/LogExtensions.cs +++ b/src/Heartbeat.Runtime/LogExtensions.cs @@ -2,6 +2,7 @@ using Heartbeat.Runtime.Proxies; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; using System.Collections.Immutable; @@ -262,7 +263,7 @@ from clrObject in heap.EnumerateObjects() logger.LogInformation($"{taskCompletionSourceAddress:X} {tcsObject.Type}"); - var task = tcsObject.ReadObjectField("m_task"); + IClrValue task = tcsObject.ReadObjectField("m_task"); if (task.IsNull) { @@ -316,7 +317,7 @@ from address in heap.EnumerateObjects() foreach (var taskInfo in taskQuery) { - var taskObject = heap.GetObject(taskInfo.Address); + IClrValue taskObject = heap.GetObject(taskInfo.Address); var taskProxy = new TaskProxy(runtimeContext, taskObject); var taskIsCompleted = taskProxy.IsCompleted; diff --git a/src/Heartbeat.Runtime/Models/AsyncRecord.cs b/src/Heartbeat.Runtime/Models/AsyncRecord.cs index d4f35e6..3582716 100644 --- a/src/Heartbeat.Runtime/Models/AsyncRecord.cs +++ b/src/Heartbeat.Runtime/Models/AsyncRecord.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Models; @@ -21,7 +21,7 @@ public class AsyncRecord public IReadOnlyList
Continuations => _continuations; - public AsyncRecord(ClrObject clrObject) + public AsyncRecord(IClrValue clrObject) { if (clrObject.Type == null) { diff --git a/src/Heartbeat.Runtime/Proxies/ArrayListProxy.cs b/src/Heartbeat.Runtime/Proxies/ArrayListProxy.cs index fdf32e7..e7b7742 100644 --- a/src/Heartbeat.Runtime/Proxies/ArrayListProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ArrayListProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -6,7 +6,7 @@ public sealed class ArrayListProxy : ProxyBase { public int Count => TargetObject.ReadField("_size"); - public ArrayListProxy(RuntimeContext context, ClrObject targetObject) + public ArrayListProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -16,7 +16,7 @@ public ArrayListProxy(RuntimeContext context, ulong address) { } - public IEnumerable GetItems() + public IEnumerable GetItems() { if (Count == 0) { @@ -27,6 +27,7 @@ public IEnumerable GetItems() for (var itemArrayIndex = 0; itemArrayIndex < Count; itemArrayIndex++) { + // TODO use array proxy yield return itemsArray.AsArray() .GetObjectValue(itemArrayIndex); } diff --git a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs index 552afca..ed72270 100644 --- a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs @@ -1,6 +1,7 @@ using Heartbeat.Runtime.Extensions; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using System.Text; @@ -8,17 +9,17 @@ namespace Heartbeat.Runtime.Proxies; public sealed class ArrayProxy : ProxyBase { - private ClrArray _clrArray; + private IClrArray _clrArray; private readonly Lazy _unusedItemsCount; - public ClrArray InnerArray => _clrArray; + public IClrArray InnerArray => _clrArray; public int Length => _clrArray.Length; public int UnusedItemsCount => _unusedItemsCount.Value; public double UnusedItemsPercent => (double)UnusedItemsCount / Length; public Size Wasted => new Size((ulong)(_clrArray.Type.ComponentSize * UnusedItemsCount)); - public ArrayProxy(RuntimeContext context, ClrObject targetObject) + public ArrayProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { _clrArray = TargetObject.AsArray(); @@ -64,14 +65,14 @@ public ArrayProxy(RuntimeContext context, ulong address) .ReadValues(0, Length); } - public ClrObject[] GetItems() + public IClrValue[] GetItems() { if (Length == 0) { - return Array.Empty(); + return Array.Empty(); } - var result = new ClrObject[Length]; + var result = new IClrValue[Length]; for (int itemIndex = 0; itemIndex < Length; itemIndex++) { @@ -81,7 +82,7 @@ public ClrObject[] GetItems() return result; } - public static IEnumerable EnumerateObjectItems(ClrArray array) + public static IEnumerable EnumerateObjectItems(IClrArray array) { var length = array.Length; @@ -119,7 +120,7 @@ public static IEnumerable EnumerateObjectItems(ClrArray array) } } - public static IEnumerable EnumerateValueTypes(ClrArray array) + public static IEnumerable EnumerateValueTypes(IClrArray array) { var length = array.Length; @@ -182,7 +183,7 @@ public IEnumerable EnumerateArrayElements() ? arrayElement.AsString() : ""; - yield return new ArrayItem(index++, new Address(arrayElement.Address), value); + yield return new ArrayItem(index++, arrayElement, value); } } else if (_clrArray.Type.ComponentType?.IsValueType ?? false) @@ -196,7 +197,7 @@ public IEnumerable EnumerateArrayElements() // !DumpVC
// new Address(arrayElement.Address) // Context.Heap.GetObject(arrayElement.Address, arrayElement.Type).Type.Fields.Single(f => f.Name == "runningValue").GetAddress(arrayElement.Address, true).ToString("x8") - yield return new ArrayItem(index++, Address.Null, arrayElement.GetValueAsString()); + yield return new ArrayItem(index++, arrayElement, arrayElement.GetValueAsString()); } } else @@ -222,4 +223,4 @@ public IEnumerable EnumerateArrayElements() } } -public record struct ArrayItem(int Index, Address Address, string? Value); \ No newline at end of file +public record struct ArrayItem(int Index, IClrValue Value, string? StringValue); \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Proxies/AsyncStateMachineBoxProxy.cs b/src/Heartbeat.Runtime/Proxies/AsyncStateMachineBoxProxy.cs index 8b89014..09ffb70 100644 --- a/src/Heartbeat.Runtime/Proxies/AsyncStateMachineBoxProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/AsyncStateMachineBoxProxy.cs @@ -4,6 +4,7 @@ using Heartbeat.Runtime.Models; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; namespace Heartbeat.Runtime.Proxies; @@ -13,7 +14,7 @@ public class AsyncStateMachineBoxProxy : ProxyBase, ILoggerDump { private static readonly bool _fullLog; - public AsyncStateMachineBoxProxy(RuntimeContext context, ClrObject targetObject) + public AsyncStateMachineBoxProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -33,7 +34,7 @@ public void Dump(ILogger logger) //DebugObjectFields(); - ClrInstanceField? stateMachineField = TargetObject.Type.GetFieldByName("StateMachine"); + IClrInstanceField? stateMachineField = TargetObject.Type.GetFieldByName("StateMachine"); if (stateMachineField != null) { asyncRecord.IsStateMachine = true; @@ -129,7 +130,7 @@ public void Dump(ILogger logger) } else { - ClrObject uTaskObject = uField.ReadObjectField("m_task"); + IClrValue uTaskObject = uField.ReadObjectField("m_task"); var statusTask = "NULL"; if (!uTaskObject.IsNull) @@ -140,7 +141,7 @@ public void Dump(ILogger logger) foreach (var refAddress in Context.HeapIndex.GetReferencesTo(uTaskObject.Address)) { - var refObject = Context.Heap.GetObject(refAddress); + IClrValue refObject = Context.Heap.GetObject(refAddress); if (_fullLog) logger.LogInformation($"ref by {refObject}"); diff --git a/src/Heartbeat.Runtime/Proxies/CancellationTokenSourceProxy.cs b/src/Heartbeat.Runtime/Proxies/CancellationTokenSourceProxy.cs index e7c2690..db1eb7b 100644 --- a/src/Heartbeat.Runtime/Proxies/CancellationTokenSourceProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/CancellationTokenSourceProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -10,7 +10,7 @@ public sealed class CancellationTokenSourceProxy : ProxyBase private int State => TargetObject.ReadField("m_state"); - public CancellationTokenSourceProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public CancellationTokenSourceProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/ConcurrentDictionaryProxy.cs b/src/Heartbeat.Runtime/Proxies/ConcurrentDictionaryProxy.cs index cbe9d80..5bbcc66 100644 --- a/src/Heartbeat.Runtime/Proxies/ConcurrentDictionaryProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ConcurrentDictionaryProxy.cs @@ -1,15 +1,15 @@ using System.Collections; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Rename Heartbeat.Runtime.Proxies.ConcurrentDictionaryProxy to end in 'Collection'.")] -public sealed class ConcurrentDictionaryProxy : ProxyBase, IReadOnlyCollection> +public sealed class ConcurrentDictionaryProxy : ProxyBase, IReadOnlyCollection> { public int Count => GetCount(); - public ConcurrentDictionaryProxy(RuntimeContext context, ClrObject targetObject) + public ConcurrentDictionaryProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -19,12 +19,12 @@ public ConcurrentDictionaryProxy(RuntimeContext context, ulong address) { } - public IReadOnlyList> GetKeyValuePair() + public IReadOnlyList> GetKeyValuePair() { var bucketsObject = TargetObject.ReadObjectField("_tables").ReadObjectField("_buckets"); var buckets = new ArrayProxy(Context, bucketsObject); - var result = new List>(); + var result = new List>(); foreach (var bucketObject in buckets.GetItems()) { @@ -33,7 +33,7 @@ public IReadOnlyList> GetKeyValuePair() { var keyObject = currentNodeObject.ReadObjectField("_key"); var valObject = currentNodeObject.ReadObjectField("_value"); - var kvp = new KeyValuePair(keyObject, valObject); + var kvp = new KeyValuePair(keyObject, valObject); result.Add(kvp); currentNodeObject = currentNodeObject.ReadObjectField("_next"); @@ -52,7 +52,7 @@ private int GetCount() return countPerLock.GetInt32Array().Sum(); } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { return new Enumerator(this); } @@ -62,12 +62,12 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - private class Enumerator : IEnumerator> + private class Enumerator : IEnumerator> { - private readonly IReadOnlyList> _items; + private readonly IReadOnlyList> _items; private int _position = -1; - public KeyValuePair Current => _items[_position]; + public KeyValuePair Current => _items[_position]; object IEnumerator.Current => Current; diff --git a/src/Heartbeat.Runtime/Proxies/ConnectionProxy.cs b/src/Heartbeat.Runtime/Proxies/ConnectionProxy.cs index b4af4e1..bede2fc 100644 --- a/src/Heartbeat.Runtime/Proxies/ConnectionProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ConnectionProxy.cs @@ -1,6 +1,6 @@ using Heartbeat.Runtime.Extensions; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -39,7 +39,7 @@ public HttpWebRequestProxy? LockedRequest private ArrayListProxy WriteList => new(Context, TargetObject.ReadObjectField("m_WriteList")); - public ConnectionProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public ConnectionProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/DictionaryProxy.cs b/src/Heartbeat.Runtime/Proxies/DictionaryProxy.cs index 1621c15..8a55f58 100644 --- a/src/Heartbeat.Runtime/Proxies/DictionaryProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/DictionaryProxy.cs @@ -1,6 +1,7 @@ using Heartbeat.Runtime.Analyzers.Interfaces; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; namespace Heartbeat.Runtime.Proxies; @@ -12,7 +13,7 @@ public class DictionaryProxy: ProxyBase, ILoggerDump public ulong KeyMethodTable { get; } public ulong ValueMethodTable { get; } - public DictionaryProxy(RuntimeContext context, ClrObject targetObject) + public DictionaryProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { (KeyMethodTable, ValueMethodTable) = GetMethodTables(); @@ -49,7 +50,7 @@ public DictionaryProxy(RuntimeContext context, ulong address) // } // } - ClrObject entries = TargetObject.ReadObjectField(EntriesFieldName); + IClrValue entries = TargetObject.ReadObjectField(EntriesFieldName); if (entries.IsNull) { return (0, 0); @@ -82,7 +83,7 @@ private IEnumerable> EnumerateKeyValuePairs(elementAddress, true); var next = nextField.Read(elementAddress, true); @@ -92,8 +93,11 @@ private IEnumerable> EnumerateKeyValuePairs> EnumerateItems() @@ -129,24 +133,24 @@ public IEnumerable> EnumerateItems() { // IClrValue var entryKey = keyField.ReadObject(entry.Address, true); - key = new Item(entryKey.Address, entryKey.Type?.IsString ?? false ? entryKey.AsString() : null); + key = new Item(entryKey, entryKey.Type?.IsString ?? false ? entryKey.AsString() : null); } else { var entryKey = keyField.ReadStruct(entry.Address, true); - key = new Item(entryKey.Address, null); + key = new Item(entryKey, null); } if (valueField.IsObjectReference) { // IClrValue var entryValue = valueField.ReadObject(entry.Address, true); - value = new Item(entryValue.Address, entryValue.Type?.IsString ?? false ? entryValue.AsString() : null); + value = new Item( entryValue, entryValue.Type?.IsString ?? false ? entryValue.AsString() : null); } else { var entryValue = valueField.ReadStruct(entry.Address, true); - value = new Item(entryValue.Address, null); + value = new Item(entryValue, ""); } yield return new KeyValuePair(key, value); @@ -207,5 +211,5 @@ public void Dump(ILogger logger) } } - public record struct Item(ulong Address, string? Value); + public record struct Item(IClrValue Value, string? StringValue); } \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Proxies/HashtableProxy.cs b/src/Heartbeat.Runtime/Proxies/HashtableProxy.cs index 0f08c65..94173e1 100644 --- a/src/Heartbeat.Runtime/Proxies/HashtableProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/HashtableProxy.cs @@ -2,17 +2,17 @@ using Heartbeat.Runtime.Analyzers.Interfaces; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.Logging; namespace Heartbeat.Runtime.Proxies; [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Rename Heartbeat.Runtime.Proxies.HashtableProxy to end in 'Collection'.")] -public sealed class HashtableProxy : ProxyBase, IReadOnlyCollection>, ILoggerDump +public sealed class HashtableProxy : ProxyBase, IReadOnlyCollection>, ILoggerDump { public int Count => TargetObject.ReadField("count"); - public HashtableProxy(RuntimeContext context, ClrObject targetObject) + public HashtableProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -22,7 +22,7 @@ public HashtableProxy(RuntimeContext context, ulong address) { } - public IReadOnlyList> GetKeyValuePair() + public IReadOnlyList> GetKeyValuePair() { // bucketsObject is an array of 'bucket' struct var bucketsObject = TargetObject.ReadObjectField("buckets"); @@ -31,20 +31,20 @@ public IReadOnlyList> GetKeyValuePair() var bucketKeyField = elementType.GetFieldByName("key"); var bucketValField = elementType.GetFieldByName("val"); var bucketsLength = bucketsObject.AsArray().Length; - var result = new List>(); + var result = new List>(); for (int bucketIndex = 0; bucketIndex < bucketsLength; bucketIndex++) { //var arrayProxy = new ArrayProxy(Context, bucketsObject); // TODO move to ArrayProxy var elementAddress = bucketsObject.Type.GetArrayElementAddress(bucketsObject.Address, bucketIndex); - var keyObject = bucketKeyField.ReadObject(elementAddress, true); + IClrValue keyObject = bucketKeyField.ReadObject(elementAddress, true); if (!keyObject.IsNull) { - var valObject = bucketValField.ReadObject(elementAddress, true); + IClrValue valObject = bucketValField.ReadObject(elementAddress, true); - var kvp = new KeyValuePair(keyObject, valObject); + var kvp = new KeyValuePair(keyObject, valObject); result.Add(kvp); } } @@ -60,7 +60,7 @@ public void Dump(ILogger logger) } } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { return new Enumerator(this); } @@ -70,12 +70,12 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - private class Enumerator : IEnumerator> + private class Enumerator : IEnumerator> { - private readonly IReadOnlyList> _items; + private readonly IReadOnlyList> _items; private int _position = -1; - public KeyValuePair Current => _items[_position]; + public KeyValuePair Current => _items[_position]; object IEnumerator.Current => Current; diff --git a/src/Heartbeat.Runtime/Proxies/HttpWebRequestProxy.cs b/src/Heartbeat.Runtime/Proxies/HttpWebRequestProxy.cs index af91cec..c0e5a57 100644 --- a/src/Heartbeat.Runtime/Proxies/HttpWebRequestProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/HttpWebRequestProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -28,7 +28,7 @@ public HttpWebResponseProxy? Response public long? ContentLength => GetContentLength(); - public HttpWebRequestProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public HttpWebRequestProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/HttpWebResponseProxy.cs b/src/Heartbeat.Runtime/Proxies/HttpWebResponseProxy.cs index fee2367..8ac6074 100644 --- a/src/Heartbeat.Runtime/Proxies/HttpWebResponseProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/HttpWebResponseProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -10,7 +10,7 @@ public sealed class HttpWebResponseProxy : ProxyBase public WebHeaderCollectionProxy Headers => new(Context, TargetObject.ReadObjectField("m_HttpResponseHeaders")); - public HttpWebResponseProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public HttpWebResponseProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/IPAddressProxy.cs b/src/Heartbeat.Runtime/Proxies/IPAddressProxy.cs index 89e2eb8..41bc8bb 100644 --- a/src/Heartbeat.Runtime/Proxies/IPAddressProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/IPAddressProxy.cs @@ -1,5 +1,6 @@ using System.Net; -using Microsoft.Diagnostics.Runtime; + +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -14,7 +15,7 @@ public string Address } } - public IPAddressProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public IPAddressProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/ListProxy.cs b/src/Heartbeat.Runtime/Proxies/ListProxy.cs index 8791866..2db9759 100644 --- a/src/Heartbeat.Runtime/Proxies/ListProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ListProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -6,7 +6,7 @@ public sealed class ListProxy : ProxyBase { public int Count => TargetObject.ReadField("_size"); - public ListProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public ListProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -14,7 +14,7 @@ public ListProxy(RuntimeContext context, ulong address) : base(context, address) { } - public IEnumerable GetItems() + public IEnumerable GetItems() { if (Count == 0) { diff --git a/src/Heartbeat.Runtime/Proxies/ProxyBase.cs b/src/Heartbeat.Runtime/Proxies/ProxyBase.cs index 9ae4f39..059abce 100644 --- a/src/Heartbeat.Runtime/Proxies/ProxyBase.cs +++ b/src/Heartbeat.Runtime/Proxies/ProxyBase.cs @@ -1,13 +1,13 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; public abstract class ProxyBase { protected RuntimeContext Context { get; } - public ClrObject TargetObject { get; } + public IClrValue TargetObject { get; } - protected ProxyBase(RuntimeContext context, ClrObject targetObject) + protected ProxyBase(RuntimeContext context, IClrValue targetObject) { Context = context ?? throw new ArgumentNullException(nameof(context)); TargetObject = targetObject; diff --git a/src/Heartbeat.Runtime/Proxies/ServicePointProxy.cs b/src/Heartbeat.Runtime/Proxies/ServicePointProxy.cs index 13ecba1..ab09461 100644 --- a/src/Heartbeat.Runtime/Proxies/ServicePointProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ServicePointProxy.cs @@ -1,6 +1,6 @@ using Heartbeat.Runtime.Exceptions; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -61,7 +61,7 @@ public int ConnectionLimit public int CurrentConnections => GetCurrentConnections(); // public DateTime LastDnsResolve => TargetObject.GetDateTimeFieldValue("m_LastDnsResolve"); - public ServicePointProxy(RuntimeContext context, ClrObject targetObject) + public ServicePointProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/TaskProxy.cs b/src/Heartbeat.Runtime/Proxies/TaskProxy.cs index 4234e67..4e9a806 100644 --- a/src/Heartbeat.Runtime/Proxies/TaskProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/TaskProxy.cs @@ -1,5 +1,4 @@ - -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -47,7 +46,7 @@ public sealed class TaskProxy : ProxyBase public bool IsCompleted => GetIsCompleted(TargetObject); public bool IsFaulted => GetIsFaulted(TargetObject); - public TaskProxy(RuntimeContext context, ClrObject targetObject) + public TaskProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } @@ -57,7 +56,7 @@ public TaskProxy(RuntimeContext context, ulong address) { } - private static TaskStatus GetStatus(ClrObject taskObject) + private static TaskStatus GetStatus(IClrValue taskObject) { var stateFlags = taskObject.ReadField("m_stateFlags"); @@ -72,7 +71,7 @@ private static TaskStatus GetStatus(ClrObject taskObject) return TaskStatus.Created; } - private static bool GetIsCancelled(ClrObject taskObject) + private static bool GetIsCancelled(IClrValue taskObject) { var stateFlags = GetStateFlags(taskObject); @@ -80,14 +79,14 @@ private static bool GetIsCancelled(ClrObject taskObject) return (stateFlags & (TaskStateCanceled | TaskStateFaulted)) == TaskStateCanceled; } - private static bool GetIsCompleted(ClrObject taskObject) + private static bool GetIsCompleted(IClrValue taskObject) { var stateFlags = GetStateFlags(taskObject); return (stateFlags & TaskStateCompletedMask) != 0; } - private static bool GetIsFaulted(ClrObject taskObject) + private static bool GetIsFaulted(IClrValue taskObject) { var stateFlags = GetStateFlags(taskObject); @@ -95,7 +94,7 @@ private static bool GetIsFaulted(ClrObject taskObject) return (stateFlags & TaskStateFaulted) != 0; } - private static int GetStateFlags(ClrObject taskObject) + private static int GetStateFlags(IClrValue taskObject) { return taskObject.ReadField("m_stateFlags"); } diff --git a/src/Heartbeat.Runtime/Proxies/UriProxy.cs b/src/Heartbeat.Runtime/Proxies/UriProxy.cs index 53feeea..3aaae65 100644 --- a/src/Heartbeat.Runtime/Proxies/UriProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/UriProxy.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; @@ -16,7 +16,7 @@ public string Value } } - public UriProxy(RuntimeContext context, ClrObject targetObject) + public UriProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/ValueTaskProxy.cs b/src/Heartbeat.Runtime/Proxies/ValueTaskProxy.cs index b8274fa..fc2e6b1 100644 --- a/src/Heartbeat.Runtime/Proxies/ValueTaskProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ValueTaskProxy.cs @@ -1,8 +1,8 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; -public sealed class ValueTaskProxy : ValueTypeProxyBase +public sealed class ValueTaskProxy : ProxyBase { public bool IsCompleted { @@ -37,15 +37,9 @@ public bool IsCompleted } } - private short Token - { - get - { - return TargetObject.ReadField("_token"); - } - } + private short Token => TargetObject.ReadField("_token"); - public ValueTaskProxy(RuntimeContext context, ClrValueType targetObject) + public ValueTaskProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/Proxies/ValueTypeProxyBase.cs b/src/Heartbeat.Runtime/Proxies/ValueTypeProxyBase.cs deleted file mode 100644 index 1f33875..0000000 --- a/src/Heartbeat.Runtime/Proxies/ValueTypeProxyBase.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Diagnostics.Runtime; - -namespace Heartbeat.Runtime.Proxies; - -public abstract class ValueTypeProxyBase -{ - protected RuntimeContext Context { get; } - public ClrValueType TargetObject { get; } - - protected ValueTypeProxyBase(RuntimeContext context, ClrValueType targetObject) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - TargetObject = targetObject; - } - - public override string ToString() - { - return TargetObject.ToString(); - } -} \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Proxies/WebHeaderCollectionProxy.cs b/src/Heartbeat.Runtime/Proxies/WebHeaderCollectionProxy.cs index 09bcb22..aafc2b2 100644 --- a/src/Heartbeat.Runtime/Proxies/WebHeaderCollectionProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/WebHeaderCollectionProxy.cs @@ -1,10 +1,10 @@ -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime.Proxies; public sealed class WebHeaderCollectionProxy : ProxyBase { - public WebHeaderCollectionProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) + public WebHeaderCollectionProxy(RuntimeContext context, IClrValue targetObject) : base(context, targetObject) { } diff --git a/src/Heartbeat.Runtime/RuntimeContext.cs b/src/Heartbeat.Runtime/RuntimeContext.cs index 62c13f0..fc57ce5 100644 --- a/src/Heartbeat.Runtime/RuntimeContext.cs +++ b/src/Heartbeat.Runtime/RuntimeContext.cs @@ -3,6 +3,7 @@ using Heartbeat.Runtime.Models; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace Heartbeat.Runtime; @@ -193,7 +194,7 @@ from clrObject in Heap.EnumerateObjects() // } // based on https://stackoverflow.com/questions/33290941/how-to-inspect-weakreference-values-with-windbg-sos-and-clrmd - public ulong GetWeakRefValue(ClrObject weakRefObject) + public ulong GetWeakRefValue(IClrValue weakRefObject) { var weakRefHandleField = weakRefObject.Type.GetFieldByName("m_handle"); ClrType intPtrType = Heap.GetTypeByName("System.IntPtr"); diff --git a/src/Heartbeat/AnalyzeCommandHandler.cs b/src/Heartbeat/AnalyzeCommandHandler.cs index ba62846..76a47b5 100644 --- a/src/Heartbeat/AnalyzeCommandHandler.cs +++ b/src/Heartbeat/AnalyzeCommandHandler.cs @@ -6,6 +6,7 @@ using Heartbeat.Runtime.Proxies; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Text.RegularExpressions; @@ -319,7 +320,7 @@ private void ProcessCommand(DataTarget dataTarget, ILogger logger) foreach (var responseObj in q) { var requestMessageObj = responseObj.ReadObjectField("requestMessage"); - var uriProxy = new UriProxy(runtimeContext, requestMessageObj.ReadObjectField("requestUri")); + var uriProxy = new UriProxy(runtimeContext, (IClrValue)requestMessageObj.ReadObjectField("requestUri")); var (requestLength, requestLOH) = GetRequestLength(responseObj, runtimeContext); var (responseLength, responseLOH) = GetResponseLength(responseObj, runtimeContext); @@ -486,7 +487,7 @@ private static (int? Length, bool? IsLargeObjectSegment) GetRequestLength(ClrObj return (null, null); } - var bufferObj = bufferedContentObj.ReadObjectField("_buffer"); + IClrValue bufferObj = bufferedContentObj.ReadObjectField("_buffer"); if (bufferObj.IsNull) { return (null, null); diff --git a/src/Heartbeat/AnalyzeCommandOptions.cs b/src/Heartbeat/AnalyzeCommandOptions.cs index f21ef84..06e2d33 100644 --- a/src/Heartbeat/AnalyzeCommandOptions.cs +++ b/src/Heartbeat/AnalyzeCommandOptions.cs @@ -1,5 +1,4 @@ -using Heartbeat.Domain; -using Heartbeat.Runtime.Domain; +using Heartbeat.Runtime.Domain; using System.CommandLine; using System.CommandLine.Binding; diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index 6b2a810..e83e77f 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -283,6 +283,37 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + /api/dump/http-requests: + get: + tags: + - Dump + operationId: GetHttpRequests + parameters: + - name: gcStatus + in: query + style: form + schema: + $ref: '#/components/schemas/ObjectGCStatus' + - name: status + in: query + style: form + schema: + $ref: '#/components/schemas/HttpRequestStatus' + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/HttpRequestInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}': get: tags: @@ -764,6 +795,55 @@ components: format: int64 readOnly: true additionalProperties: false + HttpHeader: + required: + - name + - value + type: object + properties: + name: + type: string + value: + type: string + additionalProperties: false + HttpRequestInfo: + required: + - httpMethod + - requestAddress + - requestHeaders + - requestMethodTable + - responseHeaders + - url + type: object + properties: + requestAddress: + type: integer + format: int64 + requestMethodTable: + type: integer + format: int64 + httpMethod: + type: string + url: + type: string + statusCode: + type: integer + format: int32 + nullable: true + requestHeaders: + type: array + items: + $ref: '#/components/schemas/HttpHeader' + responseHeaders: + type: array + items: + $ref: '#/components/schemas/HttpHeader' + additionalProperties: false + HttpRequestStatus: + enum: + - Pending + - Completed + type: string JwtInfo: required: - header diff --git a/src/Heartbeat/ClientApp/src/App.tsx b/src/Heartbeat/ClientApp/src/App.tsx index b4047f3..bb012f3 100644 --- a/src/Heartbeat/ClientApp/src/App.tsx +++ b/src/Heartbeat/ClientApp/src/App.tsx @@ -10,6 +10,7 @@ import objectInstances from './pages/objectInstances' import clrObject from './pages/clrObject' import roots from './pages/roots' import modules from './pages/modules' +import httpRequests from './pages/httpRequests' import arraysGrid from './pages/arraysGrid' import sparseArraysStat from './pages/sparseArraysStat' import stringsGrid from './pages/stringsGrid' @@ -43,6 +44,7 @@ const App = () => { + diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/httpRequests/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/httpRequests/index.ts new file mode 100644 index 0000000..65f9e14 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/httpRequests/index.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +// Generated by Microsoft Kiota +import { createHttpRequestInfoFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, HttpRequestStatus, ObjectGCStatus, serializeProblemDetails, type HttpRequestInfo, type ProblemDetails } from '../../../models/'; +import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions'; + +export interface HttpRequestsRequestBuilderGetQueryParameters { + gcStatus?: ObjectGCStatus; + status?: HttpRequestStatus; +} +/** + * Builds and executes requests for operations under /api/dump/http-requests + */ +export class HttpRequestsRequestBuilder extends BaseRequestBuilder { + /** + * Instantiates a new HttpRequestsRequestBuilder and sets the default values. + * @param pathParameters The raw url or the Url template parameters for the request. + * @param requestAdapter The request adapter to use to execute the requests. + */ + public constructor(pathParameters: Record | string | undefined, requestAdapter: RequestAdapter) { + super(pathParameters, requestAdapter, "{+baseurl}/api/dump/http-requests{?gcStatus*,status*}", (x, y) => new HttpRequestsRequestBuilder(x, y)); + } + /** + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a Promise of HttpRequestInfo + */ + public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendCollectionAsync(requestInfo, createHttpRequestInfoFromDiscriminatorValue, errorMapping); + } + /** + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a RequestInformation + */ + public toGetRequestInformation(requestConfiguration?: RequestConfiguration | undefined) : RequestInformation { + const requestInfo = new RequestInformation(HttpMethod.GET, this.urlTemplate, this.pathParameters); + requestInfo.configure(requestConfiguration); + requestInfo.headers.tryAdd("Accept", "application/json"); + return requestInfo; + } +} +/* tslint:enable */ +/* eslint-enable */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/index.ts index 55e6233..9e883da 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/index.ts @@ -3,6 +3,7 @@ // Generated by Microsoft Kiota import { ArraysRequestBuilder } from './arrays/'; import { HeapDumpStatisticsRequestBuilder } from './heapDumpStatistics/'; +import { HttpRequestsRequestBuilder } from './httpRequests/'; import { InfoRequestBuilder } from './info/'; import { ModulesRequestBuilder } from './modules/'; import { ObjectRequestBuilder } from './object/'; @@ -29,6 +30,12 @@ export class DumpRequestBuilder extends BaseRequestBuilder { public get heapDumpStatistics(): HeapDumpStatisticsRequestBuilder { return new HeapDumpStatisticsRequestBuilder(this.pathParameters, this.requestAdapter); } + /** + * The httpRequests property + */ + public get httpRequests(): HttpRequestsRequestBuilder { + return new HttpRequestsRequestBuilder(this.pathParameters, this.requestAdapter); + } /** * The info property */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 06e7962..45cf651 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "B5A6597B503DFBDF748026FBD234968A4A01A1C687D00711E5A910900C066A238295C318D98C4968DAA9AC5E2C494CFCB8CB95F0093A95E2DCAED37AC072D9C7", + "descriptionHash": "5A47D399D32BBFCA22D427C3C2B9F658D83E693AB0708F62E0BBD501ACFB2FDD21BBD84C9E53D0B5241083544077C5A4F8332006BAD55BDE4680EB241D308AE3", "descriptionLocation": "..\\..\\api.yml", "lockFileVersion": "1.0.0", "kiotaVersion": "1.10.1", diff --git a/src/Heartbeat/ClientApp/src/client/models/index.ts b/src/Heartbeat/ClientApp/src/client/models/index.ts index 0818682..1c698fb 100644 --- a/src/Heartbeat/ClientApp/src/client/models/index.ts +++ b/src/Heartbeat/ClientApp/src/client/models/index.ts @@ -122,6 +122,12 @@ export function createGetObjectInstancesResultFromDiscriminatorValue(parseNode: export function createHeapSegmentFromDiscriminatorValue(parseNode: ParseNode | undefined) { return deserializeIntoHeapSegment; } +export function createHttpHeaderFromDiscriminatorValue(parseNode: ParseNode | undefined) { + return deserializeIntoHttpHeader; +} +export function createHttpRequestInfoFromDiscriminatorValue(parseNode: ParseNode | undefined) { + return deserializeIntoHttpRequestInfo; +} export function createJwtInfoFromDiscriminatorValue(parseNode: ParseNode | undefined) { return deserializeIntoJwtInfo; } @@ -248,6 +254,23 @@ export function deserializeIntoHeapSegment(heapSegment: HeapSegment | undefined "start": n => { heapSegment.start = n.getNumberValue(); }, } } +export function deserializeIntoHttpHeader(httpHeader: HttpHeader | undefined = {} as HttpHeader) : Record void> { + return { + "name": n => { httpHeader.name = n.getStringValue(); }, + "value": n => { httpHeader.value = n.getStringValue(); }, + } +} +export function deserializeIntoHttpRequestInfo(httpRequestInfo: HttpRequestInfo | undefined = {} as HttpRequestInfo) : Record void> { + return { + "httpMethod": n => { httpRequestInfo.httpMethod = n.getStringValue(); }, + "requestAddress": n => { httpRequestInfo.requestAddress = n.getNumberValue(); }, + "requestHeaders": n => { httpRequestInfo.requestHeaders = n.getCollectionOfObjectValues(createHttpHeaderFromDiscriminatorValue); }, + "requestMethodTable": n => { httpRequestInfo.requestMethodTable = n.getNumberValue(); }, + "responseHeaders": n => { httpRequestInfo.responseHeaders = n.getCollectionOfObjectValues(createHttpHeaderFromDiscriminatorValue); }, + "statusCode": n => { httpRequestInfo.statusCode = n.getNumberValue(); }, + "url": n => { httpRequestInfo.url = n.getStringValue(); }, + } +} export function deserializeIntoJwtInfo(jwtInfo: JwtInfo | undefined = {} as JwtInfo) : Record void> { return { "header": n => { jwtInfo.header = n.getCollectionOfObjectValues(createJwtValueFromDiscriminatorValue); }, @@ -470,6 +493,47 @@ export interface HeapSegment extends Parsable { */ start?: number; } +export interface HttpHeader extends Parsable { + /** + * The name property + */ + name?: string; + /** + * The value property + */ + value?: string; +} +export interface HttpRequestInfo extends Parsable { + /** + * The httpMethod property + */ + httpMethod?: string; + /** + * The requestAddress property + */ + requestAddress?: number; + /** + * The requestHeaders property + */ + requestHeaders?: HttpHeader[]; + /** + * The requestMethodTable property + */ + requestMethodTable?: number; + /** + * The responseHeaders property + */ + responseHeaders?: HttpHeader[]; + /** + * The statusCode property + */ + statusCode?: number; + /** + * The url property + */ + url?: string; +} +export type HttpRequestStatus = (typeof HttpRequestStatusObject)[keyof typeof HttpRequestStatusObject]; export interface JwtInfo extends Parsable { /** * The header property @@ -681,6 +745,19 @@ export function serializeHeapSegment(writer: SerializationWriter, heapSegment: H writer.writeEnumValue("kind", heapSegment.kind); writer.writeNumberValue("start", heapSegment.start); } +export function serializeHttpHeader(writer: SerializationWriter, httpHeader: HttpHeader | undefined = {} as HttpHeader) : void { + writer.writeStringValue("name", httpHeader.name); + writer.writeStringValue("value", httpHeader.value); +} +export function serializeHttpRequestInfo(writer: SerializationWriter, httpRequestInfo: HttpRequestInfo | undefined = {} as HttpRequestInfo) : void { + writer.writeStringValue("httpMethod", httpRequestInfo.httpMethod); + writer.writeNumberValue("requestAddress", httpRequestInfo.requestAddress); + writer.writeCollectionOfObjectValues("requestHeaders", httpRequestInfo.requestHeaders, serializeHttpHeader); + writer.writeNumberValue("requestMethodTable", httpRequestInfo.requestMethodTable); + writer.writeCollectionOfObjectValues("responseHeaders", httpRequestInfo.responseHeaders, serializeHttpHeader); + writer.writeNumberValue("statusCode", httpRequestInfo.statusCode); + writer.writeStringValue("url", httpRequestInfo.url); +} export function serializeJwtInfo(writer: SerializationWriter, jwtInfo: JwtInfo | undefined = {} as JwtInfo) : void { writer.writeCollectionOfObjectValues("header", jwtInfo.header, serializeJwtValue); writer.writeCollectionOfObjectValues("payload", jwtInfo.payload, serializeJwtValue); @@ -839,6 +916,10 @@ export const GenerationObject = { Frozen: "Frozen", Unknown: "Unknown", } as const; +export const HttpRequestStatusObject = { + Pending: "Pending", + Completed: "Completed", +} as const; export const ObjectGCStatusObject = { Live: "Live", Dead: "Dead", diff --git a/src/Heartbeat/ClientApp/src/components/HttpRequestStatusSelect.tsx b/src/Heartbeat/ClientApp/src/components/HttpRequestStatusSelect.tsx new file mode 100644 index 0000000..cf13c8c --- /dev/null +++ b/src/Heartbeat/ClientApp/src/components/HttpRequestStatusSelect.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select, {SelectChangeEvent} from '@mui/material/Select'; +import {Generation, GenerationObject, HttpRequestStatus, HttpRequestStatusObject} from '../client/models'; +import {FormControl} from '@mui/material'; + +const ANY_ITEM_KEY = 'any' +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, +}; + +export type HttpRequestStatusSelectProps = { + status?: HttpRequestStatus, + onChange?: (status?: HttpRequestStatus) => void +} + +export const HttpRequestStatusSelect = (props: HttpRequestStatusSelectProps) => { + const handleChange = (event: SelectChangeEvent) => { + const isAny = event.target.value === ANY_ITEM_KEY + if (isAny) { + props.onChange?.(undefined) + } else { + const status = event.target.value as HttpRequestStatus + props.onChange?.(status) + } + }; + + return ( + + Request status + + + ); +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx b/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx index 19a99e1..df04a2a 100644 --- a/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx +++ b/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx @@ -16,9 +16,5 @@ export const ProgressContainer = (props: ProgressContainerProps) => { ); - // TODO add error message and remove notify - // if (hasError || data === undefined) - // return - return props.children ?? (

No data to display

); } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/layout/Menu.tsx b/src/Heartbeat/ClientApp/src/layout/Menu.tsx index 1d18fd8..403f0b7 100644 --- a/src/Heartbeat/ClientApp/src/layout/Menu.tsx +++ b/src/Heartbeat/ClientApp/src/layout/Menu.tsx @@ -13,6 +13,7 @@ import segments from '../pages/segments'; import roots from '../pages/roots'; import modules from '../pages/modules'; import arraysGrid from '../pages/arraysGrid'; +import httpRequests from '../pages/httpRequests'; import sparseArraysStat from '../pages/sparseArraysStat'; import stringsGrid from '../pages/stringsGrid'; import stringDuplicates from '../pages/stringDuplicates'; @@ -76,6 +77,13 @@ const Menu = ({ dense = false }: MenuProps) => { leftIcon={} dense={dense} /> + } + dense={dense} + /> { + return params.row.requestHeaders.find((h: HttpHeader) => h.name === 'traceparent')?.value + ?? params.row.responseHeaders.find((h: HttpHeader) => h.name === 'traceparent')?.value; + }, + renderCell: params => { + return
{params.value}
+ } + }, +]; + +export const HttpRequests = () => { + const {notify, notifyError} = useNotifyError(); + + const [gcStatus, setGcStatus] = React.useState() + const [status, setStatus] = React.useState() + const [httpRequests, setHttpRequests, isLoading, setIsLoading] = useStateWithLoading() + + useEffect(() => { + const fetchHttpRequests = async () => { + const client = getClient(); + return await client.api.dump.httpRequests.get( + {queryParameters: {gcStatus: gcStatus, status: status}} + ) + } + + handleFetchData(fetchHttpRequests, setHttpRequests, setIsLoading, notifyError); + }, [gcStatus, status, notify]); + + const renderTable = (httpRequests: HttpRequestInfo[]) => { + // TODO master - detail for headers + // this grid required Pro license: https://mui.com/x/react-data-grid/master-detail/ + return ( + row.requestAddress} + columns={columns} + rowHeight={25} + pageSizeOptions={[20, 50, 100]} + density='compact' + initialState={{ + // sorting: { + // sortModel: [{field: 'length', sort: 'desc'}], + // }, + pagination: {paginationModel: {pageSize: 20}}, + }} + slots={{toolbar: GridToolbar}} + slotProps={{ + toolbar: { + showQuickFilter: true, + }, + }} + /> + ); + } + + const getChildrenContent = (httpRequests?: HttpRequestInfo[]) => { + if (!httpRequests || httpRequests.length === 0) + return undefined; + + const propertyRows: PropertyRow[] = [ + {title: 'Count', value: String(httpRequests.length)}, + ] + + return + + {renderTable(httpRequests)} + + } + + return ( + + + setGcStatus(status)}/> + setStatus(status)}/> + + + {getChildrenContent(httpRequests)} + + + ); +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/pages/httpRequests/index.ts b/src/Heartbeat/ClientApp/src/pages/httpRequests/index.ts new file mode 100644 index 0000000..11d715b --- /dev/null +++ b/src/Heartbeat/ClientApp/src/pages/httpRequests/index.ts @@ -0,0 +1,7 @@ +import Icon from '@mui/icons-material/ViewModule'; +import { HttpRequests } from './HttpRequests'; + +export default { + icon: Icon, + list: HttpRequests, +}; diff --git a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs index 1e4e1ef..bc61eb5 100644 --- a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs +++ b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs @@ -19,4 +19,5 @@ namespace Heartbeat.Host.Endpoints; [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] internal partial class EndpointJsonSerializerContext : JsonSerializerContext; \ No newline at end of file diff --git a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs index d78809b..99b947c 100644 --- a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs +++ b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs @@ -50,6 +50,10 @@ public static void MapDumpEndpoints(this IEndpointRouteBuilder app) dumpGroup.MapGet("arrays/sparse/stat", RouteHandlers.GetSparseArraysStat) .Produces(StatusCodes.Status500InternalServerError) .WithName(nameof(RouteHandlers.GetSparseArraysStat)); + + dumpGroup.MapGet("http-requests", RouteHandlers.GetHttpRequests) + .Produces(StatusCodes.Status500InternalServerError) + .WithName(nameof(RouteHandlers.GetHttpRequests)); dumpGroup.MapGet("object/{address}", RouteHandlers.GetClrObject) .Produces(StatusCodes.Status404NotFound) @@ -63,7 +67,7 @@ public static void MapDumpEndpoints(this IEndpointRouteBuilder app) .WithName(nameof(RouteHandlers.GetClrObjectAsArray)); dumpGroup.MapGet("object/{address}/as-dictionary", RouteHandlers.GetClrObjectAsDictionary) - .Produces(StatusCodes.Status204NoContent) + .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError) .WithName(nameof(RouteHandlers.GetClrObjectAsDictionary)); diff --git a/src/Heartbeat/Endpoints/Models.cs b/src/Heartbeat/Endpoints/Models.cs index 7ef9fa1..8386c04 100644 --- a/src/Heartbeat/Endpoints/Models.cs +++ b/src/Heartbeat/Endpoints/Models.cs @@ -69,4 +69,7 @@ public record ArrayInfo(ulong Address, ulong MethodTable, string? TypeName, int public record SparseArrayStatistics(ulong MethodTable, string? TypeName, int Count, ulong TotalWasted); public record JwtInfo(IReadOnlyList Header, IReadOnlyList Payload); -public record JwtValue(string Key, string Value, string? Description); \ No newline at end of file +public record JwtValue(string Key, string Value, string? Description); + +public record struct HttpHeader(string Name, string Value); +public record HttpRequestInfo(ulong RequestAddress, ulong RequestMethodTable, string HttpMethod, string Url, int? StatusCode, IReadOnlyList RequestHeaders, IReadOnlyList ResponseHeaders); \ No newline at end of file diff --git a/src/Heartbeat/Endpoints/RouteHandlers.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs index bc0b8b6..00166b3 100644 --- a/src/Heartbeat/Endpoints/RouteHandlers.cs +++ b/src/Heartbeat/Endpoints/RouteHandlers.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using System.Globalization; @@ -154,7 +155,7 @@ public static IEnumerable GetSparseArrays( [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) { - var query = from obj in context.EnumerateObjects(gcStatus, generation) + var query = from IClrValue obj in context.EnumerateObjects(gcStatus, generation) where obj.IsArray let proxy = new ArrayProxy(context, obj) where proxy.UnusedItemsPercent >= 0.2 @@ -170,7 +171,7 @@ public static IEnumerable GetSparseArraysStat( [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) { - var query = from obj in context.EnumerateObjects(gcStatus, generation) + var query = from IClrValue obj in context.EnumerateObjects(gcStatus, generation) where obj.IsArray let proxy = new ArrayProxy(context, obj) where proxy.UnusedItemsCount != 0 @@ -187,6 +188,37 @@ into grp return query; } + public static IEnumerable GetHttpRequests( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + // TODO [FromQuery] Generation? generation = null, + [FromQuery] HttpRequestStatus? status = null) + { + var analyzer = new HttpRequestAnalyzer(context) { ObjectGcStatus = gcStatus, RequestStatus = status }; + + return analyzer.EnumerateHttpRequests() + .Select(MapRequest); + } + + private static HttpHeader[] MapHeaders(IReadOnlyList headers) + { + return headers.Select(header => new HttpHeader(header.Name, header.Value)) + .ToArray(); + } + + private static HttpRequestInfo MapRequest(Domain.HttpRequestInfo request) + { + return new HttpRequestInfo( + request.Request.Address, + request.Request.Type!.MethodTable, + request.HttpMethod, + request.Url, + request.StatusCode, + MapHeaders(request.RequestHeaders), + MapHeaders(request.ResponseHeaders) + ); + } + public static Results, NotFound> GetClrObject([FromServices] RuntimeContext context, ulong address) { var clrObject = context.Heap.GetObject(address); @@ -285,7 +317,7 @@ public static Results>, NotFound> GetClrObjectRoots([ public static Results>, NoContent, NotFound> GetClrObjectAsArray([FromServices] RuntimeContext context, ulong address) { - var clrObject = context.Heap.GetObject(address); + IClrValue clrObject = context.Heap.GetObject(address); if (clrObject.Type == null) { return TypedResults.NotFound(); @@ -298,7 +330,7 @@ public static Results>, NoContent, NotFound> // TODO var str = arrayProxy.AsStringValue(); var items = arrayProxy.EnumerateArrayElements() - .Select(e => new ClrObjectArrayItem(e.Index, e.Address, e.Value)); + .Select(e => new ClrObjectArrayItem(e.Index, e.Value.Address, e.StringValue)); return TypedResults.Ok(items); } @@ -308,7 +340,7 @@ public static Results>, NoContent, NotFound> public static Results, NoContent, NotFound> GetClrObjectAsDictionary([FromServices] RuntimeContext context, ulong address) { - var clrObject = context.Heap.GetObject(address); + IClrValue clrObject = context.Heap.GetObject(address); if (clrObject.Type == null) { return TypedResults.NotFound(); @@ -335,7 +367,7 @@ public static Results, NoContent, NotFound> GetClrObjectAsDic static DictionaryItem MapItem(DictionaryProxy.Item item) { - return new DictionaryItem(item.Address, item.Value); + return new DictionaryItem(item.Value.Address, item.StringValue); } } @@ -437,7 +469,11 @@ string GetAddress() var minor = clrObject.ReadField("_Minor"); var build = clrObject.ReadField("_Build"); var revision = clrObject.ReadField("_Revision"); - var version = new Version(major, minor, build, revision); + var version = build < 0 + ? new Version(major, minor) + : revision < 0 + ? new Version(major, minor, build) + : new Version(major, minor, build, revision); return version.ToString(); } diff --git a/src/Heartbeat/Program.cs b/src/Heartbeat/Program.cs index 2b8c055..90cb963 100644 --- a/src/Heartbeat/Program.cs +++ b/src/Heartbeat/Program.cs @@ -2,10 +2,7 @@ using Heartbeat.Host.Endpoints; using Heartbeat.Runtime; -using Microsoft.Extensions.FileProviders; - using System.CommandLine; -using System.Diagnostics; #if OPENAPI using Heartbeat.Host.Extensions;