Skip to content

Commit 3cc1410

Browse files
authored
perf(scan): skip manifest lists on exact snapshot cache hits (#374)
1 parent e6784e8 commit 3cc1410

7 files changed

Lines changed: 147 additions & 68 deletions

File tree

‎docs/source/user_guide/manifest_entry_cache.rst‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ scans that target the same bucket.
2828

2929
The cache stores decoded and merged live manifest entries by table path, branch,
3030
and bucket for ``ScanMode::ALL``. Each cache value can retain several snapshot
31-
results for that bucket. Exact snapshot hits are served from the cache; cache
32-
misses rebuild the target snapshot bucket from the target snapshot's data
31+
results for that bucket. Exact snapshot hits are served from the cache without reading or decoding the
32+
manifest lists again; cache misses rebuild the target snapshot bucket from the target snapshot's data
3333
manifests and store the rebuilt live entries.
3434

3535
Request-specific filters are not stored in the cache. Partition, level, and
@@ -120,3 +120,6 @@ The scan metrics expose existing counters for the last scan:
120120

121121
- ``lastScannedManifests``: how many manifest files were loaded during this
122122
scan before manifest entry decoding.
123+
124+
On an exact cache hit, ``lastScannedManifests`` is zero. The cached table file
125+
count preserves the skipped-file metric even when manifest lists are bypassed.

‎src/paimon/core/manifest/snapshot_live_manifest_entries.cpp‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
namespace paimon {
3535
namespace {
3636

37-
constexpr int32_t kMagic = 0x534d4543; // SMEC
37+
constexpr int32_t kMagic = 0x534d4544; // SMED: includes per-snapshot total file counts
3838

3939
size_t NormalizeMaxSnapshots(int32_t max_snapshots) {
4040
return static_cast<size_t>(std::max(0, max_snapshots));
@@ -61,12 +61,14 @@ std::shared_ptr<Bytes> ToBytes(const MemorySegmentOutputStream& out,
6161
SnapshotLiveManifestEntries::SnapshotLiveManifestEntries(int32_t max_snapshots)
6262
: max_snapshots_(max_snapshots) {}
6363

64-
void SnapshotLiveManifestEntries::Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries) {
64+
void SnapshotLiveManifestEntries::Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries,
65+
int64_t total_data_files) {
6566
if (NormalizeMaxSnapshots(max_snapshots_) == 0) {
6667
return;
6768
}
6869
entries_by_snapshot_[snapshot_id] =
6970
std::make_shared<const std::vector<ManifestEntry>>(std::move(entries));
71+
total_data_files_by_snapshot_[snapshot_id] = total_data_files;
7072
EvictIfNeeded();
7173
}
7274

@@ -83,6 +85,7 @@ Result<std::shared_ptr<Bytes>> SnapshotLiveManifestEntries::Serialize(
8385
ManifestEntrySerializer serializer(pool);
8486
for (const auto& [snapshot_id, entries] : entries_by_snapshot_) {
8587
out.WriteValue<int64_t>(snapshot_id);
88+
out.WriteValue<int64_t>(total_data_files_by_snapshot_.at(snapshot_id));
8689
PAIMON_RETURN_NOT_OK(serializer.SerializeList(*entries, &out));
8790
}
8891
return ToBytes(out, pool);
@@ -111,7 +114,10 @@ Result<SnapshotLiveManifestEntries> SnapshotLiveManifestEntries::Deserialize(
111114
ManifestEntrySerializer serializer(pool);
112115
for (int32_t i = 0; i < snapshot_count; i++) {
113116
PAIMON_ASSIGN_OR_RAISE(int64_t snapshot_id, in.ReadValue<int64_t>());
117+
PAIMON_ASSIGN_OR_RAISE(int64_t total_data_files, in.ReadValue<int64_t>());
114118
PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestEntry> entries, serializer.DeserializeList(&in));
119+
snapshot_live_manifest_entries.total_data_files_by_snapshot_[snapshot_id] =
120+
total_data_files;
115121
snapshot_live_manifest_entries.entries_by_snapshot_[snapshot_id] =
116122
std::make_shared<const std::vector<ManifestEntry>>(std::move(entries));
117123
}
@@ -122,6 +128,7 @@ Result<SnapshotLiveManifestEntries> SnapshotLiveManifestEntries::Deserialize(
122128
void SnapshotLiveManifestEntries::EvictIfNeeded() {
123129
size_t max_snapshots = NormalizeMaxSnapshots(max_snapshots_);
124130
while (entries_by_snapshot_.size() > max_snapshots) {
131+
total_data_files_by_snapshot_.erase(entries_by_snapshot_.begin()->first);
125132
entries_by_snapshot_.erase(entries_by_snapshot_.begin());
126133
}
127134
}
@@ -151,6 +158,7 @@ SnapshotLiveManifestEntries::ReadSnapshot(
151158
std::optional<int64_t> previous_id;
152159
for (int32_t i = 0; i < snapshot_count; ++i) {
153160
PAIMON_ASSIGN_OR_RAISE(int64_t id, in.ReadValue<int64_t>());
161+
PAIMON_ASSIGN_OR_RAISE(int64_t total_data_files, in.ReadValue<int64_t>());
154162
// Serialize emits snapshots in ascending order. Validate this before using the position
155163
// to honor a retention limit smaller than the one used when the cache was populated.
156164
if (previous_id && id <= previous_id.value()) {
@@ -165,6 +173,7 @@ SnapshotLiveManifestEntries::ReadSnapshot(
165173
if (keep) {
166174
selected.emplace();
167175
selected->candidate_entries = entry_count;
176+
selected->total_data_files = total_data_files;
168177
if (!row_filter) {
169178
selected->entries.reserve(entry_count);
170179
}

‎src/paimon/core/manifest/snapshot_live_manifest_entries.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ class SnapshotLiveManifestEntries {
4545
struct Selection {
4646
std::vector<ManifestEntry> entries;
4747
uint64_t candidate_entries = 0;
48+
int64_t total_data_files = 0;
4849
};
4950

5051
explicit SnapshotLiveManifestEntries(int32_t max_snapshots);
5152

52-
void Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries);
53+
void Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries, int64_t total_data_files);
5354
size_t Size() const;
5455

5556
Result<std::shared_ptr<Bytes>> Serialize(const std::shared_ptr<MemoryPool>& pool) const;
@@ -59,7 +60,7 @@ class SnapshotLiveManifestEntries {
5960

6061
/// Select an exact retained snapshot, applying a row filter before constructing file metadata.
6162
/// The reader validates the entry version before invoking the filter, as in
62-
/// ManifestFile::ReadSelectedEntries. The serialized cache value is unchanged. A Selection is
63+
/// ManifestFile::ReadSelectedEntries. A Selection is
6364
/// deliberately not a cache value: query-filtered entries must never replace the complete
6465
/// bucket's cached entries.
6566
static Result<std::optional<Selection>> ReadSnapshot(
@@ -71,6 +72,7 @@ class SnapshotLiveManifestEntries {
7172
void EvictIfNeeded();
7273

7374
std::map<int64_t, std::shared_ptr<const std::vector<ManifestEntry>>> entries_by_snapshot_;
75+
std::map<int64_t, int64_t> total_data_files_by_snapshot_;
7476
int32_t max_snapshots_;
7577
};
7678

‎src/paimon/core/operation/append_only_file_store_scan_test.cpp‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,9 @@ TEST(AppendOnlyFileStoreScanTest, TestDropStatsAfterFiltering) {
815815
TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCachePath) {
816816
TimezoneGuard guard("Asia/Shanghai");
817817
std::string table_path = paimon::test::GetDataDir() + "/orc/append_09.db/append_09/";
818-
auto cache = std::make_shared<LruCache>(/*max_weight=*/16 * 1024 * 1024);
818+
auto cache = std::make_shared<CountingRoutingCache>(
819+
std::map<CacheKind, int64_t>{{CacheKind::MANIFEST, 16 * 1024 * 1024},
820+
{CacheKind::SNAPSHOT_LIVE_MANIFEST, 16 * 1024 * 1024}});
819821

820822
// First scan on snapshot 5: cache miss, entries rebuilt from all manifests.
821823
auto scan_first = BuildScan(table_path, cache, /*bucket=*/0);
@@ -839,6 +841,11 @@ TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCachePath) {
839841
ASSERT_EQ(first_cache_hit, 0);
840842
ASSERT_EQ(first_cache_misses, 1);
841843

844+
const int64_t manifest_gets = cache->GetCount(CacheKind::MANIFEST);
845+
ASSERT_GT(manifest_gets, 0);
846+
ASSERT_OK_AND_ASSIGN(uint64_t first_skipped,
847+
first_metrics->GetCounter(ScanMetrics::LAST_SCAN_SKIPPED_TABLE_FILES));
848+
842849
// Second scan on the same snapshot should read the same bucket live entries from cache.
843850
auto scan_second = BuildScan(table_path, cache, /*bucket=*/0);
844851
scan_second->WithSnapshot(snapshot_5);
@@ -855,9 +862,36 @@ TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCachePath) {
855862
uint64_t materialized_rows,
856863
second_metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS));
857864
ASSERT_EQ(second_cache_hit, 1);
865+
ASSERT_EQ(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
866+
ASSERT_OK_AND_ASSIGN(uint64_t second_scanned,
867+
second_metrics->GetCounter(ScanMetrics::LAST_SCANNED_MANIFESTS));
868+
ASSERT_OK_AND_ASSIGN(uint64_t second_skipped,
869+
second_metrics->GetCounter(ScanMetrics::LAST_SCAN_SKIPPED_TABLE_FILES));
870+
ASSERT_EQ(second_scanned, 0);
871+
ASSERT_EQ(second_skipped, first_skipped);
858872
ASSERT_EQ(second_cache_hits, 1);
859873
ASSERT_GE(scanned_rows, materialized_rows);
860874
ASSERT_OK(second_metrics->GetHistogramStats(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION));
875+
876+
// A different predicate must still filter the cached, unfiltered bucket entries.
877+
std::shared_ptr<Predicate> predicate =
878+
PredicateBuilder::Equal(0, "f0", FieldType::STRING, Literal(FieldType::STRING, "David", 5));
879+
auto filtered = BuildScan(table_path, cache, /*bucket=*/0, predicate);
880+
filtered->WithSnapshot(snapshot_5);
881+
ASSERT_OK_AND_ASSIGN(auto filtered_plan, filtered->CreatePlan());
882+
auto expected = BuildScan(table_path, nullptr, /*bucket=*/0, predicate);
883+
expected->WithSnapshot(snapshot_5);
884+
ASSERT_OK_AND_ASSIGN(auto expected_plan, expected->CreatePlan());
885+
ASSERT_EQ(SortedFileNames(filtered_plan->Files()), SortedFileNames(expected_plan->Files()));
886+
ASSERT_EQ(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
887+
888+
// Eviction must rebuild from source; an exact-hit shortcut cannot survive invalidation.
889+
cache->InvalidateAll();
890+
auto rebuilt = BuildScan(table_path, cache, /*bucket=*/0);
891+
rebuilt->WithSnapshot(snapshot_5);
892+
ASSERT_OK_AND_ASSIGN(auto rebuilt_plan, rebuilt->CreatePlan());
893+
ASSERT_EQ(first_file_names, SortedFileNames(rebuilt_plan->Files()));
894+
ASSERT_GT(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
861895
}
862896

863897
TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCacheRebuildOnMiss) {
@@ -998,9 +1032,10 @@ TEST(AppendOnlyFileStoreScanTest, SnapshotCacheRebuildsCorruptValues) {
9981032
segment.PutValue<int32_t>(offset, -1);
9991033
}
10001034
MemorySegmentOutputStream out(MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool);
1001-
out.WriteValue<int32_t>(corruption == "magic" ? 0 : 0x534d4543);
1035+
out.WriteValue<int32_t>(corruption == "magic" ? 0 : 0x534d4544);
10021036
out.WriteValue<int32_t>(1);
10031037
out.WriteValue<int64_t>(snapshot.Id());
1038+
out.WriteValue<int64_t>(1);
10041039
out.WriteValue<int32_t>(1);
10051040
ASSERT_OK(row_serializer.Serialize(row, &out));
10061041
ASSERT_EQ(out.Segments().size(), 1);
@@ -1078,7 +1113,11 @@ TEST(AppendOnlyFileStoreScanTest, PartitionPruningAcrossCacheModes) {
10781113
scan->partition_filter_ = counting;
10791114
ASSERT_OK_AND_ASSIGN(auto plan, scan->CreatePlan());
10801115
ASSERT_EQ(counting->row_calls_.load(), 2);
1081-
ASSERT_GT(counting->stats_calls_.load(), 0);
1116+
if (mode == 2 && scans > 0) {
1117+
ASSERT_EQ(counting->stats_calls_.load(), 0);
1118+
} else {
1119+
ASSERT_GT(counting->stats_calls_.load(), 0);
1120+
}
10821121
ASSERT_EQ(plan->Files().size(), partition == 99 ? 0 : 1);
10831122
if (partition != 99) {
10841123
ASSERT_EQ(plan->Files()[0].Partition().GetInt(0), partition);
@@ -1156,7 +1195,7 @@ TEST(AppendOnlyFileStoreScanTest, PartitionPruningSkipsExcludedMetadataInCreateP
11561195
if (mode == 2) {
11571196
// Seed the complete snapshot directly: rebuilding must decode every partition.
11581197
SnapshotLiveManifestEntries entries(8);
1159-
entries.Put(1, {kept, excluded});
1198+
entries.Put(1, {kept, excluded}, /*total_data_files=*/2);
11601199
ASSERT_OK(scan->StoreSnapshotLiveManifestEntries(0, entries));
11611200
}
11621201
for (int32_t attempt = 0; attempt < 2; ++attempt) {

0 commit comments

Comments
 (0)