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>
This commit is contained in:
Søren Eggert Lundsteen Olsen 2026-10-02 11:35:00 +02:00
parent 4ceb64f2d5
commit d070cd872b
2 changed files with 31 additions and 3 deletions

View file

@ -42,6 +42,9 @@ public sealed class TempdbCollector(int versionStoreMegabytes = 1024, int sessio
ORDER BY t.elapsed_time_seconds DESC
""";
/// <summary>How often the version store is scanned for the databases generating it, while it is large.</summary>
public static readonly TimeSpan GeneratorsInterval = TimeSpan.FromHours(1);
// Scans the version store: only run when it is large enough to need explaining.
internal const string GeneratorsSql = """
SELECT TOP 10 DB_NAME(database_id), SUM(CAST(aggregated_record_length_in_bytes AS bigint))
@ -68,9 +71,30 @@ public sealed class TempdbCollector(int versionStoreMegabytes = 1024, int sessio
var oldest = (await Db.QueryAsync(connection, OldestSnapshotSql, r => new SnapshotTransaction(r.Int(0), r.NullableLong(1) ?? 0, r.NullableString(2), r.NullableString(3)), ct)
.ConfigureAwait(false)).FirstOrDefault();
var versionMb = space.VersionKb is { } kb ? kb / 1024.0 : space.Version * PageMb;
var generators = versionMb >= versionStoreMegabytes / 2.0
? await Db.QueryAsync(connection, GeneratorsSql, r => new VersionGenerator(r.NullableString(0) ?? "?", (r.NullableLong(1) ?? 0) / 1024.0 / 1024.0), ct).ConfigureAwait(false)
: [];
IReadOnlyList<VersionGenerator> generators = [];
DateTimeOffset? generatorsAt = null;
if (versionMb >= versionStoreMegabytes / 2.0)
{
// Scanning a large version store every 5 minutes is not cheap: at most once per GeneratorsInterval, and a
// scan that fails (times out) keeps the previous answer instead of failing the whole collection.
if (previous is { VersionGeneratorsAt: { } at } && context.Now - at < GeneratorsInterval)
{
(generators, generatorsAt) = (previous.VersionGenerators, at);
}
else
{
try
{
generators = await Db.QueryAsync(connection, GeneratorsSql, r => new VersionGenerator(r.NullableString(0) ?? "?", (r.NullableLong(1) ?? 0) / 1024.0 / 1024.0), ct)
.ConfigureAwait(false);
generatorsAt = context.Now;
}
catch (System.Data.Common.DbException)
{
(generators, generatorsAt) = (previous?.VersionGenerators ?? [], previous?.VersionGeneratorsAt);
}
}
}
return TempdbAnalysis.Advance(previous, new TempdbSnapshot
{
@ -84,6 +108,7 @@ public sealed class TempdbCollector(int versionStoreMegabytes = 1024, int sessio
TopSessions = sessions,
OldestSnapshotTransaction = oldest,
VersionGenerators = generators,
VersionGeneratorsAt = generatorsAt,
VersionStoreThresholdMegabytes = versionStoreMegabytes,
SessionThresholdMegabytes = sessionMegabytes,
});

View file

@ -32,6 +32,9 @@ public sealed record TempdbSnapshot
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; } = [];