sqlinsight-pro/src/SQLInsight.Core/Data/Tempdb.cs
Søren Eggert Lundsteen Olsen d070cd872b Review 1.2: scan the version store for its generators at most hourly
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>
2026-10-02 11:35:00 +02:00

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 };
}
}