sys.dm_tran_top_version_generators reads the whole version store. It ran every 5 minutes while the version store was over half the threshold, i.e. exactly when it is large and expensive to scan, and a timeout failed the whole tempdb collection. It now runs at most once per hour (the previous answer is kept in between, with its time) and a failed scan keeps the previous answer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
84 lines
4.3 KiB
C#
84 lines
4.3 KiB
C#
namespace SQLInsight.Core.Data;
|
|
|
|
/// <summary>A session using tempdb (user objects plus internal objects: sorts, hash tables, spools). Login and program are sensitive.</summary>
|
|
/// <param name="Since">When the session logged in; with <see cref="SessionId"/> it identifies the session across samples.</param>
|
|
public sealed record TempdbSession(int SessionId, DateTimeOffset Since, double UsedMegabytes, string? LoginName, string? ProgramName, string? HostName, string? Status)
|
|
{
|
|
public string Key => $"{SessionId}|{Since.UtcTicks}";
|
|
}
|
|
|
|
/// <summary>The oldest open snapshot transaction: it keeps every row version made since it began.</summary>
|
|
public sealed record SnapshotTransaction(int SessionId, long ElapsedSeconds, string? LoginName, string? ProgramName);
|
|
|
|
/// <summary>Version store use per database (from sys.dm_tran_top_version_generators).</summary>
|
|
public sealed record VersionGenerator(string DatabaseName, double Megabytes);
|
|
|
|
/// <summary>tempdb space and the version store, every 5 minutes.</summary>
|
|
public sealed record TempdbSnapshot
|
|
{
|
|
public required DateTimeOffset CapturedAt { get; init; }
|
|
|
|
/// <summary>The size of tempdb's data files now.</summary>
|
|
public double SizeMegabytes { get; init; }
|
|
public double FreeMegabytes { get; init; }
|
|
public double VersionStoreMegabytes { get; init; }
|
|
public double UserObjectsMegabytes { get; init; }
|
|
public double InternalObjectsMegabytes { get; init; }
|
|
|
|
/// <summary>Whether a data file can still grow (autogrowth on and not at its maximum size).</summary>
|
|
public bool CanGrow { get; init; } = true;
|
|
|
|
public IReadOnlyList<TempdbSession> TopSessions { get; init; } = [];
|
|
public SnapshotTransaction? OldestSnapshotTransaction { get; init; }
|
|
public IReadOnlyList<VersionGenerator> VersionGenerators { get; init; } = [];
|
|
|
|
/// <summary>When <see cref="VersionGenerators"/> was read (the scan is expensive and is repeated at most hourly).</summary>
|
|
public DateTimeOffset? VersionGeneratorsAt { get; init; }
|
|
|
|
/// <summary>Version store sizes of the latest samples (MB), oldest first: the trend.</summary>
|
|
public IReadOnlyList<double> VersionStoreHistory { get; init; } = [];
|
|
|
|
/// <summary>Since when each session has been above <see cref="SessionThresholdMegabytes"/>, by <see cref="TempdbSession.Key"/>.</summary>
|
|
public IReadOnlyDictionary<string, DateTimeOffset> HeavySessionsSince { get; init; } = new Dictionary<string, DateTimeOffset>();
|
|
|
|
/// <summary>The version store above this is a finding (SQLInsight:Tempdb:VersionStoreMegabytes).</summary>
|
|
public int VersionStoreThresholdMegabytes { get; init; } = 1024;
|
|
|
|
/// <summary>A session using more tempdb than this for <see cref="TempdbAnalysis.HeavySessionMinutes"/> is a finding.</summary>
|
|
public int SessionThresholdMegabytes { get; init; } = 1024;
|
|
|
|
public double FreePercent => SizeMegabytes <= 0 ? 100 : 100 * FreeMegabytes / SizeMegabytes;
|
|
}
|
|
|
|
public static class TempdbAnalysis
|
|
{
|
|
public const int HistoryLength = 12;
|
|
public const int HeavySessionMinutes = 15;
|
|
|
|
/// <summary>The version store has grown in each of the last six samples, by a quarter GB in all.</summary>
|
|
public static bool IsGrowing(IReadOnlyList<double> history)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(history);
|
|
if (history.Count < 6)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var last = history.Skip(history.Count - 6).ToList();
|
|
return last.Zip(last.Skip(1)).All(p => p.Second > p.First) && last[^1] - last[0] >= 256;
|
|
}
|
|
|
|
/// <summary>Carries the trend and the heavy sessions over from the previous sample.</summary>
|
|
public static TempdbSnapshot Advance(TempdbSnapshot? previous, TempdbSnapshot current)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(current);
|
|
var history = (previous?.VersionStoreHistory ?? []).Append(current.VersionStoreMegabytes).TakeLast(HistoryLength).ToList();
|
|
var heavy = new Dictionary<string, DateTimeOffset>(StringComparer.Ordinal);
|
|
foreach (var s in current.TopSessions.Where(s => s.UsedMegabytes >= current.SessionThresholdMegabytes))
|
|
{
|
|
heavy[s.Key] = previous?.HeavySessionsSince.TryGetValue(s.Key, out var since) == true ? since : current.CapturedAt;
|
|
}
|
|
|
|
return current with { VersionStoreHistory = history, HeavySessionsSince = heavy };
|
|
}
|
|
}
|