Skip to content

Commit 86ada82

Browse files
authored
Expose cache stats (#79)
1 parent 58c6489 commit 86ada82

7 files changed

Lines changed: 251 additions & 1 deletion

src/include/query_condition_cache_functions.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "query_condition_cache_state.hpp"
44

5+
#include "duckdb/function/scalar_function.hpp"
56
#include "duckdb/function/table_function.hpp"
67

78
namespace duckdb {
@@ -18,5 +19,7 @@ shared_ptr<ConditionCacheEntry> BuildCacheEntry(ClientContext &context, DuckTabl
1819

1920
TableFunction ConditionCacheBuildFunction();
2021
TableFunction ConditionCacheInfoFunction();
22+
TableFunction ConditionCacheStatsFunction();
23+
ScalarFunction ConditionCacheResetStatsFunction();
2124

2225
} // namespace duckdb

src/include/query_condition_cache_state.hpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "concurrency/thread_annotation.hpp"
66

77
#include "duckdb/common/array.hpp"
8+
#include "duckdb/common/atomic.hpp"
89
#include "duckdb/common/types/hash.hpp"
910
#include "duckdb/common/unordered_map.hpp"
1011
#include "duckdb/common/unordered_set.hpp"
@@ -131,6 +132,14 @@ struct TableFilterKeyIndex : public ObjectCacheEntry {
131132
bool IsEmpty();
132133
// Transfer ownership of all filter keys out. Clears the internal set.
133134
unordered_set<string> Take();
135+
// Return a copy of all filter keys without clearing the set.
136+
unordered_set<string> Snapshot();
137+
};
138+
139+
struct CacheStoreStats {
140+
idx_t total_memory_bytes;
141+
idx_t hit_count;
142+
idx_t access_count;
134143
};
135144

136145
// Stored in DuckDB's per-database ObjectCache
@@ -169,11 +178,26 @@ class ConditionCacheStore : public ObjectCacheEntry {
169178
// Get or create the store from a client context
170179
static shared_ptr<ConditionCacheStore> GetOrCreate(ClientContext &context);
171180

181+
// Record an optimizer lookup attempt.
182+
void RecordAccess(bool hit);
183+
184+
// Reset cache access stats.
185+
void ResetStats();
186+
187+
// Compute the sum of estimated memory used by all live cache entries.
188+
idx_t ComputeTotalMemoryBytes(ClientContext &context) const;
189+
190+
// Return a snapshot of current stats.
191+
CacheStoreStats GetStats(ClientContext &context) const;
192+
172193
private:
173-
concurrency::mutex lock;
194+
mutable concurrency::mutex lock;
174195
// Tracks all table OIDs that have been cached, for ClearAll
175196
unordered_set<idx_t> cached_table_oids DUCKDB_GUARDED_BY(lock);
176197

198+
atomic<idx_t> total_accesses {0};
199+
atomic<idx_t> total_hits {0};
200+
177201
static string MakeCacheKeyString(const CacheKey &key);
178202
static string MakeFilterKeyIndexKey(idx_t table_oid);
179203
};

src/query_condition_cache_extension.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ void OnQueryConditionCacheSettingChange(ClientContext &context, SetScope scope,
2828
void LoadInternal(ExtensionLoader &loader) {
2929
loader.RegisterFunction(ConditionCacheBuildFunction());
3030
loader.RegisterFunction(ConditionCacheInfoFunction());
31+
loader.RegisterFunction(ConditionCacheStatsFunction());
32+
loader.RegisterFunction(ConditionCacheResetStatsFunction());
3133

3234
// Register the internal filter function so it survives plan serialization/verification
3335
loader.RegisterFunction(ConditionCacheFilterFunction());

src/query_condition_cache_functions.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "duckdb/common/unordered_set.hpp"
1212
#include "duckdb/common/vector.hpp"
1313
#include "duckdb/execution/expression_executor.hpp"
14+
#include "duckdb/function/scalar_function.hpp"
1415
#include "duckdb/function/table_function.hpp"
1516
#include "duckdb/parallel/task_executor.hpp"
1617
#include "duckdb/parallel/task_scheduler.hpp"
@@ -380,4 +381,75 @@ TableFunction ConditionCacheInfoFunction() {
380381
return func;
381382
}
382383

384+
// ------- condition_cache_stats() -------
385+
// Returns global cache statistics: total memory, hit count, access count.
386+
387+
namespace {
388+
389+
struct ConditionCacheStatsState : public GlobalTableFunctionState {
390+
bool done = false;
391+
};
392+
393+
unique_ptr<FunctionData> ConditionCacheStatsBind(ClientContext &context, TableFunctionBindInput &input,
394+
vector<LogicalType> &return_types, vector<string> &names) {
395+
names.emplace_back("total_memory_bytes");
396+
return_types.emplace_back(LogicalType {LogicalTypeId::UBIGINT});
397+
names.emplace_back("hit_count");
398+
return_types.emplace_back(LogicalType {LogicalTypeId::UBIGINT});
399+
names.emplace_back("access_count");
400+
return_types.emplace_back(LogicalType {LogicalTypeId::UBIGINT});
401+
return nullptr;
402+
}
403+
404+
unique_ptr<GlobalTableFunctionState> ConditionCacheStatsInit(ClientContext &context, TableFunctionInitInput &input) {
405+
return make_uniq<ConditionCacheStatsState>();
406+
}
407+
408+
void ConditionCacheStatsExecute(ClientContext &context, TableFunctionInput &data_p, DataChunk &output) {
409+
auto &gstate = data_p.global_state->Cast<ConditionCacheStatsState>();
410+
if (gstate.done) {
411+
return;
412+
}
413+
gstate.done = true;
414+
415+
auto store = ConditionCacheStore::GetOrCreate(context);
416+
auto stats = store->GetStats(context);
417+
418+
output.SetCardinality(1);
419+
output.data[0].SetValue(0, Value::UBIGINT(stats.total_memory_bytes));
420+
output.data[1].SetValue(0, Value::UBIGINT(stats.hit_count));
421+
output.data[2].SetValue(0, Value::UBIGINT(stats.access_count));
422+
}
423+
424+
} // namespace
425+
426+
TableFunction ConditionCacheStatsFunction() {
427+
TableFunction func("condition_cache_stats", {}, ConditionCacheStatsExecute, ConditionCacheStatsBind,
428+
ConditionCacheStatsInit);
429+
return func;
430+
}
431+
432+
// ------- condition_cache_reset_stats() -------
433+
// Scalar function: resets hit_count and access_count, returns true.
434+
435+
namespace {
436+
437+
unique_ptr<FunctionData> ConditionCacheResetStatsBind(ClientContext &context, ScalarFunction &bound_function,
438+
vector<unique_ptr<Expression>> &arguments) {
439+
auto store = ConditionCacheStore::GetOrCreate(context);
440+
store->ResetStats();
441+
return nullptr;
442+
}
443+
444+
void ConditionCacheResetStatsFn(DataChunk &args, ExpressionState &state, Vector &result) {
445+
result.Reference(Value::BOOLEAN(true));
446+
}
447+
448+
} // namespace
449+
450+
ScalarFunction ConditionCacheResetStatsFunction() {
451+
return ScalarFunction("condition_cache_reset_stats", {}, LogicalType {LogicalTypeId::BOOLEAN},
452+
ConditionCacheResetStatsFn, ConditionCacheResetStatsBind);
453+
}
454+
383455
} // namespace duckdb

src/query_condition_cache_optimizer.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ void QueryConditionCacheOptimizer::PreOptimizeWalk(ClientContext &context, uniqu
101101

102102
auto store = ConditionCacheStore::GetOrCreate(context);
103103
auto entry = store->Lookup(context, key);
104+
store->RecordAccess(entry != nullptr);
104105

105106
if (!entry) {
106107
// TODO: Consider building cache in the background and syncing later

src/query_condition_cache_state.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ unordered_set<string> TableFilterKeyIndex::Take() {
187187
return std::move(filter_keys);
188188
}
189189

190+
unordered_set<string> TableFilterKeyIndex::Snapshot() {
191+
concurrency::lock_guard<concurrency::mutex> guard(lock);
192+
return filter_keys;
193+
}
194+
190195
// ------- CONDITION_CACHE_STORE -------
191196

192197
string ConditionCacheStore::MakeCacheKeyString(const CacheKey &key) {
@@ -275,11 +280,59 @@ void ConditionCacheStore::ClearAll(ClientContext &context) {
275280
cache.Delete(MakeFilterKeyIndexKey(table_oid));
276281
}
277282
cached_table_oids.clear();
283+
ResetStats();
278284
}
279285

280286
shared_ptr<ConditionCacheStore> ConditionCacheStore::GetOrCreate(ClientContext &context) {
281287
auto &cache = ObjectCache::GetObjectCache(context);
282288
return cache.GetOrCreate<ConditionCacheStore>(CACHE_KEY);
283289
}
284290

291+
void ConditionCacheStore::RecordAccess(bool hit) {
292+
total_accesses.fetch_add(1, std::memory_order_relaxed);
293+
if (hit) {
294+
total_hits.fetch_add(1, std::memory_order_relaxed);
295+
}
296+
}
297+
298+
void ConditionCacheStore::ResetStats() {
299+
total_accesses.store(0, std::memory_order_relaxed);
300+
total_hits.store(0, std::memory_order_relaxed);
301+
}
302+
303+
idx_t ConditionCacheStore::ComputeTotalMemoryBytes(ClientContext &context) const {
304+
auto &cache = ObjectCache::GetObjectCache(context);
305+
306+
unordered_set<idx_t> oid_snapshot;
307+
{
308+
concurrency::lock_guard<concurrency::mutex> guard(lock);
309+
oid_snapshot = cached_table_oids;
310+
}
311+
312+
idx_t total = 0;
313+
for (auto table_oid : oid_snapshot) {
314+
auto index = cache.Get<TableFilterKeyIndex>(MakeFilterKeyIndexKey(table_oid));
315+
if (!index) {
316+
continue;
317+
}
318+
for (const auto &filter_key : index->Snapshot()) {
319+
auto entry = cache.Get<ConditionCacheEntry>(MakeCacheKeyString(CacheKey {table_oid, filter_key}));
320+
if (entry) {
321+
auto mem = entry->GetEstimatedCacheMemory();
322+
ALWAYS_ASSERT(mem.IsValid());
323+
total += mem.GetIndex();
324+
}
325+
}
326+
}
327+
return total;
328+
}
329+
330+
CacheStoreStats ConditionCacheStore::GetStats(ClientContext &context) const {
331+
return CacheStoreStats {
332+
.total_memory_bytes = ComputeTotalMemoryBytes(context),
333+
.hit_count = total_hits.load(std::memory_order_relaxed),
334+
.access_count = total_accesses.load(std::memory_order_relaxed),
335+
};
336+
}
337+
285338
} // namespace duckdb
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# name: test/sql/condition_cache_stats.test
2+
# description: Test predicate cache stats
3+
# group: [sql]
4+
5+
require query_condition_cache
6+
7+
# Before any cache activity: all stats are zero
8+
query III
9+
SELECT total_memory_bytes, hit_count, access_count FROM condition_cache_stats();
10+
----
11+
0 0 0
12+
13+
statement ok
14+
CREATE TABLE t AS SELECT i AS id, i % 100 AS val FROM range(500000) t(i);
15+
16+
statement ok
17+
SELECT * FROM condition_cache_build('t', 'val = 42');
18+
19+
query I
20+
SELECT total_memory_bytes > 0 FROM condition_cache_stats();
21+
----
22+
true
23+
24+
query II
25+
SELECT hit_count, access_count FROM condition_cache_stats();
26+
----
27+
0 0
28+
29+
# First auto query: optimizer finds the pre-built entry (hit)
30+
statement ok
31+
SELECT count(*) FROM t WHERE val = 42;
32+
33+
query II
34+
SELECT hit_count, access_count FROM condition_cache_stats();
35+
----
36+
1 1
37+
38+
# Second auto query: another hit
39+
statement ok
40+
SELECT count(*) FROM t WHERE val = 42;
41+
42+
query II
43+
SELECT hit_count, access_count FROM condition_cache_stats();
44+
----
45+
2 2
46+
47+
# Capture memory before reset to verify it is unchanged afterwards
48+
statement ok
49+
CREATE TABLE mem_before AS SELECT total_memory_bytes FROM condition_cache_stats();
50+
51+
# Reset stats: clears counters but does NOT clear cache entries
52+
query I
53+
SELECT condition_cache_reset_stats();
54+
----
55+
true
56+
57+
query II
58+
SELECT hit_count, access_count FROM condition_cache_stats();
59+
----
60+
0 0
61+
62+
# Memory is unchanged after reset: must equal the value captured before reset
63+
query I
64+
SELECT (SELECT total_memory_bytes FROM condition_cache_stats()) =
65+
(SELECT total_memory_bytes FROM mem_before);
66+
----
67+
true
68+
69+
# First query on a new predicate (not pre-built): optimizer misses, builds entry
70+
statement ok
71+
SELECT count(*) FROM t WHERE val = 99;
72+
73+
# access_count = 1, hit_count = 0 (first lookup was a miss)
74+
query II
75+
SELECT hit_count, access_count FROM condition_cache_stats();
76+
----
77+
0 1
78+
79+
# Second query on the same new predicate: optimizer finds the entry
80+
statement ok
81+
SELECT count(*) FROM t WHERE val = 99;
82+
83+
query II
84+
SELECT hit_count, access_count FROM condition_cache_stats();
85+
----
86+
1 2
87+
88+
# Disabling the setting calls ClearAll, which drops all cache entries and resets all stats
89+
statement ok
90+
SET use_query_condition_cache = false;
91+
92+
query III
93+
SELECT total_memory_bytes, hit_count, access_count FROM condition_cache_stats();
94+
----
95+
0 0 0

0 commit comments

Comments
 (0)