Repository: root-project/root (C++)

Issue:
[ntuple] `UnsealPage()` should be a no-op for the page zero
### Explain what you would like to see improved

A call to `RPageStorage::UnsealPage()` should be a no-op for the page zero, i.e. no additional `memcpy()` and no unpacking required, which is currently prevented by the function prototype.
In particular, it forces us to return a `std::unique_ptr` thus yielding the ownership of the pointee which conflicts with the semantics of the page zero (a single, shared buffer).

This can be solved after merging #12279.

### Share how it could be improved

_No response_

### How to reproduce?

n/a

### ROOT version

master

### How did you install ROOT?

custom build

### Which operating system are you using?

n/a

### Additional context

_No response_



Fix (patch):
diff --git a/tree/ntuple/v7/inc/ROOT/RPageStorage.hxx b/tree/ntuple/v7/inc/ROOT/RPageStorage.hxx
index 99154a5201e41..1b502e41ba01d 100644
--- a/tree/ntuple/v7/inc/ROOT/RPageStorage.hxx
+++ b/tree/ntuple/v7/inc/ROOT/RPageStorage.hxx
@@ -416,8 +416,9 @@ protected:
    /// Helper for unstreaming a page. This is commonly used in derived, concrete page sources.  The implementation
    /// currently always makes a memory copy, even if the sealed page is uncompressed and in the final memory layout.
    /// The optimization of directly mapping pages is left to the concrete page source implementations.
-   /// Usage of this method requires construction of fDecompressor.
-   std::unique_ptr<unsigned char []> UnsealPage(const RSealedPage &sealedPage, const RColumnElementBase &element);
+   /// Usage of this method requires construction of fDecompressor. Memory is allocated via
+   /// `RPageAllocatorHeap`; use `RPageAllocatorHeap::DeletePage()` to deallocate returned pages.
+   RPage UnsealPage(const RSealedPage &sealedPage, const RColumnElementBase &element, DescriptorId_t physicalColumnId);
 
    /// Enables the default set of metrics provided by RPageSource. `prefix` will be used as the prefix for
    /// the counters registered in the internal RNTupleMetrics object.
diff --git a/tree/ntuple/v7/inc/ROOT/RPageStorageDaos.hxx b/tree/ntuple/v7/inc/ROOT/RPageStorageDaos.hxx
index 8e701a230f6d1..3a25425043c79 100644
--- a/tree/ntuple/v7/inc/ROOT/RPageStorageDaos.hxx
+++ b/tree/ntuple/v7/inc/ROOT/RPageStorageDaos.hxx
@@ -183,19 +183,6 @@ public:
    void ReleasePage(RPage &page) final;
 };
 
