diff --git a/DebugProbe.AspNetCore/Assets/css/debugprobe.css b/DebugProbe.AspNetCore/Assets/css/debugprobe.css index 43d30ba..63d4b5c 100644 --- a/DebugProbe.AspNetCore/Assets/css/debugprobe.css +++ b/DebugProbe.AspNetCore/Assets/css/debugprobe.css @@ -1,4 +1,4 @@ -/* ========================= +/* ========================= Base ========================= */ @@ -1128,3 +1128,86 @@ pre { background: #4a1717; color: #ff8a8a !important; } + +/* ========================= + Waterfall View +========================= */ + +.waterfall-container { + margin-bottom: 12px; +} + +.waterfall-container .trace-card-main { + padding: 12px 14px; +} + +.waterfall-container .trace-card-header { + padding: 0 0 12px 0; +} + +.waterfall-container .trace-dot { + background: #9b51e0; +} + +.waterfall-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.waterfall-row:last-child { + margin-bottom: 0; +} + +.wf-label { + flex: 0 0 200px; + width: 200px; + min-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; + font-size: 12px; + color: #4b5563; +} + +.wf-track { + flex: 1; + position: relative; + height: 20px; + background: #f3f4f6; + border-radius: 4px; +} + +.wf-bar { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: #9b51e0; + color: #fff; + font-size: 10px; + font-weight: bold; + border-radius: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 4px; + box-sizing: border-box; +} + +.wf-bar--error { + background: #e74c3c; +} + +@media (max-width: 640px) { + .wf-label { + flex: 0 0 100px; + width: 100px; + min-width: 100px; + } +} + diff --git a/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs b/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs index 49aeb64..38308e9 100644 --- a/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs +++ b/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs @@ -99,8 +99,12 @@ public static string RenderDetailsPage(DebugEntry x, DebugEnvironment e, string BuildPayloadSection("Body", res, "body") ]); + var waterfall = BuildWaterfallSection(x); + var outgoingRequests = string.Join("", x.OutgoingRequests.Select(BuildOutgoingRequestCard)); + var combinedOutgoing = waterfall + outgoingRequests; + var content = EmbeddedResources.Details .Replace("{{method}}", Encode(x.Method)) .Replace("{{path}}", Encode(pathWithQuery)) @@ -124,9 +128,9 @@ public static string RenderDetailsPage(DebugEntry x, DebugEnvironment e, string .Replace("{{dateFormat}}", e.DateFormat ?? "") .Replace("{{assemblyVersion}}", Encode(e.AssemblyVersion)) .Replace("{{outgoingRequests}}", - string.IsNullOrWhiteSpace(outgoingRequests) + string.IsNullOrWhiteSpace(combinedOutgoing) ? "
No outgoing dependency calls
" - : outgoingRequests) + : combinedOutgoing) .Replace("{{incomingRequest}}", incomingRequest) .Replace("{{incomingResponse}}", incomingResponse); @@ -188,6 +192,67 @@ private static string BuildOutgoingRequestCard(DebugOutgoingRequest request) details: details); } + private static string BuildWaterfallSection(DebugEntry entry) + { + const double MinPercent = 0.0; + const double MaxPercent = 100.0; + + if (entry.OutgoingRequests.Count == 0) + { + return string.Empty; + } + + var totalSpan = (double)entry.DurationMs; + if (totalSpan <= 0) + { + totalSpan = 1.0; + } + + var rowsHtml = new List(); + + foreach (var outgoing in entry.OutgoingRequests) + { + var startOffsetMs = (outgoing.TimestampUtc - entry.Timestamp.UtcDateTime).TotalMilliseconds - outgoing.DurationMs; + + var left = Math.Clamp((startOffsetMs / totalSpan) * MaxPercent, MinPercent, MaxPercent); + var width = Math.Clamp(((double)outgoing.DurationMs / totalSpan) * MaxPercent, MinPercent, MaxPercent); + + var leftStr = left.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + var widthStr = width.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + + var barClass = "wf-bar"; + if (!outgoing.IsSuccessStatusCode || !string.IsNullOrWhiteSpace(outgoing.Exception)) + { + barClass += " wf-bar--error"; + } + + var displayLabel = GetDisplayTarget(outgoing.Url); + + rowsHtml.Add($@" +
+ {Encode(displayLabel)} +
+
{outgoing.DurationMs} ms
+
+
"); + } + + return $@" +
+
+
+
+ + Waterfall Timeline +
+
+
+ {string.Join("", rowsHtml)} +
+
+
"; + } + private static string BuildTraceCard(string label, string method, string target, string classes, IEnumerable details, int? statusCode = null, string? statusText = null, long? durationMs = null) { var targetHost = GetDisplayTarget(target);