mirror of
https://github.com/8damon/Blackbird-Platform
synced 2026-06-21 13:41:12 +00:00
519 lines
23 KiB
C#
519 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Shapes;
|
|
|
|
namespace BlackbirdInterface
|
|
{
|
|
public partial class ParallelStacksWindow : Window
|
|
{
|
|
private readonly int _pid;
|
|
private readonly List<ThreadUsageRow> _threads;
|
|
private readonly Func<int, int, string, IReadOnlyList<ThreadStackSessionSnapshot>>? _historyProvider;
|
|
private readonly Func<DateTime>? _observationTimeUtcProvider;
|
|
private readonly Func<bool>? _liveCaptureAvailableProvider;
|
|
private readonly Func<IReadOnlyList<ThreadUsageRow>>? _threadSnapshotProvider;
|
|
private readonly Action<int, int, string, ThreadStackSessionSnapshot>? _onSnapshotCaptured;
|
|
private readonly List<ParallelStackRow> _rows = new();
|
|
private bool _refreshInFlight;
|
|
private Border? _selectedCard;
|
|
|
|
public ParallelStacksWindow(
|
|
int pid, IReadOnlyList<ThreadUsageRow> threads,
|
|
Func<int, int, string, IReadOnlyList<ThreadStackSessionSnapshot>>? historyProvider = null,
|
|
Func<DateTime>? observationTimeUtcProvider = null, Func<bool>? liveCaptureAvailableProvider = null,
|
|
Func<IReadOnlyList<ThreadUsageRow>>? threadSnapshotProvider = null,
|
|
Action<int, int, string, ThreadStackSessionSnapshot>? onSnapshotCaptured = null)
|
|
{
|
|
InitializeComponent();
|
|
WindowThemeHelper.WireThemeAwareTitleBar(this);
|
|
|
|
_pid = pid;
|
|
_threads = threads.Select(CloneThread).ToList();
|
|
_historyProvider = historyProvider;
|
|
_observationTimeUtcProvider = observationTimeUtcProvider;
|
|
_liveCaptureAvailableProvider = liveCaptureAvailableProvider;
|
|
_threadSnapshotProvider = threadSnapshotProvider;
|
|
_onSnapshotCaptured = onSnapshotCaptured;
|
|
SubtitleBlock.Text = $"PID {_pid} | {_threads.Count} thread(s)";
|
|
|
|
Loaded += async (_, __) => await RefreshStacksAsync();
|
|
}
|
|
|
|
private async Task RefreshStacksAsync()
|
|
{
|
|
if (_refreshInFlight)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_refreshInFlight = true;
|
|
try
|
|
{
|
|
RefreshThreadRowsFromProvider();
|
|
SummaryBlock.Text = $"Resolving {_threads.Count} thread stack(s)...";
|
|
/* Capture UI-thread values before entering Task.Run — the providers
|
|
access WPF DependencyProperty getters which must run on the UI thread. */
|
|
DateTime capturedObservedUtc = _observationTimeUtcProvider?.Invoke() ?? DateTime.UtcNow;
|
|
bool capturedLiveCapture = _liveCaptureAvailableProvider?.Invoke() ?? true;
|
|
List<ParallelStackRow> resolved =
|
|
await Task.Run(() => ResolveRows(capturedObservedUtc, capturedLiveCapture));
|
|
_rows.Clear();
|
|
_rows.AddRange(resolved);
|
|
|
|
SummaryBlock.Text = $"Resolved {_rows.Count} thread stack(s)";
|
|
if (_rows.Count > 0)
|
|
{
|
|
RenderStackMap();
|
|
SelectRow(_rows[0], _selectedCard);
|
|
}
|
|
else
|
|
{
|
|
StacksCanvas.Children.Clear();
|
|
SelectedTitleBlock.Text = "No thread data";
|
|
SelectedMetaBlock.Text = "No stack snapshots could be resolved.";
|
|
DetailBox.Text = string.Empty;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_refreshInFlight = false;
|
|
}
|
|
}
|
|
|
|
private List<ParallelStackRow> ResolveRows(DateTime observedUtc, bool liveCaptureAvailable)
|
|
{
|
|
var results = new ParallelStackRow?[_threads.Count];
|
|
using var gate = new SemaphoreSlim(4);
|
|
var tasks = new List<Task>(_threads.Count);
|
|
|
|
for (int i = 0; i < _threads.Count; i += 1)
|
|
{
|
|
int index = i;
|
|
tasks.Add(Task.Run(
|
|
async () =>
|
|
{
|
|
await gate.WaitAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
ThreadUsageRow thread = _threads[index];
|
|
IReadOnlyList<ThreadStackSessionSnapshot> history =
|
|
_historyProvider?.Invoke(_pid, thread.Tid, thread.State) ??
|
|
Array.Empty<ThreadStackSessionSnapshot>();
|
|
ThreadStackSessionSnapshot? historical = history.Where(x => x.CapturedAtUtc <= observedUtc)
|
|
.OrderByDescending(x => x.CapturedAtUtc)
|
|
.FirstOrDefault();
|
|
if (historical != null &&
|
|
(!liveCaptureAvailable || observedUtc < historical.CapturedAtUtc.AddMilliseconds(750)))
|
|
{
|
|
results[index] = BuildRow(thread, historical.Clone());
|
|
}
|
|
else
|
|
{
|
|
ThreadStackResolveResult result =
|
|
ThreadStackResolver.Resolve(_pid, thread.Tid, thread.State);
|
|
if (HasResolvedStackData(result))
|
|
{
|
|
ThreadStackSessionSnapshot snapshot = CreateSnapshot(DateTime.UtcNow, result);
|
|
_onSnapshotCaptured?.Invoke(_pid, thread.Tid, thread.State, snapshot.Clone());
|
|
results[index] = BuildRow(thread, snapshot);
|
|
}
|
|
else if (historical != null)
|
|
{
|
|
results[index] = BuildRow(thread, historical.Clone());
|
|
}
|
|
else
|
|
{
|
|
results[index] = BuildRow(thread, result);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
gate.Release();
|
|
}
|
|
}));
|
|
}
|
|
|
|
Task.WaitAll(tasks.ToArray());
|
|
return results.OfType<ParallelStackRow>().ToList();
|
|
}
|
|
|
|
private static bool HasResolvedStackData(ThreadStackResolveResult result)
|
|
{
|
|
return result.Frames.Count > 0 || result.StackPointer != 0 || result.TebAddress != 0 ||
|
|
result.ContextSnapshot != null;
|
|
}
|
|
|
|
private void RefreshThreadRowsFromProvider()
|
|
{
|
|
IReadOnlyList<ThreadUsageRow>? currentRows = _threadSnapshotProvider?.Invoke();
|
|
if (currentRows == null || currentRows.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_threads.Clear();
|
|
_threads.AddRange(currentRows.Select(CloneThread));
|
|
SubtitleBlock.Text = $"PID {_pid} | {_threads.Count} thread(s)";
|
|
}
|
|
|
|
private static ThreadStackSessionSnapshot CreateSnapshot(DateTime capturedAtUtc,
|
|
ThreadStackResolveResult result)
|
|
{
|
|
return new ThreadStackSessionSnapshot {
|
|
CapturedAtUtc = capturedAtUtc,
|
|
TebAddress = result.TebAddress,
|
|
StackBase = result.StackBase,
|
|
StackTop = result.StackTop,
|
|
TebFlags = result.TebFlags,
|
|
StackPointer = result.StackPointer,
|
|
ContextSnapshot = result.ContextSnapshot == null
|
|
? null
|
|
: new ThreadStackSessionSnapshot { ContextSnapshot = result.ContextSnapshot }
|
|
.Clone()
|
|
.ContextSnapshot,
|
|
Frames = result.Frames.Select(CloneFrame).ToList()
|
|
};
|
|
}
|
|
|
|
private static StackFrameRow CloneFrame(StackFrameRow frame)
|
|
{
|
|
return new StackFrameRow { Index = frame.Index,
|
|
Address = frame.Address,
|
|
Module = frame.Module,
|
|
Symbol = frame.Symbol,
|
|
InstructionPointerRaw = frame.InstructionPointerRaw,
|
|
FramePointerRaw = frame.FramePointerRaw,
|
|
FrameSpanBytes = frame.FrameSpanBytes,
|
|
IsCurrent = frame.IsCurrent };
|
|
}
|
|
|
|
private void RenderStackMap()
|
|
{
|
|
StacksCanvas.Children.Clear();
|
|
_selectedCard = null;
|
|
|
|
const double cardWidth = 280;
|
|
const double cardHeight = 220;
|
|
const double spacingX = 318;
|
|
const double spacingY = 248;
|
|
int columns = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(Math.Max(1, _rows.Count))));
|
|
ParallelStackRow? mainThread =
|
|
_rows.OrderBy(row => row.StartTimeUtc ?? DateTime.MaxValue).ThenBy(row => row.Tid).FirstOrDefault();
|
|
|
|
for (int i = 0; i < _rows.Count; i += 1)
|
|
{
|
|
ParallelStackRow row = _rows[i];
|
|
int col = i % columns;
|
|
int currentRow = i / columns;
|
|
double x = 26 + (col * spacingX) + ((currentRow % 2 == 0) ? 0 : 18);
|
|
double y = 24 + (currentRow * spacingY) + ((col % 2 == 0) ? 0 : 14);
|
|
row.IsMainThread = ReferenceEquals(row, mainThread);
|
|
Border card = BuildCard(row, cardWidth, cardHeight);
|
|
Canvas.SetLeft(card, x);
|
|
Canvas.SetTop(card, y);
|
|
row.Card = card;
|
|
row.Center = new Point(x + (cardWidth / 2.0), y + (cardHeight / 2.0));
|
|
}
|
|
|
|
DrawLooseLinks(mainThread);
|
|
|
|
for (int i = 0; i < _rows.Count; i += 1)
|
|
{
|
|
StacksCanvas.Children.Add(_rows[i].Card!);
|
|
}
|
|
|
|
double width = 52 + (Math.Min(columns, Math.Max(1, _rows.Count)) * spacingX);
|
|
double height = 48 + (((_rows.Count + columns - 1) / columns) * spacingY);
|
|
StacksCanvas.Width = Math.Max(920, width);
|
|
StacksCanvas.Height = Math.Max(620, height);
|
|
}
|
|
|
|
private void DrawLooseLinks(ParallelStackRow? mainThread)
|
|
{
|
|
if (_rows.Count <= 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 1; i < _rows.Count; i += 1)
|
|
{
|
|
ParallelStackRow from = _rows[i - 1];
|
|
ParallelStackRow to = _rows[i];
|
|
AddLooseLink(from.Center, to.Center, highlighted: false);
|
|
}
|
|
|
|
if (mainThread == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < _rows.Count; i += 1)
|
|
{
|
|
ParallelStackRow row = _rows[i];
|
|
if (ReferenceEquals(row, mainThread))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddLooseLink(mainThread.Center, row.Center, highlighted: true);
|
|
}
|
|
}
|
|
|
|
private void AddLooseLink(Point from, Point to, bool highlighted)
|
|
{
|
|
double dx = Math.Abs(to.X - from.X);
|
|
double controlOffset = Math.Max(42, dx * 0.22);
|
|
var figure = new PathFigure { StartPoint = from, IsClosed = false, IsFilled = false };
|
|
figure.Segments.Add(new BezierSegment(new Point(from.X + controlOffset, from.Y),
|
|
new Point(to.X - controlOffset, to.Y), to, true));
|
|
|
|
var geometry = new PathGeometry();
|
|
geometry.Figures.Add(figure);
|
|
StacksCanvas.Children.Add(
|
|
new Path { Data = geometry,
|
|
Stroke = highlighted ? new SolidColorBrush(Color.FromArgb(0x48, 0xD8, 0xD8, 0xD8))
|
|
: new SolidColorBrush(Color.FromArgb(0x22, 0x9A, 0x9A, 0x9A)),
|
|
StrokeThickness = highlighted ? 1.6 : 1.0, StrokeDashArray = new DoubleCollection { 7, 9 },
|
|
SnapsToDevicePixels = true });
|
|
}
|
|
|
|
private Border BuildCard(ParallelStackRow row, double width, double height)
|
|
{
|
|
var border = new Border {
|
|
Width = width,
|
|
Height = height,
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
CornerRadius = new CornerRadius(10),
|
|
BorderThickness = row.IsMainThread ? new Thickness(2) : new Thickness(1),
|
|
BorderBrush = row.IsMainThread ? new SolidColorBrush(Color.FromRgb(0xC8, 0xC8, 0xC8))
|
|
: new SolidColorBrush(Color.FromRgb(0x46, 0x46, 0x46)),
|
|
Background = new SolidColorBrush(Color.FromRgb(0x1A, 0x1A, 0x1A)),
|
|
Cursor = Cursors.Hand,
|
|
Tag = row,
|
|
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 18, ShadowDepth = 0,
|
|
Opacity = 0.32,
|
|
Color = Color.FromRgb(0x00, 0x00, 0x00) }
|
|
};
|
|
|
|
var stack = new StackPanel();
|
|
stack.Children.Add(new TextBlock { Text = row.IsMainThread ? $"TID {row.Tid} MAIN" : $"TID {row.Tid}",
|
|
FontWeight = FontWeights.SemiBold, FontSize = 13,
|
|
Foreground = Brushes.White });
|
|
stack.Children.Add(new TextBlock { Text = row.State, Margin = new Thickness(0, 2, 0, 0),
|
|
Foreground = new SolidColorBrush(Color.FromRgb(0xB9, 0xC2, 0xCC)) });
|
|
stack.Children.Add(new TextBlock { Text = row.Rip, Margin = new Thickness(0, 6, 0, 0),
|
|
FontFamily = new FontFamily("Cascadia Mono, Cascadia Code, Consolas"),
|
|
Foreground = new SolidColorBrush(Color.FromRgb(0xD0, 0xD0, 0xD0)) });
|
|
stack.Children.Add(new TextBlock { Text = $"{row.FrameCount} frame(s)", Margin = new Thickness(0, 2, 0, 8),
|
|
Foreground = new SolidColorBrush(Color.FromRgb(0x90, 0x90, 0x90)) });
|
|
|
|
for (int i = 0; i < row.FrameLines.Count; i += 1)
|
|
{
|
|
stack.Children.Add(
|
|
new TextBlock { Text = row.FrameLines[i], Margin = new Thickness(0, i == 0 ? 0 : 4, 0, 0),
|
|
TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.CharacterEllipsis,
|
|
FontFamily = new FontFamily("Cascadia Mono, Cascadia Code, Consolas"),
|
|
Foreground = i == 0 ? new SolidColorBrush(Color.FromRgb(0xF3, 0xF3, 0xF3))
|
|
: new SolidColorBrush(Color.FromRgb(0xB7, 0xB7, 0xB7)) });
|
|
}
|
|
|
|
border.Child = stack;
|
|
border.MouseLeftButtonUp += Card_MouseLeftButtonUp;
|
|
return border;
|
|
}
|
|
|
|
private static ParallelStackRow BuildRow(ThreadUsageRow thread, ThreadStackResolveResult result)
|
|
{
|
|
StackFrameRow? top = result.Frames.FirstOrDefault();
|
|
string rip =
|
|
top?.Address ?? (result.ContextSnapshot?.Rip is ulong value && value != 0 ? $"0x{value:X}" : "N/A");
|
|
string topFrame = top == null ? (string.IsNullOrWhiteSpace(result.Note) ? "No stack frames" : result.Note)
|
|
: $"{top.Module}!{top.Symbol}";
|
|
|
|
var detail = new StringBuilder(2048);
|
|
detail.Append("TID: ").AppendLine(thread.Tid.ToString());
|
|
detail.Append("State: ").AppendLine(thread.State);
|
|
detail.Append("RIP: ").AppendLine(rip);
|
|
detail.Append("Frames: ").AppendLine(result.Frames.Count.ToString());
|
|
if (result.TebAddress != 0)
|
|
{
|
|
detail.Append("TEB: 0x").Append(result.TebAddress.ToString("X")).AppendLine();
|
|
}
|
|
if (result.StackBase != 0 || result.StackTop != 0)
|
|
{
|
|
detail.Append("Stack: 0x")
|
|
.Append(result.StackTop.ToString("X"))
|
|
.Append(" -> 0x")
|
|
.Append(result.StackBase.ToString("X"))
|
|
.AppendLine();
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(result.Note))
|
|
{
|
|
detail.Append("Note: ").AppendLine(result.Note);
|
|
}
|
|
|
|
detail.AppendLine();
|
|
if (result.Frames.Count == 0)
|
|
{
|
|
detail.AppendLine("No frames resolved.");
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < result.Frames.Count; i += 1)
|
|
{
|
|
StackFrameRow frame = result.Frames[i];
|
|
detail.Append('[')
|
|
.Append(i)
|
|
.Append("] ")
|
|
.Append(frame.Address)
|
|
.Append(" ")
|
|
.Append(frame.Module)
|
|
.Append(" ")
|
|
.AppendLine(frame.Symbol);
|
|
}
|
|
}
|
|
|
|
return new ParallelStackRow {
|
|
Tid = thread.Tid,
|
|
State = thread.State,
|
|
StartTimeUtc = thread.StartTimeUtc,
|
|
Rip = rip,
|
|
FrameCount = result.Frames.Count,
|
|
TopFrame = topFrame,
|
|
FrameLines = result.Frames.Take(6).Select(frame => $"{frame.Module}!{frame.Symbol}").ToList(),
|
|
DetailText = detail.ToString().TrimEnd()
|
|
};
|
|
}
|
|
|
|
private static ParallelStackRow BuildRow(ThreadUsageRow thread, ThreadStackSessionSnapshot snapshot)
|
|
{
|
|
StackFrameRow? top = snapshot.Frames.FirstOrDefault();
|
|
string rip =
|
|
top?.Address ?? (snapshot.ContextSnapshot?.Rip is ulong value && value != 0 ? $"0x{value:X}" : "N/A");
|
|
string topFrame = top == null ? "No stack frames" : $"{top.Module}!{top.Symbol}";
|
|
|
|
var detail = new StringBuilder(2048);
|
|
detail.Append("TID: ").AppendLine(thread.Tid.ToString());
|
|
detail.Append("State: ").AppendLine(thread.State);
|
|
detail.Append("Captured: ").AppendLine(snapshot.CapturedAtUtc.ToString("HH:mm:ss.fff"));
|
|
detail.Append("RIP: ").AppendLine(rip);
|
|
detail.Append("Frames: ").AppendLine(snapshot.Frames.Count.ToString());
|
|
if (snapshot.TebAddress != 0)
|
|
{
|
|
detail.Append("TEB: 0x").Append(snapshot.TebAddress.ToString("X")).AppendLine();
|
|
}
|
|
if (snapshot.StackBase != 0 || snapshot.StackTop != 0)
|
|
{
|
|
detail.Append("Stack: 0x")
|
|
.Append(snapshot.StackTop.ToString("X"))
|
|
.Append(" -> 0x")
|
|
.Append(snapshot.StackBase.ToString("X"))
|
|
.AppendLine();
|
|
}
|
|
|
|
detail.AppendLine();
|
|
if (snapshot.Frames.Count == 0)
|
|
{
|
|
detail.AppendLine("No frames resolved.");
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < snapshot.Frames.Count; i += 1)
|
|
{
|
|
StackFrameRow frame = snapshot.Frames[i];
|
|
detail.Append('[')
|
|
.Append(i)
|
|
.Append("] ")
|
|
.Append(frame.Address)
|
|
.Append(" ")
|
|
.Append(frame.Module)
|
|
.Append(" ")
|
|
.AppendLine(frame.Symbol);
|
|
}
|
|
}
|
|
|
|
return new ParallelStackRow {
|
|
Tid = thread.Tid,
|
|
State = thread.State,
|
|
StartTimeUtc = thread.StartTimeUtc,
|
|
Rip = rip,
|
|
FrameCount = snapshot.Frames.Count,
|
|
TopFrame = topFrame,
|
|
FrameLines = snapshot.Frames.Take(6).Select(frame => $"{frame.Module}!{frame.Symbol}").ToList(),
|
|
DetailText = detail.ToString().TrimEnd()
|
|
};
|
|
}
|
|
|
|
private static ThreadUsageRow CloneThread(ThreadUsageRow row) => new(new ThreadUsageSample {
|
|
Tid = row.Tid, CpuMsDelta = row.CpuMs, State = row.State,
|
|
WaitReason = row.IsSuspended ? "Suspended" : string.Empty, Kind = row.ThreadKind,
|
|
StartTimeUtc = row.StartTimeUtc, TargetSuspended = row.IsSuspended
|
|
});
|
|
|
|
private async void Refresh_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
await RefreshStacksAsync();
|
|
}
|
|
|
|
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
|
|
|
private void Card_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
if (sender is not Border border || border.Tag is not ParallelStackRow row)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SelectRow(row, border);
|
|
}
|
|
|
|
private void SelectRow(ParallelStackRow row, Border? border)
|
|
{
|
|
if (_selectedCard != null)
|
|
{
|
|
_selectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(0x46, 0x46, 0x46));
|
|
_selectedCard.BorderThickness = new Thickness(1);
|
|
}
|
|
|
|
_selectedCard = border ?? row.Card;
|
|
if (_selectedCard != null)
|
|
{
|
|
_selectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(0x7C, 0xD5, 0xFF));
|
|
_selectedCard.BorderThickness = new Thickness(2);
|
|
}
|
|
|
|
SelectedTitleBlock.Text = $"Thread {row.Tid}";
|
|
SelectedMetaBlock.Text = $"{row.State} | RIP {row.Rip} | {row.FrameCount} frame(s)";
|
|
DetailBox.Text = row.DetailText;
|
|
}
|
|
|
|
private sealed class ParallelStackRow
|
|
{
|
|
public int Tid { get; init; }
|
|
public string State { get; init; } = string.Empty;
|
|
public DateTime? StartTimeUtc { get; init; }
|
|
public string Rip { get; init; } = string.Empty;
|
|
public int FrameCount { get; init; }
|
|
public string TopFrame { get; init; } = string.Empty;
|
|
public List<string> FrameLines { get; init; } = new();
|
|
public string DetailText { get; init; } = string.Empty;
|
|
public bool IsMainThread { get; set; }
|
|
public Point Center { get; set; }
|
|
public Border? Card { get; set; }
|
|
}
|
|
}
|
|
}
|