-// clang-format off
-/**
-\class ROOT::Experimental::Detail::RPageAllocatorDaos
-\ingroup NTuple
-\brief Manages pages read from a DAOS container
-*/
-// clang-format on
-class RPageAllocatorDaos {
-public:
-   static RPage NewPage(ColumnId_t columnId, void *mem, std::size_t elementSize, std::size_t nElements);
-   static void DeletePage(const RPage &page);
-};
-
 // clang-format off
 /**
 \class ROOT::Experimental::Detail::RPageSourceDaos
@@ -217,10 +204,7 @@ private:
 
    ntuple_index_t fNTupleIndex{0};
 
-   /// Populated pages might be shared; the memory buffer is managed by the RPageAllocatorDaos
-   std::unique_ptr<RPageAllocatorDaos> fPageAllocator;
-   // TODO: the page pool should probably be handled by the base class.
-   /// The page pool might, at some point, be used by multiple page sources
+   /// Populated pages might be shared; the page pool might, at some point, be used by multiple page sources
    std::shared_ptr<RPagePool> fPagePool;
    /// The last cluster from which a page got populated.  Points into fClusterPool->fPool
    RCluster *fCurrentCluster = nullptr;
diff --git a/tree/ntuple/v7/inc/ROOT/RPageStorageFile.hxx b/tree/ntuple/v7/inc/ROOT/RPageStorageFile.hxx
index 3285b7b33bfc9..b8774cd0231ec 100644
--- a/tree/ntuple/v7/inc/ROOT/RPageStorageFile.hxx
+++ b/tree/ntuple/v7/inc/ROOT/RPageStorageFile.hxx
@@ -92,21 +92,6 @@ public:
    void ReleasePage(RPage &page) final;
 };
 
-
-// clang-format off
-/**
-\class ROOT::Experimental::Detail::RPageAllocatorFile
-\ingroup NTuple
-\brief Manages pages read from a the file
-*/
-// clang-format on
-class RPageAllocatorFile {
-public:
-   static RPage NewPage(ColumnId_t columnId, void *mem, std::size_t elementSize, std::size_t nElements);
-   static void DeletePage(const RPage& page);
-};
-
-
 // clang-format off
 /**
 \class ROOT::Experimental::Detail::RPageSourceFile
@@ -128,9 +113,7 @@ private:
       std::uint64_t fColumnOffset = 0;
    };
 
-   /// Populated pages might be shared; there memory buffer is managed by the RPageAllocatorFile
-   std::unique_ptr<RPageAllocatorFile> fPageAllocator;
-   /// The page pool might, at some point, be used by multiple page sources
+   /// Populated pages might be shared; the page pool might, at some point, be used by multiple page sources
    std::shared_ptr<RPagePool> fPagePool;
    /// The last cluster from which a page got populated.  Points into fClusterPool->fPool
    RCluster *fCurrentCluster = nullptr;
diff --git a/tree/ntuple/v7/src/RPageAllocator.cxx b/tree/ntuple/v7/src/RPageAllocator.cxx
index 466a948d9716e..b7e7f781119ca 100644
--- a/tree/ntuple/v7/src/RPageAllocator.cxx
+++ b/tree/ntuple/v7/src/RPageAllocator.cxx
@@ -29,5 +29,6 @@ ROOT::Experimental::Detail::RPage ROOT::Experimental::Detail::RPageAllocatorHeap
 
 void ROOT::Experimental::Detail::RPageAllocatorHeap::DeletePage(const RPage& page)
 {
-   delete[] reinterpret_cast<unsigned char *>(page.GetBuffer());
+   if (!page.IsPageZero())
+      delete[] reinterpret_cast<unsigned char *>(page.GetBuffer());
 }
diff --git a/tree/ntuple/v7/src/RPageStorage.cxx b/tree/ntuple/v7/src/RPageStorage.cxx
index 8a6fac831d2a6..579f400b2dcd5 100644
--- a/tree/ntuple/v7/src/RPageStorage.cxx
+++ b/tree/ntuple/v7/src/RPageStorage.cxx
@@ -141,34 +141,39 @@ void ROOT::Experimental::Detail::RPageSource::UnzipCluster(RCluster *cluster)
       UnzipClusterImpl(cluster);
 }
 
-
-std::unique_ptr<unsigned char []> ROOT::Experimental::Detail::RPageSource::UnsealPage(
-   const RSealedPage &sealedPage, const RColumnElementBase &element)
+ROOT::Experimental::Detail::RPage ROOT::Experimental::Detail::RPageSource::UnsealPage(const RSealedPage &sealedPage,
+                                                                                      const RColumnElementBase &element,
+                                                                                      DescriptorId_t physicalColumnId)
 {
-   const auto bytesPacked = element.GetPackedSize(sealedPage.fNElements);
-   const auto pageSize = element.GetSize() * sealedPage.fNElements;
+   // Unsealing a page zero is a no-op.  `RPageRange::ExtendToFitColumnRange()` guarantees that the page zero buffer is
+   // large enough to hold `sealedPage.fNElements`
+   if (sealedPage.fBuffer == RPage::GetPageZeroBuffer()) {
+      auto page = RPage::MakePageZero(physicalColumnId, element.GetSize());
+      page.GrowUnchecked(sealedPage.fNElements);
+      return page;
+   }
 
-   // TODO(jblomer): We might be able to do better memory handling for unsealing pages than a new malloc for every
-   // new page.
-   auto pageBuffer = std::make_unique<unsigned char[]>(bytesPacked);
+   const auto bytesPacked = element.GetPackedSize(sealedPage.fNElements);
+   using Allocator_t = RPageAllocatorHeap;
+   auto page = Allocator_t::NewPage(physicalColumnId, element.GetSize(), sealedPage.fNElements);
    if (sealedPage.fSize != bytesPacked) {
-      fDecompressor->Unzip(sealedPage.fBuffer, sealedPage.fSize, bytesPacked, pageBuffer.get());
+      fDecompressor->Unzip(sealedPage.fBuffer, sealedPage.fSize, bytesPacked, page.GetBuffer());
    } else {
       // We cannot simply map the sealed page as we don't know its life time. Specialized page sources
       // may decide to implement to not use UnsealPage but to custom mapping / decompression code.
       // Note that usually pages are compressed.
-      // TODO(jalopezg): unsealing a page zero should be a no-op (i.e., no additional memcpy), but the current prototype
-      // of `UnsealPage()` prevents that
-      memcpy(pageBuffer.get(), sealedPage.fBuffer, bytesPacked);
+      memcpy(page.GetBuffer(), sealedPage.fBuffer, bytesPacked);
    }
 
    if (!element.IsMappable()) {
-      auto unpackedBuffer = new unsigned char[pageSize];
-      element.Unpack(unpackedBuffer, pageBuffer.get(), sealedPage.fNElements);
-      pageBuffer = std::unique_ptr<unsigned char []>(unpackedBuffer);
+      auto tmp = Allocator_t::NewPage(physicalColumnId, element.GetSize(), sealedPage.fNElements);
+      element.Unpack(tmp.GetBuffer(), page.GetBuffer(), sealedPage.fNElements);
+      Allocator_t::DeletePage(page);
+      page = tmp;
    }
 
-   return pageBuffer;
+   page.GrowUnchecked(sealedPage.fNElements);
+   return page;
 }
 
 void ROOT::Experimental::Detail::RPageSource::EnableDefaultMetrics(const std::string &prefix)
diff --git a/tree/ntuple/v7/src/RPageStorageDaos.cxx b/tree/ntuple/v7/src/RPageStorageDaos.cxx
index e3dd918edfe29..1aad17f84a5f1 100644
--- a/tree/ntuple/v7/src/RPageStorageDaos.cxx
+++ b/tree/ntuple/v7/src/RPageStorageDaos.cxx
@@ -454,28 +454,11 @@ void ROOT::Experimental::Detail::RPageSinkDaos::ReleasePage(RPage &page)
 
 ////////////////////////////////////////////////////////////////////////////////
 
-ROOT::Experimental::Detail::RPage
-ROOT::Experimental::Detail::RPageAllocatorDaos::NewPage(ColumnId_t columnId, void *mem, std::size_t elementSize,
-                                                        std::size_t nElements)
-{
-   RPage newPage(columnId, mem, elementSize, nElements);
-   newPage.GrowUnchecked(nElements);
-   return newPage;
-}
-
-void ROOT::Experimental::Detail::RPageAllocatorDaos::DeletePage(const RPage &page)
-{
-   if (page.IsNull())
-      return;
-   delete[] reinterpret_cast<unsigned char *>(page.GetBuffer());
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
 ROOT::Experimental::Detail::RPageSourceDaos::RPageSourceDaos(std::string_view ntupleName, std::string_view uri,
                                                              const RNTupleReadOptions &options)
-   : RPageSource(ntupleName, options), fPageAllocator(std::make_unique<RPageAllocatorDaos>()),
-     fPagePool(std::make_shared<RPagePool>()), fURI(uri),
+   : RPageSource(ntupleName, options),
+     fPagePool(std::make_shared<RPagePool>()),
+     fURI(uri),
      fClusterPool(std::make_unique<RClusterPool>(*this, options.GetClusterBunchSize()))
 {
    fDecompressor = std::make_unique<RNTupleDecompressor>();
@@ -608,19 +591,18 @@ ROOT::Experimental::Detail::RPageSourceDaos::PopulatePageFromCluster(ColumnHandl
       sealedPageBuffer = onDiskPage->GetAddress();
    }
 
-   std::unique_ptr<unsigned char[]> pageBuffer;
+   RPage newPage;
    {
       RNTupleAtomicTimer timer(fCounters->fTimeWallUnzip, fCounters->fTimeCpuUnzip);
-      pageBuffer = UnsealPage({sealedPageBuffer, bytesOnStorage, pageInfo.fNElements}, *element);
+      newPage = UnsealPage({sealedPageBuffer, bytesOnStorage, pageInfo.fNElements}, *element, columnId);
       fCounters->fSzUnzip.Add(elementSize * pageInfo.fNElements);
    }
 
-   auto newPage = fPageAllocator->NewPage(columnId, pageBuffer.release(), elementSize, pageInfo.fNElements);
    newPage.SetWindow(clusterInfo.fColumnOffset + pageInfo.fFirstInPage,
                      RPage::RClusterInfo(clusterId, clusterInfo.fColumnOffset));
    fPagePool->RegisterPage(
       newPage,
-      RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorDaos::DeletePage(page); }, nullptr));
+      RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorHeap::DeletePage(page); }, nullptr));
    fCounters->fNPagePopulated.Inc();
    return newPage;
 }
@@ -828,14 +810,13 @@ void ROOT::Experimental::Detail::RPageSourceDaos::UnzipClusterImpl(RCluster *clu
          auto taskFunc = [this, columnId, clusterId, firstInPage, onDiskPage, element = allElements.back().get(),
                           nElements = pi.fNElements,
                           indexOffset = clusterDescriptor.GetColumnRange(columnId).fFirstElementIndex]() {
-            auto pageBuffer = UnsealPage({onDiskPage->GetAddress(), onDiskPage->GetSize(), nElements}, *element);
+            auto newPage = UnsealPage({onDiskPage->GetAddress(), onDiskPage->GetSize(), nElements}, *element, columnId);
             fCounters->fSzUnzip.Add(element->GetSize() * nElements);
 
-            auto newPage = fPageAllocator->NewPage(columnId, pageBuffer.release(), element->GetSize(), nElements);
             newPage.SetWindow(indexOffset + firstInPage, RPage::RClusterInfo(clusterId, indexOffset));
             fPagePool->PreloadPage(
                newPage,
-               RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorDaos::DeletePage(page); },
+               RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorHeap::DeletePage(page); },
                             nullptr));
          };
 
diff --git a/tree/ntuple/v7/src/RPageStorageFile.cxx b/tree/ntuple/v7/src/RPageStorageFile.cxx
index 3567b3fd58a33..18235f8011df0 100644
--- a/tree/ntuple/v7/src/RPageStorageFile.cxx
+++ b/tree/ntuple/v7/src/RPageStorageFile.cxx
@@ -190,32 +190,11 @@ void ROOT::Experimental::Detail::RPageSinkFile::ReleasePage(RPage &page)
 
 ////////////////////////////////////////////////////////////////////////////////
 
-
-ROOT::Experimental::Detail::RPage ROOT::Experimental::Detail::RPageAllocatorFile::NewPage(
-   ColumnId_t columnId, void *mem, std::size_t elementSize, std::size_t nElements)
-{
-   RPage newPage(columnId, mem, elementSize, nElements);
-   newPage.GrowUnchecked(nElements);
-   return newPage;
-}
-
-void ROOT::Experimental::Detail::RPageAllocatorFile::DeletePage(const RPage& page)
-{
-   if (page.IsNull())
-      return;
-   delete[] reinterpret_cast<unsigned char *>(page.GetBuffer());
-}
-
-
-////////////////////////////////////////////////////////////////////////////////
-
-
 ROOT::Experimental::Detail::RPageSourceFile::RPageSourceFile(std::string_view ntupleName,
-   const RNTupleReadOptions &options)
-   : RPageSource(ntupleName, options)
-   , fPageAllocator(std::make_unique<RPageAllocatorFile>())
-   , fPagePool(std::make_shared<RPagePool>())
-   , fClusterPool(std::make_unique<RClusterPool>(*this, options.GetClusterBunchSize()))
+                                                             const RNTupleReadOptions &options)
+   : RPageSource(ntupleName, options),
+     fPagePool(std::make_shared<RPagePool>()),
+     fClusterPool(std::make_unique<RClusterPool>(*this, options.GetClusterBunchSize()))
 {
    fDecompressor = std::make_unique<RNTupleDecompressor>();
    EnableDefaultMetrics("RPageSourceFile");
@@ -363,21 +342,18 @@ ROOT::Experimental::Detail::RPageSourceFile::PopulatePageFromCluster(ColumnHandl
       sealedPageBuffer = onDiskPage->GetAddress();
    }
 
-   std::unique_ptr<unsigned char []> pageBuffer;
+   RPage newPage;
    {
       RNTupleAtomicTimer timer(fCounters->fTimeWallUnzip, fCounters->fTimeCpuUnzip);
-      pageBuffer = UnsealPage({sealedPageBuffer, bytesOnStorage, pageInfo.fNElements}, *element);
+      newPage = UnsealPage({sealedPageBuffer, bytesOnStorage, pageInfo.fNElements}, *element, columnId);
       fCounters->fSzUnzip.Add(elementSize * pageInfo.fNElements);
    }
 
-   auto newPage = fPageAllocator->NewPage(columnId, pageBuffer.release(), elementSize, pageInfo.fNElements);
    newPage.SetWindow(clusterInfo.fColumnOffset + pageInfo.fFirstInPage,
                      RPage::RClusterInfo(clusterId, clusterInfo.fColumnOffset));
-   fPagePool->RegisterPage(newPage,
-      RPageDeleter([](const RPage &page, void * /*userData*/)
-      {
-         RPageAllocatorFile::DeletePage(page);
-      }, nullptr));
+   fPagePool->RegisterPage(
+      newPage,
+      RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorHeap::DeletePage(page); }, nullptr));
    fCounters->fNPagePopulated.Inc();
    return newPage;
 }
