Skip to content

Commit 42fcc76

Browse files
committed
[ntuple] Add flamegraph visualizator backend supporting Speedscope
1 parent 9d2d5f7 commit 42fcc76

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

tree/ntupleutil/inc/ROOT/RNTupleInspector.hxx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ enum class ENTupleInspectorHist {
5050
kUncompressedSize
5151
};
5252

53+
enum class EFlamegraphSpecificationFormat {
54+
kSpeedscopeJSON
55+
};
56+
5357
// clang-format off
5458
/**
5559
\class ROOT::Experimental::RNTupleInspector
@@ -493,6 +497,15 @@ public:
493497
{
494498
PrintFieldTreeAsDot(GetDescriptor().GetFieldZero(), output);
495499
}
500+
501+
/////////////////////////////////////////////////////////////////////////////
502+
/// \brief Print a string that represents the tree of the (sub)fields and columns of an RNTuple in a format which a
503+
/// flamegraph visualizer can render
504+
///
505+
/// \param[in] format The output format for the flamegraph specification (right now only supports Speedscope's JSON)
506+
///
507+
void PrintFieldTreeAsFlamegraphSpecification(EFlamegraphSpecificationFormat format,
508+
std::ostream &output = std::cout) const;
496509
};
497510
} // namespace Experimental
498511
} // namespace ROOT

tree/ntupleutil/src/RNTupleInspector.cxx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,113 @@ void ROOT::Experimental::RNTupleInspector::PrintFieldTreeAsDot(const ROOT::RFiel
565565
if (isZeroField)
566566
output << "}";
567567
}
568+
569+
void ROOT::Experimental::RNTupleInspector::PrintFieldTreeAsFlamegraphSpecification(
570+
EFlamegraphSpecificationFormat format, std::ostream &output) const
571+
{
572+
(void)format; // There is only one format at the moment
573+
574+
const auto &tupleDescriptor = GetDescriptor();
575+
ROOT::DescriptorId_t rootId = tupleDescriptor.GetFieldZeroId();
576+
const auto &rootFieldDescriptor = tupleDescriptor.GetFieldDescriptor(rootId);
577+
578+
struct FrameDescription {
579+
std::string name;
580+
std::string type;
581+
size_t byteSize = 0;
582+
char kind; // 'F' or 'C' for field or column
583+
};
584+
585+
struct TimelineOcurrence {
586+
size_t frameDescriptionIndex;
587+
unsigned int timestamp;
588+
char type; // 'O' or 'C' for open or close
589+
};
590+
591+
std::vector<FrameDescription> frameDescriptions;
592+
std::vector<TimelineOcurrence> timelineOcurrences;
593+
unsigned int currentTime = 0;
594+
595+
auto visitFieldsDFS = [&](auto &self, const ROOT::RFieldDescriptor &fieldDescriptor) -> size_t {
596+
FrameDescription fieldFrame;
597+
fieldFrame.name = tupleDescriptor.GetQualifiedFieldName(fieldDescriptor.GetId());
598+
fieldFrame.type = fieldDescriptor.GetTypeName();
599+
fieldFrame.kind = 'F';
600+
frameDescriptions.push_back(fieldFrame);
601+
602+
size_t frameDescriptionIndex = frameDescriptions.size() - 1;
603+
604+
timelineOcurrences.push_back({frameDescriptionIndex, currentTime, 'O'});
605+
606+
size_t subTreeSize = 0;
607+
const auto &childIds = fieldDescriptor.GetLinkIds();
608+
609+
for (const auto &childFieldId : childIds) {
610+
const auto &childFieldDescriptor = tupleDescriptor.GetFieldDescriptor(childFieldId);
611+
subTreeSize += self(self, childFieldDescriptor);
612+
}
613+
614+
for (const auto &columnDescriptor : tupleDescriptor.GetColumnIterable(fieldDescriptor.GetId())) {
615+
const auto &columnInfo = GetColumnInspector(columnDescriptor.GetPhysicalId());
616+
size_t columnSize = columnInfo.GetCompressedSize();
617+
618+
FrameDescription columnFrame;
619+
620+
columnFrame.name = tupleDescriptor.GetQualifiedFieldName(fieldDescriptor.GetId()) + " [col#" +
621+
std::to_string(columnDescriptor.GetPhysicalId()) + "]";
622+
columnFrame.type = ROOT::Internal::RColumnElementBase::GetColumnTypeName(columnDescriptor.GetType());
623+
columnFrame.byteSize = columnSize;
624+
columnFrame.kind = 'C';
625+
frameDescriptions.push_back(columnFrame);
626+
627+
size_t columnFrameIdx = frameDescriptions.size() - 1;
628+
629+
timelineOcurrences.push_back({columnFrameIdx, currentTime, 'O'});
630+
currentTime += columnSize;
631+
timelineOcurrences.push_back({columnFrameIdx, currentTime, 'C'});
632+
633+
subTreeSize += columnSize;
634+
}
635+
636+
frameDescriptions[frameDescriptionIndex].byteSize = subTreeSize;
637+
638+
timelineOcurrences.push_back({frameDescriptionIndex, currentTime, 'C'});
639+
640+
return subTreeSize;
641+
};
642+
643+
visitFieldsDFS(visitFieldsDFS, rootFieldDescriptor);
644+
645+
output << "{\n";
646+
output << " \"$schema\":\"https://www.speedscope.app/file-format-schema.json\",\n";
647+
output << " \"shared\":{\n";
648+
output << " \"frames\":[\n";
649+
650+
for (size_t i = 0; i < frameDescriptions.size(); ++i) {
651+
output << " { \"name\":\"" << frameDescriptions[i].name
652+
<< "\", \"file\":\"Type: " << frameDescriptions[i].type << ", Size: " << frameDescriptions[i].byteSize
653+
<< "B\" }" << (i + 1 < frameDescriptions.size() ? ",\n" : "\n");
654+
}
655+
656+
output << " ]\n";
657+
output << " },\n";
658+
output << " \"profiles\":[\n";
659+
output << " {\n";
660+
output << " \"type\":\"evented\",\n";
661+
output << " \"name\":\"Flattened Timeline\",\n";
662+
output << " \"unit\":\"bytes\",\n";
663+
output << " \"startValue\":0,\n";
664+
output << " \"endValue\":" << currentTime << ",\n";
665+
output << " \"events\":[\n";
666+
667+
for (size_t i = 0; i < timelineOcurrences.size(); ++i) {
668+
const auto &e = timelineOcurrences[i];
669+
output << " {\"type\":\"" << e.type << "\",\"frame\":" << e.frameDescriptionIndex
670+
<< ",\"at\":" << e.timestamp << "}" << (i + 1 < timelineOcurrences.size() ? ",\n" : "\n");
671+
}
672+
673+
output << " ]\n";
674+
output << " }\n";
675+
output << " ]\n";
676+
output << "}\n";
677+
}

tree/ntupleutil/test/ntuple_inspector.cxx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,3 +862,60 @@ TEST(RNTupleInspector, FieldTreeAsDot)
862862
"</b>int<br></br><b>Type: </b>std::int32_t<br></br><b>ID: </b>1<br></br>>]\n}";
863863
EXPECT_EQ(dot, expected);
864864
}
865+
866+
TEST(RNTupleInspector, FieldTreeAsFlamegraphSpecification)
867+
{
868+
FileRaii fileGuard("test_ntuple_inspector_field_tree_as_flamegraph_specification");
869+
{
870+
auto model = RNTupleModel::Create();
871+
auto fieldFloat1 = model->MakeField<float>("float1");
872+
auto fieldInt = model->MakeField<std::int32_t>("int");
873+
auto writer = RNTupleWriter::Recreate(std::move(model), "ntuple", fileGuard.GetPath());
874+
875+
for (int i = 0; i < 10; ++i) {
876+
*fieldFloat1 = 3.14f * i;
877+
*fieldInt = 42 * i;
878+
writer->Fill();
879+
}
880+
}
881+
auto inspector = RNTupleInspector::Create("ntuple", fileGuard.GetPath());
882+
std::ostringstream flamegraphSpecificationStream;
883+
inspector->PrintFieldTreeAsFlamegraphSpecification(
884+
ROOT::Experimental::EFlamegraphSpecificationFormat::kSpeedscopeJSON, flamegraphSpecificationStream);
885+
const std::string flamegraphSpecification = flamegraphSpecificationStream.str();
886+
const std::string &expected = R"({
887+
"$schema":"https://www.speedscope.app/file-format-schema.json",
888+
"shared":{
889+
"frames":[
890+
{ "name":"", "file":"Type: , Size: 80B" },
891+
{ "name":"float1", "file":"Type: float, Size: 40B" },
892+
{ "name":"float1 [col#0]", "file":"Type: SplitReal32, Size: 40B" },
893+
{ "name":"int", "file":"Type: std::int32_t, Size: 40B" },
894+
{ "name":"int [col#1]", "file":"Type: SplitInt32, Size: 40B" }
895+
]
896+
},
897+
"profiles":[
898+
{
899+
"type":"evented",
900+
"name":"Flattened Timeline",
901+
"unit":"bytes",
902+
"startValue":0,
903+
"endValue":80,
904+
"events":[
905+
{"type":"O","frame":0,"at":0},
906+
{"type":"O","frame":1,"at":0},
907+
{"type":"O","frame":2,"at":0},
908+
{"type":"C","frame":2,"at":40},
909+
{"type":"C","frame":1,"at":40},
910+
{"type":"O","frame":3,"at":40},
911+
{"type":"O","frame":4,"at":40},
912+
{"type":"C","frame":4,"at":80},
913+
{"type":"C","frame":3,"at":80},
914+
{"type":"C","frame":0,"at":80}
915+
]
916+
}
917+
]
918+
}
919+
)";
920+
EXPECT_EQ(flamegraphSpecification, expected);
921+
}

0 commit comments

Comments
 (0)