using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace D365MetadataBridge.Services.Trace
{
public enum XppTraceEventKind { Enter, Exit, Sql, Fetch, Bind }
///
/// One event of a D365FO ETW capture, reduced to what the analysis needs. The reader maps
/// Microsoft-Dynamics-AX-XppExecutionTraces 24500/24501 to Enter/Exit and
/// Microsoft-Dynamics-AX-ExecutionTraces 4922/4924/4923 to Sql/Fetch/Bind. A Bind event's
/// parameter VALUE is never carried here: it is customer data and is only counted.
///
public sealed class XppTraceEvent
{
public XppTraceEventKind Kind { get; set; }
public int ProcessId { get; set; }
public int ThreadId { get; set; }
/// UTC ticks (100 ns).
public long Ticks { get; set; }
/// X++ method (Enter/Exit only). In the SQL templates methodName is the C++ kernel, so it is not read.
public string? Method { get; set; }
public string? SqlStatement { get; set; }
public double PreparationSeconds { get; set; }
public double ExecutionSeconds { get; set; }
public string? SessionId { get; set; }
public string? UserGuid { get; set; }
}
public sealed class XppTraceOptions
{
/// Case-insensitive substrings of an X++ method name. A thread is analysed when it entered a matching method; empty = every thread with X++ activity.
public IReadOnlyList Focus { get; set; } = Array.Empty();
/// 0 = any process.
public int ProcessId { get; set; }
/// Only count work done while a focus method is on the thread's stack (needs Focus).
public bool SubtreeOnly { get; set; }
public int Top { get; set; } = 25;
public int RepeatedSqlThreshold { get; set; } = 50;
public int StatementLength { get; set; } = 300;
}
///
/// Turns a stream of X++ enter/exit and SQL events into per-method and per-statement totals.
///
/// Accuracy rules, each one stated in the result:
/// - Frames pair per thread (process id + thread id) on a stack. An exception unwinding X++
/// methods still logs their exits, caught or not (measured on a dev VM, 7.0.7858.111). An
/// exit whose method is deeper in the stack (a lost event) closes the frames above it at
/// that exit's time (an upper bound) and counts them.
/// - An exit with no matching frame belongs to a frame entered before the capture started: it
/// is counted and ignored, never guessed. Frames still open at the end are not added to the
/// totals; the longest ones are listed with their time so far (a lower bound).
/// - Inclusive time is added for the outermost frame of a method only, so recursion is not
/// counted twice; exclusive time is inclusive minus the direct children.
/// - A SQL statement is attributed to the X++ method on top of its thread's stack when the
/// AOS logs it (after execution: the method that issued it). Its time also rolls up into the
/// callers' inclusive SQL time.
/// - Per-event ETW overhead inflates small, frequently called methods: rankings are reliable,
/// absolute seconds are not.
///
public sealed class XppTraceAggregator
{
private sealed class Frame
{
public string Method = "";
public long Start;
public long ChildTicks;
public double SqlSelf;
public double SqlChildren;
public bool InScope;
public bool IsFocus;
}
private sealed class MethodStat
{
public long Calls;
public long Closed;
public long InclusiveTicks;
public long ExclusiveTicks;
public double SqlSelfSeconds;
public double SqlInclusiveSeconds;
public long SqlStatements;
}
private sealed class SqlStat
{
public long Count;
public double Execution;
public double Preparation;
public double Max;
}
private sealed class ThreadState
{
public int ProcessId;
public int ThreadId;
public readonly Stack Stack = new Stack();
public readonly Dictionary OnStack = new Dictionary(StringComparer.Ordinal);
///
/// In-scope frames per method. Recursion is judged on these, not OnStack: in subtree scope a method
/// can sit both above the focus (out of scope) and below it, and only the in-scope one is counted.
///
public readonly Dictionary OnStackInScope = new Dictionary(StringComparer.Ordinal);
public readonly Dictionary Methods = new Dictionary(StringComparer.Ordinal);
public readonly Dictionary Sql = new Dictionary(StringComparer.Ordinal);
public readonly Dictionary Fetch = new Dictionary(StringComparer.Ordinal);
public readonly HashSet Sessions = new HashSet(StringComparer.OrdinalIgnoreCase);
public readonly HashSet Users = new HashSet(StringComparer.OrdinalIgnoreCase);
public long First = long.MaxValue, Last, Enters, Exits, SqlEvents, BindEvents;
public long ExitsWithoutFrame, FramesClosedByOuterExit;
public long FocusTicks;
/// Time inside closed root frames: the thread running X++, not idling between calls.
public long BusyTicks;
/// SQL issued with no X++ frame on the stack, which BusyTicks cannot contain.
public double SqlWithoutFrame;
public int FocusDepth;
public bool MatchedFocus;
public readonly Dictionary RootCounts = new Dictionary(StringComparer.Ordinal);
/// A BatchRun:: root was seen, even past the RootCounts cap.
public bool SawBatchRun;
/// Roots a focus method ran under, listed first in threads[].roots.
public readonly HashSet FocusRoots = new HashSet(StringComparer.Ordinal);
}
private readonly XppTraceOptions options;
private readonly Dictionary threads = new Dictionary();
private long eventsSeen;
public XppTraceAggregator(XppTraceOptions options) { this.options = options; }
public void Add(XppTraceEvent e)
{
if (options.ProcessId != 0 && e.ProcessId != options.ProcessId) return;
eventsSeen++;
long key = ((long)e.ProcessId << 32) | (uint)e.ThreadId;
if (!threads.TryGetValue(key, out var ts))
{
ts = new ThreadState { ProcessId = e.ProcessId, ThreadId = e.ThreadId };
threads[key] = ts;
}
if (e.Ticks < ts.First) ts.First = e.Ticks;
if (e.Ticks > ts.Last) ts.Last = e.Ticks;
if (!string.IsNullOrEmpty(e.SessionId) && e.SessionId != "NULL" && ts.Sessions.Count < 16) ts.Sessions.Add(e.SessionId!);
if (!string.IsNullOrEmpty(e.UserGuid) && ts.Users.Count < 16) ts.Users.Add(e.UserGuid!);
switch (e.Kind)
{
case XppTraceEventKind.Enter: Enter(ts, e); break;
case XppTraceEventKind.Exit: Exit(ts, e); break;
case XppTraceEventKind.Sql: Sql(ts, e); break;
case XppTraceEventKind.Fetch: Fetch(ts, e); break;
case XppTraceEventKind.Bind: ts.BindEvents++; break;
}
}
private bool IsFocus(string method)
{
foreach (var p in options.Focus)
if (p.Length > 0 && method.IndexOf(p, StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
private static MethodStat Stat(Dictionary map, string method)
{
if (!map.TryGetValue(method, out var s)) { s = new MethodStat(); map[method] = s; }
return s;
}
private void Enter(ThreadState ts, XppTraceEvent e)
{
string m = e.Method ?? "?";
ts.Enters++;
if (ts.Stack.Count == 0)
{
// Capped by distinct roots, not by calls: a root already counted keeps counting.
if (ts.RootCounts.TryGetValue(m, out var rc)) ts.RootCounts[m] = rc + 1;
else if (ts.RootCounts.Count < 64) ts.RootCounts[m] = 1;
if (m.StartsWith("BatchRun::", StringComparison.Ordinal)) ts.SawBatchRun = true;
}
bool focus = options.Focus.Count > 0 && IsFocus(m);
if (focus) { ts.MatchedFocus = true; ts.FocusDepth++; }
var frame = new Frame { Method = m, Start = e.Ticks, IsFocus = focus, InScope = !options.SubtreeOnly || ts.FocusDepth > 0 };
ts.Stack.Push(frame);
if (focus && ts.FocusRoots.Count < 8) ts.FocusRoots.Add(ts.Stack.Last().Method); // Last() is the bottom
ts.OnStack[m] = ts.OnStack.TryGetValue(m, out var d) ? d + 1 : 1;
if (frame.InScope)
{
Stat(ts.Methods, m).Calls++;
ts.OnStackInScope[m] = ts.OnStackInScope.TryGetValue(m, out var di) ? di + 1 : 1;
}
}
private void Exit(ThreadState ts, XppTraceEvent e)
{
string m = e.Method ?? "?";
ts.Exits++;
if (!ts.OnStack.TryGetValue(m, out var depth) || depth == 0) { ts.ExitsWithoutFrame++; return; }
while (ts.Stack.Count > 0)
{
var f = ts.Stack.Pop();
Close(ts, f, e.Ticks);
if (f.Method == m) return;
ts.FramesClosedByOuterExit++;
}
}
private void Close(ThreadState ts, Frame f, long at)
{
long inclusive = Math.Max(0, at - f.Start);
ts.OnStack[f.Method]--;
if (f.InScope)
{
int left = --ts.OnStackInScope[f.Method];
var s = Stat(ts.Methods, f.Method);
s.Closed++;
s.ExclusiveTicks += Math.Max(0, inclusive - f.ChildTicks);
s.SqlSelfSeconds += f.SqlSelf;
if (left == 0)
{
s.InclusiveTicks += inclusive;
s.SqlInclusiveSeconds += f.SqlSelf + f.SqlChildren;
}
}
if (f.IsFocus)
{
ts.FocusDepth--;
if (ts.FocusDepth == 0) ts.FocusTicks += inclusive;
}
if (ts.Stack.Count > 0)
{
var parent = ts.Stack.Peek();
parent.ChildTicks += inclusive;
parent.SqlChildren += f.SqlSelf + f.SqlChildren;
}
else
{
// A root frame is the thread working, in either scope: in subtree scope the root is
// usually above the focus (out of scope), and skipping it reported busySeconds = 0.
ts.BusyTicks += inclusive;
}
}
private bool SqlInScope(ThreadState ts) => !options.SubtreeOnly || ts.FocusDepth > 0;
private static readonly Regex TempTable = new Regex(@"(tempdb\.""?dbo""?\.""?)(t\d+)[\w-]*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
///
/// The grouping key of a statement: the WHOLE statement, whitespace collapsed. It is never
/// truncated: the kernel lists every column (SELECT T1.A,T1.B,… FROM …), so on a wide table
/// FROM and WHERE start well past any display length, and a key cut there would merge
/// every query on that table into one row (and flag them as one repeated statement).
///
private static string Normalize(string? statement)
{
if (string.IsNullOrEmpty(statement)) return "";
var collapsed = string.Join(" ", statement!.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
// A temp table's name carries the session that made it (t18093Batch__),
// which would split one statement into one row per session: keep the table id only.
return TempTable.Replace(collapsed, "$1$2_*");
}
///
/// The statement as shown, at most StatementLength characters (plus the ellipsis). When FROM lies
/// beyond that, the head of the column list is kept short and the rest shows FROM … WHERE, which
/// names the table and the filter: the part that tells two statements apart.
///
internal string Display(string statement)
{
int limit = Math.Max(20, options.StatementLength);
if (statement.Length <= limit) return statement;
int from = statement.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase);
if (from < 0 || from + 6 <= limit) return statement.Substring(0, limit) + "…";
const string gap = " … ";
int head = Math.Min(from, limit / 4);
string tail = statement.Substring(from + 1);
int room = limit - head - gap.Length;
return statement.Substring(0, head) + gap + (tail.Length > room ? tail.Substring(0, room) + "…" : tail);
}
private void Sql(ThreadState ts, XppTraceEvent e)
{
ts.SqlEvents++;
if (!SqlInScope(ts)) return;
double seconds = Math.Max(0, e.ExecutionSeconds) + Math.Max(0, e.PreparationSeconds);
string caller = ts.Stack.Count > 0 ? ts.Stack.Peek().Method : NoFrame;
if (ts.Stack.Count > 0) ts.Stack.Peek().SqlSelf += seconds;
else ts.SqlWithoutFrame += seconds;
Stat(ts.Methods, caller).SqlStatements++;
string key = caller + "\u0001" + Normalize(e.SqlStatement);
if (!ts.Sql.TryGetValue(key, out var s)) { s = new SqlStat(); ts.Sql[key] = s; }
s.Count++;
s.Execution += Math.Max(0, e.ExecutionSeconds);
s.Preparation += Math.Max(0, e.PreparationSeconds);
if (seconds > s.Max) s.Max = seconds;
}
private void Fetch(ThreadState ts, XppTraceEvent e)
{
if (!SqlInScope(ts)) return;
string caller = ts.Stack.Count > 0 ? ts.Stack.Peek().Method : NoFrame;
if (!ts.Fetch.TryGetValue(caller, out var s)) { s = new SqlStat(); ts.Fetch[caller] = s; }
s.Count++;
s.Execution += Math.Max(0, e.ExecutionSeconds);
if (e.ExecutionSeconds > s.Max) s.Max = e.ExecutionSeconds;
}
public const string NoFrame = "(no X++ frame on the thread)";
public XppTraceResult Build()
{
var all = threads.Values.Where(t => t.Enters > 0 || t.SqlEvents > 0).ToList();
var selected = options.Focus.Count > 0 ? all.Where(t => t.MatchedFocus).ToList() : all.Where(t => t.Enters > 0).ToList();
var result = new XppTraceResult
{
EventsAnalysed = eventsSeen,
ThreadsWithActivity = all.Count,
ThreadsSelected = selected.Count,
Options = new XppTraceResultOptions
{
Focus = options.Focus.ToList(), ProcessId = options.ProcessId, Scope = options.SubtreeOnly ? "subtree" : "thread",
Top = options.Top, RepeatedSqlThreshold = options.RepeatedSqlThreshold,
},
};
result.Threads = (selected.Count > 0 ? selected : all)
.OrderByDescending(t => t.Enters).Take(20)
.Select(t => new XppTraceThread
{
ProcessId = t.ProcessId, ThreadId = t.ThreadId, Selected = selected.Contains(t),
XppEnters = t.Enters, SqlStatements = t.SqlEvents, SpanSeconds = Seconds(t.Last - t.First),
Roots = t.FocusRoots.OrderByDescending(r => t.RootCounts.TryGetValue(r, out var c) ? c : 0)
.Concat(t.RootCounts.Where(r => !t.FocusRoots.Contains(r.Key)).OrderByDescending(r => r.Value).Select(r => r.Key))
.Take(3).ToList(),
Sessions = t.Sessions.Take(3).ToList(), Users = t.Users.Take(3).ToList(),
}).ToList();
if (selected.Count == 0)
{
result.Caveats.Add(options.Focus.Count > 0
? "No thread entered a method matching the focus. Check the names in threads[].roots, or capture while the process runs."
: "No X++ method events in the capture: was Microsoft-Dynamics-AX-XppExecutionTraces enabled?");
return result;
}
var methods = new Dictionary(StringComparer.Ordinal);
var sql = new Dictionary(StringComparer.Ordinal);
var fetch = new Dictionary(StringComparer.Ordinal);
var open = new Dictionary(StringComparer.Ordinal);
long first = long.MaxValue, last = 0, threadTicks = 0, focusTicks = 0, busyTicks = 0;
double sqlWithoutFrame = 0;
foreach (var t in selected)
{
first = Math.Min(first, t.First);
last = Math.Max(last, t.Last);
threadTicks += t.Last - t.First;
focusTicks += t.FocusTicks;
busyTicks += t.BusyTicks;
sqlWithoutFrame += t.SqlWithoutFrame;
// A root frame still open at the end was busy until the thread's last event.
if (t.Stack.Count > 0) busyTicks += Math.Max(0, t.Last - t.Stack.Last().Start);
// Likewise an outermost focus frame still open: its SQL is in the numerator, so its time
// so far belongs in the denominator, or the share can exceed 1.
var openFocus = t.Stack.LastOrDefault(f => f.IsFocus);
if (openFocus != null) focusTicks += Math.Max(0, t.Last - openFocus.Start);
result.Pairing.Enters += t.Enters;
result.Pairing.Exits += t.Exits;
result.Pairing.ExitsWithoutFrame += t.ExitsWithoutFrame;
result.Pairing.FramesClosedByOuterExit += t.FramesClosedByOuterExit;
result.Pairing.FramesOpenAtEnd += t.Stack.Count;
result.SqlBindEventsCounted += t.BindEvents;
foreach (var kv in t.Methods)
{
var a = Stat(methods, kv.Key);
a.Calls += kv.Value.Calls; a.Closed += kv.Value.Closed;
a.InclusiveTicks += kv.Value.InclusiveTicks; a.ExclusiveTicks += kv.Value.ExclusiveTicks;
a.SqlSelfSeconds += kv.Value.SqlSelfSeconds; a.SqlInclusiveSeconds += kv.Value.SqlInclusiveSeconds;
a.SqlStatements += kv.Value.SqlStatements;
}
Merge(sql, t.Sql);
Merge(fetch, t.Fetch);
foreach (var f in t.Stack)
{
long so = t.Last - f.Start;
open[f.Method] = open.TryGetValue(f.Method, out var o) ? (o.Count + 1, Math.Max(o.Max, so)) : (1, so);
}
}
double sqlExecution = sql.Values.Sum(s => s.Execution), sqlPreparation = sql.Values.Sum(s => s.Preparation);
double rowFetch = fetch.Values.Sum(s => s.Execution);
result.Span = new XppTraceSpan { FirstUtc = new DateTime(first, DateTimeKind.Utc), LastUtc = new DateTime(last, DateTimeKind.Utc), WallSeconds = Seconds(last - first) };
// Not the threads' spans: a pool thread idle for most of the capture would dilute the share
// (two batch threads, one idle: 10.6% of the spans against 23.9% of the time doing work).
double busySeconds = busyTicks / 1e7 + sqlWithoutFrame;
double denominator = options.SubtreeOnly ? focusTicks / 1e7 : busySeconds;
result.Totals = new XppTraceTotals
{
ThreadSeconds = Seconds(threadTicks),
BusySeconds = Round(busySeconds),
FocusSeconds = options.Focus.Count > 0 ? Seconds(focusTicks) : (double?)null,
SqlExecutionSeconds = Round(sqlExecution), SqlPreparationSeconds = Round(sqlPreparation), RowFetchSeconds = Round(rowFetch),
SqlStatements = sql.Values.Sum(s => s.Count),
SqlShareOfTime = denominator > 0 ? Round((sqlExecution + sqlPreparation) / denominator) : (double?)null,
RowFetchShareOfTime = denominator > 0 ? Round(rowFetch / denominator) : (double?)null,
ShareBasis = options.SubtreeOnly
? "time inside focus frames (one still open at the end counts until the thread's last event)"
: "busySeconds: time the selected threads spent inside X++ root frames, plus SQL issued outside any frame; idle gaps excluded",
};
int top = Math.Max(1, options.Top);
Func, XppTraceMethod> toMethod = kv => new XppTraceMethod
{
Method = kv.Key, Calls = kv.Value.Calls, ClosedCalls = kv.Value.Closed,
ExclusiveSeconds = Seconds(kv.Value.ExclusiveTicks), InclusiveSeconds = Seconds(kv.Value.InclusiveTicks),
SqlSelfSeconds = Round(kv.Value.SqlSelfSeconds), SqlInclusiveSeconds = Round(kv.Value.SqlInclusiveSeconds),
SqlStatements = kv.Value.SqlStatements,
};
var named = methods.Where(kv => kv.Key != NoFrame).ToList();
result.TopExclusive = named.OrderByDescending(kv => kv.Value.ExclusiveTicks).Take(top).Select(toMethod).ToList();
result.TopInclusive = named.OrderByDescending(kv => kv.Value.InclusiveTicks).Take(top).Select(toMethod).ToList();
result.TopCalls = named.OrderByDescending(kv => kv.Value.Calls).Take(top).Select(toMethod).ToList();
var byStatement = new Dictionary Callers)>(StringComparer.Ordinal);
foreach (var kv in sql)
{
int split = kv.Key.IndexOf('\u0001');
string caller = kv.Key.Substring(0, split), statement = kv.Key.Substring(split + 1);
if (!byStatement.TryGetValue(statement, out var entry)) { entry = (new SqlStat(), new Dictionary(StringComparer.Ordinal)); byStatement[statement] = entry; }
entry.Total.Count += kv.Value.Count; entry.Total.Execution += kv.Value.Execution; entry.Total.Preparation += kv.Value.Preparation;
entry.Total.Max = Math.Max(entry.Total.Max, kv.Value.Max);
if (!entry.Callers.TryGetValue(caller, out var c)) { c = new SqlStat(); entry.Callers[caller] = c; }
c.Count += kv.Value.Count; c.Execution += kv.Value.Execution; c.Preparation += kv.Value.Preparation;
}
Func Callers)>, XppTraceStatement> toStatement = kv => new XppTraceStatement
{
Statement = Display(kv.Key), Executions = kv.Value.Total.Count,
TotalSeconds = Round(kv.Value.Total.Execution + kv.Value.Total.Preparation),
ExecutionSeconds = Round(kv.Value.Total.Execution), PreparationSeconds = Round(kv.Value.Total.Preparation),
MaxSeconds = Round(kv.Value.Total.Max),
Callers = kv.Value.Callers.OrderByDescending(c => c.Value.Execution + c.Value.Preparation).ThenByDescending(c => c.Value.Count).Take(3)
.Select(c => new XppTraceCaller { Method = c.Key, Executions = c.Value.Count, Seconds = Round(c.Value.Execution + c.Value.Preparation) }).ToList(),
};
result.TopSqlByTime = byStatement.OrderByDescending(kv => kv.Value.Total.Execution + kv.Value.Total.Preparation).Take(Math.Min(top, 20)).Select(toStatement).ToList();
result.TopSqlByCount = byStatement.OrderByDescending(kv => kv.Value.Total.Count).Take(Math.Min(top, 20)).Select(toStatement).ToList();
result.RepeatedSql = sql.Where(kv => kv.Value.Count >= options.RepeatedSqlThreshold)
.OrderByDescending(kv => kv.Value.Count).Take(Math.Min(top, 20))
.Select(kv =>
{
int split = kv.Key.IndexOf('\u0001');
return new XppTraceRepeated
{
Caller = kv.Key.Substring(0, split), Statement = Display(kv.Key.Substring(split + 1)), Executions = kv.Value.Count,
TotalSeconds = Round(kv.Value.Execution + kv.Value.Preparation),
};
}).ToList();
result.TopRowFetch = fetch.OrderByDescending(kv => kv.Value.Execution).Take(10)
.Select(kv => new XppTraceCaller { Method = kv.Key, Executions = kv.Value.Count, Seconds = Round(kv.Value.Execution) }).ToList();
result.OpenAtEnd = open.OrderByDescending(kv => kv.Value.Max).Take(10)
.Select(kv => new XppTraceOpenFrame { Method = kv.Key, Frames = kv.Value.Count, OpenForAtLeastSeconds = Seconds(kv.Value.Max) }).ToList();
result.Caveats.Add("ETW adds a cost to every X++ method event, which inflates small, frequently called methods: compare rankings, not absolute seconds, and measure the untraced run for the real duration.");
if (result.Pairing.ExitsWithoutFrame > 0)
result.Caveats.Add($"{result.Pairing.ExitsWithoutFrame} exits had no frame in the capture (entered before it started); those frames' time is not counted.");
if (result.Pairing.FramesOpenAtEnd > 0)
result.Caveats.Add($"{result.Pairing.FramesOpenAtEnd} frames were still open at the end; they are not in the totals, see openAtEnd for their time so far.");
if (result.Pairing.FramesClosedByOuterExit > 0)
result.Caveats.Add($"{result.Pairing.FramesClosedByOuterExit} frames had no exit of their own and were closed by an outer method's exit (a lost event); their time is an upper bound.");
if (options.Focus.Count > 0 && methods.Keys.Any(m => m == "Batch::insert" || m == "BatchHeader::save")
&& !selected.Any(t => t.SawBatchRun))
result.Caveats.Add("The focus threads scheduled batch tasks (Batch::insert / BatchHeader::save) and none was seen running under BatchRun::. If this is the dialog or controller (a short thread in the web process), the work runs later in Batch.exe: analyse again with focus on the task's class and pid set to Batch.exe, whose threads[].roots start with BatchRun:: when the task began inside the capture.");
if (options.SubtreeOnly && selected.All(t => t.FocusTicks == 0))
result.Caveats.Add("Subtree scope counted nothing closed: the focus method may have started before the capture. Use thread scope for a window taken mid-run.");
return result;
}
private static void Merge(Dictionary into, Dictionary from)
{
foreach (var kv in from)
{
if (!into.TryGetValue(kv.Key, out var a)) { a = new SqlStat(); into[kv.Key] = a; }
a.Count += kv.Value.Count; a.Execution += kv.Value.Execution; a.Preparation += kv.Value.Preparation; a.Max = Math.Max(a.Max, kv.Value.Max);
}
}
private static double Seconds(long ticks) => Round(ticks / 1e7);
private static double Round(double value) => Math.Round(value, 4);
}
public sealed class XppTraceResult
{
public int Version { get; set; } = 1;
public string? File { get; set; }
public long? FileBytes { get; set; }
public double? AnalysisSeconds { get; set; }
public long EventsRead { get; set; }
public long EventsAnalysed { get; set; }
public long EventsUnreadable { get; set; }
public XppTraceResultOptions Options { get; set; } = new XppTraceResultOptions();
public int ThreadsWithActivity { get; set; }
public int ThreadsSelected { get; set; }
public List Threads { get; set; } = new List();
public XppTraceSpan? Span { get; set; }
public XppTraceTotals? Totals { get; set; }
public XppTracePairing Pairing { get; set; } = new XppTracePairing();
public long SqlBindEventsCounted { get; set; }
public List TopExclusive { get; set; } = new List();
public List TopInclusive { get; set; } = new List();
public List TopCalls { get; set; } = new List();
public List TopSqlByTime { get; set; } = new List();
public List TopSqlByCount { get; set; } = new List();
public List RepeatedSql { get; set; } = new List();
public List TopRowFetch { get; set; } = new List();
public List OpenAtEnd { get; set; } = new List();
public List Caveats { get; set; } = new List();
}
public sealed class XppTraceResultOptions
{
public List Focus { get; set; } = new List();
public int ProcessId { get; set; }
public string Scope { get; set; } = "thread";
public int Top { get; set; }
public int RepeatedSqlThreshold { get; set; }
}
public sealed class XppTraceThread
{
public int ProcessId { get; set; }
public int ThreadId { get; set; }
public bool Selected { get; set; }
public long XppEnters { get; set; }
public long SqlStatements { get; set; }
public double SpanSeconds { get; set; }
public List Roots { get; set; } = new List();
public List Sessions { get; set; } = new List();
public List Users { get; set; } = new List();
}
public sealed class XppTraceSpan
{
public DateTime FirstUtc { get; set; }
public DateTime LastUtc { get; set; }
public double WallSeconds { get; set; }
}
public sealed class XppTraceTotals
{
public double ThreadSeconds { get; set; }
public double BusySeconds { get; set; }
public double? FocusSeconds { get; set; }
public double SqlExecutionSeconds { get; set; }
public double SqlPreparationSeconds { get; set; }
public double RowFetchSeconds { get; set; }
public long SqlStatements { get; set; }
public double? SqlShareOfTime { get; set; }
public double? RowFetchShareOfTime { get; set; }
public string ShareBasis { get; set; } = "";
}
public sealed class XppTracePairing
{
public long Enters { get; set; }
public long Exits { get; set; }
public long ExitsWithoutFrame { get; set; }
public long FramesClosedByOuterExit { get; set; }
public long FramesOpenAtEnd { get; set; }
}
public sealed class XppTraceMethod
{
public string Method { get; set; } = "";
public long Calls { get; set; }
public long ClosedCalls { get; set; }
public double ExclusiveSeconds { get; set; }
public double InclusiveSeconds { get; set; }
public double SqlSelfSeconds { get; set; }
public double SqlInclusiveSeconds { get; set; }
public long SqlStatements { get; set; }
}
public sealed class XppTraceStatement
{
public string Statement { get; set; } = "";
public long Executions { get; set; }
public double TotalSeconds { get; set; }
public double ExecutionSeconds { get; set; }
public double PreparationSeconds { get; set; }
public double MaxSeconds { get; set; }
public List Callers { get; set; } = new List();
}
public sealed class XppTraceCaller
{
public string Method { get; set; } = "";
public long Executions { get; set; }
public double? Seconds { get; set; }
}
public sealed class XppTraceRepeated
{
public string Caller { get; set; } = "";
public string Statement { get; set; } = "";
public long Executions { get; set; }
public double TotalSeconds { get; set; }
}
public sealed class XppTraceOpenFrame
{
public string Method { get; set; } = "";
public int Frames { get; set; }
public double OpenForAtLeastSeconds { get; set; }
}
}