diff --git a/src/SQLInsight.Repository/InsightRepository.DynamicsAx.cs b/src/SQLInsight.Repository/InsightRepository.DynamicsAx.cs
index eab4957..5ac6f39 100644
--- a/src/SQLInsight.Repository/InsightRepository.DynamicsAx.cs
+++ b/src/SQLInsight.Repository/InsightRepository.DynamicsAx.cs
@@ -32,7 +32,9 @@ public sealed partial class InsightRepository
StartedAt = Iso(r.StartedUtc),
EndedAt = Iso(r.EndedUtc),
Status = (byte)r.Status,
- })).Where(r => r.RunKey.Length <= 100).ToList();
+ })).Where(r => r.RunKey.Length <= 100)
+ .GroupBy(r => (r.Database, r.RunKey)).Select(g => g.First()) // one row per key: two runs with one key would break the primary key
+ .ToList();
return runs.Count == 0
? Task.FromResult(0)
: connection.ExecuteAsync("""
@@ -112,26 +114,58 @@ public sealed partial class InsightRepository
/// The daily samples since , keyed as : the agent's forecasts start from them after a restart.
public async Task>> GetAxSequenceHistoryAsync(int instanceId, DateTimeOffset since, CancellationToken cancellationToken = default)
{
- var samples = await GetAxSequenceSamplesAsync(instanceId, null, null, null, since, cancellationToken).ConfigureAwait(false);
+ var samples = await GetAxSequenceSamplesAsync(instanceId, null, null, null, since, cancellationToken: cancellationToken).ConfigureAwait(false);
return samples.GroupBy(s => AxSequenceSnapshot.HistoryKey(s.DatabaseName, s.Kind, s.SequenceKey), StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => (IReadOnlyList)g.OrderBy(s => s.SampledAt).Select(s => new AxSample(s.SampledAt, s.Value)).ToList(), StringComparer.Ordinal);
}
- /// Daily samples of AX sequences, oldest first; for one database, kind and key when given.
+ ///
+ /// Daily samples of AX sequences, oldest first; for one database, kind and key when given. At most
+ /// rows when given (API v1), in the order database, kind, key, day.
+ ///
public async Task> GetAxSequenceSamplesAsync(int instanceId, string? database, char? kind, string? key, DateTimeOffset since,
- CancellationToken cancellationToken = default)
+ int? top = null, CancellationToken cancellationToken = default)
{
await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
- var rows = await connection.QueryAsync("""
- SELECT DatabaseName, Kind, SequenceKey, SampledAt, Value
+ var rows = await connection.QueryAsync(new CommandDefinition("""
+ SELECT TOP (@top) DatabaseName, Kind, SequenceKey, SampledAt, Value
FROM insight.AxSequenceSamples
WHERE InstanceId = @instanceId AND [Day] >= @since
AND (@database IS NULL OR DatabaseName = @database) AND (@kind IS NULL OR Kind = @kind) AND (@key IS NULL OR SequenceKey = @key)
ORDER BY DatabaseName, Kind, SequenceKey, [Day];
- """, new { instanceId, database, kind = kind?.ToString(), key, since = Utc(since).Date }).ConfigureAwait(false);
+ """, new { instanceId, database, kind = kind?.ToString(), key, since = Utc(since).Date, top = top ?? int.MaxValue },
+ cancellationToken: cancellationToken)).ConfigureAwait(false);
return rows.Select(r => new StoredAxSequenceSample(r.DatabaseName, r.Kind[0], r.SequenceKey, AsUtc(r.SampledAt), r.Value)).ToList();
}
+ ///
+ /// Blocking events since where the head blocker or a blocked session was in one of
+ /// (the AX databases), newest first, at most . Filtered in SQL, so
+ /// blocking elsewhere on the instance cannot push the AX events out of the limit.
+ ///
+ public async Task> GetBlockingInDatabasesAsync(int instanceId, IReadOnlyCollection databases, DateTimeOffset from, int top,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(databases);
+ if (databases.Count == 0)
+ {
+ return [];
+ }
+
+ await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
+ var rows = await connection.QueryAsync<(DateTime SampledAt, int HeadSessionId, int BlockedCount, long MaxWaitMs, string SessionsJson)>(new CommandDefinition("""
+ SELECT TOP (@top) b.SampledAt, b.HeadSessionId, b.BlockedCount, b.MaxWaitMs, b.SessionsJson
+ FROM insight.BlockingEvents b
+ WHERE b.InstanceId = @instanceId AND b.SampledAt >= @from
+ AND EXISTS (SELECT 1 FROM OPENJSON(b.SessionsJson) WITH (DatabaseName nvarchar(128) '$.DatabaseName') s
+ WHERE s.DatabaseName IN (SELECT [value] FROM OPENJSON(@databases)))
+ ORDER BY b.SampledAt DESC;
+ """, new { instanceId, from = from.UtcDateTime, top, databases = JsonSerializer.Serialize(databases, Json) }, cancellationToken: cancellationToken))
+ .ConfigureAwait(false);
+ return rows.Select(r => new StoredBlocking(r.SampledAt, r.HeadSessionId, r.BlockedCount, r.MaxWaitMs,
+ JsonSerializer.Deserialize>(r.SessionsJson, Json) ?? [])).ToList();
+ }
+
private static DateTime AsUtc(DateTime value) => DateTime.SpecifyKind(value, DateTimeKind.Utc);
private sealed record AxBatchRunRow
diff --git a/src/SQLInsight.Web/DynamicsAxView.cs b/src/SQLInsight.Web/DynamicsAxView.cs
index 0521c49..e3d98d7 100644
--- a/src/SQLInsight.Web/DynamicsAxView.cs
+++ b/src/SQLInsight.Web/DynamicsAxView.cs
@@ -21,18 +21,26 @@ public static class DynamicsAxView
public const string BatchKind = "dynamics-ax-batch";
public const string SequencesKind = "dynamics-ax-sequences";
- /// The instances with recognised, monitored AX databases; one instance when is given.
- public static async Task> LoadAsync(InsightRepository repository, string? instance, CancellationToken cancellationToken = default)
+ /// At most this many blocking events per instance (the newest).
+ public const int MaxBlockingEvents = 1000;
+
+ /// The instances (not removed) with recognised, monitored AX databases; one instance when is given.
+ /// Also instances without AX databases where the detection could not read a database.
+ public static async Task> LoadAsync(InsightRepository repository, string? instance, bool withProblems = false,
+ CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(repository);
IReadOnlyList instances = instance is { Length: > 0 }
? (await repository.GetInstanceAsync(instance, cancellationToken).ConfigureAwait(false)) is { } one ? [one] : []
: await repository.GetInstancesAsync(cancellationToken).ConfigureAwait(false);
+
+ // Like the overview: an instance removed from monitoring is not shown (its history stays in the repository).
+ instances = instances.Where(i => !i.Removed).ToList();
var result = new List();
foreach (var i in instances)
{
var databases = await repository.GetSnapshotAsync(i.InstanceId, DatabasesKind, cancellationToken).ConfigureAwait(false);
- if (databases is not { Databases.Count: > 0 })
+ if (databases is null || (databases.Databases.Count == 0 && !(withProblems && databases.Problems.Count > 0)))
{
continue;
}
@@ -54,7 +62,8 @@ public static class DynamicsAxView
var rows = new List();
foreach (var view in instances)
{
- foreach (var e in await repository.GetBlockingAsync(view.Instance.InstanceId, from, cancellationToken).ConfigureAwait(false))
+ var names = view.Databases.Databases.Select(d => d.Database).ToList();
+ foreach (var e in await repository.GetBlockingInDatabasesAsync(view.Instance.InstanceId, names, from, MaxBlockingEvents, cancellationToken).ConfigureAwait(false))
{
var head = e.Sessions.FirstOrDefault(s => s.SessionId == e.HeadSessionId);
var database = view.Databases.Find(head?.DatabaseName) ?? e.Sessions.Select(s => view.Databases.Find(s.DatabaseName)).FirstOrDefault(d => d is not null);