Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RNTupleDS.cxx
Go to the documentation of this file.
1/// \file RNTupleDS.cxx
2/// \author Jakob Blomer <jblomer@cern.ch>
3/// \author Enrico Guiraud <enrico.guiraud@cern.ch>
4/// \date 2018-10-04
5/// \author Vincenzo Eduardo Padulano CERN
6/// \date 2026-10-19
7
8/*************************************************************************
9 * Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. *
10 * All rights reserved. *
11 * *
12 * For the licensing terms see $ROOTSYS/LICENSE. *
13 * For the list of contributors see $ROOTSYS/README/CREDITS. *
14 *************************************************************************/
15
17#include <ROOT/RDataFrame.hxx>
18#include <ROOT/RDF/Utils.hxx>
19#include <ROOT/RField.hxx>
20#include <ROOT/RFieldUtils.hxx>
23#include <ROOT/RNTupleDS.hxx>
24#include <ROOT/RNTupleTypes.hxx>
25#include <ROOT/RPageStorage.hxx>
26
27#include "RNTupleDSFields.hxx"
28
29#include <TError.h>
30#include <TSystem.h>
31
32#include <cassert>
33#include <limits>
34#include <memory>
35#include <mutex>
36#include <string>
37#include <string_view>
38#include <vector>
39#include <typeinfo>
40#include <type_traits>
41#include <utility>
42#ifdef R__USE_IMT
43#include <ROOT/RSpan.hxx>
44#include <ROOT/RSlotStack.hxx>
47#endif
48
49// clang-format off
50/**
51* \class ROOT::RDF::RNTupleDS
52* \ingroup dataframe
53* \brief The RDataSource implementation for RNTuple. It lets RDataFrame read RNTuple data.
54*
55* An RDataFrame that reads RNTuple data can be constructed using FromRNTuple().
56*
57* For each column containing an array or a collection, a corresponding column `#colname` is available to access
58* `colname.size()` without reading and deserializing the collection values.
59*
60**/
61// clang-format on
62
63namespace ROOT::Internal::RDF {
64/// Every RDF column is represented by exactly one RNTuple field
68
69 RNTupleDS *fDataSource; ///< The data source that owns this column reader
70 RFieldBase *fProtoField; ///< The prototype field from which fField is cloned
71 std::unique_ptr<RFieldBase> fField; ///< The field backing the RDF column
72 std::unique_ptr<RFieldBase::RValue> fValue; ///< The memory location used to read from fField
73 std::shared_ptr<void> fValuePtr; ///< Used to reuse the object created by fValue when reconnecting sources
74 Long64_t fLastEntry = -1; ///< Last entry number that was read
75 /// For chains, the logical entry and the physical entry in any particular file can be different.
76 /// The entry offset stores the logical entry number (sum of all previous physical entries) when a file of the
77 /// corresponding data source was opened.
79
80public:
81 RNTupleColumnReader(RNTupleDS *ds, RFieldBase *protoField) : fDataSource(ds), fProtoField(protoField) {}
82 ~RNTupleColumnReader() override = default;
83
84 /// Connect the field and its subfields to the page source
85 void Connect(RPageSource &source, Long64_t entryOffset)
86 {
87 assert(fLastEntry == -1);
88
89 fEntryOffset = entryOffset;
90
91 // Create a new, real field from the prototype and set its field ID in the context of the given page source
93 {
94 auto descGuard = source.GetSharedDescriptorGuard();
95 // Set the on-disk field IDs for the field and the subfield
96 fField->SetOnDiskId(
97 descGuard->FindFieldId(fDataSource->fFieldId2QualifiedName.at(fProtoField->GetOnDiskId())));
98 auto iProto = fProtoField->cbegin();
99 auto iReal = fField->begin();
100 for (; iReal != fField->end(); ++iProto, ++iReal) {
101 iReal->SetOnDiskId(descGuard->FindFieldId(fDataSource->fFieldId2QualifiedName.at(iProto->GetOnDiskId())));
102 }
103 }
104
105 RFieldZero fieldZero;
107 fieldZero.Attach(std::move(fField));
108 try {
110 } catch (const ROOT::RException &) {
111 fField = std::move(fieldZero.ReleaseSubfields()[0]);
112 auto onDiskType = source.GetSharedDescriptorGuard()->GetFieldDescriptor(fField->GetOnDiskId()).GetTypeName();
113 std::string msg = "RNTupleDS: invalid type \"" + fField->GetTypeName() + "\" for column \"" +
114 fDataSource->fFieldId2QualifiedName[fField->GetOnDiskId()] + "\" with on-disk type \"" +
115 onDiskType + "\"";
116 throw std::runtime_error(msg);
117 }
118 fField = std::move(fieldZero.ReleaseSubfields()[0]);
119
120 if (fValuePtr) {
121 // When the reader reconnects to a new file, the fValuePtr is already set
122 fValue = std::make_unique<RFieldBase::RValue>(fField->BindValue(fValuePtr));
123 fValuePtr = nullptr;
124 } else {
125 // For the first file, create a new object for this field (reader)
126 fValue = std::make_unique<RFieldBase::RValue>(fField->CreateValue());
127 }
128 }
129
130 void Disconnect(bool keepValue)
131 {
132 if (fValue && keepValue) {
133 fValuePtr = fValue->GetPtr<void>();
134 }
135 fValue = nullptr;
136 fField = nullptr;
137 fLastEntry = -1;
138 }
139
140 void *GetImpl(Long64_t entry) final
141 {
142 if (entry != fLastEntry) {
143 fValue->Read(entry - fEntryOffset);
144 fLastEntry = entry;
145 }
146 return fValue->GetPtr<void>().get();
147 }
148};
149} // namespace ROOT::Internal::RDF
150
152
153void ROOT::RDF::RNTupleDS::AddField(const ROOT::RNTupleDescriptor &desc, std::string_view colName,
154 ROOT::DescriptorId_t fieldId, std::vector<RNTupleDS::RFieldInfo> fieldInfos,
155 bool convertToRVec)
156{
157 // As an example for the mapping of RNTuple fields to RDF columns, let's consider an RNTuple
158 // using the following types and with a top-level field named "event" of type Event:
159 //
160 // struct Event {
161 // int id;
162 // std::vector<Track> tracks;
163 // };
164 // struct Track {
165 // std::vector<Hit> hits;
166 // };
167 // struct Hit {
168 // float x;
169 // float y;
170 // };
171 //
172 // AddField() will be called from the constructor with the RNTuple root field (ENTupleStructure::kRecord).
173 // From there, we recurse into the "event" sub field (also ENTupleStructure::kRecord) and further down the
174 // tree of sub fields and expose the following RDF columns:
175 //
176 // "event" [Event]
177 // "event.id" [int]
178 // "event.tracks" [RVec<Track>]
179 // "R_rdf_sizeof_event.tracks" [unsigned int]
180 // "event.tracks.hits" [RVec<RVec<Hit>>]
181 // "R_rdf_sizeof_event.tracks.hits" [RVec<unsigned int>]
182 // "event.tracks.hits.x" [RVec<RVec<float>>]
183 // "R_rdf_sizeof_event.tracks.hits.x" [RVec<unsigned int>]
184 // "event.tracks.hits.y" [RVec<RVec<float>>]
185 // "R_rdf_sizeof_event.tracks.hits.y" [RVec<unsigned int>]
186
187 const auto &fieldDesc = desc.GetFieldDescriptor(fieldId);
188 const auto &nRepetitions = fieldDesc.GetNRepetitions();
189 if ((fieldDesc.GetStructure() == ROOT::ENTupleStructure::kCollection) || (nRepetitions > 0)) {
190 // The field is a collection or a fixed-size array.
191 // We open a new collection scope with fieldID being the inner most collection. E.g. for "event.tracks.hits",
192 // fieldInfos would already contain the fieldID of "event.tracks"
193 fieldInfos.emplace_back(fieldId, nRepetitions);
194 }
195
196 if (fieldDesc.GetStructure() == ROOT::ENTupleStructure::kCollection) {
197 // Inner fields of collections are provided as projected collections of only that inner field,
198 // E.g. we provide a projected collection RVec<RVec<float>> for "event.tracks.hits.x" in the example
199 // above.
200 bool representableAsRVec =
201 convertToRVec && (fieldDesc.GetTypeName().substr(0, 19) == "ROOT::VecOps::RVec<" ||
202 fieldDesc.GetTypeName().substr(0, 12) == "std::vector<" || fieldDesc.GetTypeName() == "");
203 const auto &f = *desc.GetFieldIterable(fieldDesc.GetId()).begin();
204 AddField(desc, colName, f.GetId(), fieldInfos, representableAsRVec);
205
206 // Note that at the end of the recursion, we handled the inner sub collections as well as the
207 // collection as whole, so we are done.
208 return;
209
210 } else if (nRepetitions > 0) {
211 // Fixed-size array, same logic as ROOT::RVec.
212 const auto &f = *desc.GetFieldIterable(fieldDesc.GetId()).begin();
213 AddField(desc, colName, f.GetId(), fieldInfos);
214 return;
215 } else if (fieldDesc.GetStructure() == ROOT::ENTupleStructure::kRecord) {
216 // Inner fields of records are provided as individual RDF columns, e.g. "event.id"
217 for (const auto &f : desc.GetFieldIterable(fieldDesc.GetId())) {
218 auto innerName = colName.empty() ? f.GetFieldName() : (std::string(colName) + "." + f.GetFieldName());
219 // Inner fields of collections of records are always exposed as ROOT::RVec
220 AddField(desc, innerName, f.GetId(), fieldInfos);
221 }
222 }
223
224 // The fieldID could be the root field or the class of fieldId might not be loaded.
225 // In these cases, only the inner fields are exposed as RDF columns.
226 auto fieldOrException = ROOT::RFieldBase::Create(fieldDesc.GetFieldName(), fieldDesc.GetTypeName());
227 if (!fieldOrException)
228 return;
229 auto valueField = fieldOrException.Unwrap();
230 if (const auto cardinalityField = dynamic_cast<const ROOT::RCardinalityField *>(valueField.get())) {
231 // Cardinality fields in RDataFrame are presented as integers
232 if (cardinalityField->As32Bit()) {
233 valueField =
234 std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint32_t>>(fieldDesc.GetFieldName());
235 } else if (cardinalityField->As64Bit()) {
236 valueField =
237 std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint64_t>>(fieldDesc.GetFieldName());
238 } else {
239 R__ASSERT(false && "cardinality field stored with an unexpected integer type");
240 }
241 }
242 valueField->SetOnDiskId(fieldId);
243 for (auto &f : *valueField) {
244 f.SetOnDiskId(desc.FindFieldId(f.GetFieldName(), f.GetParent()->GetOnDiskId()));
245 }
246 std::unique_ptr<ROOT::RFieldBase> cardinalityField;
247 // Collections get the additional "number of" RDF column (e.g. "R_rdf_sizeof_tracks")
248 if (!fieldInfos.empty()) {
249 const auto &info = fieldInfos.back();
250 const std::string name = "R_rdf_sizeof_" + desc.GetFieldDescriptor(info.fFieldId).GetFieldName();
251 if (info.fNRepetitions > 0) {
252 cardinalityField = std::make_unique<ROOT::Internal::RDF::RArraySizeField>(name, info.fNRepetitions);
253 } else {
254 cardinalityField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::size_t>>(name);
255 }
256 cardinalityField->SetOnDiskId(info.fFieldId);
257 }
258
259 for (auto i = fieldInfos.rbegin(); i != fieldInfos.rend(); ++i) {
260 const auto &fieldInfo = *i;
261
262 const auto valueFieldName = valueField->GetFieldName();
263
264 if (fieldInfo.fNRepetitions > 0) {
265 // Fixed-size array, read it as ROOT::RVec in memory
266 valueField =
267 std::make_unique<ROOT::RArrayAsRVecField>(valueFieldName, valueField->Clone("_0"), fieldInfo.fNRepetitions);
268 } else {
269 // Actual collection. A std::vector or ROOT::RVec gets added as a ROOT::RVec. All other collection types keep
270 // their original type.
271 if (convertToRVec) {
272 valueField = std::make_unique<ROOT::RRVecField>(valueFieldName, valueField->Clone("_0"));
273 } else {
274 auto outerFieldType = desc.GetFieldDescriptor(fieldInfo.fFieldId).GetTypeName();
275 valueField = ROOT::RFieldBase::Create(valueFieldName, outerFieldType).Unwrap();
276 }
277 }
278
279 valueField->SetOnDiskId(fieldInfo.fFieldId);
280
281 // Skip the inner-most collection level to construct the cardinality column
282 // It's taken care of by the `if (!fieldInfos.empty())` scope above
283 if (i != fieldInfos.rbegin()) {
284 const auto cardinalityFieldName = cardinalityField->GetFieldName();
285 if (fieldInfo.fNRepetitions > 0) {
286 // This collection level refers to a fixed-size array
287 cardinalityField = std::make_unique<ROOT::RArrayAsRVecField>(
288 cardinalityFieldName, cardinalityField->Clone("_0"), fieldInfo.fNRepetitions);
289 } else {
290 // This collection level refers to an RVec
291 cardinalityField = std::make_unique<ROOT::RRVecField>(cardinalityFieldName, cardinalityField->Clone("_0"));
292 }
293
294 cardinalityField->SetOnDiskId(fieldInfo.fFieldId);
295 }
296 }
297
298 if (cardinalityField) {
299 fColumnNames.emplace_back("R_rdf_sizeof_" + std::string(colName));
300 fColumnTypes.emplace_back(cardinalityField->GetTypeName());
301 fProtoFields.emplace_back(std::move(cardinalityField));
302 }
303
304 fieldInfos.emplace_back(fieldId, nRepetitions);
305 fColumnNames.emplace_back(colName);
306 fColumnTypes.emplace_back(valueField->GetTypeName());
307 fProtoFields.emplace_back(std::move(valueField));
308}
309
310ROOT::RDF::RNTupleDS::RNTupleDS(std::unique_ptr<ROOT::Internal::RPageSource> pageSource)
311{
312 pageSource->Attach();
313 fPrincipalDescriptor = pageSource->GetSharedDescriptorGuard()->Clone();
314 // TODO(vpadulan): file staging is disabled with the current MT strategy, but it would be good to re-enable it.
316 fStagingArea.emplace_back(std::move(pageSource));
317
318 AddField(fPrincipalDescriptor, "", fPrincipalDescriptor.GetFieldZeroId(),
319 std::vector<ROOT::RDF::RNTupleDS::RFieldInfo>());
320
321 auto topLevelFields = fPrincipalDescriptor.GetTopLevelFields();
322 const auto nTopLevelFields = std::distance(topLevelFields.begin(), topLevelFields.end());
323 fTopLevelFieldNames.reserve(nTopLevelFields);
324 for (const auto &field : topLevelFields)
325 fTopLevelFieldNames.push_back(field.GetFieldName());
326}
327
328namespace {
329
330const ROOT::RNTupleReadOptions &GetOpts()
331{
332 // The setting is for now a global one, must be decided before running the
333 // program by setting the appropriate environment variable. Make sure that
334 // option configuration is thread-safe and happens only once.
335 static ROOT::RNTupleReadOptions opts;
336 static std::once_flag flag;
337 std::call_once(flag, []() {
338 if (auto env = gSystem->Getenv("ROOT_RNTUPLE_CLUSTERBUNCHSIZE"); env != nullptr && strlen(env) > 0) {
339 std::string envStr{env};
340 auto envNum{std::stoul(envStr)};
341 envNum = envNum == 0 ? 1 : envNum;
343 }
344 });
345 return opts;
346}
347
348std::unique_ptr<ROOT::Internal::RPageSource> CreatePageSource(std::string_view ntupleName, std::string_view fileName)
349{
350 return ROOT::Internal::RPageSource::Create(ntupleName, fileName, GetOpts());
351}
352} // namespace
353
354ROOT::RDF::RNTupleDS::RNTupleDS(std::string_view ntupleName, std::string_view fileName)
355 : RNTupleDS(CreatePageSource(ntupleName, fileName))
356{
357 fNTupleName = ntupleName;
358 fFileNames = std::vector<std::string>{std::string{fileName}};
359}
360
361ROOT::RDF::RNTupleDS::RNTupleDS(std::string_view ntupleName, const std::vector<std::string> &fileNames)
362 : RNTupleDS(CreatePageSource(ntupleName, fileNames[0]))
363{
364 fNTupleName = ntupleName;
365 fFileNames = fileNames;
366 fStagingArea.resize(fFileNames.size());
367}
368
369ROOT::RDF::RNTupleDS::RNTupleDS(std::string_view ntupleName, const std::vector<std::string> &fileNames,
370 const std::pair<ULong64_t, ULong64_t> &range)
371 : RNTupleDS(ntupleName, fileNames)
372{
373 fGlobalEntryRange = range;
374}
375
377ROOT::RDF::RNTupleDS::GetColumnReadersImpl(std::string_view /* name */, const std::type_info & /* ti */)
378{
379 // This datasource uses the newer GetColumnReaders() API
380 return {};
381}
382
383ROOT::RFieldBase *ROOT::RDF::RNTupleDS::GetFieldWithTypeChecks(std::string_view fieldName, const std::type_info &tid)
384{
385 // At this point we can assume that `name` will be found in fColumnNames
386 const auto index =
387 std::distance(fColumnNames.begin(), std::find(fColumnNames.begin(), fColumnNames.end(), fieldName));
388
389 // A reader was requested but we don't have RTTI for it, this is encoded with the tag UseNativeDataType. We can just
390 // return the available protofield
391 if (tid == typeid(ROOT::Internal::RDF::UseNativeDataType)) {
392 return fProtoFields[index].get();
393 }
394
395 // The user explicitly requested a type
397
398 // If the requested type is different from the proto field that was created when the data source was constructed,
399 // we first have to create an alternative proto field for the column reader.
400 // Otherwise, we can directly use the existing proto field.
401 if (requestedType != fColumnTypes[index]) {
402 auto &altProtoFields = fAlternativeProtoFields[index];
403
404 // If we can find the requested type in the registered alternative protofields, return the corresponding field
405 if (auto altProtoField = std::find_if(altProtoFields.begin(), altProtoFields.end(),
406 [&requestedType](const std::unique_ptr<ROOT::RFieldBase> &fld) {
407 return fld->GetTypeName() == requestedType;
408 });
409 altProtoField != altProtoFields.end()) {
410 return altProtoField->get();
411 }
412
413 // Otherwise, create a new protofield and register it in the alternatives before returning
414 std::unique_ptr<RFieldBase> newAltProtoField;
415 const std::string strName = std::string(fieldName);
416 if (dynamic_cast<ROOT::Internal::RDF::RRDFCardinalityFieldBase *>(fProtoFields[index].get())) {
417 if (requestedType == "bool") {
418 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<bool>>(strName);
419 } else if (requestedType == "char") {
420 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<char>>(strName);
421 } else if (requestedType == "std::int8_t") {
422 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::int8_t>>(strName);
423 } else if (requestedType == "std::uint8_t") {
424 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint8_t>>(strName);
425 } else if (requestedType == "std::int16_t") {
426 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::int16_t>>(strName);
427 } else if (requestedType == "std::uint16_t") {
428 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint16_t>>(strName);
429 } else if (requestedType == "std::int32_t") {
430 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::int32_t>>(strName);
431 } else if (requestedType == "std::uint32_t") {
432 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint32_t>>(strName);
433 } else if (requestedType == "std::int64_t") {
434 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::int64_t>>(strName);
435 } else if (requestedType == "std::uint64_t") {
436 newAltProtoField = std::make_unique<ROOT::Internal::RDF::RRDFCardinalityField<std::uint64_t>>(strName);
437 } else {
438 throw std::runtime_error("RNTupleDS: Could not create field with type \"" + requestedType +
439 "\" for column \"" + std::string(fieldName) + "\"");
440 }
441 } else {
442 auto newAltProtoFieldOrException = ROOT::RFieldBase::Create(strName, requestedType);
443 if (!newAltProtoFieldOrException) {
444 throw std::runtime_error("RNTupleDS: Could not create field with type \"" + requestedType +
445 "\" for column \"" + std::string(fieldName) + "\"");
446 }
447 newAltProtoField = newAltProtoFieldOrException.Unwrap();
448 }
449 newAltProtoField->SetOnDiskId(fProtoFields[index]->GetOnDiskId());
450 auto *newField = newAltProtoField.get();
451 altProtoFields.emplace_back(std::move(newAltProtoField));
452 return newField;
453 }
454
455 // General case: there was a correspondence between the user-requested type and the corresponding column type
456 return fProtoFields[index].get();
457}
458
459std::unique_ptr<ROOT::Detail::RDF::RColumnReaderBase>
460ROOT::RDF::RNTupleDS::GetColumnReaders(unsigned int slot, std::string_view name, const std::type_info &tid)
461{
462 ROOT::RFieldBase *field = GetFieldWithTypeChecks(name, tid);
463 assert(field != nullptr);
464
465 // Map the field's and subfields' IDs to qualified names so that we can later connect the fields to
466 // other page sources from the chain
467 fFieldId2QualifiedName[field->GetOnDiskId()] = fPrincipalDescriptor.GetQualifiedFieldName(field->GetOnDiskId());
468 for (const auto &s : *field) {
469 fFieldId2QualifiedName[s.GetOnDiskId()] = fPrincipalDescriptor.GetQualifiedFieldName(s.GetOnDiskId());
470 }
471
472 auto reader = std::make_unique<ROOT::Internal::RDF::RNTupleColumnReader>(this, field);
473 fActiveColumnReaders[slot].emplace_back(reader.get());
474
475 return reader;
476}
477
479{
480 while (true) {
481 std::unique_lock lock(fMutexStaging);
482 fCvStaging.wait(lock, [this] { return fIsReadyForStaging || fStagingThreadShouldTerminate; });
483 if (fStagingThreadShouldTerminate)
484 return;
485
486 assert(!fHasNextSources);
487 StageNextSources();
488 fHasNextSources = true;
489 fIsReadyForStaging = false;
490
491 lock.unlock();
492 fCvStaging.notify_one();
493 }
494}
495
497{
498 const auto nFiles = fFileNames.empty() ? 1 : fFileNames.size();
499
500 for (auto i = fNextFileIndex; (i < nFiles) && ((i - fNextFileIndex) < fNSlots); ++i) {
501
502 if (fStagingThreadShouldTerminate)
503 return;
504
505 if (fStagingArea[i]) {
506 // The first file is already open and was used to read the schema
507 assert(i == 0);
508 } else {
509 fStagingArea[i] = CreatePageSource(fNTupleName, fFileNames[i]);
510 fStagingArea[i]->LoadStructure();
511 }
512 }
513}
514
516{
517 assert(fNextRanges.empty());
518
519 auto nFiles = fFileNames.empty() ? 1 : fFileNames.size();
520 auto nRemainingFiles = nFiles - fNextFileIndex;
521
522 if (nRemainingFiles == 0)
523 return;
524
525 // Easy work scheduling: one file per slot. We skip empty files (files without entries).
526
527 if ((nRemainingFiles >= fNSlots) || (fGlobalEntryRange.has_value())) {
528 while ((fNextRanges.size() < fNSlots) && (fNextFileIndex < nFiles)) {
529 REntryRangeDS range;
530
531 std::swap(fStagingArea[fNextFileIndex], range.fSource);
532
533 if (!range.fSource) {
534 // Typically, the prestaged source should have been present. Only if some of the files are empty, we need
535 // to open and attach files here.
536 range.fSource = CreatePageSource(fNTupleName, fFileNames[fNextFileIndex]);
537 }
538 range.fFileName = fFileNames[fNextFileIndex];
539 range.fSource->Attach();
540 fNextFileIndex++;
541 auto nEntries = range.fSource->GetNEntries();
542 if (nEntries == 0)
543 continue;
544 range.fLastEntry = nEntries; // whole file per slot, i.e. entry range [0..nEntries - 1]
545
546 fNextRanges.emplace_back(std::move(range));
547 }
548 return;
549 }
550}
551
552std::vector<std::pair<ULong64_t, ULong64_t>> ROOT::RDF::RNTupleDS::GetEntryRanges()
553{
554 assert(!ROOT::IsImplicitMTEnabled()); // MT scheduling doesn't use this function
555 std::vector<std::pair<ULong64_t, ULong64_t>> ranges;
556
557 // We need to distinguish between single threaded and multi-threaded runs.
558 // In single threaded mode, InitSlot is only called once and column readers have to be rewired
559 // to new page sources of the chain in GetEntryRanges. In multi-threaded mode, on the other hand,
560 // InitSlot is called for every returned range, thus rewiring the column readers takes place in
561 // InitSlot and FinalizeSlot.
562 for (auto r : fActiveColumnReaders[0]) {
563 r->Disconnect(true /* keepValue */);
564 }
565
566 // If we have fewer files than slots and we run multiple event loops, we can reuse fCurrentRanges and don't need
567 // to worry about loading the fNextRanges. I.e., in this case we don't enter the if block.
568 if (fCurrentRanges.empty() || fSeenEntriesNoGlobalRange > 0) {
569 // Otherwise, i.e. start of the first event loop or in the middle of the event loop, prepare the next ranges
570 // and swap with the current ones.
571 {
572 std::unique_lock lock(fMutexStaging);
573 fCvStaging.wait(lock, [this] { return fHasNextSources; });
574 }
575 PrepareNextRanges();
576 if (fNextRanges.empty()) {
577 // No more data
578 return ranges;
579 }
580
581 assert(fNextRanges.size() <= fNSlots);
582
583 fCurrentRanges.clear();
584 std::swap(fCurrentRanges, fNextRanges);
585 }
586
587 // Stage next batch of files for the next call to GetEntryRanges()
588 {
589 std::lock_guard _(fMutexStaging);
590 fIsReadyForStaging = true;
591 fHasNextSources = false;
592 }
593 fCvStaging.notify_one();
594
595 // Create ranges for the RDF loop manager from the list of REntryRangeDS records.
596 // The entry ranges that are relative to the page source in REntryRangeDS are translated into absolute
597 // entry ranges, given the current state of the entry cursor.
598 // We remember the connection from first absolute entry index of a range to its REntryRangeDS record
599 // so that we can properly rewire the column reader in InitSlot
600 fFirstEntry2RangeIdx.clear();
601 fOriginalRanges.clear();
602
603 ULong64_t nEntriesPerSource = 0;
604
605 for (std::size_t i = 0; i < fCurrentRanges.size(); ++i) {
606
607 // Several consecutive ranges may operate on the same file (each with their own page source clone).
608 // We can detect a change of file when the first entry number jumps back to 0.
609 if (fCurrentRanges[i].fFirstEntry == 0) {
610 // New source
611 fSeenEntriesNoGlobalRange += nEntriesPerSource;
612 nEntriesPerSource = 0;
613 }
614
615 auto start = fCurrentRanges[i].fFirstEntry + fSeenEntriesNoGlobalRange;
616 auto end = fCurrentRanges[i].fLastEntry + fSeenEntriesNoGlobalRange;
617
618 nEntriesPerSource += end - start;
619
620 if (fGlobalEntryRange.has_value()) {
621
622 // We need to consider different scenarios for when we have GlobalRanges set by the user.
623 // Consider a simple case of 3 files, with original ranges set as (consecutive entries of 3 files):
624 // [0, 20], [20, 45], [45, 65]
625 // we will now see what happens in each of the scenarios below when GlobalRanges can be set to different
626 // values:
627 // a) [2, 5] - we stay in file 1
628 // - hence we will use the 1st case and get the range [2,5], in this case we also need to quit further
629 // processing from the other files by entering case 3
630 // b) [2, 21] - we start in file 1 and finish in file 2
631 // - use the 2nd case first, as 21 > 20 (end of first file), then we will go to case 1, resulting in ranges:
632 // [2, 20], [20, 21], c) [21 - 40] - we skip file 1, start in file 2 and stay in file 2
633 // - to skip the first file, we use the 4th case, followed by the 1st case, resulting range is: [21, 40]
634 // d) [21 - 65] - we skip file 1, start in file 2 and continue to file 3
635 // - to skip the first file, we use the 4th case, we continue with the 2nd case, and use the 1st case at the
636 // end, resulting ranges are [21, 45], [45, 65]
637 // The first case
638 if (fGlobalEntryRange->first >= start && fGlobalEntryRange->second <= end) {
639 fOriginalRanges.emplace_back(start, end);
640 fFirstEntry2RangeIdx[fGlobalEntryRange->first] = i;
641 ranges.emplace_back(fGlobalEntryRange->first, fGlobalEntryRange->second);
642 }
643
644 // The second case:
645 // The `fGlobalEntryRange->first < end` condition is to distinguish this case from the 4th case.
646 else if (fGlobalEntryRange->second > end && fGlobalEntryRange->first < end) {
647 fOriginalRanges.emplace_back(start, end);
648 fFirstEntry2RangeIdx[fGlobalEntryRange->first] = i;
649 ranges.emplace_back(fGlobalEntryRange->first, end);
650 std::optional<std::pair<ULong64_t, ULong64_t>> newvalues({end, fGlobalEntryRange->second});
651 fGlobalEntryRange.swap(newvalues);
652 }
653 // The third case, needed to correctly quit processing if we only stay in the first file
654 else if (fGlobalEntryRange->second < start) {
655 return ranges;
656 }
657
658 // The fourth case:
659 else if (fGlobalEntryRange->first >= end) {
660 fOriginalRanges.emplace_back(start, end);
661 fFirstEntry2RangeIdx[start] = i;
662 ranges.emplace_back(start, start);
663 }
664 }
665
666 else {
667 fFirstEntry2RangeIdx[start] = i;
668 fOriginalRanges.emplace_back(start, end);
669 ranges.emplace_back(start, end);
670 }
671 }
672
673 fSeenEntriesNoGlobalRange += nEntriesPerSource;
674
675 if (fCurrentRanges[0].fSource) {
676 for (auto r : fActiveColumnReaders[0]) {
677 r->Connect(*fCurrentRanges[0].fSource, fOriginalRanges[0].first);
678 }
679 }
680
681 return ranges;
682}
683
684void ROOT::RDF::RNTupleDS::InitSlot(unsigned int slot, ULong64_t columnReaderOffset)
685{
686 // Only MT scheduling uses this function
688 return;
689
690 const auto &entryRangeDS = fActiveRangesPerSlot.at(slot * ROOT::Internal::RDF::CacheLineStep<REntryRangeDS>());
691 assert(entryRangeDS.fSource != nullptr);
692
693 for (auto r : fActiveColumnReaders[slot]) {
694 r->Connect(*entryRangeDS.fSource, columnReaderOffset);
695 }
696}
697
699{
700 // Only MT scheduling uses this function
702 return;
703
704 for (auto r : fActiveColumnReaders[slot]) {
705 r->Disconnect(true /* keepValue */);
706 }
707}
708
709std::string ROOT::RDF::RNTupleDS::GetTypeName(std::string_view colName) const
710{
711 auto colNamePos = std::find(fColumnNames.begin(), fColumnNames.end(), colName);
712
713 if (colNamePos == fColumnNames.end()) {
714 auto msg = std::string("RNTupleDS: There is no column with name \"") + std::string(colName) + "\"";
715 throw std::runtime_error(msg);
716 }
717
718 const auto index = std::distance(fColumnNames.begin(), colNamePos);
719 return fColumnTypes[index];
720}
721
722bool ROOT::RDF::RNTupleDS::HasColumn(std::string_view colName) const
723{
724 return std::find(fColumnNames.begin(), fColumnNames.end(), colName) != fColumnNames.end();
725}
726
728{
729 fSeenEntriesNoGlobalRange = 0;
730 fNextFileIndex = 0;
731 fIsReadyForStaging = fHasNextSources = fStagingThreadShouldTerminate = false;
732 // TODO(vpadulan): file staging is disabled with the current MT strategy, but it would be good to re-enable it.
734 fThreadStaging = std::thread(&RNTupleDS::ExecStaging, this);
735 assert(fNextRanges.empty());
736
737 if (fCurrentRanges.empty() || (fFileNames.size() > fNSlots)) {
738 // First event loop or large number of files: start the staging process.
739 {
740 std::lock_guard _(fMutexStaging);
741 fIsReadyForStaging = true;
742 }
743 fCvStaging.notify_one();
744 } else {
745 // Otherwise, we will reuse fCurrentRanges. Make sure that staging and preparing next ranges will be a noop
746 // (already at the end of the list of files).
747 fNextFileIndex = std::max(fFileNames.size(), std::size_t(1));
748 }
749}
750
752{
753 for (unsigned int i = 0; i < fNSlots; ++i) {
754 for (auto r : fActiveColumnReaders[i]) {
755 r->Disconnect(false /* keepValue */);
756 }
757 }
758 {
759 std::lock_guard _(fMutexStaging);
760 fStagingThreadShouldTerminate = true;
761 }
762 fCvStaging.notify_one();
763 // TODO(vpadulan): file staging is disabled with the current MT strategy, but it would be good to re-enable it.
765 fThreadStaging.join();
766 // If we have a chain with more files than the number of slots, the files opened at the end of the
767 // event loop won't be reused when the event loop restarts, so we can close them.
768 if (fFileNames.size() > fNSlots) {
769 fCurrentRanges.clear();
770 fNextRanges.clear();
771 fStagingArea.clear();
772 fStagingArea.resize(fFileNames.size());
773 }
774}
775
776void ROOT::RDF::RNTupleDS::SetNSlots(unsigned int nSlots)
777{
778 assert(fNSlots == 0);
779 assert(nSlots > 0);
780 fNSlots = nSlots;
781 fActiveColumnReaders.resize(fNSlots);
782 fActiveRangesPerSlot.resize(fNSlots * ROOT::Internal::RDF::CacheLineStep<REntryRangeDS>());
783}
784
785ROOT::RDataFrame ROOT::RDF::FromRNTuple(std::string_view ntupleName, std::string_view fileName)
786{
787 return ROOT::RDataFrame(std::make_unique<ROOT::RDF::RNTupleDS>(ntupleName, fileName));
788}
789
790ROOT::RDataFrame ROOT::RDF::FromRNTuple(std::string_view ntupleName, const std::vector<std::string> &fileNames)
791{
792 return ROOT::RDataFrame(std::make_unique<ROOT::RDF::RNTupleDS>(ntupleName, fileNames));
793}
794
795ROOT::RDF::RSampleInfo ROOT::Internal::RDF::RNTupleDS::CreateSampleInfo(
796 unsigned int slot, const std::unordered_map<std::string, ROOT::RDF::Experimental::RSample *> &sampleMap) const
797{
798 // The same slot ID could be picked multiple times in the same execution, thus
799 // ending up processing different page sources. Here we re-establish the
800 // connection between the slot and the correct page source by retrieving
801 // which range is connected currently to the slot
802
803 const auto &entryRangeDS = (!ROOT::IsImplicitMTEnabled())
804 ? fCurrentRanges[0]
805 : fActiveRangesPerSlot.at(slot * ROOT::Internal::RDF::CacheLineStep<REntryRangeDS>());
806
807 // Missing source if a file does not exist
808 if (!entryRangeDS.fSource)
809 return ROOT::RDF::RSampleInfo{};
810
811 const auto &ntupleName = entryRangeDS.fSource->GetNTupleName();
812 const auto &ntuplePath = entryRangeDS.fFileName;
813 const auto ntupleID = std::string(ntuplePath) + '/' + ntupleName;
814
815 if (sampleMap.empty())
816 return ROOT::RDF::RSampleInfo(ntupleID,
817 std::make_pair(entryRangeDS.fFirstEntry - entryRangeDS.fEntryOffset,
818 entryRangeDS.fLastEntry - entryRangeDS.fEntryOffset),
819 nullptr, fPrincipalDescriptor.GetNEntries());
820
821 if (sampleMap.find(ntupleID) == sampleMap.end())
822 throw std::runtime_error("Full sample identifier '" + ntupleID + "' cannot be found in the available samples.");
823
824 return ROOT::RDF::RSampleInfo(ntupleID,
825 std::make_pair(entryRangeDS.fFirstEntry - entryRangeDS.fEntryOffset,
826 entryRangeDS.fLastEntry - entryRangeDS.fEntryOffset),
827 sampleMap.at(ntupleID), fPrincipalDescriptor.GetNEntries());
828}
829
831 const ROOT::RDF::ColumnNames_t &fileNames,
832 const std::pair<ULong64_t, ULong64_t> &range)
833{
834 std::unique_ptr<ROOT::RDF::RNTupleDS> ds{new ROOT::RDF::RNTupleDS(ntupleName, fileNames, range)};
835 return ROOT::RDataFrame(std::move(ds));
836}
837
838std::pair<std::vector<ROOT::Internal::RNTupleClusterBoundaries>, ROOT::NTupleSize_t>
839ROOT::Internal::RDF::GetClustersAndEntries(std::string_view ntupleName, std::string_view location)
840{
841 auto source = ROOT::Internal::RPageSource::Create(ntupleName, location);
842 source->Attach();
843 // TODO(jblomer): improve MT scheduling such that not all page lists are required right away
844 source->LoadAllPageLists();
845 auto descGuard = source->GetSharedDescriptorGuard();
846 return std::make_pair(ROOT::Internal::GetClusterBoundaries(descGuard.GetConstRef()), descGuard->GetNEntries());
847}
848
849#ifdef R__USE_IMT
850namespace {
851
852/// \brief Implementation of the Range type to be used in a parallel_for when processing RNTuple data with RDataFrame.
853///
854/// The class represents one range of entries (N consecutive clusters) tied to a specific page source to be processed.
855/// The current MT strategy launches an outer parallel_for with exactly one task per page source, then within each of
856/// these launches another parallel_for that will take an instance of this class. The initial state of the class is to
857/// include the whole list of clusters of the corresponding page source. TBB will eagerly split this range in as many
858/// ranges as possible, until there is a full set of non-splittable ranges (based on IsDivisible). The split ranges
859/// only have a different begin and end cluster, there is no page source copying or cloning done by this range, instead
860/// that happens in the ParallelForBody so that it can be limited by the number of RDF slots used.
861class RSplittableRangeOfClusterBoundaries final : public ROOT::Internal::IMTUtils::RAbstractTBBRangeImpl {
862 // A view of all the cluster boundaries in the page source
863 std::span<const ROOT::Internal::RNTupleClusterBoundaries> fClusters;
864 std::size_t fClusterIdxBegin; // inclusive
865 std::size_t fClusterIdxEnd; // exclusive
866
867public:
868 RSplittableRangeOfClusterBoundaries(std::span<const ROOT::Internal::RNTupleClusterBoundaries> clusters,
869 std::size_t clusterIdxBegin, std::size_t clusterIdxEnd)
870 : fClusters(clusters), fClusterIdxBegin(clusterIdxBegin), fClusterIdxEnd(clusterIdxEnd)
871 {
872 assert(fClusterIdxEnd > fClusterIdxBegin);
873 }
874
875 std::unique_ptr<ROOT::Internal::IMTUtils::RAbstractTBBRangeImpl> SplitWork() final
876 {
877 // Split the cluster range in half
878 auto clusterIdxMid = fClusterIdxBegin + (fClusterIdxEnd - fClusterIdxBegin) / 2;
879 auto newClusterIdxBegin = clusterIdxMid;
880 auto newClusterIdxEnd = fClusterIdxEnd;
881 fClusterIdxEnd = clusterIdxMid;
882
883 return std::make_unique<RSplittableRangeOfClusterBoundaries>(fClusters, newClusterIdxBegin, newClusterIdxEnd);
884 }
885
886 bool IsDivisible() const final
887 {
888 // We choose two because the page source prefetches one cluster ahead of the current one.
889 return fClusterIdxEnd - fClusterIdxBegin > 2;
890 }
891
892 bool Empty() const final
893 {
894 // empty is called once at the beginning of the parallel_for, to guard against empty tasks. We only pass tasks
895 // with actual work by construction
896 return false;
897 }
898
899 std::uint64_t GetCurrentBeginEntry() const { return fClusters[fClusterIdxBegin].fFirstEntry; }
900
901 std::uint64_t GetCurrentEndEntry() const { return fClusters[fClusterIdxEnd - 1].fLastEntryPlusOne; }
902
903 bool IsProcessingFirstCluster() const { return fClusterIdxBegin == 0; }
904};
905} // namespace
906
907// For now we need this function as a pass-through for the information contained in a
908// RSplittableRangeOfClusterBoundaries to be available to the rest of the computation graph (e.g. to connect the column
909// readers and update the sample info)
910void ROOT::RDF::RNTupleDS::InsertActiveEntryRange(unsigned int slot, std::string_view fileName,
911 std::shared_ptr<ROOT::Internal::RPageSource> pageSource,
912 std::uint64_t beginEntry, std::uint64_t endEntry,
913 std::uint64_t offset)
914{
915 REntryRangeDS entryRangeDS;
916 entryRangeDS.fFileName = fileName;
917 entryRangeDS.fSource = std::move(pageSource);
918 entryRangeDS.fFirstEntry = beginEntry + offset;
919 entryRangeDS.fLastEntry = endEntry + offset;
920 entryRangeDS.fEntryOffset = offset;
921 fActiveRangesPerSlot.at(slot * ROOT::Internal::RDF::CacheLineStep<REntryRangeDS>()) = std::move(entryRangeDS);
922}
923
924/// \brief Apply the computation graph to an input range.
925/// \param pageSource The page source to read data from.
926/// \param rangeImpl A Range type carrying information on the range of entries to process.
927/// \param lm The RLoopManager which will run the computation graph.
928/// \param fileName Path to the storage location of the page source.
929/// \param slotStack A stack of slots, each thread needs to request one before running the computation graph.
930/// \param processedEntries Cumulating number of processed entries by this computation graph run.
931/// \param columnReaderOffset Offset value to pass to column readers used in this task. The column reader will request
932/// entries from the page source receiving a global entry number, this offset is subtracted so that the column reader
933/// requests the local entry from the page source.
934void ROOT::RDF::RNTupleDS::ParallelForBody(std::shared_ptr<ROOT::Internal::RPageSource> pageSource,
936 ROOT::Detail::RDF::RLoopManager &lm, std::string_view fileName,
938 std::atomic<ULong64_t> &processedEntries, ULong64_t columnReaderOffset)
939{
940 ROOT::Internal::RSlotStackRAII slotRAII{slotStack};
941 const auto &slot = slotRAII.fSlot;
942
943 const auto &range = static_cast<const RSplittableRangeOfClusterBoundaries &>(rangeImpl);
944 const auto &beginEntry = range.GetCurrentBeginEntry();
945 const auto &endEntry = range.GetCurrentEndEntry();
946
947 // We clone the page source only within this function scope, to limit the number of open page sources to 2*fNSlots
948 auto localPageSource = [&]() -> std::shared_ptr<ROOT::Internal::RPageSource> {
949 // In order to avoid an extra Clone, we need to choose one of the ranges that will take the original page source,
950 // we choose it to be the one processing the first valid cluster
951 if (range.IsProcessingFirstCluster())
952 return pageSource;
953
954 auto newPageSource = pageSource->Clone();
955 newPageSource->Attach();
956 return newPageSource;
957 }();
958
959 // Notify the current page source about the current entry range
960 localPageSource->SetEntryRange({beginEntry, endEntry - beginEntry});
961
962 processedEntries.fetch_add(endEntry - beginEntry);
963
964 // At this point we let the RNTupleDS know about the range being processed by this task. This is used for relevant
965 // operations e.g. connecting the column readers as well as populating sample information.
966 InsertActiveEntryRange(slot, fileName, std::move(localPageSource), beginEntry, endEntry, columnReaderOffset);
967
968 lm.RNTupleThreadTask({beginEntry + columnReaderOffset, endEntry + columnReaderOffset}, slot, columnReaderOffset);
969
970 // Clear the active range after having processed it, to avoid keeping around the page source for longer than intended
971 fActiveRangesPerSlot.at(slot * ROOT::Internal::RDF::CacheLineStep<REntryRangeDS>()) = {};
972}
973
974/// \brief Call parallel_for to schedule a splittable task for a page source
975/// \param pool The pool of threads used to schedule the task.
976/// \param pageSource The RNTuple page source for the task.
977/// \param fileName Path to the storage location of the page source.
978/// \param clusterBoundaries A list of cluster boundaries valid for the page source. This may be computed differently,
979/// see ProcessMT.
980/// \param nEntries Number of entries in the page source.
981/// \param lm The RLoopManager which will run the computation graph.
982/// \param slotStack A stack of slots, each thread needs to request one before running the computation graph.
983/// \param processedEntries Cumulating number of processed entries by this computation graph run.
984/// \param globalEntries Cumulating total number of entries in the input dataset.
985///
986/// This function calls the parallel_for overload that takes two arguments, one respecting the Range and the other
987/// respecting the Body requirements. The Range argument is a RSplittableRangeOfClusterBoundaries, a struct with a range
988/// of N cluster boundaries to process (initialised to the whole list of clusters). Immediately when calling
989/// parallel_for, TBB will recursively ask the RSplittableRangeOfClusterBoundaries to split itself into two roughly
990/// equal sub-tasks, until there are only leaf tasks that cannot be split further. This helps balancing cases where some
991/// tasks may end up running much longer than others.
993 ROOT::TThreadExecutor &pool, std::unique_ptr<ROOT::Internal::RPageSource> pageSource, std::string_view fileName,
994 std::span<const ROOT::Internal::RNTupleClusterBoundaries> clusterBoundaries, std::uint64_t nEntries,
995 ROOT::Detail::RDF::RLoopManager &lm, ROOT::Internal::RSlotStack &slotStack, std::atomic<ULong64_t> &processedEntries,
996 std::atomic<ULong64_t> &globalEntries)
997{
998
999 const auto columnReaderOffset{globalEntries.fetch_add(nEntries)};
1000
1001 // We need to share the same page source pointer across all splitted tasks, to decide if we clone it (in most cases)
1002 // or not (one task will take the original page source).
1003 std::shared_ptr<ROOT::Internal::RPageSource> sharedPageSource{std::move(pageSource)};
1004
1005 auto parallelForBody = [this, &sharedPageSource, &lm, &fileName, &slotStack, &processedEntries,
1006 &columnReaderOffset](const ROOT::Internal::IMTUtils::RAbstractTBBRangeImpl &rangeImpl) {
1007 this->ParallelForBody(sharedPageSource, rangeImpl, lm, fileName, slotStack, processedEntries, columnReaderOffset);
1008 };
1009
1011 pool, std::make_unique<RSplittableRangeOfClusterBoundaries>(clusterBoundaries, 0, clusterBoundaries.size()),
1012 parallelForBody);
1013}
1014
1015/// \brief Computes global entry offsets for the dataset
1016///
1017/// Used in MT scheduling when the user selects a global entry range to restrict processing to.
1019{
1020 std::vector<std::uint64_t> fileEntries(fFileNames.size());
1021 auto getEntries = [](std::string_view ntupleName, std::string_view fileName) {
1022 auto source = ROOT::Internal::RPageSource::Create(ntupleName, fileName);
1023 source->Attach();
1024 const auto descGuard = source->GetSharedDescriptorGuard();
1025 return descGuard->GetNEntries();
1026 };
1027 pool.Foreach([&fileEntries, this,
1028 getEntries](std::size_t idx) { fileEntries[idx] = getEntries(fNTupleName, fFileNames[idx]); },
1029 ROOT::TSeq<std::size_t>(fFileNames.size()));
1030
1031 // We want the output list of offsets to start from zero:
1032 // {0, n_entries_file_0, n_entries_file_1 + n_entries_file_0, ...}
1033 std::uint64_t offset{};
1034 std::vector<std::uint64_t> ret;
1035 ret.reserve(fFileNames.size());
1036 for (auto nEntries : fileEntries) {
1037 ret.push_back(offset);
1038 offset += nEntries;
1039 }
1040
1041 return ret;
1042}
1043
1044/// \brief Launch the task(s) for the input file index, taking into account dataset global entry offsets.
1045///
1046/// This function is used in MT scheduling to launch a task processing one input file. It is selected when
1047/// the user specifies a global entry range to restrict processing to. In this scenario we have precomputed the global
1048/// entry offsets of each file upfront so we can then adjust the range of entries in each file accordingly. The task may
1049/// be split into smaller tasks based on entry ranges aligned to cluster boundaries, see ScheduleTaskForPageSource.
1051 std::size_t fileIdx, const std::vector<std::uint64_t> &globalFileOffsets, ROOT::TThreadExecutor &pool,
1052 ROOT::Detail::RDF::RLoopManager &lm, ROOT::Internal::RSlotStack &slotStack, std::atomic<ULong64_t> &processedEntries,
1053 std::atomic<ULong64_t> &globalEntries)
1054{
1055 const auto &fileName = fFileNames[fileIdx];
1056 auto source = CreatePageSource(fNTupleName, fileName);
1057 source->Attach();
1058 // TODO: improve MT scheduling such that not all page lists are required right away
1059 source->LoadAllPageLists();
1060
1061 const std::uint64_t globalFileOffset = globalFileOffsets[fileIdx];
1062 // captured structured bindings are a C++20 extension
1063 const auto globalBegin = fGlobalEntryRange.value().first;
1064 const auto globalEnd = fGlobalEntryRange.value().second;
1065
1066 const auto clustersAndEntries =
1067 [&]() -> std::pair<std::vector<ROOT::Internal::RNTupleClusterBoundaries>, ROOT::NTupleSize_t> {
1068 const auto descGuard = source->GetSharedDescriptorGuard();
1069 const auto nEntries = descGuard->GetNEntries();
1070 // Early return:
1071 // - There are no entries in the file
1072 // - The boundaries of this file are not touched by the global entry range
1073 if (nEntries == 0 || (globalFileOffset + nEntries) < globalBegin || globalFileOffset > globalEnd)
1074 return {{}, ROOT::kInvalidNTupleIndex};
1075
1076 const auto clustersInFile = ROOT::Internal::GetClusterBoundaries(descGuard.GetConstRef());
1077
1078 // The list of cluster boundaries in this file needs to be adjusted if either the beginning or the end of the
1079 // global entry range fall within the boundaries of the entries of this file.
1080 if ((globalBegin >= globalFileOffset) || (globalEnd < (globalFileOffset + nEntries))) {
1081 std::vector<ROOT::Internal::RNTupleClusterBoundaries> adjustedClusters;
1082 for (const auto &cluster : clustersInFile) {
1083 const std::uint64_t localBegin = globalBegin < globalFileOffset ? 0 : globalBegin - globalFileOffset;
1084 const std::uint64_t localEnd =
1085 globalEnd > globalFileOffset + nEntries ? globalFileOffset + nEntries : globalEnd - globalFileOffset;
1086 const auto currentBegin = std::max(cluster.fFirstEntry, localBegin);
1087 const auto currentEnd = std::min(cluster.fLastEntryPlusOne, localEnd);
1088 // This is not satified if the desired begin is larger than the last entry of some cluster
1089 // In this case, this cluster is not going to be processed further
1090 if (currentBegin < currentEnd)
1091 adjustedClusters.push_back(ROOT::Internal::RNTupleClusterBoundaries{currentBegin, currentEnd});
1092 }
1093 return std::make_pair(adjustedClusters, nEntries);
1094 }
1095
1096 return std::make_pair(clustersInFile, nEntries);
1097 }();
1098 const ROOT::NTupleSize_t nEntries = clustersAndEntries.second;
1099 if (nEntries == ROOT::kInvalidNTupleIndex)
1100 return;
1101 const auto &clusters = clustersAndEntries.first;
1102 ScheduleTaskForPageSource(pool, std::move(source), fileName, clusters, nEntries, lm, slotStack, processedEntries,
1103 globalEntries);
1104}
1105
1106/// \brief Launch the task(s) for the input file index
1107///
1108/// This function is used in MT scheduling to launch a task processing one input file. It is selected when
1109/// there is no global entry range. The task may be split into smaller tasks based on entry ranges aligned to cluster
1110/// boundaries, see ScheduleTaskForPageSource.
1113 ROOT::Internal::RSlotStack &slotStack,
1114 std::atomic<ULong64_t> &processedEntries,
1115 std::atomic<ULong64_t> &globalEntries)
1116{
1117 // Evaluate clusters (with local entry numbers) and number of entries for this file
1118 const auto &fileName = fFileNames[fileIdx];
1119 auto source = CreatePageSource(fNTupleName, fFileNames[fileIdx]);
1120 source->Attach();
1121 // TODO: improve MT scheduling such that not all page lists are required right away
1122 source->LoadAllPageLists();
1123 const auto clustersAndEntries = [&]() {
1124 const auto descGuard = source->GetSharedDescriptorGuard();
1125 return std::make_pair(ROOT::Internal::GetClusterBoundaries(descGuard.GetConstRef()), descGuard->GetNEntries());
1126 }();
1127 const ROOT::NTupleSize_t nEntries = clustersAndEntries.second;
1128 if (nEntries == 0)
1129 return;
1130 const auto &clusters = clustersAndEntries.first;
1131 ScheduleTaskForPageSource(pool, std::move(source), fileName, clusters, nEntries, lm, slotStack, processedEntries,
1132 globalEntries);
1133}
1134
1135/// \brief Run the computation graph by scheduling MT tasks
1136///
1137/// The data source interacts with the loop manager and with a pool of threads (via TThreadExecutor) at the same time.
1138/// The function schedules one MT task per input file, eventually calling the parallel_for overload taking two input
1139/// arguments respectively respecting the Range and Body requirements. This means that each of the file-based tasks
1140/// can be split into various sub-tasks, each processing a set of consecutive clusters of the same file. Before
1141/// launching the file-based tasks, the function checks if the user has selected a global range of entries to restrict
1142/// processing to. If so, the function first computes the global entry offsets of all the input files, so they can be
1143/// matched against the user-provided global range. These will be used to adjust the list of clusters of the file before
1144/// spawning the corresponding task.
1146{
1148 ROOT::Internal::RSlotStack slotStack(fNSlots);
1149 std::atomic<ULong64_t> processedEntries(0ull);
1150 std::atomic<ULong64_t> globalEntries(0ull);
1151
1152 if (fGlobalEntryRange.has_value()) {
1153 const auto globalFileOffsets = ComputeGlobalEntryOffsets(pool);
1154 pool.Foreach(
1155 [&](std::size_t fileIdx) {
1156 ScheduleTasksForFileWithGlobalOffset(fileIdx, globalFileOffsets, pool, lm, slotStack, processedEntries,
1157 globalEntries);
1158 },
1159 ROOT::TSeq<std::size_t>(fFileNames.size()));
1160 } else {
1161 pool.Foreach(
1162 [&](std::size_t fileIdx) {
1163 ScheduleTasksForFile(fileIdx, pool, lm, slotStack, processedEntries, globalEntries);
1164 },
1165 ROOT::TSeq<std::size_t>(fFileNames.size()));
1166 }
1167
1168 if (fGlobalEntryRange.has_value()) {
1169 const auto [begin, end] = fGlobalEntryRange.value();
1170 ULong64_t finalProcessedEntries = processedEntries.load();
1171 // This check serves two purposes:
1172 // - The immediate purpose is for when the user specifies an ending entry of the range which is actually beyond
1173 // the true last entry in the dataset. This check will inform the user they should restrict their input range.
1174 // This case is also tested in dataframe_datasetspec.cxx.
1175 // - The second purpose is as a sanity check that we indeed processed as many entries as the user wanted. This
1176 // is what we expect, but we take the chance here as a last resort to double check that our expectation
1177 // corresponds to reality.
1178 if ((end - begin) > finalProcessedEntries) {
1179 Warning("RDataFrame::Run",
1180 "RDataFrame stopped processing after %lld entries, whereas an entry range (begin=%lld,end=%lld) was "
1181 "requested. Consider adjusting the end value of the entry range to a maximum of %lld.",
1182 finalProcessedEntries, begin, end, begin + finalProcessedEntries);
1183 }
1184 }
1185}
1186#endif
#define f(i)
Definition RSha256.hxx:104
long long Long64_t
Portable signed long integer 8 bytes.
Definition RtypesCore.h:84
unsigned long long ULong64_t
Portable unsigned long integer 8 bytes.
Definition RtypesCore.h:85
#define R__ASSERT(e)
Checks condition e and reports a fatal error if it's false.
Definition TError.h:139
void Warning(const char *location, const char *msgfmt,...)
Use this function in warning situations.
Definition TError.cxx:252
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h offset
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t r
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t index
char name[80]
Definition TGX11.cxx:142
R__EXTERN TSystem * gSystem
Definition TSystem.h:582
#define _(A, B)
Definition cfortran.h:112
The head node of a RDF computation graph.
void RNTupleThreadTask(const std::pair< ULong64_t, ULong64_t > &entryRange, unsigned int slot, std::uint64_t columnReaderOffset)
The task run by every thread on the input entry range, for the RNTuple data source.
Abstract implementation of the oneAPI Range requirements.
Every RDF column is represented by exactly one RNTuple field.
Definition RNTupleDS.cxx:65
void * GetImpl(Long64_t entry) final
void Connect(RPageSource &source, Long64_t entryOffset)
Connect the field and its subfields to the page source.
Definition RNTupleDS.cxx:85
RNTupleColumnReader(RNTupleDS *ds, RFieldBase *protoField)
Definition RNTupleDS.cxx:81
std::unique_ptr< RFieldBase::RValue > fValue
The memory location used to read from fField.
Definition RNTupleDS.cxx:72
std::unique_ptr< RFieldBase > fField
The field backing the RDF column.
Definition RNTupleDS.cxx:71
Long64_t fEntryOffset
For chains, the logical entry and the physical entry in any particular file can be different.
Definition RNTupleDS.cxx:78
std::shared_ptr< void > fValuePtr
Used to reuse the object created by fValue when reconnecting sources.
Definition RNTupleDS.cxx:73
RNTupleDS * fDataSource
The data source that owns this column reader.
Definition RNTupleDS.cxx:69
RFieldBase * fProtoField
The prototype field from which fField is cloned.
Definition RNTupleDS.cxx:70
Long64_t fLastEntry
Last entry number that was read.
Definition RNTupleDS.cxx:74
Base for an artificial field that transforms an RNTuple column containing the offset of collections i...
static void SetClusterBunchSize(RNTupleReadOptions &options, unsigned int val)
Abstract interface to read data from an ntuple.
RSharedDescriptorGuard GetSharedDescriptorGuard() const
Takes the read lock for the descriptor.
static std::unique_ptr< RPageSource > Create(std::string_view ntupleName, std::string_view location, const ROOT::RNTupleReadOptions &options=ROOT::RNTupleReadOptions())
Guess the concrete derived page source from the file name (location)
A thread-safe list of N indexes (0 to size - 1).
An artificial field that transforms an RNTuple column that contains the offset of collections into co...
Definition RField.hxx:369
std::vector< void * > Record_t
friend void ROOT::Internal::RDF::ProcessMT(RDataSource &, ROOT::Detail::RDF::RLoopManager &)
std::optional< std::pair< ULong64_t, ULong64_t > > fGlobalEntryRange
The RDataSource implementation for RNTuple.
void AddField(const ROOT::RNTupleDescriptor &desc, std::string_view colName, ROOT::DescriptorId_t fieldId, std::vector< RFieldInfo > fieldInfos, bool convertToRVec=true)
Provides the RDF column "colName" given the field identified by fieldID.
std::vector< std::pair< ULong64_t, ULong64_t > > GetEntryRanges() final
Return ranges of entries to distribute to tasks.
void ScheduleTaskForPageSource(ROOT::TThreadExecutor &pool, std::unique_ptr< ROOT::Internal::RPageSource > pageSource, std::string_view fileName, std::span< const ROOT::Internal::RNTupleClusterBoundaries > clusterBoundaries, std::uint64_t nEntries, ROOT::Detail::RDF::RLoopManager &lm, ROOT::Internal::RSlotStack &slotStack, std::atomic< ULong64_t > &processedEntries, std::atomic< ULong64_t > &globalEntries)
Call parallel_for to schedule a splittable task for a page source.
void InsertActiveEntryRange(unsigned int slot, std::string_view fileName, std::shared_ptr< ROOT::Internal::RPageSource > pageSource, std::uint64_t beginEntry, std::uint64_t endEntry, std::uint64_t offset)
void ExecStaging()
The main function of the fThreadStaging background thread.
void ScheduleTasksForFile(std::size_t fileIdx, ROOT::TThreadExecutor &pool, ROOT::Detail::RDF::RLoopManager &lm, ROOT::Internal::RSlotStack &slotStack, std::atomic< ULong64_t > &processedEntries, std::atomic< ULong64_t > &globalEntries)
Launch the task(s) for the input file index.
std::unique_ptr< ROOT::Detail::RDF::RColumnReaderBase > GetColumnReaders(unsigned int, std::string_view, const std::type_info &) final
If the other GetColumnReaders overload returns an empty vector, this overload will be called instead.
std::vector< std::unique_ptr< ROOT::RFieldBase > > fProtoFields
We prepare a prototype field for every column.
void SetNSlots(unsigned int nSlots) final
Inform RDataSource of the number of processing slots (i.e.
ROOT::RFieldBase * GetFieldWithTypeChecks(std::string_view fieldName, const std::type_info &tid)
std::vector< std::string > fFileNames
void InitSlot(unsigned int slot, ULong64_t firstEntry) final
Convenience method called at the start of the data processing associated to a slot.
RNTupleDS(std::unique_ptr< ROOT::Internal::RPageSource > pageSource)
void ParallelForBody(std::shared_ptr< ROOT::Internal::RPageSource > pageSource, const ROOT::Internal::IMTUtils::RAbstractTBBRangeImpl &rangeImpl, ROOT::Detail::RDF::RLoopManager &lm, std::string_view fileName, ROOT::Internal::RSlotStack &slotStack, std::atomic< ULong64_t > &processedEntries, ULong64_t columnReaderOffset)
Apply the computation graph to an input range.
std::string GetTypeName(std::string_view colName) const final
Type of a column as a string, e.g.
std::unordered_map< ROOT::DescriptorId_t, std::string > fFieldId2QualifiedName
Connects the IDs of active proto fields and their subfields to their fully qualified name (a....
std::string fNTupleName
The data source may be constructed with an ntuple name and a list of files.
void ScheduleTasksForFileWithGlobalOffset(std::size_t fileIdx, const std::vector< std::uint64_t > &globalFileOffsets, ROOT::TThreadExecutor &pool, ROOT::Detail::RDF::RLoopManager &lm, ROOT::Internal::RSlotStack &slotStack, std::atomic< ULong64_t > &processedEntries, std::atomic< ULong64_t > &globalEntries)
Launch the task(s) for the input file index, taking into account dataset global entry offsets.
void PrepareNextRanges()
Populates fNextRanges with the next set of entry ranges.
void StageNextSources()
Starting from fNextFileIndex, opens the next fNSlots files.
std::vector< std::uint64_t > ComputeGlobalEntryOffsets(ROOT::TThreadExecutor &pool) const
Computes global entry offsets for the dataset.
void Finalize() final
Convenience method called after concluding an event-loop.
std::vector< std::shared_ptr< ROOT::Internal::RPageSource > > fStagingArea
The staging area is relevant for chains of files, i.e.
std::vector< std::string > fColumnTypes
void Initialize() final
Convenience method called before starting an event-loop.
std::vector< std::string > fColumnNames
bool HasColumn(std::string_view colName) const final
Checks if the dataset has a certain column.
Record_t GetColumnReadersImpl(std::string_view name, const std::type_info &) final
type-erased vector of pointers to pointers to column values - one per slot
void FinalizeSlot(unsigned int slot) final
Convenience method called at the end of the data processing associated to a slot.
This type represents a sample identifier, to be used in conjunction with RDataFrame features such as ...
ROOT's RDataFrame offers a modern, high-level interface for analysis of data stored in TTree ,...
Base class for all ROOT issued exceptions.
Definition RError.hxx:78
A field translates read and write calls from/to underlying columns to/from tree values.
RConstSchemaIterator cbegin() const
const std::string & GetFieldName() const
static RResult< std::unique_ptr< RFieldBase > > Create(const std::string &fieldName, const std::string &typeName, const ROOT::RCreateFieldOptions &options, const ROOT::RNTupleDescriptor *desc, ROOT::DescriptorId_t fieldId)
Factory method to resurrect a field from the stored on-disk type information.
ROOT::DescriptorId_t GetOnDiskId() const
std::unique_ptr< RFieldBase > Clone(std::string_view newName) const
Copies the field and its subfields using a possibly new name and a new, unconnected set of columns.
std::uint64_t GetNRepetitions() const
const std::string & GetFieldName() const
const std::string & GetTypeName() const
The container field for an ntuple model, which itself has no physical representation.
Definition RField.hxx:58
std::vector< std::unique_ptr< RFieldBase > > ReleaseSubfields()
Moves all subfields into the returned vector.
Definition RField.cxx:64
void Attach(std::unique_ptr< RFieldBase > child)
A public version of the Attach method that allows piece-wise construction of the zero field.
Definition RField.cxx:40
The on-storage metadata of an RNTuple.
RFieldDescriptorIterable GetFieldIterable(const RFieldDescriptor &fieldDesc) const
const RFieldDescriptor & GetFieldDescriptor(ROOT::DescriptorId_t fieldId) const
ROOT::DescriptorId_t FindFieldId(std::string_view fieldName, ROOT::DescriptorId_t parentId) const
Common user-tunable settings for reading RNTuples.
A pseudo container class which is a generator of indices.
Definition TSeq.hxx:67
This class provides a simple interface to execute the same task multiple times in parallel threads,...
void Foreach(F func, unsigned nTimes, unsigned nChunks=0)
Execute a function without arguments several times in parallel, dividing the execution in nChunks.
virtual const char * Getenv(const char *env)
Get environment variable.
Definition TSystem.cxx:1687
void ParallelFor(ROOT::TThreadExecutor &pool, std::unique_ptr< RAbstractTBBRangeImpl > rangeImpl, const std::function< void(const RAbstractTBBRangeImpl &)> &body)
std::string TypeID2TypeName(const std::type_info &id)
Returns the name of a type starting from its type_info An empty string is returned in case of failure...
Definition RDFUtils.cxx:212
ROOT::RDataFrame FromRNTuple(std::string_view ntupleName, const std::vector< std::string > &fileNames, const std::pair< ULong64_t, ULong64_t > &range)
Internal overload of the function that allows passing a range of entries.
std::pair< std::vector< ROOT::Internal::RNTupleClusterBoundaries >, ROOT::NTupleSize_t > GetClustersAndEntries(std::string_view ntupleName, std::string_view location)
Retrieves the cluster boundaries and the number of entries for the input RNTuple.
void SetAllowFieldSubstitutions(RFieldZero &fieldZero, bool val)
Definition RField.cxx:35
void CallConnectPageSourceOnField(RFieldBase &, ROOT::Internal::RPageSource &)
std::vector< ROOT::Internal::RNTupleClusterBoundaries > GetClusterBoundaries(const RNTupleDescriptor &desc)
Return the cluster boundaries for each cluster in this RNTuple.
std::string GetRenormalizedTypeName(const std::string &metaNormalizedName)
Given a type name normalized by ROOT meta, renormalize it for RNTuple. E.g., insert std::prefix.
RDataFrame FromRNTuple(std::string_view ntupleName, std::string_view fileName)
std::vector< std::string > ColumnNames_t
Bool_t IsImplicitMTEnabled()
Returns true if the implicit multi-threading in ROOT is enabled.
Definition TROOT.cxx:673
std::uint64_t DescriptorId_t
Distriniguishes elements of the same type within a descriptor, e.g. different fields.
constexpr NTupleSize_t kInvalidNTupleIndex
std::uint64_t NTupleSize_t
Integer type long enough to hold the maximum number of entries in a column.
Tag to let data sources use the native data type when creating a column reader.
Definition Utils.hxx:334
A RAII object to pop and push slot numbers from a RSlotStack object.
Page source information consumed while running the computation graph.
std::string_view fFileName
Offset of both first and last entries w.r.t. the page source.
ULong64_t fLastEntry
End entry index in fSource, e.g. the number of entries in the range is fLastEntry - fFirstEntry.
ULong64_t fFirstEntry
First entry index in fSource.
std::shared_ptr< ROOT::Internal::RPageSource > fSource
The page source. It can be shared between this object and other multithreaded tasks.