diff --git a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx index 3cb1c2db0ee32..0555829d5fd24 100644 --- a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx @@ -42,6 +42,8 @@ namespace Internal { class RRawFile; class RPageAllocatorHeap; +RNTuple GetAnchor(const RPageSource &source); + // clang-format off /** \class ROOT::Internal::RPageSinkFile @@ -122,6 +124,7 @@ public: // clang-format on class RPageSourceFile : public RPageSource { friend class ROOT::RNTuple; + friend ROOT::RNTuple ROOT::Internal::GetAnchor(const RPageSource &); private: /// Either provided by CreateFromAnchor, or read from the ROOT file given the ntuple name diff --git a/tree/ntuple/src/RPageStorageFile.cxx b/tree/ntuple/src/RPageStorageFile.cxx index 831746209dfd1..c4197a2c0e996 100644 --- a/tree/ntuple/src/RPageStorageFile.cxx +++ b/tree/ntuple/src/RPageStorageFile.cxx @@ -306,6 +306,16 @@ ROOT::Internal::RPageSinkFile::CloneAsHidden(std::string_view name, const ROOT:: //////////////////////////////////////////////////////////////////////////////// +ROOT::RNTuple ROOT::Internal::GetAnchor(const RPageSource &source) +{ + const auto *fileSource = dynamic_cast(&source); + if (!fileSource) + throw RException(R__FAIL("Cannot retrieve an RNTuple anchor from a non-file page source")); + if (!fileSource->fAnchor) + throw RException(R__FAIL("Cannot retrieve RNTuple anchor: no anchor is available")); + return *fileSource->fAnchor; +} + ROOT::Internal::RPageSourceFile::RPageSourceFile(std::string_view ntupleName, const ROOT::RNTupleReadOptions &opts) : RPageSource(ntupleName, opts) { diff --git a/tree/ntupleutil/inc/ROOT/RNTupleInspector.hxx b/tree/ntupleutil/inc/ROOT/RNTupleInspector.hxx index 9ba5b9816fdbd..88bf518fd6130 100644 --- a/tree/ntupleutil/inc/ROOT/RNTupleInspector.hxx +++ b/tree/ntupleutil/inc/ROOT/RNTupleInspector.hxx @@ -503,6 +503,13 @@ public: /// \brief Print a string that represents the tree of the (sub)fields and columns of an RNTuple in a format which a /// performance profile visualizer can render void PrintSchemaProfile(ESchemaProfileFormat format, std::ostream &output = std::cout) const; + + //////////////////////////////////////////////////////////////////////////// + /// \brief Print a string that represents the on-disk storage of the cluster groups, clusters, column ranges, pages, + /// header, footer and page lists on an RNTuple in a format which a performance profile visualizer can render + /// + /// \warning ntuple must have a file-based backend + void PrintDiskProfile(ESchemaProfileFormat format, std::ostream &output = std::cout) const; }; } // namespace Experimental } // namespace ROOT diff --git a/tree/ntupleutil/src/RNTupleInspector.cxx b/tree/ntupleutil/src/RNTupleInspector.cxx index 7e5543fb04cb8..73c371ce34687 100644 --- a/tree/ntupleutil/src/RNTupleInspector.cxx +++ b/tree/ntupleutil/src/RNTupleInspector.cxx @@ -691,3 +691,139 @@ void ROOT::Experimental::RNTupleInspector::PrintSchemaProfile([[maybe_unused]] E PrintSpeedscopeFrames(frames, output); } + +void ROOT::Experimental::RNTupleInspector::PrintDiskProfile([[maybe_unused]] ESchemaProfileFormat format, + std::ostream &output) const +{ + // There is only one format at the moment + assert(format == ESchemaProfileFormat::kSpeedscopeJSON); + + // This method only supports file based backend. Will need the anchor later, but better to check early + const auto anchor = ROOT::Internal::GetAnchor(*fPageSource); + + const auto &descriptor = GetDescriptor(); + + struct RDiskPageLeaf { + std::uint64_t fPosition = 0; + std::uint64_t fSize = 0; + std::string fName; + std::array fAncestorIds; // clusterGroup, cluster, columnRange + std::array fAncestorNames; // clusterGroup, cluster, columnRange + }; + std::vector pageLeaves; + + // Collect all pageLeaves in whichever order the iterator provides + for (const auto &clusterGroupDescriptor : descriptor.GetClusterGroupIterable()) { + const auto groupId = clusterGroupDescriptor.GetId(); + const std::string groupName = "[cluster group " + std::to_string(groupId) + "]"; + + for (const auto clusterId : clusterGroupDescriptor.GetClusterIds()) { + const auto &clusterDescriptor = descriptor.GetClusterDescriptor(clusterId); + const std::string clusterName = "[cluster " + std::to_string(clusterId) + "]"; + + for (const auto &columnRange : clusterDescriptor.GetColumnRangeIterable()) { + const auto columnId = columnRange.GetPhysicalColumnId(); + const std::string columnRangeName = "[column range " + std::to_string(columnId) + "]"; + + const auto &pageRange = clusterDescriptor.GetPageRange(columnId); + for (const auto &pageInfo : pageRange.GetPageInfos()) { + const auto &locator = pageInfo.GetLocator(); + + RDiskPageLeaf pageLeaf; + pageLeaf.fPosition = locator.GetPosition(); + pageLeaf.fSize = locator.GetNBytesOnStorage(); + pageLeaf.fName = "[page @" + std::to_string(pageLeaf.fPosition) + "]"; + pageLeaf.fAncestorIds = {groupId + 1, clusterId + 1, columnId + 1}; + pageLeaf.fAncestorNames = {groupName, clusterName, columnRangeName}; + pageLeaves.push_back(pageLeaf); + } + } + } + } + + // Sort pageLeafs by on-disk address + std::sort(pageLeaves.begin(), pageLeaves.end(), + [](const RDiskPageLeaf &a, const RDiskPageLeaf &b) { return a.fPosition < b.fPosition; }); + + // Remove aliases (the ntuple specification allows complete, but not partial, overlap between pages) + pageLeaves.erase( + std::unique(pageLeaves.begin(), pageLeaves.end(), + [](const RDiskPageLeaf &a, const RDiskPageLeaf &b) { return a.fPosition == b.fPosition; }), + pageLeaves.end()); + + std::vector frames; + std::vector openIds; + std::vector openFrameIndexes; + std::uint64_t previouspageLeafEnd = 0; + + // Construct frame for ntuple header + SpeedscopeFrame headerFrame; + headerFrame.fString = "ntuple header"; + headerFrame.fOpeningPosition = anchor.GetSeekHeader(); + headerFrame.fClosingPosition = anchor.GetSeekHeader() + anchor.GetNBytesHeader(); + frames.push_back(headerFrame); + + // Construct frames from the bottom (leafs ordered by disk address) upwards + for (const auto &pageLeaf : pageLeaves) { + std::size_t sharedDepth = 0; + + // How many of the currently open ancestors does this pageLeaf share? + while (sharedDepth < openIds.size() && sharedDepth < pageLeaf.fAncestorIds.size() && + openIds[sharedDepth] == pageLeaf.fAncestorIds[sharedDepth]) { + sharedDepth++; + } + + // Close ancestors not shared with this pageLeaf (innermost first order) + while (openIds.size() > sharedDepth) { + frames[openFrameIndexes.back()].fClosingPosition = previouspageLeafEnd; + openIds.pop_back(); + openFrameIndexes.pop_back(); + } + + // Open the ancestors this pageLeaf needs (outermost first order) + for (std::size_t depth = sharedDepth; depth < pageLeaf.fAncestorIds.size(); ++depth) { + SpeedscopeFrame ancestorFrame; + ancestorFrame.fString = pageLeaf.fAncestorNames[depth]; + ancestorFrame.fOpeningPosition = pageLeaf.fPosition; + frames.push_back(ancestorFrame); + openIds.push_back(pageLeaf.fAncestorIds[depth]); + openFrameIndexes.push_back(frames.size() - 1); + } + + // Emit the pageLeaf itself + SpeedscopeFrame pageLeafFrame; + pageLeafFrame.fString = pageLeaf.fName; + pageLeafFrame.fOpeningPosition = pageLeaf.fPosition; + pageLeafFrame.fClosingPosition = pageLeaf.fPosition + pageLeaf.fSize; + frames.push_back(pageLeafFrame); + + previouspageLeafEnd = pageLeaf.fPosition + pageLeaf.fSize; + } + + // Close whatever is still open after the last pageLeaf + while (!openIds.empty()) { + frames[openFrameIndexes.back()].fClosingPosition = previouspageLeafEnd; + openIds.pop_back(); + openFrameIndexes.pop_back(); + } + + // Construct frames for page lists + for (const auto &clusterGroupDescriptor : descriptor.GetClusterGroupIterable()) { + const auto locator = clusterGroupDescriptor.GetPageListLocator(); + + SpeedscopeFrame pageListFrame; + pageListFrame.fString = "[page list " + std::to_string(clusterGroupDescriptor.GetId()) + "]"; + pageListFrame.fOpeningPosition = locator.GetPosition(); + pageListFrame.fClosingPosition = locator.GetPosition() + locator.GetNBytesOnStorage(); + frames.push_back(pageListFrame); + } + + // Construct frame for ntuple footer + SpeedscopeFrame footerFrame; + footerFrame.fString = "ntuple footer"; + footerFrame.fOpeningPosition = anchor.GetSeekFooter(); + footerFrame.fClosingPosition = anchor.GetSeekFooter() + anchor.GetNBytesFooter(); + frames.push_back(footerFrame); + + PrintSpeedscopeFrames(frames, output); +} diff --git a/tree/ntupleutil/test/ntuple_inspector.cxx b/tree/ntupleutil/test/ntuple_inspector.cxx index 5c3b4c326778b..11abe67c0da35 100644 --- a/tree/ntupleutil/test/ntuple_inspector.cxx +++ b/tree/ntupleutil/test/ntuple_inspector.cxx @@ -1,4 +1,5 @@ #include +#include #include #include @@ -13,10 +14,20 @@ using ROOT::ENTupleColumnType; using ROOT::RField; using ROOT::RFieldBase; using ROOT::RNTuple; +using ROOT::RNTupleDescriptor; +using ROOT::RNTupleLocator; using ROOT::RNTupleModel; using ROOT::RNTupleWriteOptions; using ROOT::RNTupleWriter; using ROOT::Experimental::RNTupleInspector; +using ROOT::Internal::MakeUninitArray; +using ROOT::Internal::RClusterDescriptorBuilder; +using ROOT::Internal::RClusterGroupDescriptorBuilder; +using ROOT::Internal::RColumnDescriptorBuilder; +using ROOT::Internal::RFieldDescriptorBuilder; +using ROOT::Internal::RNTupleDescriptorBuilder; +using ROOT::Internal::RNTupleFileWriter; +using ROOT::Internal::RNTupleSerializer; TEST(RNTupleInspector, CreateFromPointer) { @@ -915,3 +926,306 @@ TEST(RNTupleInspector, SchemaProfile) )foo"; EXPECT_EQ(schemaProfile, expected); } + +namespace { + +void WriteShuffledNTuple(const std::string &ntupleName, const std::string &path) +{ + // Specify logical schema + RNTupleDescriptorBuilder nTupleDescriptorBuilder; + + nTupleDescriptorBuilder.SetVersionForWriting(); + nTupleDescriptorBuilder.SetNTuple(ntupleName, "Non-contiguous cluster group, cluster and column range"); + + nTupleDescriptorBuilder.AddField(RFieldDescriptorBuilder() + .FieldId(0) + .FieldName("") + .Structure(ROOT::ENTupleStructure::kRecord) + .MakeDescriptor() + .Unwrap()); + + for (std::uint32_t i = 0; i < 6; ++i) { + const ROOT::DescriptorId_t fieldId = 1 + i; + const ROOT::DescriptorId_t columnId = i; + + nTupleDescriptorBuilder.AddField(RFieldDescriptorBuilder() + .FieldId(fieldId) + .FieldName("tag" + std::to_string(i)) + .Structure(ROOT::ENTupleStructure::kPlain) + .MakeDescriptor() + .Unwrap()); + + nTupleDescriptorBuilder.AddFieldLink(0, fieldId).ThrowOnError(); + + nTupleDescriptorBuilder.AddColumn(RColumnDescriptorBuilder() + .LogicalColumnId(columnId) + .PhysicalColumnId(columnId) + .FieldId(fieldId) + .BitsOnStorage(32) + .Type(ROOT::ENTupleColumnType::kIndex32) + .Index(0) + .MakeDescriptor() + .Unwrap()); + } + + RNTupleWriteOptions options; + auto writer = + RNTupleFileWriter::Recreate("shuffled_ntuple", path, RNTupleFileWriter::EContainerFormat::kTFile, options); + + const RNTupleDescriptor &schemaDescriptor = nTupleDescriptorBuilder.GetDescriptor(); + + // Serialize and write header + auto context = RNTupleSerializer::SerializeHeader(nullptr, schemaDescriptor).Unwrap(); + auto headerBuffer = MakeUninitArray(context.GetHeaderSize()); + context = RNTupleSerializer::SerializeHeader(headerBuffer.get(), schemaDescriptor).Unwrap(); + writer->WriteNTupleHeader(headerBuffer.get(), context.GetHeaderSize(), context.GetHeaderSize()); + + // Serialize and write pages + auto serializePage = [&](std::uint32_t firstValue, std::uint32_t numberOfElements, bool addChecksum) { + const std::size_t payloadBytes = std::size_t(numberOfElements) * 4; // kIndex32 -> 4 bytes/element + const std::size_t blobBytes = payloadBytes + (addChecksum ? 8 : 0); + auto blob = MakeUninitArray(blobBytes); + + for (std::uint32_t i = 0; i < numberOfElements; ++i) + RNTupleSerializer::SerializeUInt32(firstValue + i, blob.get() + i * 4); + + if (addChecksum) { + std::uint64_t xxhash3 = 0; + RNTupleSerializer::SerializeXxHash3(blob.get(), payloadBytes, xxhash3, blob.get() + payloadBytes); + } + + const std::uint64_t offset = writer->WriteBlob(blob.get(), blobBytes, payloadBytes); + + ROOT::RClusterDescriptor::RPageInfo pageInfo; + pageInfo.SetNElements(numberOfElements); + pageInfo.SetHasChecksum(addChecksum); + pageInfo.GetLocator().SetPosition(offset); + pageInfo.GetLocator().SetNBytesOnStorage(payloadBytes); // excludes the checksum + return pageInfo; + }; + + auto page1 = serializePage(0, 50, true); + auto page2 = serializePage(50, 25, false); + auto page3 = serializePage(0, 100, true); + auto page4 = serializePage(75, 25, false); + auto page5 = serializePage(0, 100, true); + auto page6 = serializePage(0, 100, false); + auto page7 = serializePage(0, 100, true); + auto page8 = serializePage(0, 100, false); + + auto makePageRange = [](ROOT::DescriptorId_t physicalColumnID, + std::initializer_list pages) { + ROOT::RClusterDescriptor::RPageRange pageRange; + pageRange.SetPhysicalColumnId(physicalColumnID); + for (const auto &p : pages) + pageRange.GetPageInfos().emplace_back(p); + return pageRange; + }; + + // Specify clusters and column ranges + { + RClusterDescriptorBuilder builder; + builder.ClusterId(0).FirstEntryIndex(0).NEntries(100); + builder.CommitColumnRange(0, 0, 0, makePageRange(0, {page1, page2, page4})).ThrowOnError(); + builder.CommitColumnRange(1, 0, 0, makePageRange(1, {page3})).ThrowOnError(); + builder.CommitColumnRange(3, 0, 0, makePageRange(3, {page6})).ThrowOnError(); + nTupleDescriptorBuilder.AddCluster(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + } + { + RClusterDescriptorBuilder builder; + builder.ClusterId(1).FirstEntryIndex(100).NEntries(100); + builder.CommitColumnRange(2, 0, 0, makePageRange(2, {page5})).ThrowOnError(); + nTupleDescriptorBuilder.AddCluster(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + } + { + RClusterDescriptorBuilder builder; + builder.ClusterId(2).FirstEntryIndex(200).NEntries(100); + builder.CommitColumnRange(4, 0, 0, makePageRange(4, {page7})).ThrowOnError(); + nTupleDescriptorBuilder.AddCluster(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + } + { + RClusterDescriptorBuilder builder; + builder.ClusterId(3).FirstEntryIndex(300).NEntries(100); + builder.CommitColumnRange(5, 0, 0, makePageRange(5, {page8})).ThrowOnError(); + nTupleDescriptorBuilder.AddCluster(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + } + + std::vector clusterGroup0Clusters{0, 1, 3}; + std::vector clusterGroup1Clusters{2}; + std::vector clusterGroup0PhysicalID, clusterGroup1PhysicalID; + + for (auto id : clusterGroup0Clusters) + clusterGroup0PhysicalID.emplace_back(context.MapClusterId(id)); + for (auto id : clusterGroup1Clusters) + clusterGroup1PhysicalID.emplace_back(context.MapClusterId(id)); + + // Serialize and write page lists + auto writePageList = [&](std::vector &physicalClusterIds, RNTupleLocator &locator) { + const auto size = RNTupleSerializer::SerializePageList(nullptr, nTupleDescriptorBuilder.GetDescriptor(), + physicalClusterIds, context) + .Unwrap(); + auto buf = MakeUninitArray(size); + RNTupleSerializer::SerializePageList(buf.get(), nTupleDescriptorBuilder.GetDescriptor(), physicalClusterIds, + context) + .Unwrap(); + const std::uint64_t pageListOffset = writer->WriteBlob(buf.get(), size, size); + locator.SetPosition(pageListOffset); + locator.SetNBytesOnStorage(size); + return size; + }; + + // Specify cluster groups + RNTupleLocator clusterGroup0Location, clusterGroup1Location; + const auto clusterGroup0Size = writePageList(clusterGroup0PhysicalID, clusterGroup0Location); + const auto clusterGroup1Size = writePageList(clusterGroup1PhysicalID, clusterGroup1Location); + + { + RClusterGroupDescriptorBuilder builder; + builder.ClusterGroupId(0) + .PageListLength(clusterGroup0Size) + .PageListLocator(clusterGroup0Location) + .MinEntry(0) + .EntrySpan(400) + .NClusters(3); + builder.AddSortedClusters(clusterGroup0Clusters); + nTupleDescriptorBuilder.AddClusterGroup(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + context.MapClusterGroupId(0); + } + { + RClusterGroupDescriptorBuilder builder; + builder.ClusterGroupId(1) + .PageListLength(clusterGroup1Size) + .PageListLocator(clusterGroup1Location) + .MinEntry(200) + .EntrySpan(100) + .NClusters(1); + builder.AddSortedClusters(clusterGroup1Clusters); + nTupleDescriptorBuilder.AddClusterGroup(builder.MoveDescriptor().Unwrap()).ThrowOnError(); + context.MapClusterGroupId(1); + } + + auto nTupleDescriptor = nTupleDescriptorBuilder.MoveDescriptor(); + + // Serialize and write footer + const auto footerSize = RNTupleSerializer::SerializeFooter(nullptr, nTupleDescriptor, context).Unwrap(); + auto footerBuffer = MakeUninitArray(footerSize); + RNTupleSerializer::SerializeFooter(footerBuffer.get(), nTupleDescriptor, context).Unwrap(); + writer->WriteNTupleFooter(footerBuffer.get(), footerSize, footerSize); + + // Commit writes and call writer destructor to flush it + writer->Commit(); + writer = nullptr; +} +} // namespace + +TEST(RNTupleInspector, DiskProfile) +{ + FileRaii fileGuard("test_disk_profile.root"); + WriteShuffledNTuple("shuffled_ntuple", fileGuard.GetPath()); + + auto inspector = RNTupleInspector::Create("shuffled_ntuple", fileGuard.GetPath()); + std::ostringstream diskProfileStream; + inspector->PrintDiskProfile(ROOT::Experimental::ESchemaProfileFormat::kSpeedscopeJSON, diskProfileStream); + const std::string diskProfile = diskProfileStream.str(); + const std::string expected = R"foo({ + "$schema":"https://www.speedscope.app/file-format-schema.json", + "shared":{ + "frames":[ + { "name":"ntuple header" }, + { "name":"[cluster group 0]" }, + { "name":"[cluster 0]" }, + { "name":"[column range 0]" }, + { "name":"[page @882]" }, + { "name":"[page @1132]" }, + { "name":"[column range 1]" }, + { "name":"[page @1274]" }, + { "name":"[column range 0]" }, + { "name":"[page @1724]" }, + { "name":"[cluster 1]" }, + { "name":"[column range 0]" }, + { "name":"[page @1866]" }, + { "name":"[cluster 0]" }, + { "name":"[column range 2]" }, + { "name":"[page @2316]" }, + { "name":"[cluster group 1]" }, + { "name":"[cluster 3]" }, + { "name":"[column range 0]" }, + { "name":"[page @2758]" }, + { "name":"[cluster group 0]" }, + { "name":"[cluster 2]" }, + { "name":"[column range 0]" }, + { "name":"[page @3208]" }, + { "name":"[page list 0]" }, + { "name":"[page list 1]" }, + { "name":"ntuple footer" } + ] + }, + "profiles":[ + { + "type":"evented", + "name":"Flattened Timeline", + "unit":"bytes", + "startValue":0, + "endValue":4454, + "events":[ + {"type":"O","frame":0,"at":290}, + {"type":"C","frame":0,"at":840}, + {"type":"O","frame":1,"at":882}, + {"type":"O","frame":2,"at":882}, + {"type":"O","frame":3,"at":882}, + {"type":"O","frame":4,"at":882}, + {"type":"C","frame":4,"at":1082}, + {"type":"O","frame":5,"at":1132}, + {"type":"C","frame":5,"at":1232}, + {"type":"C","frame":3,"at":1232}, + {"type":"O","frame":6,"at":1274}, + {"type":"O","frame":7,"at":1274}, + {"type":"C","frame":7,"at":1674}, + {"type":"C","frame":6,"at":1674}, + {"type":"O","frame":8,"at":1724}, + {"type":"O","frame":9,"at":1724}, + {"type":"C","frame":9,"at":1824}, + {"type":"C","frame":8,"at":1824}, + {"type":"C","frame":2,"at":1824}, + {"type":"O","frame":10,"at":1866}, + {"type":"O","frame":11,"at":1866}, + {"type":"O","frame":12,"at":1866}, + {"type":"C","frame":12,"at":2266}, + {"type":"C","frame":11,"at":2266}, + {"type":"C","frame":10,"at":2266}, + {"type":"O","frame":13,"at":2316}, + {"type":"O","frame":14,"at":2316}, + {"type":"O","frame":15,"at":2316}, + {"type":"C","frame":15,"at":2716}, + {"type":"C","frame":14,"at":2716}, + {"type":"C","frame":13,"at":2716}, + {"type":"C","frame":1,"at":2716}, + {"type":"O","frame":16,"at":2758}, + {"type":"O","frame":17,"at":2758}, + {"type":"O","frame":18,"at":2758}, + {"type":"O","frame":19,"at":2758}, + {"type":"C","frame":19,"at":3158}, + {"type":"C","frame":18,"at":3158}, + {"type":"C","frame":17,"at":3158}, + {"type":"C","frame":16,"at":3158}, + {"type":"O","frame":20,"at":3208}, + {"type":"O","frame":21,"at":3208}, + {"type":"O","frame":22,"at":3208}, + {"type":"O","frame":23,"at":3208}, + {"type":"C","frame":23,"at":3608}, + {"type":"C","frame":22,"at":3608}, + {"type":"C","frame":21,"at":3608}, + {"type":"C","frame":20,"at":3608}, + {"type":"O","frame":24,"at":3650}, + {"type":"C","frame":24,"at":4038}, + {"type":"O","frame":25,"at":4080}, + {"type":"C","frame":25,"at":4204}, + {"type":"O","frame":26,"at":4246}, + {"type":"C","frame":26,"at":4454} + ] + } + ] +} +)foo"; + EXPECT_EQ(diskProfile, expected); +}