diff --git a/tree/CMakeLists.txt b/tree/CMakeLists.txt index dbcaa91ae6e2b..7adcca10627eb 100644 --- a/tree/CMakeLists.txt +++ b/tree/CMakeLists.txt @@ -9,3 +9,4 @@ add_subdirectory(treeplayer) add_subdirectory(treeviewer) add_subdirectory(dataframe) add_subdirectory(ntuple) +add_subdirectory(ntupledraw) diff --git a/tree/ntuple/v7/inc/ROOT/RColumnModel.hxx b/tree/ntuple/v7/inc/ROOT/RColumnModel.hxx index c5673bba4bc86..d9b9c3eceea22 100644 --- a/tree/ntuple/v7/inc/ROOT/RColumnModel.hxx +++ b/tree/ntuple/v7/inc/ROOT/RColumnModel.hxx @@ -16,8 +16,10 @@ #ifndef ROOT7_RColumnModel #define ROOT7_RColumnModel +#include #include +#include #include namespace ROOT { @@ -43,11 +45,53 @@ enum class EColumnType { kBit, kReal64, kReal32, + // kReal24, to uncomment after implementing custom-sized float kReal16, kReal8, kInt64, kInt32, kInt16, + // kCustomDouble, to uncomment after implementing custom-sized float + // kCustomFloat, to uncomment after implementing custom-sized float +}; + +// clang-format off +/** +\class ROOT::Experimental::RColumnTypeIdentifier +\ingroup NTuple +\brief Holds static arrays with EColumnType MetaData + +Contains static arrays to obtain information about a specific columnType. +*/ +// clang-format on +class RColumnTypeIdentifier { +public: + static constexpr std::array fColumnTypeNames{ + "Unknown", "Index", "Switch", "Byte", "Bit", "Real64", "Real32", /*"Real24", */ "Real16", + "Real8", "Int64", "Int32", "Int16" /*, "CustomDouble", "CustomFloat"*/}; + static constexpr std::array fColumnBitSizeOnDisk{ + 0, + sizeof(ClusterSize_t) * 8, + sizeof(ROOT::Experimental::RColumnSwitch) * 8, + sizeof(char) * 8, + sizeof(bool) * 8, + sizeof(double) * 8, + sizeof(float) * 8,/*, 24*/ + 16, + 8, + 64, + 32, + 16 /*, sizeof(double)*8, sizeof(float)*8*/}; + static const char *GetColumnTypeNames(std::uint32_t index) + { + assert(index < fColumnTypeNames.size()); + return fColumnTypeNames[index]; + } + static ClusterSize_t::ValueType GetColumnBitSizeOnDisk(std::uint32_t index) + { + assert(index < fColumnBitSizeOnDisk.size()); + return fColumnBitSizeOnDisk[index]; + } }; // clang-format off @@ -69,9 +113,7 @@ public: EColumnType GetType() const { return fType; } bool GetIsSorted() const { return fIsSorted; } - bool operator ==(const RColumnModel &other) const { - return (fType == other.fType) && (fIsSorted == other.fIsSorted); - } + bool operator==(const RColumnModel &other) const { return (fType == other.fType) && (fIsSorted == other.fIsSorted); } }; } // namespace Experimental diff --git a/tree/ntuple/v7/inc/ROOT/RPage.hxx b/tree/ntuple/v7/inc/ROOT/RPage.hxx index 43310e51a8285..a0ee21afa3caf 100644 --- a/tree/ntuple/v7/inc/ROOT/RPage.hxx +++ b/tree/ntuple/v7/inc/ROOT/RPage.hxx @@ -74,7 +74,7 @@ public: {} ~RPage() = default; - ColumnId_t GetColumnId() { return fColumnId; } + ColumnId_t GetColumnId() const { return fColumnId; } /// The total space available in the page ClusterSize_t::ValueType GetCapacity() const { return fCapacity; } /// The space taken by column elements in the buffer diff --git a/tree/ntupledraw/CMakeLists.txt b/tree/ntupledraw/CMakeLists.txt new file mode 100644 index 0000000000000..fd9126564bf93 --- /dev/null +++ b/tree/ntupledraw/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +############################################################################ +# CMakeLists.txt file for building ROOT ntupledraw package +# @author Simon Leisibach CERN +############################################################################ + +if(NOT root7) + return() +endif() + +ROOT_STANDARD_LIBRARY_PACKAGE(ROOTNTupleDraw +HEADERS + ROOT/RDrawStorage.hxx +SOURCES + v7/src/RDrawStorage.cxx +LINKDEF + LinkDef.h +DEPENDENCIES + ROOTNTuple + Hist + Gpad +) + +ROOT_ADD_TEST_SUBDIRECTORY(v7/test) diff --git a/tree/ntupledraw/inc/LinkDef.h b/tree/ntupledraw/inc/LinkDef.h new file mode 100644 index 0000000000000..a9359da9a4e0f --- /dev/null +++ b/tree/ntupledraw/inc/LinkDef.h @@ -0,0 +1,25 @@ +// Author: Simon Leisibach CERN 11/2019 + +/************************************************************************* + * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifdef __CLING__ + +#pragma link off all globals; +#pragma link off all classes; +#pragma link off all functions; + +#pragma link C++ nestedtypedefs; +#pragma link C++ nestedclasses; + +// Support for auto-loading +#pragma link C++ class ROOT::Experimental::RNTupleDraw-; +#pragma link C++ class ROOT::Experimental::Detail::RMetaDataBox+; +#pragma link C++ class ROOT::Experimental::Detail::RPageBox+; + +#endif diff --git a/tree/ntupledraw/v7/inc/ROOT/RDrawStorage.hxx b/tree/ntupledraw/v7/inc/ROOT/RDrawStorage.hxx new file mode 100644 index 0000000000000..919ee4b1524eb --- /dev/null +++ b/tree/ntupledraw/v7/inc/ROOT/RDrawStorage.hxx @@ -0,0 +1,188 @@ +/// \file ROOT/RDrawStorage.hxx +/// \ingroup NTupleDraw ROOT7 +/// \author Simon Leisibach +/// \date 2019-11-07 +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +/************************************************************************* + * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT7_RDrawStorage +#define ROOT7_RDrawStorage + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ROOT { +namespace Experimental { +namespace Detail { +class RDrawStorage; + +// clang-format off +/** +\class ROOT::Experimental::Detail::RMetaDataBox +\ingroup NTupleDraw +\brief A TBox which contains metadata information of a RNTuple + +A RMetaDataBox is drawn on the TCanvas showing the RNTuple storage layout and represents some metadata (header or +footer) in the RNTuple. It also holds some data of the metadata it represents, like its byte size. +*/ +// clang-format on +class RMetaDataBox : public TBox { +private: + std::string fDescription; // e.g. "Header" or "Footer" + const std::uint32_t fNBytesInStorage; + RDrawStorage *fParent; + +public: + RMetaDataBox() : RMetaDataBox(0, 0, 0, 0, "", 0, nullptr) {} + RMetaDataBox(double x1, double y1, double x2, double y2, std::string description, std::uint32_t nBytes, + RDrawStorage *parent, std::int32_t color = kGray); + std::uint32_t GetNBytesInStorage() const { return fNBytesInStorage; } + void Dump() const final; // *MENU* + void Inspect() const final; // *MENU* + ClassDef(RMetaDataBox, 1) +}; + +// clang-format off +/** +\class ROOT::Experimental::RPageBox +\ingroup NTupleDraw +\brief A TBox which represents a RPage + +A RPageBox is drawn on the TCanvas showing the RNTuple storage layout and represents a RPage in the RNTuple. It also +holds various data of a RPage, which allows the user to dump/inspect the RPageBox to obtain information about the RPage. +*/ +// clang-format on +class RPageBox : public TBox { +private: + std::string fFieldName; + std::string fFieldType; + std::string fColumnType; + DescriptorId_t fFieldId; + DescriptorId_t fColumnId; + DescriptorId_t fClusterId; + ClusterSize_t::ValueType fNElements; + ClusterSize_t::ValueType fElementSizeOnDisk; + NTupleSize_t fGlobalRangeStart; + NTupleSize_t fClusterRangeStart; + RClusterDescriptor::RLocator fLocator; // required for sorting + RDrawStorage *fParent; + std::size_t fPageBoxId; + +public: + RPageBox() + : RPageBox(0, 0, 0, 0, "", "", 0, 0, 0, EColumnType::kUnknown, 0, 0, 0, RClusterDescriptor::RLocator(), nullptr) + { + } + RPageBox(double x1, double y1, double x2, double y2, std::string fieldName, std::string fieldType, + DescriptorId_t fieldId, DescriptorId_t columnId, DescriptorId_t clusterId, EColumnType columnType, + ClusterSize_t::ValueType nElements, NTupleSize_t globalRangeStart, NTupleSize_t clusterRangeStart, + RClusterDescriptor::RLocator locator, RDrawStorage *parent, std::size_t pageBoxId = 0); + DescriptorId_t GetFieldId() const { return fFieldId; } + DescriptorId_t GetClusterId() const { return fClusterId; } + const RClusterDescriptor::RLocator &GetLocator() const { return fLocator; } + void SetPageId(std::size_t pageId) { fPageBoxId = pageId; } + void Dump() const final; // *MENU* + void Inspect() const final; // *MENU* + ClassDef(RPageBox, 1) +}; + +// clang-format off +/** +\class ROOT::Experimental::RDrawStorage +\ingroup NTupleDraw +\brief Main class for drawing the storage layout of a RNTuple + +This class coordinates the drawing process of the storage layout of a RNTuple. It also holds all generated unique +pointers with a static member variable until ROOT is terminated, in order for the drawing to persist. +*/ +// clang-format on +class RDrawStorage { +private: + std::size_t fNEntries; + std::size_t fTotalNumBytes; + // E.g. if the total number of bytes in a file is 42*1024*1024 bytes = 42 MB, then fScalingFactorOfAxis is set to + // 1024*1024, so that 42 can be displayed on the axis. + std::size_t fScalingFactorOfAxis = 1; + NTupleSize_t fNFields; + NTupleSize_t fNColumns; + NTupleSize_t fNClusters; + /// For every page in the ntuple, there exists an entry in fPageBoxes. + std::vector> fPageBoxes; + std::vector> fTexts; + std::vector> fLines; + // Not Stored as a unique_ptr because ROOT will sometimes delete fPad before the destructor of this class is called. + TPad *fPad; + // Not Stored as as unique_ptrs in a vector because ROOT will sometimes delete fTH1Fs before the destructor of this + // class is called. + std::vector fTH1Fs; + std::string fNTupleName; + std::string fAxisTitle; // for TLatex in RDrawStorage::Draw() + std::unique_ptr fHeaderBox; + std::unique_ptr fFooterBox; + std::unique_ptr fLegend; + void SetPageIds(); + +public: + // holds all generated cavas pointers in case they need to be manually destructed in the future due to changes in + // ROOT. + std::vector fCanvasPtrs; + RDrawStorage(RNTupleReader *reader); + /// holds all created RDrawStorage instances until the ROOT program is terminated. Deleting a RDrawStorage earlier + /// could cause objects drawn on the canvas to get deleted along with it. + static std::vector fgDrawStorageVec; + static std::int32_t GetColourFromFieldId(DescriptorId_t fieldId); + std::size_t GetPageBoxSize() const { return fPageBoxes.size(); } + std::size_t GetTotalNumBytes() const { return fTotalNumBytes; } + std::size_t GetScalingFactorOfAxis() const { return fScalingFactorOfAxis; } + NTupleSize_t GetNFields() const { return fNFields; } + NTupleSize_t GetNColumns() const { return fNColumns; } + NTupleSize_t GetNClusters() const { return fNClusters; } + static void RPageBoxClicked(); + void Draw(); +}; +} // namespace Detail + +// clang-format off +/** +\class ROOT::Experimental::RNTupleDraw +\ingroup NTupleDraw +\brief User interface for drawing the structure of an ntuple + +This class acts as a user interface like RNTupleWriter and RNTupleReader. It acts as a delegeator to RDrawStorage +instead of doing the drawing job itself. This way the lifetime of the data displayed on the canvas is tied to the +termination of the ROOT program and not to the destruction of a RNTupleDraw or RNTupleReader instance. +*/ +// clang-format on +class RNTupleDraw { +private: + Detail::RDrawStorage *fStorage; + bool fEmpty = false; + +public: + RNTupleDraw(const std::unique_ptr &reader); + void Draw(); +}; +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/tree/ntupledraw/v7/src/RDrawStorage.cxx b/tree/ntupledraw/v7/src/RDrawStorage.cxx new file mode 100644 index 0000000000000..dc07b8a55a135 --- /dev/null +++ b/tree/ntupledraw/v7/src/RDrawStorage.cxx @@ -0,0 +1,535 @@ +/// \file RDrawStorage.cxx +/// \ingroup NTupleDraw ROOT7 +/// \author Simon Leisibach +/// \date 2019-11-07 +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +/************************************************************************* + * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +std::vector ROOT::Experimental::Detail::RDrawStorage::fgDrawStorageVec; + +// ------------------------------ RMetaDataBox ------------------------------ + +ROOT::Experimental::Detail::RMetaDataBox::RMetaDataBox(double x1, double y1, double x2, double y2, + std::string description, std::uint32_t nBytes, + RDrawStorage *parent, std::int32_t color) + : TBox(x1, y1, x2, y2), fDescription{description}, fNBytesInStorage{nBytes}, fParent{parent} +{ + SetFillColor(color); +} + +void ROOT::Experimental::Detail::RMetaDataBox::Dump() const +{ + std::cout << " ==> Dumping Page information:\n\n"; + std::cout << "Description: \t\t" << fDescription << std::endl; + std::cout << "Size: \t\t" << fNBytesInStorage << " bytes" << std::endl; +} + +void ROOT::Experimental::Detail::RMetaDataBox::Inspect() const +{ + static std::int32_t index = 0; + // The canvases need to have unique names, or else there will be an error saying that not all were found when trying + // to delete them when quitting the program. + std::string uniqueCanvasName{"MetaDataDetails" + std::to_string(++index)}; + + fParent->fCanvasPtrs.emplace_back(new TCanvas(uniqueCanvasName.c_str(), "Page Details", 500, 300)); + TLatex latex; + + // Draw Title + latex.SetTextAlign(12); + latex.SetTextSize(0.08); + latex.DrawLatex(0.01, 0.96, fDescription.c_str()); + + // Write Details + latex.SetTextSize(0.06); + std::string sizeString = "Size:" + std::string(30, ' ') + std::to_string(fNBytesInStorage) + " bytes"; + latex.DrawLatex(0.01, 0.85, sizeString.c_str()); + fParent->fCanvasPtrs.back()->Update(); +} + +ClassImp(ROOT::Experimental::Detail::RMetaDataBox) + +// ------------------------------ RPageBox ---------------------------------- + +void ROOT::Experimental::Detail::RPageBox::Dump() const +{ + std::cout << " ==> Dumping Page information:\n\n"; + std::cout << "Page Id: \t\t" << fPageBoxId << " / " << fParent->GetPageBoxSize() << std::endl; + std::cout << "Cluster Id: \t\t" << fClusterId << " / " << fParent->GetNClusters() - 1 << std::endl; + std::cout << "Field Id: \t\t" << fFieldId << " / " << fParent->GetNFields() - 1 << std::endl; + std::cout << "FieldName: \t\t" << fFieldName << std::endl; + std::cout << "FieldType: \t\t" << fFieldType << std::endl; + std::cout << "Column Id: \t\t" << fColumnId << " / " << fParent->GetNColumns() - 1 << std::endl; + std::cout << "ColumnType: \t\t" << fColumnType << std::endl; + std::cout << "NElements: \t\t" << fNElements << std::endl; + std::cout << "Element Size On Disk: \t\t" << fElementSizeOnDisk << " bits" << std::endl; + std::cout << "Element Size On Storage:\t\t" << 8 * fLocator.fBytesOnStorage / fNElements << " bits" << std::endl; + std::cout << "Page Size On Disk: \t\t" << fNElements * fElementSizeOnDisk / 8 << " bytes" << std::endl; + std::cout << "Page Size On Storage: \t\t" << fLocator.fBytesOnStorage << " bytes" << std::endl; + std::cout << "Global Page Range: \t\t" << fGlobalRangeStart << " - " << fGlobalRangeStart + fNElements - 1 + << std::endl; + std::cout << "Cluster Page Range: \t\t" << fClusterRangeStart << " - " << fClusterRangeStart + fNElements - 1 + << std::endl; + std::size_t totalNumBytes = fParent->GetTotalNumBytes(); + std::size_t scalingFactorOfAxis = fParent->GetScalingFactorOfAxis(); + std::cout.setf(std::ios::fixed); + std::cout << "Location in Storage: \t\t" << static_cast(GetX1() * scalingFactorOfAxis) << " / " + << totalNumBytes << " bytes" << std::endl; + std::cout.unsetf(std::ios::fixed); +} + +void ROOT::Experimental::Detail::RPageBox::Inspect() const +{ + static std::int32_t index = 0; + // The canvases need to have unique names, or else there will be an error saying that not all were found when trying + // to delete them when quitting the program. + std::string uniqueCanvasName{"PageDetails" + std::to_string(++index)}; + fParent->fCanvasPtrs.emplace_back(new TCanvas(uniqueCanvasName.c_str(), "Page Details", 500, 300)); + + TLatex latex; + // Draw Title + latex.SetTextAlign(12); + latex.SetTextSize(0.08); + std::string pageNumbering = + "Page No. " + std::to_string(fPageBoxId) + " / " + std::to_string(fParent->GetPageBoxSize()); + latex.DrawLatex(0.01, 0.96, pageNumbering.c_str()); + + // Write details about page + latex.SetTextSize(0.06); + std::string clusterIdString = "Cluster Id:" + std::string(37, ' ') + std::to_string(fClusterId) + " / " + + std::to_string(fParent->GetNClusters() - 1); + latex.DrawLatex(0.01, 0.85, clusterIdString.c_str()); + std::string fieldIdString = + "Field Id:" + std::string(41, ' ') + std::to_string(fFieldId) + " / " + std::to_string(fParent->GetNFields() - 1); + latex.DrawLatex(0.01, 0.80, fieldIdString.c_str()); + std::string fieldName = "FieldName:" + std::string(35, ' ') + fFieldName; + latex.DrawLatex(0.01, 0.75, fieldName.c_str()); + std::string fieldType = "FieldType:" + std::string(37, ' ') + fFieldType; + latex.DrawLatex(0.01, 0.70, fieldType.c_str()); + std::string columnIdString = "Column Id:" + std::string(36, ' ') + std::to_string(fColumnId) + " / " + + std::to_string(fParent->GetNColumns() - 1); + latex.DrawLatex(0.01, 0.65, columnIdString.c_str()); + std::string columnTypeString = "ColumnType:" + std::string(32, ' ') + fColumnType; + latex.DrawLatex(0.01, 0.60, columnTypeString.c_str()); + std::string nElements = "NElements:" + std::string(35, ' ') + std::to_string(fNElements); + latex.DrawLatex(0.01, 0.55, nElements.c_str()); + std::string elementSizeOnDisk = + "Element Size On Disk:" + std::string(17, ' ') + std::to_string(fElementSizeOnDisk) + " bits"; + latex.DrawLatex(0.01, 0.50, elementSizeOnDisk.c_str()); + std::string elementSizeOnStorage = "Element Size On Storage:" + std::string(11, ' ') + + std::to_string(8 * fLocator.fBytesOnStorage / fNElements) + " bits"; + latex.DrawLatex(0.01, 0.45, elementSizeOnStorage.c_str()); + std::string pageSize = + "Page Size On Disk:" + std::string(22, ' ') + std::to_string(fNElements * fElementSizeOnDisk / 8) + " bytes"; + latex.DrawLatex(0.01, 0.40, pageSize.c_str()); + std::string pageSizeStorage = + "Page Size On Storage:" + std::string(16, ' ') + std::to_string(fLocator.fBytesOnStorage) + " bytes"; + latex.DrawLatex(0.01, 0.35, pageSizeStorage.c_str()); + std::string globalRange = "Global Page Range:" + std::string(21, ' ') + std::to_string(fGlobalRangeStart) + " - " + + std::to_string(fGlobalRangeStart + fNElements - 1); + latex.DrawLatex(0.01, 0.30, globalRange.c_str()); + std::string clusterRange = "Cluster Page Range:" + std::string(20, ' ') + std::to_string(fClusterRangeStart) + + " - " + std::to_string(fClusterRangeStart + fNElements - 1); + latex.DrawLatex(0.01, 0.25, clusterRange.c_str()); + std::size_t totalNumBytes = fParent->GetTotalNumBytes(); + std::size_t scalingFactorOfAxis = fParent->GetScalingFactorOfAxis(); + std::string locationString = "Location in Storage:" + std::string(20, ' ') + + std::to_string(static_cast(GetX1() * scalingFactorOfAxis)) + " / " + + std::to_string(totalNumBytes) + " bytes"; + latex.DrawLatex(0.01, 0.20, locationString.c_str()); + + fParent->fCanvasPtrs.back()->Update(); +} + +ClassImp(ROOT::Experimental::Detail::RPageBox) + +// ------------------------------ RDrawStorage ------------------------------ + +constexpr std::array ROOT::Experimental::RColumnTypeIdentifier::fColumnTypeNames; +constexpr std::array ROOT::Experimental::RColumnTypeIdentifier::fColumnBitSizeOnDisk; + +ROOT::Experimental::Detail::RPageBox::RPageBox(double x1, double y1, double x2, double y2, std::string fieldName, + std::string fieldType, DescriptorId_t fieldId, + DescriptorId_t columnId, DescriptorId_t clusterId, + EColumnType columnType, ClusterSize_t::ValueType nElements, + NTupleSize_t globalRangeStart, NTupleSize_t clusterRangeStart, + RClusterDescriptor::RLocator locator, RDrawStorage *parent, + std::size_t pageBoxId) + : TBox(x1, y1, x2, y2), fFieldName{fieldName}, fFieldType{fieldType}, fFieldId{fieldId}, fColumnId{columnId}, + fClusterId{clusterId}, fNElements{nElements}, fGlobalRangeStart{globalRangeStart}, + fClusterRangeStart{clusterRangeStart}, fLocator{locator}, fParent{parent}, fPageBoxId{pageBoxId} +{ + fColumnType = std::string{RColumnTypeIdentifier::GetColumnTypeNames(static_cast(columnType))}; + fElementSizeOnDisk = RColumnTypeIdentifier::GetColumnBitSizeOnDisk(static_cast(columnType)); +} + +ROOT::Experimental::Detail::RDrawStorage::RDrawStorage(ROOT::Experimental::RNTupleReader *reader) +{ + // RDrawStorage::Draw() should work without the descriptor because its lifetime is not coupled to this object. The + // job of this constructor is to get all information necessary for drawing from the descriptor, so that it isn't + // required later anymore. + auto &desc = reader->GetDescriptor(); + + /* Procedure: + * 1. Check for special cases like empty ntuple + * 2. Prepare Title and TLegend + * 3. Create all boxes and colour them + * 4. Sort RPageBoxes by page order and set Page Ids + * 5. Create CumulativeNBytes to later to set x1 and x2 values for the boxes + * 6. Prepare StorageSizeAxis + * 7. Set x1 and x2 values for all boxes + * 8. Include box entries in the legend + * 9. Prepare ClusterAxis + */ + + // 1. Check for special cases like empty ntuple + fNFields = desc.GetNFields(); + fNEntries = desc.GetNEntries(); + if (fNFields <= 1 || fNEntries <= 0) + return; + + // 2. Prepare Title and TLegend + fNTupleName = desc.GetName(); + std::string title = "Storage layout of " + fNTupleName; + fTexts.emplace_back(std::make_unique(.5, .94, title.c_str())); + fTexts.back()->SetTextAlign(22); + fTexts.back()->SetTextSize(0.08); + + fLegend = std::make_unique(0.05, 0.05, .95, .55); + std::int32_t nColumnsInLegend = 2; + if (fNFields > 150) { + nColumnsInLegend = 10; + } else if (fNFields > 120) { + nColumnsInLegend = 9; + } else if (fNFields > 100) { + nColumnsInLegend = 8; + } else if (fNFields > 75) { + nColumnsInLegend = 7; + } else if (fNFields > 33) { + nColumnsInLegend = 6; + } else if (fNFields > 26) { + nColumnsInLegend = 5; + } else if (fNFields > 19) { + nColumnsInLegend = 4; + } else if (fNFields > 4) { + nColumnsInLegend = 3; + } + fLegend->SetNColumns(nColumnsInLegend); + + // 3. Create all boxes and colour them + constexpr double boxY1 = 0; + constexpr double boxY2 = 1; + fHeaderBox = + std::make_unique(0.0, boxY1, 0.0, boxY2, "Header", desc.SerializeHeader(nullptr), this, kGray); + fFooterBox = + std::make_unique(0.0, boxY1, 0.0, boxY2, "Footer", desc.SerializeFooter(nullptr), this, kGray); + fNColumns = desc.GetNColumns(); + fNClusters = desc.GetNClusters(); + for (std::size_t i = 0; i < fNClusters; ++i) { + for (std::size_t j = 0; j < fNColumns; ++j) { + ClusterSize_t::ValueType localIndex{0}; + auto &pageRange = desc.GetClusterDescriptor(i).GetPageRange(j); + for (std::size_t k = 0; k < pageRange.fPageInfos.size(); ++k) { + // Only use the descriptor to obtain information about a page + DescriptorId_t fieldId = desc.GetColumnDescriptor(j).GetFieldId(); + std::string fieldName = desc.GetFieldDescriptor(fieldId).GetFieldName(); + std::string fieldType = desc.GetFieldDescriptor(fieldId).GetTypeName(); + EColumnType columnType = desc.GetColumnDescriptor(j).GetModel().GetType(); + ClusterSize_t::ValueType nElements = pageRange.fPageInfos.at(k).fNElements; + auto clusterRangeFirst = localIndex; + auto globalRangeFirst = localIndex + desc.GetClusterDescriptor(i).GetColumnRange(j).fFirstElementIndex; + + fPageBoxes.emplace_back(std::make_unique( + 0, boxY1, 0, boxY2, fieldName, fieldType, fieldId, j, i, columnType, nElements, globalRangeFirst, + clusterRangeFirst, pageRange.fPageInfos.at(k).fLocator, this)); + fPageBoxes.back()->SetFillColor(GetColourFromFieldId(fieldId)); + + localIndex += nElements; + } + } + } + + // 4. Sort RPageBoxes by page order and set Page Ids + std::sort(fPageBoxes.begin(), fPageBoxes.end(), + [](const std::unique_ptr &a, const std::unique_ptr &b) -> bool { + if (a->GetClusterId() != b->GetClusterId()) + return a->GetClusterId() < b->GetClusterId(); + return a->GetLocator().fPosition < b->GetLocator().fPosition; + }); + SetPageIds(); + + // 5. Create CumulativeNBytes to later to set x1 and x2 values for the boxes + // Size is +2, because +1 for header and +1 for footer + std::vector cumulativeNBytes(fPageBoxes.size() + 2); + cumulativeNBytes.at(0) = fHeaderBox->GetNBytesInStorage(); + for (std::size_t i = 1; i < cumulativeNBytes.size() - 1; ++i) { + cumulativeNBytes.at(i) = cumulativeNBytes.at(i - 1) + fPageBoxes.at(i - 1)->GetLocator().fBytesOnStorage; + } + cumulativeNBytes.back() = fFooterBox->GetNBytesInStorage() + cumulativeNBytes.at(cumulativeNBytes.size() - 2); + + // 6. Prepare StorageSizeAxis + fAxisTitle = "#splitline{Data}{#splitline{Size}{#splitlie{in}{Bytes}}}"; + fTotalNumBytes = cumulativeNBytes.back(); + fScalingFactorOfAxis = 1; + if (fTotalNumBytes > 1024 * 1024 * 1024) { + fScalingFactorOfAxis = 1024 * 1024 * 1024; + fAxisTitle = "#splitline{Data}{#splitline{Size}{in GB}}"; + } else if (fTotalNumBytes > 1024 * 1024) { + fScalingFactorOfAxis = 1024 * 1024; + fAxisTitle = "#splitline{Data}{#splitline{Size}{in MB}}"; + } else if (fTotalNumBytes > 1024) { + fScalingFactorOfAxis = 1024; + fAxisTitle = "#splitline{Data}{#splitline{Size}{in KB}}"; + } + + // 7. Set x1 and x2 values for all boxes + fHeaderBox->SetX1(0); + fHeaderBox->SetX2((double)cumulativeNBytes.at(0) / fScalingFactorOfAxis); + for (std::size_t i = 0; i < fPageBoxes.size(); ++i) { + fPageBoxes.at(i)->SetX1((double)cumulativeNBytes.at(i) / fScalingFactorOfAxis); + fPageBoxes.at(i)->SetX2((double)cumulativeNBytes.at(i + 1) / fScalingFactorOfAxis); + } + fFooterBox->SetX1((double)cumulativeNBytes.at(cumulativeNBytes.size() - 2) / fScalingFactorOfAxis); + fFooterBox->SetX2((double)cumulativeNBytes.back() / fScalingFactorOfAxis); + + // 8. Include box entries in the legend + fLegend->AddEntry(fHeaderBox.get(), "Header", "f"); + // start at 1 to skip rootField + for (std::size_t i = 1; i < fNFields; ++i) { + // for each field find the first PageBox which represents that field and add it to the legend + auto vecIt = std::find_if(fPageBoxes.begin(), fPageBoxes.end(), + [i](const std::unique_ptr &a) -> bool { return a->GetFieldId() == i; }); + if (vecIt != fPageBoxes.end()) { + fLegend->AddEntry((*vecIt).get(), desc.GetFieldDescriptor(i).GetFieldName().c_str(), "f"); + } + } + fLegend->AddEntry(fFooterBox.get(), "Footer", "f"); + + // 9. Prepare ClusterAxis + double distanceBetweenLines = 0.001 * (fTotalNumBytes / fScalingFactorOfAxis); + std::size_t start = cumulativeNBytes.at(0); + std::size_t end{0}; + for (std::size_t i = 0; i < fNClusters; ++i) { + auto &cluster = desc.GetClusterDescriptor(i); + std::size_t nBytes = cluster.GetLocator().fBytesOnStorage; + // For some data formats (e.g. root-Files) this value is equal to 0. In that case get nBytes manually from all + // columns. + if (nBytes == 0) { + for (std::size_t j = 0; j < fNColumns; ++j) { + for (std::size_t k = 0; k < cluster.GetPageRange(j).fPageInfos.size(); ++k) { + nBytes += cluster.GetPageRange(j).fPageInfos.at(k).fLocator.fBytesOnStorage; + } + } + } + end = start + nBytes; + double x1 = (double)start / fScalingFactorOfAxis + distanceBetweenLines / 2; + double x2 = (double)end / fScalingFactorOfAxis - distanceBetweenLines / 2; + fLines.emplace_back(std::make_unique(x1, 1.05, x2, 1.05)); + fLines.back()->SetLineWidth(3); + fTexts.emplace_back(std::make_unique((x1 + x2) / 2, 1.2, std::to_string(i).c_str())); + fTexts.back()->SetTextAlign(22); + fTexts.back()->SetTextSize(0.15); + start = end; + } + fLegend->AddEntry(fLines.back().get(), "Cluster Id", "l"); +} + +void ROOT::Experimental::Detail::RDrawStorage::Draw() +{ + /* Procedure: + * 1. Check for special cases like empty ntuple + * 2. Create a new canvas + * 3. Create a TPad in the canvas so that when zooming only the boxes and axis get zoomed + * 4. Draw an empty histogram without a y-axis for zooming + * 5. Draw all boxes and add possibility to click on RPageBox to obtain information about a page + * 6. Draw clusterAxis + * 7. Return to canvas, draw title, legend and description of x-axis + */ + + // 1. Check for special cases like empty ntuple + if (fNFields <= 1) { + std::cout << "The ntuple has no fields. No storage layout was drawn." << std::endl; + return; + } + if (fNEntries <= 0) { + std::cout << "The ntuple has no entries. No storage layout was drawn." << std::endl; + return; + } + + // 2. Create a new canvas + static std::int32_t uniqueId = 0; + // Trying to delete multiple canvases with the same name leads to an error or when two canvases have the same name, + // only 1 may get deleted, causing a memory leak. + std::string uniqueCanvasName = "RDrawStorage" + std::to_string(++uniqueId); + fCanvasPtrs.emplace_back(new TCanvas(uniqueCanvasName.c_str(), fNTupleName.c_str(), 1000, 300)); + + // 3. Create a TPad in the canvas so that when zooming only the boxes and axis get zoomed + constexpr double marginlength = 0.03; + std::string uniquePadName = "RDrawStoragePad" + std::to_string(uniqueId); + fPad = new TPad(uniquePadName.c_str(), "", marginlength, 0.55, 1 - marginlength, 0.87); + fPad->SetTopMargin(0.2); + fPad->SetBottomMargin(0.2); + fPad->SetLeftMargin(0.01); + fPad->SetRightMargin(0.01); + fPad->Draw(); + fPad->cd(); + + // 4. Draw an empty histogram without a y-axis for zooming + std::string uniqueTH1FName = "RDrawStorageTH1F" + std::to_string(uniqueId); + fTH1Fs.emplace_back(new TH1F(uniqueTH1FName.c_str(), "", 500, 0, (double)fTotalNumBytes / fScalingFactorOfAxis)); + fTH1Fs.back()->SetMaximum(1); + fTH1Fs.back()->SetMinimum(0); + fTH1Fs.back()->GetYaxis()->SetTickLength(0); + fTH1Fs.back()->GetYaxis()->SetLabelSize(0); + fTH1Fs.back()->GetXaxis()->SetLabelSize(0.18); + fTH1Fs.back()->SetStats(0); + fTH1Fs.back()->DrawCopy(); + + // 5. Draw all boxes and add possibility to click on RPageBox to obtain information about a page + fHeaderBox->Draw(); + for (const auto &b : fPageBoxes) { + b->Draw(); + } + fFooterBox->Draw(); + fPad->AddExec("ShowPageDetails", "ROOT::Experimental::Detail::RDrawStorage::RPageBoxClicked()"); + + // 6. Draw clusterAxis + // fTexts.at(0) points to Title so skip + for (std::size_t i = 1; i < fTexts.size(); ++i) { + fTexts.at(i)->Draw(); + } + for (const auto &l : fLines) { + l->Draw(); + } + fPad->Update(); + + // 7. Return to canvas, draw title, legend and description of x-axis + fCanvasPtrs.back()->cd(); + fTexts.at(0)->Draw(); // Title + fLegend->Draw(); + TLatex latex; + latex.SetTextSize(0.04); + latex.DrawLatex(0.955, 0.5, fAxisTitle.c_str()); + + fCanvasPtrs.back()->Update(); +} + +void ROOT::Experimental::Detail::RDrawStorage::SetPageIds() +{ + for (std::size_t i = 0; i < fPageBoxes.size(); ++i) { + fPageBoxes.at(i)->SetPageId(i + 1); + } +} + +void ROOT::Experimental::Detail::RDrawStorage::RPageBoxClicked() +{ + int event = gPad->GetEvent(); + if (event != kButton1Up) + return; + TObject *select = gPad->GetSelected(); + if (!select) + return; + if (select->InheritsFrom(ROOT::Experimental::Detail::RPageBox::Class())) { + ROOT::Experimental::Detail::RPageBox *pageBox = (ROOT::Experimental::Detail::RPageBox *)select; + pageBox->Inspect(); + } else if (select->InheritsFrom(ROOT::Experimental::Detail::RMetaDataBox::Class())) { + ROOT::Experimental::Detail::RMetaDataBox *metaBox = (ROOT::Experimental::Detail::RMetaDataBox *)select; + metaBox->Inspect(); + } +} + +std::int32_t ROOT::Experimental::Detail::RDrawStorage::GetColourFromFieldId(ROOT::Experimental::DescriptorId_t fieldId) +{ + std::int32_t colour = 0; + fieldId %= 61; + switch (fieldId % 12) { + case 0: colour = kRed; break; + case 1: colour = kMagenta; break; + case 2: colour = kBlue; break; + case 3: colour = kCyan; break; + case 4: colour = kGreen; break; + case 5: colour = kYellow; break; + case 6: colour = kPink; break; + case 7: colour = kViolet; break; + case 8: colour = kAzure; break; + case 9: colour = kTeal; break; + case 10: colour = kSpring; break; + case 11: colour = kOrange; break; + default: + // never here + assert(false); + break; + } + switch (fieldId / 12) { + case 0: colour -= 2; break; + case 1: break; + case 2: colour += 3; break; + case 3: colour -= 6; break; + case 4: colour -= 9; break; + case 5: + if (fieldId == 60) + return kGray; + default: + // never here + assert(false); + break; + } + return colour; +} + +// ------------------------------ RNTupleDraw ------------------------------- + +ROOT::Experimental::RNTupleDraw::RNTupleDraw(const std::unique_ptr &reader) +{ + if (reader == nullptr) { + std::cout << "The RNTupleReader is invalid! Drawing is not possible with this object." << std::endl; + fEmpty = true; + } + Detail::RDrawStorage::fgDrawStorageVec.emplace_back(reader.get()); + fStorage = &Detail::RDrawStorage::fgDrawStorageVec.back(); +} + + +void ROOT::Experimental::RNTupleDraw::Draw() +{ + if (fEmpty == false) { + fStorage->Draw(); + } else { + std::cout << "Cannot draw object from an empty storage." << std::endl; + } +} diff --git a/tree/ntupledraw/v7/test/CMakeLists.txt b/tree/ntupledraw/v7/test/CMakeLists.txt new file mode 100644 index 0000000000000..791c7b21ab20b --- /dev/null +++ b/tree/ntupledraw/v7/test/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +# @author Simon Leisibach CERN + +ROOT_ADD_GTEST(ntupledraw ntupledraw.cxx LIBRARIES ROOTNTupleDraw) diff --git a/tree/ntupledraw/v7/test/ntupledraw.cxx b/tree/ntupledraw/v7/test/ntupledraw.cxx new file mode 100644 index 0000000000000..b264cda39c026 --- /dev/null +++ b/tree/ntupledraw/v7/test/ntupledraw.cxx @@ -0,0 +1,149 @@ +#include +#include +#include +#include + +#include + +#include "gtest/gtest.h" + +#include +#include + +using RNTupleReader = ROOT::Experimental::RNTupleReader; +using RNTupleWriter = ROOT::Experimental::RNTupleWriter; +using RNTupleModel = ROOT::Experimental::RNTupleModel; +using RFieldBase = ROOT::Experimental::Detail::RFieldBase; +using RNTupleDraw = ROOT::Experimental::RNTupleDraw; +using RDrawStorage = ROOT::Experimental::Detail::RDrawStorage; + +/** + * It is hard to test the exact behaviour inside a TCanvas, so this test only checks if no + * exception occurs and the most important member variables hold the correct value. + */ + +namespace { + /** + * An RAII wrapper around an open temporary file on disk. It cleans up the guarded file when the wrapper object + * goes out of scope. + */ +class FileRaii { +private: + std::string fPath; + TFile *file; +public: + FileRaii(const std::string &path) : + fPath(path), + file(TFile::Open(fPath.c_str(), "RECREATE")) + { } + FileRaii(const FileRaii&) = delete; + FileRaii& operator=(const FileRaii&) = delete; + TFile* GetFile() const { return file; } + std::string GetPath() const { return fPath; } + ~FileRaii() { + file->Close(); + std::remove(fPath.c_str()); + } +}; +} // anonymous namespace + +// Should print a message, that nothing was drawn. +TEST(PrintStorageLayout, emptyNTuple) +{ + FileRaii fileGuard("test_printStorageLayout_empty.root"); + std::string_view ntupleName = "emptyFile"; + { + auto model = RNTupleModel::Create(); + auto ntuple = RNTupleWriter::Recreate(std::move(model), ntupleName, fileGuard.GetPath()); + } // flushes content to file + auto reader = RNTupleReader::Open(ntupleName, fileGuard.GetPath()); + auto draw = RNTupleDraw(reader); + draw.Draw(); + + FileRaii fileGuard2("test_printStorageLayout_empty2.root"); + std::string_view ntupleName2 = "emptyFile2"; + { + auto model = RNTupleModel::Create(); + auto intfld = model->MakeField("intfield"); + auto ntuple = RNTupleWriter::Recreate(std::move(model), ntupleName2, fileGuard2.GetPath()); + } // flushes content to file + auto reader2 = RNTupleReader::Open(ntupleName2, fileGuard2.GetPath()); + auto draw2 = RNTupleDraw(reader2); + draw2.Draw(); +} + +TEST(PrintStorageLayout, rootFile) +{ + FileRaii fileGuard("test_printStorageLayout_rootFile.root"); + std::string_view ntupleName = "rootFile"; + { + auto model = RNTupleModel::Create(); + auto intfld = model->MakeField("intField"); + auto doublefld = model->MakeField("doubleField"); + auto ntuple = RNTupleWriter::Recreate(std::move(model), ntupleName, fileGuard.GetPath()); + *intfld = 9; + *doublefld = 3.14; + ntuple->Fill(); + } // flushes content to file + auto model = RNTupleModel::Create(); + auto intptr = model->MakeField("intField"); + auto reader = RNTupleReader::Open(std::move(model), ntupleName, fileGuard.GetPath()); + auto drawStorage = RDrawStorage(reader.get()); + EXPECT_EQ(3ull, drawStorage.GetNFields()); + EXPECT_EQ(2ull, drawStorage.GetNColumns()); + EXPECT_EQ(1ull, drawStorage.GetNClusters()); + EXPECT_EQ(2ull, drawStorage.GetPageBoxSize()); + drawStorage.Draw(); +} + +TEST(PrintStorageLayout, rawFile) +{ + FileRaii fileGuard("test_printStorageLayout_rawFile.ntuple"); + std::string_view ntupleName = "rawFile"; + { + auto model = RNTupleModel::Create(); + auto stringptr = model->MakeField("stringField"); + auto doubleVecptr = model->MakeField>("doubleVectorField"); + auto ntuple = RNTupleWriter::Recreate(std::move(model), ntupleName, fileGuard.GetPath()); + *stringptr = std::string("bus"); + *doubleVecptr = std::vector(2, 2); + ntuple->Fill(); + } // flushes content to file + auto model = RNTupleModel::Create(); + auto stringptr = model->MakeField("stringField"); + auto reader = RNTupleReader::Open(std::move(model), ntupleName, fileGuard.GetPath()); + auto drawStorage = RDrawStorage(reader.get()); + EXPECT_EQ(4ull, drawStorage.GetNFields()); + EXPECT_EQ(4ull, drawStorage.GetNColumns()); + EXPECT_EQ(1ull, drawStorage.GetNClusters()); + EXPECT_EQ(4ull, drawStorage.GetPageBoxSize()); + drawStorage.Draw(); + // To check if RNTupleReader is not dead after drawing + std::stringstream os; + reader->PrintInfo(ROOT::Experimental::ENTupleInfo::kSummary, os); +} + +TEST(PrintStorageLayout, callTwice) +{ + FileRaii fileGuard("test_printStorageLayout_callTwice.root"); + std::string_view ntupleName = "rootFile"; + { + auto model = RNTupleModel::Create(); + auto intfld = model->MakeField>("intarrayField"); + auto vecvecfld = model->MakeField>>("vecvecdoubleField"); + auto ntuple = RNTupleWriter::Recreate(std::move(model), ntupleName, fileGuard.GetPath()); + *intfld = { 1, 2 }; + *vecvecfld = { { 8.2, 8.3 }, { 3.4, 5,6 } }; + ntuple->Fill(); + } // flushes content to file + auto model = RNTupleModel::Create(); + auto intfld = model->MakeField>("intarrayField"); + auto reader = RNTupleReader::Open(std::move(model), ntupleName, fileGuard.GetPath()); + auto drawStorage = RDrawStorage(reader.get()); + drawStorage.Draw(); + drawStorage.Draw(); + EXPECT_EQ(6ull, drawStorage.GetNFields()); + EXPECT_EQ(4ull, drawStorage.GetNColumns()); + EXPECT_EQ(1ull, drawStorage.GetNClusters()); + EXPECT_EQ(4ull, drawStorage.GetPageBoxSize()); +}