Repository navigation
Fixes #2800: document Lite's server-scoped watermark read as deliberately unbounded - #2879
Conversation
#2800) Measured, not inherited. #2800 read the asymmetry between Lite's GetLastCollectedTimeAsync (unbounded) and GetLastCollectedTimeForDatabaseAsync (bounded since #2344) as the same oversight #2795 fixed on the Postgres side. Against DuckDB it is not, on two independent grounds. The #2795 failure mechanism cannot occur. That was Npgsql's undocumented 30 s default CommandTimeout cancelling a 40.7-50.6 s read, the cancellation being swallowed, and the null reading as a first run. DuckDBCommand.CommandTimeout defaults to 0 - no limit - so there is no ceiling to exceed. And the bound does not pay. On DuckDB 1.5.5 against the shipped generated DDL at 50M rows / 4.7 GB, connection already open so the figure is query cost: one server (common Lite shape) unbounded 0.34 ms bounded 0.56 ms (LOSS) five servers unbounded 8.26 ms bounded 0.84 ms (9.8x) The per-database twin's bound IS earned and stays - ~10x in both shapes, because database_name is not in idx_query_store_time(server_id, collection_time) so that MAX genuinely scans and genuinely prunes. The two reads have different cost structures in a columnar engine; in Postgres both scanned every chunk, which is why the shapes match there and diverge here. Held across 1M-50M rows, narrow vs realistic payload width, and correlated vs scattered last_execution_time. No behaviour change - the doc comment now carries the numbers and names CommandTimeout's default as the assumption to re-check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
|
Reviewed. This is a documentation-only change (a doc comment on Spot-checked the factual claims against the current code rather than taking them on faith:
No parity drift: Darling's own doc comment on its bounded server-scoped read is untouched and still correct, so both sides remain internally consistent — Lite explains why it stays unbounded, Darling explains why its bound is earned. Nothing to flag on correctness, security, or performance — there's no behavior change, and the perf numbers/reasoning in the comment are plausible and internally consistent with the DuckDB zonemap-pruning explanation given. No missing-index-DMV concerns apply since no queries changed. |
Fixes #2800.
Verdict: Lite does not have this defect, and the proposed bound would make the common Lite deployment slower. No behaviour change — the decision is recorded where the next person will look.
#2800 was a reasonable read of the code:
GetLastCollectedTimeForDatabaseAsynctakes acollectedSincebound andGetLastCollectedTimeAsyncdoes not, which is exactly the asymmetry #2344 left on the Postgres side and #2795 later fixed there. The issue explicitly asked for Lite's own measurement first, citing #2762's revert as the precedent for not porting an optimization on another store's numbers. That measurement is below, and it says don't.The #2795 failure mechanism cannot occur here
#2795 was not fundamentally about a slow query. It was about Npgsql's undocumented 30 s default
CommandTimeoutcancelling a 40,743–50,560 ms read, the cancellation being swallowed, and the resultingnullbeing indistinguishable from a first run — so the collector silently fell back to its 60-minute window and re-collected what it already held.DuckDBCommand.CommandTimeoutdefaults to0, meaning no limit (verified against DuckDB.NET.Data 1.5.5). There is no ceiling to exceed and nothing to cancel. The silent-degradation path has no way to open.And the bound does not pay
Measured on DuckDB 1.5.5 — the version Lite ships — against this table's shipped generated DDL (
DuckDbSchemaGenerator.CreateTable+CreateIndexon the realQueryStoreCollector, not a retyped copy), using the shipped query strings. Connection already open, so the figure is query cost rather than connect cost (aSELECT 1control put connection-open at ~0.1 ms warm, so it was not dominating).At 50M rows / 4.7 GB, far past any realistic Lite store:
The single-server case is the common Lite deployment — it is a desktop tool. There,
server_idselects everything, and DuckDB answers the unfilteredMAXstraight from column zonemap metadata; adding acollection_timepredicate forces real evaluation instead. The inversion is stable across repeated runs and holds under adversarial data.For scale context, the whole curve (per-call, including DuckDB re-opening the file, as Lite does):
Against Darling's 40,743–50,560 ms, that is roughly three orders of magnitude apart in absolute terms.
The per-database twin's bound IS earned, so the asymmetry is correct
The same harness, same store, same run:
That bound pays in both shapes, because
database_nameis not inidx_query_store_time(server_id, collection_time), so thatMAXgenuinely has to scan and thecollection_timepredicate genuinely prunes.So the two reads have different cost structures in a columnar engine. In Postgres both scanned every chunk, which is why the shapes match there and diverge here. #2344's per-database bound was right; extending it to the server-scoped read would not be.
What was varied, so the conclusion is not scale- or shape-dependent
query_text/query_plan_text. Identical (2.2 ms both at 5M), as expected when the query reads three narrow columns out of 60 in a columnar store.last_execution_timecorrelation — correlated withcollection_timevs fully scattered. Scatter is the adversarial case for the unbounded form because its zonemaps stop being selective; it moved the multi-server number to 14.3 ms warm and left the single-server inversion intact.One useful side-observation: in the scattered runs the bounded form returns a different, earlier watermark than the unbounded one. That is expected and is precisely why
WatermarkPolicy.ReadFloor's bound is only sound behind the caller's clamp and theQueryStoreCollectorname guard — the harness surfacing it independently is a small confirmation that the guard is load-bearing rather than decorative.What lands
A doc comment on
GetLastCollectedTimeAsynccarrying the measurement and the reasoning, so this reads as a decision rather than an oversight. It namesDuckDBCommand.CommandTimeout's default as the load-bearing assumption to re-check, since that is the single fact the whole argument rests on and a DuckDB.NET change could invalidate it.No pin added, deliberately. A test asserting the method does not take a bound would forbid a future change that might become correct, and would be an assertion about today's cost curve wearing the costume of an invariant. The comment is the right vehicle; #2800 exists because the code did not explain itself.
Not addressed here
#2797 — skipping the server-scoped read entirely for collectors that discard its answer — is a genuine win in both engines and is unaffected by any of this. It remains the better fix for
query_storespecifically, as #2800 itself noted.Verification:
dotnet build Lite/PerformanceMonitorLite.csproj -p:EnableWindowsTargeting=truesucceeded (the one warning is pre-existing, in an unrelatedPerformanceMonitor.Uifile);global.jsonrestored. The Windows-only suites cannot run on macOS, and this change adds no executable code for them to cover. CHANGELOG staged as 2 insertions / 0 deletions with a plaingit add, so #2858's renormalization is holding.🤖 Generated with Claude Code
https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy