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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Monitored-server connections use 32 KB TDS packets instead of the 8 KB default** ([#2164]) - the heavy collectors read rows carrying query text and execution-plan XML, and the client reads each row whole, so per-packet overhead is paid against payloads hundreds of KB wide. New per-database timing (open vs drain) measured on a cross-region fleet showed effective throughput tracking LOB size rather than total bytes: a database averaging ~180 KB per row moved 256 KB/s while one averaging ~41 KB per row moved 1,080 KB/s over the same link with the same code. Fewer, larger packets attack exactly that. A server that dislikes the larger size negotiates down rather than failing.

- **Force-plan findings carry a machine-first verdict for MCP consumers** ([#2138]; Erik: agents will read these more than people) - analyze_server and get_analysis_findings (both apps) now emit structured_remediation alongside the copy-paste command: per target, eligible plus NAMED blockers (parameter_sensitivity_cofired, secondary_replica_evidence), the evidence numbers, join keys, and split force_sql / unforce_sql / verify_sql artifacts (verify asks both post-force questions: did the force stick, and the per-interval cost since). The blocker list comes from FactRemediation.ForcePlanBlockers - the ONE policy gate a future auto-force feature would consult, so what agents inspect today is what any later automation enforces, as a testable data contract. Computed at read time from the persisted targets (never persisted itself - one source of truth, no DTO mirror to forget).
- **Plan-regression detection scores CPU as the primary signal and gates on absolute spend** ([#2138] Phase 0, the advise-only foundation for the auto-force-plan bot) - the old score was GREATEST(cpu ratio, duration ratio), so a plan whose CPU never moved could fire on duration alone - but duration is confounded by blocking, IO waits and machine contention that no plan choice caused, exactly the false positive a plan-forcing bot must never act on. Now a CPU regression scores at its own ratio; a duration-dominant one fires only when EXTREME (>= 4x) AND corroborated by at least mild CPU worsening (>= 1.25x), scored at HALF the duration ratio so it competes honestly with CPU-detected rows. A new noise floor drops offenders whose latest plan burned under 10 CPU-seconds across the whole 14-day comparison window - a 12x ratio on a query costing 12ms per run is sampling jitter, not a finding. The regressed-queries drill-down previously kept its OWN copy of the old score and could surface rows the fact never counted; it now runs the same scoring, in both SKUs, pinned by split-signal tests that move CPU and duration independently.
- **The force-plan recommendation now warns when the regressed query is parameter-sensitive** ([#2138] gap 3) - each regressed_queries row carries a `parameter_sensitivity_cofired` flag, computed inside the drill-down with the PARAMETER_SENSITIVITY detector's own thresholds (one cached plan whose per-execution cost varies >= 10x across parameter values, same floors, same window) joined by query hash - so the flag can never claim evidence the detector would not report. A flagged target's force-plan preview gains a caution block naming the risk (forcing pins ONE shape for every parameter value; the population that preferred the other plan inherits the wrong one permanently, quietly, because a forced plan no longer recompiles away) and the gentler first levers (statistics updates; PSP optimization / Query Store hints on 2022+), and the copy-paste surface gets a compact two-line version of the same warning. Unflagged targets render byte-identically to before. This flag is also the standing gate for the future auto-force bot: a flagged target is never auto-forced. Both SKUs, pinned by live tests in both stores.
Expand Down
51 changes: 51 additions & 0 deletions Darling/Darling.Tests/PacketSizeTuningTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using Microsoft.Data.SqlClient;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Service;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// Pins the TDS packet size both apps apply to monitored-server connections (#2164). It exists because
/// the setting is invisible at runtime — a connection with the wrong packet size behaves correctly and
/// merely runs slowly, so nothing but a pin would notice it being dropped in a builder refactor.
/// </summary>
public sealed class PacketSizeTuningTests
{
[Fact]
public void DarlingBuildsMonitoredConnectionsWithTheTunedPacketSize()
{
var cs = MonitoredServerConnection.BuildConnectionString(new MonitoredServer
{
Name = "s",
Host = "example.database.windows.net",
Auth = "integrated",
});

var parsed = new SqlConnectionStringBuilder(cs);
Assert.Equal(CollectorTdsTuning.MonitoredServerPacketSize, parsed.PacketSize);
}

[Fact]
public void TunedPacketSizeIsWithinWhatTheDriverAndProtocolAccept()
{
/* The driver accepts 512-32768 and SQL Server negotiates down to the smaller of the two ends, so a
value in range can never fail a connection — it can only fail to help. A value OUTSIDE the range
throws at connect time, which on a monitoring fleet means every server going dark at once, so
the bound matters more than the tuning does. */
Assert.InRange(CollectorTdsTuning.MonitoredServerPacketSize, 512, 32768);

/* Not the 8 KB default — if someone "cleans up" the constant back to the default, this fails
rather than silently reverting the measurement this was built on. */
Assert.NotEqual(8000, CollectorTdsTuning.MonitoredServerPacketSize);
Assert.NotEqual(8192, CollectorTdsTuning.MonitoredServerPacketSize);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using System;
using Microsoft.Data.SqlClient;
using PerformanceMonitor.Common;

namespace PerformanceMonitor.Darling.Service;

Expand Down Expand Up @@ -37,6 +38,9 @@ public static string BuildConnectionString(MonitoredServer server, string? resol
MultipleActiveResultSets = true,
ApplicationIntent = server.ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
MultiSubnetFailover = server.MultiSubnetFailover,
/* #2164: fewer, larger TDS packets for the plan-XML/query-text payloads the heavy
collectors read. See CollectorTdsTuning for the measurement that motivated it. */
PacketSize = CollectorTdsTuning.MonitoredServerPacketSize,
};

/* Encrypt fail-closed: unknown/blank modes get Mandatory, matching Lite. */
Expand Down
5 changes: 4 additions & 1 deletion Lite/Models/ServerConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,10 @@ private string BuildConnectionString(ResolvedAuth auth)
TrustServerCertificate = TrustServerCertificate,
MultipleActiveResultSets = true,
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
MultiSubnetFailover = MultiSubnetFailover
MultiSubnetFailover = MultiSubnetFailover,
/* #2164: fewer, larger TDS packets for the plan-XML/query-text payloads the heavy
collectors read. Shared constant so Lite and Darling cannot drift. */
PacketSize = PerformanceMonitor.Common.CollectorTdsTuning.MonitoredServerPacketSize
};

// Set encryption mode
Expand Down
42 changes: 42 additions & 0 deletions PerformanceMonitor.Common/CollectorTdsTuning.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

namespace PerformanceMonitor.Common;

/// <summary>
/// TDS-level transport settings both apps apply when they build a monitored-server connection string
/// (#2164). One definition so Lite and Darling cannot drift apart on how they talk to a monitored
/// instance — the two builders are otherwise independent code.
/// </summary>
public static class CollectorTdsTuning
{
/// <summary>
/// The TDS packet size, in bytes, for monitored-server connections. Neither app used to set this, so
/// every connection ran at the driver's 8 KB default.
///
/// <para><b>Why it matters here.</b> The heavy collectors read rows carrying two
/// <c>nvarchar(max)</c> columns — query text and execution-plan XML — and the client reads each row
/// whole before the loop sees it. Measured on a cross-region production fleet, effective drain
/// throughput tracked LOB size rather than total bytes: a database averaging ~180 KB per row moved
/// 256 KB/s while one averaging ~41 KB per row moved 1,080 KB/s over the same link, same code. That
/// inverse relationship is per-packet overhead, and 8 KB packets mean a 180 KB plan costs ~22 packets
/// where 32 KB packets cost ~6.</para>
///
/// <para>32 KB is the largest value SQL Server negotiates (the protocol maximum is 32,767; the driver
/// accepts 512–32,768 and both ends agree down to the smaller). Chosen over an intermediate value
/// because the payloads at issue are hundreds of KB — the whole point is fewer, larger packets — and
/// because a server that dislikes it negotiates down rather than failing.</para>
///
/// <para><b>Cost of being wrong:</b> a larger packet size raises per-connection network-buffer memory
/// on both ends. At the fleet's connection count (one per server per sweep, bounded by the sweep
/// width) that is kilobytes, not megabytes — cheap enough that it is not worth a knob until someone
/// shows it hurts. This is measured against a fleet where it should help most; if the numbers say
/// otherwise it comes back out, and the comment records what evidence would justify that.</para>
/// </summary>
public const int MonitoredServerPacketSize = 32768;
}
Loading