Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions Darling/Darling.Tests/PgTargetBloatLiveTests.cs

Large diffs are not rendered by default.

531 changes: 531 additions & 0 deletions Darling/Darling.Tests/PgTargetBloatTests.cs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Darling/Darling.Tests/PgTargetSharedSwitchRoutingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,10 @@ family moves its own line here and says so. Lane 5 (the wait profile) moved the
equality above), and no SQL Server composer claims the key. Each lane moves its own line to NotNull. */
Assert.Null(PgTargetAdvice.Static(PgTargetFactKeys.IoReadLatencyMs)); /* lane 11 */
Assert.Null(PgTargetAdvice.Static(PgTargetFactKeys.ReplicationLag)); /* lane 12 */
Assert.Null(PgTargetAdvice.Static(PgTargetFactKeys.BloatTrend)); /* lane 13 */
/* Lane 13 filled the bloat family: both keys compose their own block (the static shape when no fact is
in the lookup), and the delegation equality above proves the shared entry points answer that block. */
Assert.NotNull(PgTargetAdvice.Static(PgTargetFactKeys.BloatTrend)); /* lane 13 */
Assert.NotNull(PgTargetAdvice.Static(PgTargetFactKeys.IndexBloatTrend)); /* lane 13 */

/* ANOMALY_PG_WAIT_PROFILE must not fall into the SQL Server ANOMALY_WAIT_ composer, which would render
"Anomalous spike in PG_WAIT_PROFILE" for it. Lane 9 filled the anomaly family, so the line moved from
Expand Down

Large diffs are not rendered by default.

301 changes: 298 additions & 3 deletions PerformanceMonitor.Analysis/PgTargetAdvice.Bloat.cs

Large diffs are not rendered by default.

62 changes: 57 additions & 5 deletions PerformanceMonitor.Analysis/PgTargetRelationshipGraph.Bloat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,70 @@
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System.Collections.Generic;

namespace PerformanceMonitor.Analysis;

/// <summary>
/// The bloat chain (filled by lane 13 of #3691, design §3.12): <c>PG_BLOAT_TREND</c> → <c>PG_AUTOVACUUM_BACKLOG</c> /
/// <c>PG_XMIN_HOLD</c> (dead tuples vacuum could not reclaim are where the growth comes from), and the
/// buffer-cache leaf (a bloated heap is more pages for the same rows). Predicates read the destination fact's
/// verdict, never a bar of their own.
/// The bloat chain (filled by lane 13 of #3691, design §3.4): <c>PG_BLOAT_TREND</c> ↔ <c>PG_AUTOVACUUM_BACKLOG</c>
/// on the SAME table — bloat is the DAMAGE whose cause is §3.1's backlog (dead tuples autovacuum did not
/// reclaim are where the growth comes from) — and <c>PG_INDEX_BLOAT_TREND</c> → <c>PG_BLOAT_TREND</c> when the
/// index's parent table is one the table trend names. Predicates read the destination fact's verdict and the
/// two facts' object names, never a bar of their own.
///
/// <para><b>Why the table intersection, and what happens without names.</b> A backlog on <c>orders</c> says
/// nothing about bloat growing on <c>events</c>; an edge on co-presence alone would tell a story whose two
/// halves are about different tables. So the predicate intersects the backlog fact's one table
/// (<see cref="Fact.ObjectName"/>) with the set the bloat fact names (its worst table plus the
/// <c>growth_bytes_&lt;schema.table&gt;</c> metadata keys, through <see cref="PgTargetScorer.BloatNamedObjects"/>).
/// When the backlog fact carries no name at all the intersection cannot be tested and the edge falls back
/// to co-presence — and the description says "a table this trend names", which in that one case is the
/// weaker claim; <see cref="PgTargetScorer.BloatAndBacklogShareATable"/> is the single definition both the
/// edge and the amplifier use, so they cannot disagree about which it was.</para>
///
/// <para><b>Why both directions between trend and backlog.</b> The inference engine roots at the highest
/// amplified severity and walks outward, consuming what it reaches — the vacuum chain's lesson
/// (<c>PgTargetRelationshipGraph.Vacuum.cs</c>). Which of the two leads is not fixed: a 900 MB growth over
/// fourteen days grades higher than a table just past its trigger line, and a 40× backlog grades higher
/// than a growth just over the 256 MiB line. One direction would leave the other fact rooting a second story
/// about the same table. The backlog → trend edge is declared HERE, in the bloat partial, because it exists
/// only when the trend does; the vacuum mesh's own pins are untouched (they have no bloat fact, so the edge
/// never fires there).</para>
///
/// <para><b>What it does not do.</b> No edge into <c>PG_XMIN_HOLD</c> (the hold reaches bloat through the
/// backlog, which already meshes with it — a direct edge would make the story a triangle told twice; the
/// hold's role in the growth is the amplifier's sentence). No buffer-cache leaf (a bloated heap IS more pages
/// for the same rows, but the buffer fact grades a server-wide miss share and cannot be attributed to one
/// table). No config co-fire: <c>fillfactor</c> is per-object and no server knob makes a table bloat.</para>
/// </summary>
public sealed partial class PgTargetRelationshipGraph
{
/* filled by lane 13 */
private const string BloatCategory = "bloat_growth";

private partial void BuildBloatEdges()
{
/* filled by lane 13 of #3691. */

/* trend ↔ backlog, same table: the damage and its cause, either one leading. */
AddEdge(PgTargetFactKeys.BloatTrend, PgTargetFactKeys.AutovacuumBacklog, BloatCategory,
"PG_AUTOVACUUM_BACKLOG fired on a table this trend names — the dead tuples autovacuum is not clearing are where the growth is coming from",
facts => BloatTrendAndBacklogCoFire(facts));
AddEdge(PgTargetFactKeys.AutovacuumBacklog, PgTargetFactKeys.BloatTrend, BloatCategory,
"PG_BLOAT_TREND fired on this table — the backlog's damage: the bloat estimate has grown past the measured line across the lookback",
facts => BloatTrendAndBacklogCoFire(facts));

/* index trend → table trend, same parent table: the heap and its index churning together. */
AddEdge(PgTargetFactKeys.IndexBloatTrend, PgTargetFactKeys.BloatTrend, BloatCategory,
"PG_BLOAT_TREND fired on this index's table — heap and index are growing together, which is churn on the table, not an index-only shape",
facts => Fired(facts, PgTargetFactKeys.BloatTrend)
&& facts.TryGetValue(PgTargetFactKeys.IndexBloatTrend, out var index)
&& facts.TryGetValue(PgTargetFactKeys.BloatTrend, out var table)
&& PgTargetScorer.BloatNamedObjects(index, parentTables: true).Overlaps(PgTargetScorer.BloatNamedObjects(table)));
}

/// <summary>Both fired, and they share a table (or the backlog carries no name to test — see the class summary).</summary>
private static bool BloatTrendAndBacklogCoFire(IReadOnlyDictionary<string, Fact> facts) =>
Fired(facts, PgTargetFactKeys.BloatTrend)
&& Fired(facts, PgTargetFactKeys.AutovacuumBacklog)
&& PgTargetScorer.BloatAndBacklogShareATable(facts[PgTargetFactKeys.BloatTrend], facts[PgTargetFactKeys.AutovacuumBacklog]);
}
Loading
Loading