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()
: "