This is an automated email from the ASF dual-hosted git repository.

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 3cc1410a perf(scan): skip manifest lists on exact snapshot cache hits 
(#374)
3cc1410a is described below

commit 3cc1410aa43049d78cdb63c8c5ace483028d211d
Author: wangyong9999 <[email protected]>
AuthorDate: Mon Oct 26 18:27:04 2026 +0800

    perf(scan): skip manifest lists on exact snapshot cache hits (#374)
---
 docs/source/user_guide/manifest_entry_cache.rst    |  7 +-
 .../manifest/snapshot_live_manifest_entries.cpp    | 13 ++-
 .../core/manifest/snapshot_live_manifest_entries.h |  6 +-
 .../operation/append_only_file_store_scan_test.cpp | 47 ++++++++++-
 src/paimon/core/operation/file_store_scan.cpp      | 98 +++++++++++++---------
 src/paimon/core/operation/file_store_scan.h        | 20 +++--
 src/paimon/core/operation/file_store_scan_test.cpp | 24 +++---
 7 files changed, 147 insertions(+), 68 deletions(-)

diff --git a/docs/source/user_guide/manifest_entry_cache.rst 
b/docs/source/user_guide/manifest_entry_cache.rst
index 0f37138c..995d77e3 100644
--- a/docs/source/user_guide/manifest_entry_cache.rst
+++ b/docs/source/user_guide/manifest_entry_cache.rst
@@ -28,8 +28,8 @@ scans that target the same bucket.
 
 The cache stores decoded and merged live manifest entries by table path, 
branch,
 and bucket for ``ScanMode::ALL``. Each cache value can retain several snapshot
-results for that bucket. Exact snapshot hits are served from the cache; cache
-misses rebuild the target snapshot bucket from the target snapshot's data
+results for that bucket. Exact snapshot hits are served from the cache without 
reading or decoding the
+manifest lists again; cache misses rebuild the target snapshot bucket from the 
target snapshot's data
 manifests and store the rebuilt live entries.
 
 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:
 
 - ``lastScannedManifests``: how many manifest files were loaded during this
   scan before manifest entry decoding.
+
+On an exact cache hit, ``lastScannedManifests`` is zero. The cached table file
+count preserves the skipped-file metric even when manifest lists are bypassed.
diff --git a/src/paimon/core/manifest/snapshot_live_manifest_entries.cpp 
b/src/paimon/core/manifest/snapshot_live_manifest_entries.cpp
index c6d981e1..6f925ba1 100644
--- a/src/paimon/core/manifest/snapshot_live_manifest_entries.cpp
+++ b/src/paimon/core/manifest/snapshot_live_manifest_entries.cpp
@@ -34,7 +34,7 @@
 namespace paimon {
 namespace {
 
-constexpr int32_t kMagic = 0x534d4543;  // SMEC
+constexpr int32_t kMagic = 0x534d4544;  // SMED: includes per-snapshot total 
file counts
 
 size_t NormalizeMaxSnapshots(int32_t max_snapshots) {
     return static_cast<size_t>(std::max(0, max_snapshots));
@@ -61,12 +61,14 @@ std::shared_ptr<Bytes> ToBytes(const 
MemorySegmentOutputStream& out,
 SnapshotLiveManifestEntries::SnapshotLiveManifestEntries(int32_t max_snapshots)
     : max_snapshots_(max_snapshots) {}
 
-void SnapshotLiveManifestEntries::Put(int64_t snapshot_id, 
std::vector<ManifestEntry>&& entries) {
+void SnapshotLiveManifestEntries::Put(int64_t snapshot_id, 
std::vector<ManifestEntry>&& entries,
+                                      int64_t total_data_files) {
     if (NormalizeMaxSnapshots(max_snapshots_) == 0) {
         return;
     }
     entries_by_snapshot_[snapshot_id] =
         std::make_shared<const std::vector<ManifestEntry>>(std::move(entries));
+    total_data_files_by_snapshot_[snapshot_id] = total_data_files;
     EvictIfNeeded();
 }
 
@@ -83,6 +85,7 @@ Result<std::shared_ptr<Bytes>> 
SnapshotLiveManifestEntries::Serialize(
     ManifestEntrySerializer serializer(pool);
     for (const auto& [snapshot_id, entries] : entries_by_snapshot_) {
         out.WriteValue<int64_t>(snapshot_id);
+        out.WriteValue<int64_t>(total_data_files_by_snapshot_.at(snapshot_id));
         PAIMON_RETURN_NOT_OK(serializer.SerializeList(*entries, &out));
     }
     return ToBytes(out, pool);
@@ -111,7 +114,10 @@ Result<SnapshotLiveManifestEntries> 
SnapshotLiveManifestEntries::Deserialize(
     ManifestEntrySerializer serializer(pool);
     for (int32_t i = 0; i < snapshot_count; i++) {
         PAIMON_ASSIGN_OR_RAISE(int64_t snapshot_id, in.ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(int64_t total_data_files, 
in.ReadValue<int64_t>());
         PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestEntry> entries, 
serializer.DeserializeList(&in));
+        
snapshot_live_manifest_entries.total_data_files_by_snapshot_[snapshot_id] =
+            total_data_files;
         snapshot_live_manifest_entries.entries_by_snapshot_[snapshot_id] =
             std::make_shared<const 
std::vector<ManifestEntry>>(std::move(entries));
     }
@@ -122,6 +128,7 @@ Result<SnapshotLiveManifestEntries> 
SnapshotLiveManifestEntries::Deserialize(
 void SnapshotLiveManifestEntries::EvictIfNeeded() {
     size_t max_snapshots = NormalizeMaxSnapshots(max_snapshots_);
     while (entries_by_snapshot_.size() > max_snapshots) {
+        
total_data_files_by_snapshot_.erase(entries_by_snapshot_.begin()->first);
         entries_by_snapshot_.erase(entries_by_snapshot_.begin());
     }
 }
@@ -151,6 +158,7 @@ SnapshotLiveManifestEntries::ReadSnapshot(
     std::optional<int64_t> previous_id;
     for (int32_t i = 0; i < snapshot_count; ++i) {
         PAIMON_ASSIGN_OR_RAISE(int64_t id, in.ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(int64_t total_data_files, 
in.ReadValue<int64_t>());
         // Serialize emits snapshots in ascending order. Validate this before 
using the position
         // to honor a retention limit smaller than the one used when the cache 
was populated.
         if (previous_id && id <= previous_id.value()) {
@@ -165,6 +173,7 @@ SnapshotLiveManifestEntries::ReadSnapshot(
         if (keep) {
             selected.emplace();
             selected->candidate_entries = entry_count;
+            selected->total_data_files = total_data_files;
             if (!row_filter) {
                 selected->entries.reserve(entry_count);
             }
diff --git a/src/paimon/core/manifest/snapshot_live_manifest_entries.h 
b/src/paimon/core/manifest/snapshot_live_manifest_entries.h
index 784d31b2..f2d4343a 100644
--- a/src/paimon/core/manifest/snapshot_live_manifest_entries.h
+++ b/src/paimon/core/manifest/snapshot_live_manifest_entries.h
@@ -45,11 +45,12 @@ class SnapshotLiveManifestEntries {
     struct Selection {
         std::vector<ManifestEntry> entries;
         uint64_t candidate_entries = 0;
+        int64_t total_data_files = 0;
     };
 
     explicit SnapshotLiveManifestEntries(int32_t max_snapshots);
 
-    void Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries);
+    void Put(int64_t snapshot_id, std::vector<ManifestEntry>&& entries, 
int64_t total_data_files);
     size_t Size() const;
 
     Result<std::shared_ptr<Bytes>> Serialize(const 
std::shared_ptr<MemoryPool>& pool) const;
@@ -59,7 +60,7 @@ class SnapshotLiveManifestEntries {
 
     /// Select an exact retained snapshot, applying a row filter before 
constructing file metadata.
     /// The reader validates the entry version before invoking the filter, as 
in
-    /// ManifestFile::ReadSelectedEntries. The serialized cache value is 
unchanged. A Selection is
+    /// ManifestFile::ReadSelectedEntries. A Selection is
     /// deliberately not a cache value: query-filtered entries must never 
replace the complete
     /// bucket's cached entries.
     static Result<std::optional<Selection>> ReadSnapshot(
@@ -71,6 +72,7 @@ class SnapshotLiveManifestEntries {
     void EvictIfNeeded();
 
     std::map<int64_t, std::shared_ptr<const std::vector<ManifestEntry>>> 
entries_by_snapshot_;
+    std::map<int64_t, int64_t> total_data_files_by_snapshot_;
     int32_t max_snapshots_;
 };
 
diff --git a/src/paimon/core/operation/append_only_file_store_scan_test.cpp 
b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
index 29d19d7f..b8dfbfce 100644
--- a/src/paimon/core/operation/append_only_file_store_scan_test.cpp
+++ b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
@@ -815,7 +815,9 @@ TEST(AppendOnlyFileStoreScanTest, 
TestDropStatsAfterFiltering) {
 TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCachePath) {
     TimezoneGuard guard("Asia/Shanghai");
     std::string table_path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09/";
-    auto cache = std::make_shared<LruCache>(/*max_weight=*/16 * 1024 * 1024);
+    auto cache = std::make_shared<CountingRoutingCache>(
+        std::map<CacheKind, int64_t>{{CacheKind::MANIFEST, 16 * 1024 * 1024},
+                                     {CacheKind::SNAPSHOT_LIVE_MANIFEST, 16 * 
1024 * 1024}});
 
     // First scan on snapshot 5: cache miss, entries rebuilt from all 
manifests.
     auto scan_first = BuildScan(table_path, cache, /*bucket=*/0);
@@ -839,6 +841,11 @@ TEST(AppendOnlyFileStoreScanTest, 
TestSnapshotLiveManifestCachePath) {
     ASSERT_EQ(first_cache_hit, 0);
     ASSERT_EQ(first_cache_misses, 1);
 
+    const int64_t manifest_gets = cache->GetCount(CacheKind::MANIFEST);
+    ASSERT_GT(manifest_gets, 0);
+    ASSERT_OK_AND_ASSIGN(uint64_t first_skipped,
+                         
first_metrics->GetCounter(ScanMetrics::LAST_SCAN_SKIPPED_TABLE_FILES));
+
     // Second scan on the same snapshot should read the same bucket live 
entries from cache.
     auto scan_second = BuildScan(table_path, cache, /*bucket=*/0);
     scan_second->WithSnapshot(snapshot_5);
@@ -855,9 +862,36 @@ TEST(AppendOnlyFileStoreScanTest, 
TestSnapshotLiveManifestCachePath) {
         uint64_t materialized_rows,
         
second_metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS));
     ASSERT_EQ(second_cache_hit, 1);
+    ASSERT_EQ(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
+    ASSERT_OK_AND_ASSIGN(uint64_t second_scanned,
+                         
second_metrics->GetCounter(ScanMetrics::LAST_SCANNED_MANIFESTS));
+    ASSERT_OK_AND_ASSIGN(uint64_t second_skipped,
+                         
second_metrics->GetCounter(ScanMetrics::LAST_SCAN_SKIPPED_TABLE_FILES));
+    ASSERT_EQ(second_scanned, 0);
+    ASSERT_EQ(second_skipped, first_skipped);
     ASSERT_EQ(second_cache_hits, 1);
     ASSERT_GE(scanned_rows, materialized_rows);
     
ASSERT_OK(second_metrics->GetHistogramStats(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION));
+
+    // A different predicate must still filter the cached, unfiltered bucket 
entries.
+    std::shared_ptr<Predicate> predicate =
+        PredicateBuilder::Equal(0, "f0", FieldType::STRING, 
Literal(FieldType::STRING, "David", 5));
+    auto filtered = BuildScan(table_path, cache, /*bucket=*/0, predicate);
+    filtered->WithSnapshot(snapshot_5);
+    ASSERT_OK_AND_ASSIGN(auto filtered_plan, filtered->CreatePlan());
+    auto expected = BuildScan(table_path, nullptr, /*bucket=*/0, predicate);
+    expected->WithSnapshot(snapshot_5);
+    ASSERT_OK_AND_ASSIGN(auto expected_plan, expected->CreatePlan());
+    ASSERT_EQ(SortedFileNames(filtered_plan->Files()), 
SortedFileNames(expected_plan->Files()));
+    ASSERT_EQ(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
+
+    // Eviction must rebuild from source; an exact-hit shortcut cannot survive 
invalidation.
+    cache->InvalidateAll();
+    auto rebuilt = BuildScan(table_path, cache, /*bucket=*/0);
+    rebuilt->WithSnapshot(snapshot_5);
+    ASSERT_OK_AND_ASSIGN(auto rebuilt_plan, rebuilt->CreatePlan());
+    ASSERT_EQ(first_file_names, SortedFileNames(rebuilt_plan->Files()));
+    ASSERT_GT(cache->GetCount(CacheKind::MANIFEST), manifest_gets);
 }
 
 TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCacheRebuildOnMiss) {
@@ -998,9 +1032,10 @@ TEST(AppendOnlyFileStoreScanTest, 
SnapshotCacheRebuildsCorruptValues) {
             segment.PutValue<int32_t>(offset, -1);
         }
         MemorySegmentOutputStream 
out(MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool);
-        out.WriteValue<int32_t>(corruption == "magic" ? 0 : 0x534d4543);
+        out.WriteValue<int32_t>(corruption == "magic" ? 0 : 0x534d4544);
         out.WriteValue<int32_t>(1);
         out.WriteValue<int64_t>(snapshot.Id());
+        out.WriteValue<int64_t>(1);
         out.WriteValue<int32_t>(1);
         ASSERT_OK(row_serializer.Serialize(row, &out));
         ASSERT_EQ(out.Segments().size(), 1);
@@ -1078,7 +1113,11 @@ TEST(AppendOnlyFileStoreScanTest, 
PartitionPruningAcrossCacheModes) {
                 scan->partition_filter_ = counting;
                 ASSERT_OK_AND_ASSIGN(auto plan, scan->CreatePlan());
                 ASSERT_EQ(counting->row_calls_.load(), 2);
-                ASSERT_GT(counting->stats_calls_.load(), 0);
+                if (mode == 2 && scans > 0) {
+                    ASSERT_EQ(counting->stats_calls_.load(), 0);
+                } else {
+                    ASSERT_GT(counting->stats_calls_.load(), 0);
+                }
                 ASSERT_EQ(plan->Files().size(), partition == 99 ? 0 : 1);
                 if (partition != 99) {
                     ASSERT_EQ(plan->Files()[0].Partition().GetInt(0), 
partition);
@@ -1156,7 +1195,7 @@ TEST(AppendOnlyFileStoreScanTest, 
PartitionPruningSkipsExcludedMetadataInCreateP
         if (mode == 2) {
             // Seed the complete snapshot directly: rebuilding must decode 
every partition.
             SnapshotLiveManifestEntries entries(8);
-            entries.Put(1, {kept, excluded});
+            entries.Put(1, {kept, excluded}, /*total_data_files=*/2);
             ASSERT_OK(scan->StoreSnapshotLiveManifestEntries(0, entries));
         }
         for (int32_t attempt = 0; attempt < 2; ++attempt) {
diff --git a/src/paimon/core/operation/file_store_scan.cpp 
b/src/paimon/core/operation/file_store_scan.cpp
index 4e508bab..ca2de308 100644
--- a/src/paimon/core/operation/file_store_scan.cpp
+++ b/src/paimon/core/operation/file_store_scan.cpp
@@ -158,8 +158,8 @@ Result<std::vector<PartitionEntry>> 
FileStoreScan::ReadPartitionEntries() const
     PAIMON_RETURN_NOT_OK(
         ReadManifests(&snapshot, &all_manifest_file_metas, 
&filtered_manifest_file_metas));
     std::vector<ManifestEntry> manifest_entries;
-    PAIMON_RETURN_NOT_OK(ReadFileEntries(filtered_manifest_file_metas, 
&manifest_entries,
-                                         /*apply_scan_filter=*/true));
+    PAIMON_RETURN_NOT_OK(ReadFileEntries(filtered_manifest_file_metas, 
/*apply_scan_filter=*/true,
+                                         &manifest_entries));
     std::unordered_map<BinaryRow, PartitionEntry> partitions;
     PAIMON_RETURN_NOT_OK(PartitionEntry::Merge(manifest_entries, &partitions));
 
@@ -178,11 +178,10 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
     Duration manifest_read_duration;
     metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_LOAD_DURATION, 0);
     metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_STORE_DURATION, 0);
-    std::optional<Snapshot> snapshot;
+    PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> snapshot, ReadSnapshot());
+    int64_t all_data_files = 0;
     std::vector<ManifestFileMeta> all_manifest_file_metas;
     std::vector<ManifestFileMeta> filtered_manifest_file_metas;
-    PAIMON_RETURN_NOT_OK(
-        ReadManifests(&snapshot, &all_manifest_file_metas, 
&filtered_manifest_file_metas));
 
     std::vector<ManifestEntry> manifest_entries;
     std::optional<int32_t> cache_bucket = bucket_filter_;
@@ -197,11 +196,19 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
     uint64_t lazy_decode_scanned_rows = 0;
     bool snapshot_cache_hit = false;
     if (use_snapshot_live_manifest_cache) {
-        PAIMON_ASSIGN_OR_RAISE(lazy_decode_scanned_rows,
-                               ReadManifestEntriesWithCache(
-                                   snapshot.value(), all_manifest_file_metas, 
cache_bucket.value(),
-                                   &manifest_entries, &snapshot_cache_hit));
+        PAIMON_ASSIGN_OR_RAISE(
+            lazy_decode_scanned_rows,
+            ReadManifestEntriesWithCache(snapshot.value(), 
cache_bucket.value(),
+                                         &all_manifest_file_metas, 
&filtered_manifest_file_metas,
+                                         &manifest_entries, 
&snapshot_cache_hit, &all_data_files));
     } else {
+        PAIMON_RETURN_NOT_OK(
+            ReadManifestLists(snapshot, &all_manifest_file_metas, 
&filtered_manifest_file_metas));
+        all_data_files =
+            std::accumulate(all_manifest_file_metas.begin(), 
all_manifest_file_metas.end(),
+                            int64_t{0}, [](int64_t sum, const 
ManifestFileMeta& meta) {
+                                return sum + meta.NumAddedFiles() - 
meta.NumDeletedFiles();
+                            });
         lazy_decode_scanned_rows = std::accumulate(
             filtered_manifest_file_metas.begin(), 
filtered_manifest_file_metas.end(), uint64_t{0},
             [](uint64_t sum, const ManifestFileMeta& meta) {
@@ -240,11 +247,6 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
             entry = entry.CopyWithoutStats();
         }
     }
-    const int64_t all_data_files = std::accumulate(
-        all_manifest_file_metas.begin(), all_manifest_file_metas.end(), 
int64_t{0},
-        [](const int64_t sum, const ManifestFileMeta& manifest_file_meta) {
-            return sum + manifest_file_meta.NumAddedFiles() - 
manifest_file_meta.NumDeletedFiles();
-        });
     const uint64_t scan_duration_ms = duration.Get();
     metrics_->SetCounter(ScanMetrics::LAST_SCAN_DURATION, scan_duration_ms);
     metrics_->ObserveHistogram(ScanMetrics::SCAN_DURATION, 
static_cast<double>(scan_duration_ms));
@@ -276,27 +278,31 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
                                                     
std::move(manifest_entries));
 }
 
-Status FileStoreScan::ReadManifests(std::optional<Snapshot>* snapshot_ptr,
-                                    std::vector<ManifestFileMeta>* 
all_manifests_ptr,
-                                    std::vector<ManifestFileMeta>* 
filter_manifests_ptr) const {
-    auto& snapshot = *snapshot_ptr;
-    auto& all_manifests = *all_manifests_ptr;
-    auto& filtered_manifests = *filter_manifests_ptr;
-    if (specified_snapshot_ != std::nullopt) {
-        snapshot = specified_snapshot_;
-    } else {
-        PAIMON_ASSIGN_OR_RAISE(snapshot, snapshot_manager_->LatestSnapshot());
+Result<std::optional<Snapshot>> FileStoreScan::ReadSnapshot() const {
+    if (specified_snapshot_) {
+        return specified_snapshot_;
     }
-    if (snapshot == std::nullopt) {
-        all_manifests = std::vector<ManifestFileMeta>();
-        filtered_manifests = std::vector<ManifestFileMeta>();
+    return snapshot_manager_->LatestSnapshot();
+}
+
+Status FileStoreScan::ReadManifests(std::optional<Snapshot>* snapshot,
+                                    std::vector<ManifestFileMeta>* 
all_manifests,
+                                    std::vector<ManifestFileMeta>* 
filtered_manifests) const {
+    PAIMON_ASSIGN_OR_RAISE(*snapshot, ReadSnapshot());
+    return ReadManifestLists(*snapshot, all_manifests, filtered_manifests);
+}
+
+Status FileStoreScan::ReadManifestLists(const std::optional<Snapshot>& 
snapshot,
+                                        std::vector<ManifestFileMeta>* 
all_manifests,
+                                        std::vector<ManifestFileMeta>* 
filtered_manifests) const {
+    if (!snapshot) {
         return Status::OK();
     }
-    PAIMON_RETURN_NOT_OK(ReadManifestsWithSnapshot(snapshot.value(), 
&all_manifests));
-    for (const auto& meta : all_manifests) {
-        PAIMON_ASSIGN_OR_RAISE(bool filter_meta_result, 
FilterManifestFileMeta(meta));
-        if (filter_meta_result) {
-            filtered_manifests.push_back(meta);
+    PAIMON_RETURN_NOT_OK(ReadManifestsWithSnapshot(snapshot.value(), 
all_manifests));
+    for (const auto& meta : *all_manifests) {
+        PAIMON_ASSIGN_OR_RAISE(bool keep, FilterManifestFileMeta(meta));
+        if (keep) {
+            filtered_manifests->push_back(meta);
         }
     }
     return Status::OK();
@@ -343,8 +349,8 @@ Status FileStoreScan::ReadManifestsWithSnapshot(const 
Snapshot& snapshot,
 }
 
 Status FileStoreScan::ReadFileEntries(const std::vector<ManifestFileMeta>& 
manifest_metas,
-                                      std::vector<ManifestEntry>* 
manifest_entries,
-                                      bool apply_scan_filter) const {
+                                      bool apply_scan_filter,
+                                      std::vector<ManifestEntry>* 
manifest_entries) const {
     std::vector<std::future<Result<std::vector<ManifestEntry>>>> futures;
     for (const auto& meta : manifest_metas) {
         auto read_meta_task = [this, meta,
@@ -389,8 +395,9 @@ Status FileStoreScan::ReadManifestEntries(const 
std::vector<ManifestFileMeta>& m
 // can be returned directly; cache misses rebuild the target snapshot bucket 
from the target
 // snapshot's data manifests.
 Result<uint64_t> FileStoreScan::ReadManifestEntriesWithCache(
-    const Snapshot& snapshot, const std::vector<ManifestFileMeta>& 
all_manifest_metas,
-    int32_t bucket, std::vector<ManifestEntry>* manifest_entries, bool* 
cache_hit) const {
+    const Snapshot& snapshot, int32_t bucket, std::vector<ManifestFileMeta>* 
all_manifest_metas,
+    std::vector<ManifestFileMeta>* filtered_manifest_metas,
+    std::vector<ManifestEntry>* manifest_entries, bool* cache_hit, int64_t* 
all_data_files) const {
     Duration cache_load_duration;
     const int32_t max_snapshots = 
core_options_.GetScanManifestEntryCacheMaxSnapshots();
     SnapshotLiveManifestEntries cached_entries(max_snapshots);
@@ -413,6 +420,7 @@ Result<uint64_t> 
FileStoreScan::ReadManifestEntriesWithCache(
         }
         if (selected.ok() && selected.value()) {
             *cache_hit = true;
+            *all_data_files = selected.value()->total_data_files;
             partition_filtered = static_cast<bool>(row_filter);
             candidate_entries = selected.value()->candidate_entries;
             *manifest_entries = std::move(selected.value()->entries);
@@ -432,8 +440,16 @@ Result<uint64_t> 
FileStoreScan::ReadManifestEntriesWithCache(
     metrics_->ObserveHistogram(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION,
                                static_cast<double>(cache_load_duration_ms));
     if (!*cache_hit) {
+        // The selected snapshot is unchanged. Exact hits skip both lists and 
their decoding.
+        PAIMON_RETURN_NOT_OK(
+            ReadManifestLists(snapshot, all_manifest_metas, 
filtered_manifest_metas));
+        *all_data_files =
+            std::accumulate(all_manifest_metas->begin(), 
all_manifest_metas->end(), int64_t{0},
+                            [](int64_t sum, const ManifestFileMeta& meta) {
+                                return sum + meta.NumAddedFiles() - 
meta.NumDeletedFiles();
+                            });
         std::vector<ManifestFileMeta> bucket_manifest_metas;
-        for (const auto& meta : all_manifest_metas) {
+        for (const auto& meta : *all_manifest_metas) {
             if ((!bucket_filter_ && bucket_selector_) || 
MayContainBucket(meta, bucket)) {
                 bucket_manifest_metas.push_back(meta);
             }
@@ -442,7 +458,7 @@ Result<uint64_t> 
FileStoreScan::ReadManifestEntriesWithCache(
             ReadAndMergeBucketFileEntries(bucket_manifest_metas, bucket, 
manifest_entries));
         candidate_entries = manifest_entries->size();
         std::vector<ManifestEntry> cache_entries = *manifest_entries;
-        cached_entries.Put(snapshot.Id(), std::move(cache_entries));
+        cached_entries.Put(snapshot.Id(), std::move(cache_entries), 
*all_data_files);
         Duration cache_store_duration;
         PAIMON_RETURN_NOT_OK(StoreSnapshotLiveManifestEntries(bucket, 
cached_entries));
         const uint64_t cache_store_duration_ms = cache_store_duration.Get();
@@ -534,7 +550,7 @@ Status FileStoreScan::ReadAndMergeBucketFileEntries(
 
     std::vector<ManifestEntry> unmerged_entries;
     std::vector<ManifestEntry> entries;
-    PAIMON_RETURN_NOT_OK(ReadFileEntries(manifest_metas, &entries, 
/*apply_scan_filter=*/false));
+    PAIMON_RETURN_NOT_OK(ReadFileEntries(manifest_metas, 
/*apply_scan_filter=*/false, &entries));
     unmerged_entries.reserve(entries.size());
     for (auto& entry : entries) {
         if (entry.Bucket() == bucket ||
@@ -566,14 +582,14 @@ Status FileStoreScan::ReadAndMergeFileEntries(const 
std::vector<ManifestFileMeta
                                               std::vector<ManifestEntry>* 
merged_entries) const {
     std::vector<ManifestEntry> unmerged_entries;
     PAIMON_RETURN_NOT_OK(
-        ReadFileEntries(manifest_metas, &unmerged_entries, 
/*apply_scan_filter=*/true));
+        ReadFileEntries(manifest_metas, /*apply_scan_filter=*/true, 
&unmerged_entries));
     return MergeLiveEntries(unmerged_entries, merged_entries);
 }
 
 Status FileStoreScan::ReadAndNoMergeFileEntries(
     const std::vector<ManifestFileMeta>& manifest_metas,
     std::vector<ManifestEntry>* manifest_entries) const {
-    return ReadFileEntries(manifest_metas, manifest_entries, 
/*apply_scan_filter=*/true);
+    return ReadFileEntries(manifest_metas, /*apply_scan_filter=*/true, 
manifest_entries);
 }
 
 bool FileStoreScan::MayContainBucket(const ManifestFileMeta& manifest, int32_t 
bucket) const {
diff --git a/src/paimon/core/operation/file_store_scan.h 
b/src/paimon/core/operation/file_store_scan.h
index 25911d0e..df8c6d61 100644
--- a/src/paimon/core/operation/file_store_scan.h
+++ b/src/paimon/core/operation/file_store_scan.h
@@ -276,9 +276,13 @@ class FileStoreScan {
                                const std::shared_ptr<TableSchema>& 
data_schema) const;
 
  private:
-    Status ReadManifests(std::optional<Snapshot>* snapshot_ptr,
-                         std::vector<ManifestFileMeta>* all_manifests_ptr,
-                         std::vector<ManifestFileMeta>* 
filtered_manifests_ptr) const;
+    Status ReadManifests(std::optional<Snapshot>* snapshot,
+                         std::vector<ManifestFileMeta>* all_manifests,
+                         std::vector<ManifestFileMeta>* filtered_manifests) 
const;
+    Result<std::optional<Snapshot>> ReadSnapshot() const;
+    Status ReadManifestLists(const std::optional<Snapshot>& snapshot,
+                             std::vector<ManifestFileMeta>* all_manifests,
+                             std::vector<ManifestFileMeta>* 
filtered_manifests) const;
 
     Status ReadManifestsWithSnapshot(const Snapshot& snapshot,
                                      std::vector<ManifestFileMeta>* manifests) 
const;
@@ -289,8 +293,10 @@ class FileStoreScan {
     /// Read request-filtered entries while caching complete bucket snapshots.
     /// @return Number of entries considered before request-specific filtering.
     Result<uint64_t> ReadManifestEntriesWithCache(
-        const Snapshot& snapshot, const std::vector<ManifestFileMeta>& 
bucket_manifest_metas,
-        int32_t bucket, std::vector<ManifestEntry>* manifest_entries, bool* 
cache_hit) const;
+        const Snapshot& snapshot, int32_t bucket, 
std::vector<ManifestFileMeta>* all_manifest_metas,
+        std::vector<ManifestFileMeta>* filtered_manifest_metas,
+        std::vector<ManifestEntry>* manifest_entries, bool* cache_hit,
+        int64_t* all_data_files) const;
     std::shared_ptr<CacheKey> 
CreateSnapshotLiveManifestEntriesCacheKey(int32_t bucket) const;
     Status StoreSnapshotLiveManifestEntries(int32_t bucket,
                                             const SnapshotLiveManifestEntries& 
entries) const;
@@ -306,8 +312,8 @@ class FileStoreScan {
                                      std::vector<ManifestEntry>* 
manifest_entries) const;
 
     Status ReadFileEntries(const std::vector<ManifestFileMeta>& manifest_metas,
-                           std::vector<ManifestEntry>* manifest_entries,
-                           bool apply_scan_filter) const;
+                           bool apply_scan_filter,
+                           std::vector<ManifestEntry>* manifest_entries) const;
 
     bool MayContainBucket(const ManifestFileMeta& manifest, int32_t bucket) 
const;
 
diff --git a/src/paimon/core/operation/file_store_scan_test.cpp 
b/src/paimon/core/operation/file_store_scan_test.cpp
index 5c53f138..eb49f354 100644
--- a/src/paimon/core/operation/file_store_scan_test.cpp
+++ b/src/paimon/core/operation/file_store_scan_test.cpp
@@ -232,8 +232,8 @@ TEST_F(FileStoreScanTest, 
TestSnapshotLiveManifestEntriesSerialization) {
     manifest_entries.emplace_back(FileKind::Add(), BinaryRow::EmptyRow(), 
/*bucket=*/0,
                                   /*total_buckets=*/1, file1);
     SnapshotLiveManifestEntries entries(/*max_snapshots=*/2);
-    entries.Put(/*snapshot_id=*/1, std::move(manifest_entries));
-    entries.Put(/*snapshot_id=*/3, {});
+    entries.Put(/*snapshot_id=*/1, std::move(manifest_entries), 
/*total_data_files=*/1);
+    entries.Put(/*snapshot_id=*/3, {}, /*total_data_files=*/0);
 
     ASSERT_OK_AND_ASSIGN(auto bytes, entries.Serialize(GetDefaultPool()));
     ASSERT_OK_AND_ASSIGN(auto deserialized,
@@ -273,9 +273,9 @@ TEST_F(FileStoreScanTest, SelectExactRetainedSnapshot) {
                              BinaryRowGenerator::GenerateRow({partition}, 
pool.get()), 0, 1, file);
     }
     SnapshotLiveManifestEntries entries(3);
-    entries.Put(1, {records[0]});
-    entries.Put(3, {records[1], records[2]});
-    entries.Put(5, {});
+    entries.Put(1, {records[0]}, 1);
+    entries.Put(3, {records[1], records[2]}, 17);
+    entries.Put(5, {}, 0);
     ASSERT_OK_AND_ASSIGN(auto bytes, entries.Serialize(pool));
     const MemorySegment segment = MemorySegment::Wrap(bytes);
     ASSERT_OK_AND_ASSIGN(auto first,
@@ -300,6 +300,7 @@ TEST_F(FileStoreScanTest, SelectExactRetainedSnapshot) {
                              pool));
     ASSERT_TRUE(selected);
     ASSERT_EQ(selected->candidate_entries, 2);
+    ASSERT_EQ(selected->total_data_files, 17);
     ASSERT_EQ(filter_calls, 2);
     ASSERT_EQ(selected->entries, std::vector<ManifestEntry>({records[2]}));
     ASSERT_OK_AND_ASSIGN(auto absent,
@@ -326,7 +327,7 @@ TEST_F(FileStoreScanTest, SelectExactRetainedSnapshot) {
                             pool),
                         "injected filter error");
 
-    entries.Put(7, {});
+    entries.Put(7, {}, 0);
     ASSERT_EQ(entries.Size(), 3);
     ASSERT_OK_AND_ASSIGN(auto updated, entries.Serialize(pool));
     // Put must evict the oldest snapshot even when the reader permits more 
snapshots.
@@ -336,6 +337,7 @@ TEST_F(FileStoreScanTest, SelectExactRetainedSnapshot) {
     ASSERT_OK_AND_ASSIGN(auto retained, 
SnapshotLiveManifestEntries::ReadSnapshot(
                                             MemorySegment::Wrap(updated), 3, 
4, nullptr, pool));
     ASSERT_TRUE(retained);
+    ASSERT_EQ(retained->total_data_files, 17);
     ASSERT_EQ(retained->entries, std::vector<ManifestEntry>({records[1], 
records[2]}));
 }
 
@@ -349,7 +351,7 @@ TEST_F(FileStoreScanTest, 
SkipDecodingFilteredManifestEntries) {
     ManifestEntry excluded(FileKind(static_cast<int8_t>(2)),
                            BinaryRowGenerator::GenerateRow({10}, pool.get()), 
0, 1, file);
     SnapshotLiveManifestEntries entries(1);
-    entries.Put(1, {excluded});
+    entries.Put(1, {excluded}, 1);
     ASSERT_OK_AND_ASSIGN(auto bytes, entries.Serialize(pool));
     const MemorySegment segment = MemorySegment::Wrap(bytes);
     ASSERT_OK_AND_ASSIGN(auto selected, 
SnapshotLiveManifestEntries::ReadSnapshot(
@@ -373,9 +375,10 @@ TEST_F(FileStoreScanTest, 
RejectUnsupportedSnapshotEntryVersionBeforeFiltering)
     BinaryRow row = BinaryRowGenerator::GenerateRow({3}, pool.get());
     BinaryRowSerializer serializer(1, pool);
     MemorySegmentOutputStream out(64, pool);
-    out.WriteValue<int32_t>(0x534d4543);
+    out.WriteValue<int32_t>(0x534d4544);
     out.WriteValue<int32_t>(1);
     out.WriteValue<int64_t>(1);
+    out.WriteValue<int64_t>(7);
     out.WriteValue<int32_t>(1);
     ASSERT_OK(serializer.Serialize(row, &out));
     bool filter_called = false;
@@ -400,16 +403,17 @@ TEST_F(FileStoreScanTest, 
RejectInvalidSnapshotSelectionHeader) {
         out.WriteValue<int32_t>(magic);
         out.WriteValue<int32_t>(snapshots);
         out.WriteValue<int64_t>(1);
+        out.WriteValue<int64_t>(7);
         out.WriteValue<int32_t>(entries);
         return out.Segments().front();
     };
     auto segment = header(0, 1, 0);
     ASSERT_NOK_WITH_MSG(SnapshotLiveManifestEntries::ReadSnapshot(segment, 1, 
3, nullptr, pool),
                         "invalid snapshot live manifest entries magic");
-    segment = header(0x534d4543, -1, 0);
+    segment = header(0x534d4544, -1, 0);
     ASSERT_NOK_WITH_MSG(SnapshotLiveManifestEntries::ReadSnapshot(segment, 1, 
3, nullptr, pool),
                         "snapshot count is negative");
-    segment = header(0x534d4543, 1, -1);
+    segment = header(0x534d4544, 1, -1);
     ASSERT_NOK_WITH_MSG(SnapshotLiveManifestEntries::ReadSnapshot(segment, 1, 
3, nullptr, pool),
                         "entry count is negative");
     auto truncated = MemorySegment::WrapView(segment.Data(), 7);

Reply via email to