@@ -621,23 +597,18 @@ void ROOT::Experimental::Detail::RPageSourceFile::UnzipClusterImpl(RCluster *clu
          auto onDiskPage = cluster->GetOnDiskPage(key);
          R__ASSERT(onDiskPage && (onDiskPage->GetSize() == pi.fLocator.fBytesOnStorage));
 
-         auto taskFunc =
-            [this, columnId, clusterId, firstInPage, onDiskPage,
-             element = allElements.back().get(),
-             nElements = pi.fNElements,
-             indexOffset = clusterDescriptor.GetColumnRange(columnId).fFirstElementIndex
-            ] () {
-               auto pageBuffer = UnsealPage({onDiskPage->GetAddress(), onDiskPage->GetSize(), nElements}, *element);
-               fCounters->fSzUnzip.Add(element->GetSize() * nElements);
-
-               auto newPage = fPageAllocator->NewPage(columnId, pageBuffer.release(), element->GetSize(), nElements);
-               newPage.SetWindow(indexOffset + firstInPage, RPage::RClusterInfo(clusterId, indexOffset));
-               fPagePool->PreloadPage(newPage,
-                  RPageDeleter([](const RPage &page, void * /*userData*/)
-                  {
-                     RPageAllocatorFile::DeletePage(page);
-                  }, nullptr));
-            };
+         auto taskFunc = [this, columnId, clusterId, firstInPage, onDiskPage, element = allElements.back().get(),
+                          nElements = pi.fNElements,
+                          indexOffset = clusterDescriptor.GetColumnRange(columnId).fFirstElementIndex]() {
+            auto newPage = UnsealPage({onDiskPage->GetAddress(), onDiskPage->GetSize(), nElements}, *element, columnId);
+            fCounters->fSzUnzip.Add(element->GetSize() * nElements);
+
+            newPage.SetWindow(indexOffset + firstInPage, RPage::RClusterInfo(clusterId, indexOffset));
+            fPagePool->PreloadPage(
+               newPage,
+               RPageDeleter([](const RPage &page, void * /*userData*/) { RPageAllocatorHeap::DeletePage(page); },
+                            nullptr));
+         };
 
          fTaskScheduler->AddTask(taskFunc);