Project
Loading...
Searching...
No Matches
TTreePlugin.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
13#include "Framework/Plugins.h"
14#include "Framework/Signpost.h"
15#include "Framework/BigEndian.h"
16#include <TBufferFile.h>
17#include <TBufferIO.h>
18#include <arrow/buffer.h>
19#include <arrow/dataset/file_base.h>
20#include <arrow/extension_type.h>
21#include <arrow/memory_pool.h>
22#include <arrow/status.h>
23#include <arrow/type.h>
24#include <arrow/type_fwd.h>
25#include <arrow/util/key_value_metadata.h>
26#include <arrow/array/array_nested.h>
27#include <arrow/array/array_primitive.h>
28#include <arrow/array/builder_nested.h>
29#include <arrow/array/builder_primitive.h>
30#include <arrow/array/util.h>
31#include <arrow/record_batch.h>
32#include <TTree.h>
33#include <TBranch.h>
34#include <TFile.h>
35#include <TLeaf.h>
36#include <unistd.h>
37#include <cstdint>
38#include <memory>
39#include <stdexcept>
40
42
43namespace o2::framework
44{
45
46enum struct ReadOpKind {
47 Unknown,
48 Offsets,
49 Values,
51 VLA
52};
53
54struct ReadOps {
55 TBranch* branch = nullptr;
56 std::shared_ptr<arrow::Buffer> targetBuffer = nullptr;
57 int64_t rootBranchEntries = 0;
58 size_t typeSize = 0;
59 size_t listSize = 0;
60 // If this is an offset reading op, keep track of the actual
61 // range for the offsets, not only how many VLAs are there.
62 int64_t offsetCount = 0;
64};
65
71{
72 public:
73 explicit TTreeDeferredReadOutputStream(std::vector<ReadOps>& ops,
74 const std::shared_ptr<arrow::ResizableBuffer>& buffer);
75
82 static arrow::Result<std::shared_ptr<TTreeDeferredReadOutputStream>> Create(
83 std::vector<ReadOps>& ops,
84 int64_t initial_capacity = 4096,
85 arrow::MemoryPool* pool = arrow::default_memory_pool());
86
87 // By the time we call the destructor, the contents
88 // of the buffer are already moved to fairmq
89 // for being sent.
90 ~TTreeDeferredReadOutputStream() override = default;
91
92 // Implement the OutputStream interface
93
95 arrow::Status Close() override;
96 [[nodiscard]] bool closed() const override;
97 [[nodiscard]] arrow::Result<int64_t> Tell() const override;
98 arrow::Status Write(const void* data, int64_t nbytes) override;
99
101 using OutputStream::Write;
103
105 arrow::Result<std::shared_ptr<arrow::Buffer>> Finish();
106
112 arrow::Status Reset(std::vector<ReadOps> ops,
113 int64_t initial_capacity, arrow::MemoryPool* pool);
114
115 [[nodiscard]] int64_t capacity() const { return capacity_; }
116
117 private:
119 std::vector<ReadOps> ops_;
120
121 // Ensures there is sufficient space available to write nbytes
122 arrow::Status Reserve(int64_t nbytes);
123
124 std::shared_ptr<arrow::ResizableBuffer> buffer_;
125 bool is_open_;
126 int64_t capacity_;
127 int64_t position_;
128 uint8_t* mutable_data_;
129};
130
131static constexpr int64_t kBufferMinimumSize = 256;
132
133TTreeDeferredReadOutputStream::TTreeDeferredReadOutputStream()
134 : is_open_(false), capacity_(0), position_(0), mutable_data_(nullptr) {}
135
136TTreeDeferredReadOutputStream::TTreeDeferredReadOutputStream(std::vector<ReadOps>& ops,
137 const std::shared_ptr<arrow::ResizableBuffer>& buffer)
138 : ops_(ops),
139 buffer_(buffer),
140 is_open_(true),
141 capacity_(buffer->size()),
142 position_(0),
143 mutable_data_(buffer->mutable_data()) {}
144
145arrow::Result<std::shared_ptr<TTreeDeferredReadOutputStream>> TTreeDeferredReadOutputStream::Create(
146 std::vector<ReadOps>& ops,
147 int64_t initial_capacity, arrow::MemoryPool* pool)
148{
149 // ctor is private, so cannot use make_shared
150 auto ptr = std::shared_ptr<TTreeDeferredReadOutputStream>(new TTreeDeferredReadOutputStream);
151 RETURN_NOT_OK(ptr->Reset(ops, initial_capacity, pool));
152 return ptr;
153}
154
155arrow::Status TTreeDeferredReadOutputStream::Reset(std::vector<ReadOps> ops,
156 int64_t initial_capacity, arrow::MemoryPool* pool)
157{
158 ARROW_ASSIGN_OR_RAISE(buffer_, AllocateResizableBuffer(initial_capacity, pool));
159 ops_ = ops;
160 is_open_ = true;
161 capacity_ = initial_capacity;
162 position_ = 0;
163 mutable_data_ = buffer_->mutable_data();
164 return arrow::Status::OK();
165}
166
168{
169 if (is_open_) {
170 is_open_ = false;
171 if (position_ < capacity_) {
172 RETURN_NOT_OK(buffer_->Resize(position_, false));
173 }
174 }
175 return arrow::Status::OK();
176}
177
178bool TTreeDeferredReadOutputStream::closed() const { return !is_open_; }
179
180arrow::Result<std::shared_ptr<arrow::Buffer>> TTreeDeferredReadOutputStream::Finish()
181{
182 RETURN_NOT_OK(Close());
183 buffer_->ZeroPadding();
184 is_open_ = false;
185 return std::move(buffer_);
186}
187
188arrow::Result<int64_t> TTreeDeferredReadOutputStream::Tell() const { return position_; }
189
190// Bulk reads follow the basket boundaries in the file, so a corrupted file must not overrun the target buffers.
191auto checkReadRange = [](ReadOps const& op, int readEntries, int readLast) {
192 if (readLast <= 0) {
193 throw runtime_error_f("Error while reading branch %s starting from %d: got %d entries.", op.branch->GetName(), readEntries, readLast);
194 }
195 if (static_cast<int64_t>(readEntries) + readLast > op.rootBranchEntries) {
196 throw runtime_error_f("Invalid read range for branch %s: starting from %d, read %d entries, total entries %lld.",
197 op.branch->GetName(), readEntries, readLast, static_cast<long long>(op.rootBranchEntries));
198 }
199};
200
201auto checkBasketBytes = [](ReadOps const& op, int readEntries, int64_t bytesNeeded, TBufferFile const& rootBuffer) {
202 int64_t available = static_cast<int64_t>(rootBuffer.BufferSize()) - rootBuffer.Length();
203 if (bytesNeeded < 0 || bytesNeeded > available) {
204 throw runtime_error_f("Basket of branch %s starting from %d holds %lld bytes, but %lld are needed.",
205 op.branch->GetName(), readEntries, static_cast<long long>(available), static_cast<long long>(bytesNeeded));
206 }
207};
208
210 int readEntries = 0;
211 rootBuffer.Reset();
212 while (readEntries < op.rootBranchEntries) {
213 auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
214 checkReadRange(op, readEntries, readLast);
215 int size = readLast * op.listSize;
216 checkBasketBytes(op, readEntries, static_cast<int64_t>(size) * op.typeSize, rootBuffer);
217 readEntries += readLast;
218 bigEndianCopy(target, rootBuffer.GetCurrent(), size, op.typeSize);
219 target += (ptrdiff_t)(size * op.typeSize);
220 }
221};
222
224 int readEntries = 0;
225 rootBuffer.Reset();
226 // Set to 0
227 memset(target, 0, op.targetBuffer->size());
228 int readLast = 0;
229 while (readEntries < op.rootBranchEntries) {
230 auto beginValue = readEntries;
231 readLast = op.branch->GetBulkRead().GetBulkEntries(readEntries, rootBuffer);
232 checkReadRange(op, readEntries, readLast);
233 int size = readLast * op.listSize;
234 checkBasketBytes(op, readEntries, size, rootBuffer);
235 readEntries += readLast;
236 for (int i = beginValue; i < beginValue + size; ++i) {
237 auto value = static_cast<uint8_t>(rootBuffer.GetCurrent()[i - beginValue] << (i % 8));
238 target[i / 8] |= value;
239 }
240 }
241};
242
244 int readEntries = 0;
245 // The offsets are only valid for as many entries as the size branch has.
246 if (op.rootBranchEntries != offsetOp.rootBranchEntries) {
247 throw runtime_error_f("Branch %s has %lld entries, but its size branch %s has %lld.",
248 op.branch->GetName(), static_cast<long long>(op.rootBranchEntries),
249 offsetOp.branch->GetName(), static_cast<long long>(offsetOp.rootBranchEntries));
250 }
251 auto* tPtrOffset = reinterpret_cast<const int*>(offsetOp.targetBuffer->data());
252 std::span<int const> const offsets{tPtrOffset, tPtrOffset + offsetOp.rootBranchEntries + 1};
253
254 rootBuffer.Reset();
255 while (readEntries < op.rootBranchEntries) {
256 auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
257 checkReadRange(op, readEntries, readLast);
258 int size = offsets[readEntries + readLast] - offsets[readEntries];
259 if (size < 0) {
260 throw runtime_error_f("Invalid offset range for branch %s: offsets[%d]=%d, offsets[%d]=%d.",
261 op.branch->GetName(), readEntries, offsets[readEntries], readEntries + readLast, offsets[readEntries + readLast]);
262 }
263 checkBasketBytes(op, readEntries, static_cast<int64_t>(size) * op.typeSize, rootBuffer);
264 readEntries += readLast;
265 bigEndianCopy(target, rootBuffer.GetCurrent(), size, op.typeSize);
266 target += (ptrdiff_t)(size * op.typeSize);
267 }
268};
269
271{
272 // FIXME: we will need more than one once we have multithreaded reading.
273 static TBufferFile rootBuffer{TBuffer::EMode::kWrite, 4 * 1024 * 1024};
274 return rootBuffer;
275}
276
277arrow::Status TTreeDeferredReadOutputStream::Write(const void* data, int64_t nbytes)
278{
279 if (ARROW_PREDICT_FALSE(!is_open_)) {
280 return arrow::Status::IOError("OutputStream is closed");
281 }
282 if (ARROW_PREDICT_TRUE(nbytes == 0)) {
283 return arrow::Status::OK();
284 }
285 if (ARROW_PREDICT_FALSE(position_ + nbytes >= capacity_)) {
286 RETURN_NOT_OK(Reserve(nbytes));
287 }
288 // This is a real address which needs to be copied. Do it!
289 auto ref = (int64_t)data;
290 if (ref >= ops_.size()) {
291 memcpy(mutable_data_ + position_, data, nbytes);
292 position_ += nbytes;
293 return arrow::Status::OK();
294 }
295 auto& op = ops_[ref];
296
297 switch (op.kind) {
298 // Offsets need to be read in advance because we need to know
299 // how many elements are there in total (since TTree does not allow discovering such informantion)
301 break;
303 readValues(mutable_data_ + position_, op, rootBuffer());
304 break;
305 case ReadOpKind::VLA:
306 readVLAValues(mutable_data_ + position_, op, ops_[ref - 1], rootBuffer());
307 break;
309 readBoolValues(mutable_data_ + position_, op, rootBuffer());
310 break;
312 throw runtime_error("Unknown Op");
313 }
314 op.branch->SetStatus(false);
315 op.branch->DropBaskets("all");
316 op.branch->Reset();
317 op.branch->GetTransientBuffer(0)->Expand(0);
318
319 position_ += nbytes;
320 return arrow::Status::OK();
321}
322
323arrow::Status TTreeDeferredReadOutputStream::Reserve(int64_t nbytes)
324{
325 // Always overallocate by doubling. It seems that it is a better growth
326 // strategy, at least for memory_benchmark.cc.
327 // This may be because it helps match the allocator's allocation buckets
328 // more exactly. Or perhaps it hits a sweet spot in jemalloc.
329 int64_t new_capacity = std::max(kBufferMinimumSize, capacity_);
330 new_capacity = position_ + nbytes;
331 if (new_capacity > capacity_) {
332 RETURN_NOT_OK(buffer_->Resize(new_capacity));
333 capacity_ = new_capacity;
334 mutable_data_ = buffer_->mutable_data();
335 }
336 return arrow::Status::OK();
337}
338
340{
341 public:
342 TTreeFileWriteOptions(std::shared_ptr<arrow::dataset::FileFormat> format)
343 : FileWriteOptions(format)
344 {
345 }
346};
347
348// A filesystem which allows me to get a TTree
350{
351 public:
353
354 arrow::Result<std::shared_ptr<arrow::io::OutputStream>> OpenOutputStream(
355 const std::string& path,
356 const std::shared_ptr<const arrow::KeyValueMetadata>& metadata) override;
357
358 virtual std::unique_ptr<TTree>& GetTree(arrow::dataset::FileSource source) = 0;
359};
360
362{
363 size_t& mTotCompressedSize;
364 size_t& mTotUncompressedSize;
365
366 public:
367 TTreeFileFormat(size_t& totalCompressedSize, size_t& totalUncompressedSize)
368 : FileFormat({}),
369 mTotCompressedSize(totalCompressedSize),
370 mTotUncompressedSize(totalUncompressedSize)
371 {
372 }
373
374 ~TTreeFileFormat() override = default;
375
376 std::string type_name() const override
377 {
378 return "ttree";
379 }
380
381 bool Equals(const FileFormat& other) const override
382 {
383 return other.type_name() == this->type_name();
384 }
385
386 arrow::Result<bool> IsSupported(const arrow::dataset::FileSource& source) const override
387 {
388 auto fs = std::dynamic_pointer_cast<VirtualRootFileSystemBase>(source.filesystem());
389 if (!fs) {
390 return false;
391 }
392 return fs->CheckSupport(source);
393 }
394
395 arrow::Result<std::shared_ptr<arrow::Schema>> Inspect(const arrow::dataset::FileSource& source) const override;
397 arrow::Result<std::shared_ptr<arrow::dataset::FileFragment>> MakeFragment(
398 arrow::dataset::FileSource source, arrow::compute::Expression partition_expression,
399 std::shared_ptr<arrow::Schema> physical_schema) override;
400
401 arrow::Result<std::shared_ptr<arrow::dataset::FileWriter>> MakeWriter(std::shared_ptr<arrow::io::OutputStream> destination, std::shared_ptr<arrow::Schema> schema, std::shared_ptr<arrow::dataset::FileWriteOptions> options, arrow::fs::FileLocator destination_locator) const override;
402
403 std::shared_ptr<arrow::dataset::FileWriteOptions> DefaultWriteOptions() override;
404
405 arrow::Result<arrow::RecordBatchGenerator> ScanBatchesAsync(
406 const std::shared_ptr<arrow::dataset::ScanOptions>& options,
407 const std::shared_ptr<arrow::dataset::FileFragment>& fragment) const override;
408};
409
411{
412 public:
413 SingleTreeFileSystem(TTree* tree, size_t& totalCompressedSize, size_t& totalUncompressedSize)
414 : TTreeFileSystem(),
415 mTotUncompressedSize(totalUncompressedSize),
416 mTotCompressedSize(totalCompressedSize),
417 mTree(tree)
418 {
419 }
420
421 arrow::Result<arrow::fs::FileInfo> GetFileInfo(std::string const& path) override;
422
423 std::string type_name() const override
424 {
425 return "ttree";
426 }
427
428 std::shared_ptr<RootObjectHandler> GetObjectHandler(arrow::dataset::FileSource source) override
429 {
430 return std::make_shared<RootObjectHandler>((void*)mTree.get(), std::make_shared<TTreeFileFormat>(mTotCompressedSize, mTotUncompressedSize));
431 }
432
433 std::unique_ptr<TTree>& GetTree(arrow::dataset::FileSource) override
434 {
435 // Simply return the only TTree we have
436 return mTree;
437 }
438
439 private:
440 // References, not values: a TTreeFileFormat built in GetObjectHandler binds to these,
441 // so by-value members would have it accumulate into copies that are thrown away (and,
442 // being uninitialised here, read as indeterminate).
443 size_t& mTotUncompressedSize;
444 size_t& mTotCompressedSize;
445 std::unique_ptr<TTree> mTree;
446};
447
448arrow::Result<arrow::fs::FileInfo> SingleTreeFileSystem::GetFileInfo(std::string const& path)
449{
450 arrow::dataset::FileSource source(path, shared_from_this());
451 arrow::fs::FileInfo result;
452 result.set_path(path);
453 result.set_type(arrow::fs::FileType::File);
454 return result;
455}
456
457// A fragment which holds a tree
459{
460 public:
461 TTreeFileFragment(arrow::dataset::FileSource source,
462 std::shared_ptr<arrow::dataset::FileFormat> format,
463 arrow::compute::Expression partition_expression,
464 std::shared_ptr<arrow::Schema> physical_schema)
465 : FileFragment(source, format, std::move(partition_expression), physical_schema)
466 {
467 auto rootFS = std::dynamic_pointer_cast<VirtualRootFileSystemBase>(this->source().filesystem());
468 if (rootFS.get() == nullptr) {
469 throw runtime_error_f("Unknown filesystem %s when reading %s.",
470 source.filesystem()->type_name().c_str(), source.path().c_str());
471 }
472 auto objectHandler = rootFS->GetObjectHandler(source);
473 if (!objectHandler->format->Equals(*format)) {
474 throw runtime_error_f("Cannot read source %s with format %s to pupulate a TTreeFileFragment.",
475 source.path().c_str(), objectHandler->format->type_name().c_str());
476 };
477 mTree = objectHandler->GetObjectAsOwner<TTree>();
478 }
479
480 TTree* GetTree()
481 {
482 return mTree.get();
483 }
484
485 std::vector<ReadOps>& ops()
486 {
487 return mOps;
488 }
489
492 std::shared_ptr<arrow::Buffer> GetPlaceholderForOp(size_t size)
493 {
494 return std::make_shared<arrow::Buffer>((uint8_t*)(mOps.size() - 1), size);
495 }
496
497 private:
498 std::unique_ptr<TTree> mTree;
499 std::vector<ReadOps> mOps;
500};
501
502// An arrow outputstream which allows to write to a TTree. Eventually
503// with a prefix for the branches.
505{
506 public:
507 // Using a pointer means that the tree itself is owned by another
508 // class
509 TTreeOutputStream(TTree*, std::string branchPrefix);
510
511 arrow::Status Close() override;
512
513 arrow::Result<int64_t> Tell() const override;
514
515 arrow::Status Write(const void* data, int64_t nbytes) override;
516
517 bool closed() const override;
518
519 TBranch* CreateBranch(char const* branchName, char const* sizeBranch);
520
521 TTree* GetTree()
522 {
523 return mTree;
524 }
525
526 private:
527 TTree* mTree;
528 std::string mBranchPrefix;
529};
530
531// An arrow outputstream which allows to write to a ttree
532// @a branch prefix is to be used to identify a set of branches which all belong to
533// the same table.
534TTreeOutputStream::TTreeOutputStream(TTree* f, std::string branchPrefix)
535 : mTree(f),
536 mBranchPrefix(std::move(branchPrefix))
537{
538}
539
541{
542 if (mTree->GetCurrentFile() == nullptr) {
543 return arrow::Status::Invalid("Cannot close a tree not attached to a file");
544 }
545 mTree->GetCurrentFile()->Close();
546 return arrow::Status::OK();
547}
548
549arrow::Result<int64_t> TTreeOutputStream::Tell() const
550{
551 return arrow::Result<int64_t>(arrow::Status::NotImplemented("Cannot move"));
552}
553
554arrow::Status TTreeOutputStream::Write(const void* data, int64_t nbytes)
555{
556 return arrow::Status::NotImplemented("Cannot write raw bytes to a TTree");
557}
558
560{
561 // A standalone tree is never closed.
562 if (mTree->GetCurrentFile() == nullptr) {
563 return false;
564 }
565 return mTree->GetCurrentFile()->IsOpen() == false;
566}
567
568TBranch* TTreeOutputStream::CreateBranch(char const* branchName, char const* sizeBranch)
569{
570 if (mBranchPrefix.empty() == true) {
571 return mTree->Branch(branchName, (char*)nullptr, sizeBranch);
572 }
573 return mTree->Branch((mBranchPrefix + "/" + branchName).c_str(), (char*)nullptr, (mBranchPrefix + sizeBranch).c_str());
574}
575
579 std::shared_ptr<o2::framework::TTreeFileFormat> format = nullptr;
580};
581
584 {
585 auto context = new TTreePluginContext;
586 context->format = std::make_shared<o2::framework::TTreeFileFormat>(context->totalCompressedSize, context->totalUncompressedSize);
587 return new RootArrowFactory{
588 .options = [context]() { return context->format->DefaultWriteOptions(); },
589 .format = [context]() { return context->format; },
590 .deferredOutputStreamer = [](std::shared_ptr<arrow::dataset::FileFragment> fragment, const std::shared_ptr<arrow::ResizableBuffer>& buffer) -> std::shared_ptr<arrow::io::OutputStream> {
591 auto treeFragment = std::dynamic_pointer_cast<TTreeFileFragment>(fragment);
592 return std::make_shared<TTreeDeferredReadOutputStream>(treeFragment->ops(), buffer);
593 }};
594 }
595};
596
602
604 int64_t offset = 0;
605 std::span<int> offsets;
606 int readEntries = 0;
607 int count = 0;
608 auto* tPtrOffset = reinterpret_cast<int*>(op.targetBuffer->mutable_data());
609 offsets = std::span<int>{tPtrOffset, tPtrOffset + op.rootBranchEntries + 1};
610
611 // read sizes first
612 rootBuffer.Reset();
613 while (readEntries < op.rootBranchEntries) {
614 auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
615 checkReadRange(op, readEntries, readLast);
616 checkBasketBytes(op, readEntries, static_cast<int64_t>(readLast) * sizeof(uint32_t), rootBuffer);
617 readEntries += readLast;
618 for (auto i = 0; i < readLast; ++i) {
619 offsets[count++] = (int)offset;
620 uint32_t raw = reinterpret_cast<uint32_t*>(rootBuffer.GetCurrent())[i];
621 offset += (std::endian::native == std::endian::little) ? __builtin_bswap32(raw) : raw;
622 // Arrow lists use 32 bit offsets, a larger total can only come from corrupted sizes.
623 if (offset > INT32_MAX) {
624 throw runtime_error_f("Invalid sizes for branch %s: offsets overflow at entry %d.", op.branch->GetName(), count - 1);
625 }
626 }
627 }
628 offsets[count] = (int)offset;
629 op.offsetCount = offset;
630};
631
632arrow::Result<arrow::RecordBatchGenerator> TTreeFileFormat::ScanBatchesAsync(
633 const std::shared_ptr<arrow::dataset::ScanOptions>& options,
634 const std::shared_ptr<arrow::dataset::FileFragment>& fragment) const
635{
636 assert(options->dataset_schema != nullptr);
637 // This is the schema we want to read
638 auto dataset_schema = options->dataset_schema;
639 auto treeFragment = std::dynamic_pointer_cast<TTreeFileFragment>(fragment);
640 if (treeFragment.get() == nullptr) {
641 return {arrow::Status::NotImplemented("Not a ttree fragment")};
642 }
643
644 auto generator = [pool = options->pool, treeFragment, dataset_schema, &totalCompressedSize = mTotCompressedSize,
645 &totalUncompressedSize = mTotUncompressedSize]() -> arrow::Future<std::shared_ptr<arrow::RecordBatch>> {
646 O2_SIGNPOST_ID_FROM_POINTER(tid, root_arrow_fs, treeFragment->GetTree());
647 O2_SIGNPOST_START(root_arrow_fs, tid, "Generator", "Creating batch for tree %{public}s", treeFragment->GetTree()->GetName());
648 std::vector<std::shared_ptr<arrow::Array>> columns;
649 std::vector<std::shared_ptr<arrow::Field>> fields = dataset_schema->fields();
650 auto physical_schema = *treeFragment->ReadPhysicalSchema();
651
652 if (dataset_schema->num_fields() > physical_schema->num_fields()) {
653 throw runtime_error_f("One TTree must have all the fields requested in a table");
654 }
655
656 // Register physical fields into the cache
657 std::vector<BranchFieldMapping> mappings;
658
659 // We need to count the number of readops to avoid moving the vector.
660 int opsCount = 0;
661 for (int fi = 0; fi < dataset_schema->num_fields(); ++fi) {
662 auto dataset_field = dataset_schema->field(fi);
663 // This is needed because for now the dataset_field
664 // is actually the schema of the ttree
665 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Processing dataset field %{public}s.", dataset_field->name().c_str());
666 int physicalFieldIdx = physical_schema->GetFieldIndex(dataset_field->name());
667
668 if (physicalFieldIdx < 0) {
669 throw runtime_error_f("Cannot find physical field associated to %s. Possible fields: %s",
670 dataset_field->name().c_str(), physical_schema->ToString().c_str());
671 }
672 if (physicalFieldIdx > 0 && physical_schema->field(physicalFieldIdx - 1)->name().ends_with("_size")) {
673 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Field %{public}s has sizes in %{public}s.", dataset_field->name().c_str(),
674 physical_schema->field(physicalFieldIdx - 1)->name().c_str());
675 mappings.push_back({physicalFieldIdx, physicalFieldIdx - 1, fi});
676 opsCount += 2;
677 } else {
678 if (physicalFieldIdx > 0) {
679 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Field %{public}s previous field is %{public}s.", dataset_field->name().c_str(),
680 physical_schema->field(physicalFieldIdx - 1)->name().c_str());
681 }
682 mappings.push_back({physicalFieldIdx, -1, fi});
683 opsCount++;
684 }
685 }
686
687 auto* tree = treeFragment->GetTree();
688 auto branches = tree->GetListOfBranches();
689 size_t totalTreeSize = 0;
690 std::vector<TBranch*> selectedBranches;
691 for (auto& mapping : mappings) {
692 selectedBranches.push_back((TBranch*)branches->At(mapping.mainBranchIdx));
693 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Adding branch %{public}s to stream.", selectedBranches.back()->GetName());
694 totalTreeSize += selectedBranches.back()->GetTotalSize();
695 if (mapping.vlaIdx != -1) {
696 selectedBranches.push_back((TBranch*)branches->At(mapping.vlaIdx));
697 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Adding branch %{public}s to stream.", selectedBranches.back()->GetName());
698 totalTreeSize += selectedBranches.back()->GetTotalSize();
699 }
700 }
701
702 size_t cacheSize = std::max(std::min(totalTreeSize, 25000000UL), 1000000UL);
703 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Generator", "Resizing cache to %zu.", cacheSize);
704 tree->SetCacheSize(cacheSize);
705 for (auto* branch : selectedBranches) {
706 tree->AddBranchToCache(branch, false);
707 }
708 tree->StopCacheLearningPhase();
709
710 // Intermediate buffer to bulk read. Two for now
711 std::vector<ReadOps>& ops = treeFragment->ops();
712 ops.clear();
713 ops.reserve(opsCount);
714 for (size_t mi = 0; mi < mappings.size(); ++mi) {
715 BranchFieldMapping mapping = mappings[mi];
716 // The field actually on disk
717 auto datasetField = dataset_schema->field(mapping.datasetFieldIdx);
718 auto physicalField = physical_schema->field(mapping.mainBranchIdx);
719
720 if (mapping.vlaIdx != -1) {
721 auto* branch = (TBranch*)branches->At(mapping.vlaIdx);
722 ops.emplace_back(ReadOps{
723 .branch = branch,
724 .rootBranchEntries = branch->GetEntries(),
725 .typeSize = 4,
726 .listSize = 1,
727 .kind = ReadOpKind::Offsets,
728 });
729 auto& op = ops.back();
730 ARROW_ASSIGN_OR_RAISE(op.targetBuffer, arrow::AllocateBuffer((op.rootBranchEntries + 1) * op.typeSize, pool));
731 // Offsets need to be read immediately to know how many values are there
733 }
734 ops.push_back({});
735 auto& valueOp = ops.back();
736 valueOp.branch = (TBranch*)branches->At(mapping.mainBranchIdx);
737 valueOp.rootBranchEntries = valueOp.branch->GetEntries();
738 // In case this is a vla, we set the offsetCount as totalEntries
739 // In case we read booleans we need a special coversion from bytes to bits.
740 auto listType = std::dynamic_pointer_cast<arrow::FixedSizeListType>(datasetField->type());
741 valueOp.typeSize = physicalField->type()->byte_width();
742 // Notice how we are not (yet) allocating buffers at this point. We merely
743 // create placeholders to subsequently fill.
744 if ((datasetField->type() == arrow::boolean())) {
745 valueOp.kind = ReadOpKind::Booleans;
746 valueOp.listSize = 1;
747 valueOp.targetBuffer = treeFragment->GetPlaceholderForOp((valueOp.rootBranchEntries + 7) / 8);
748 } else if (listType && datasetField->type()->field(0)->type() == arrow::boolean()) {
749 valueOp.typeSize = physicalField->type()->field(0)->type()->byte_width();
750 valueOp.listSize = listType->list_size();
751 valueOp.kind = ReadOpKind::Booleans;
752 valueOp.targetBuffer = treeFragment->GetPlaceholderForOp((valueOp.rootBranchEntries * valueOp.listSize) / 8 + 1);
753 } else if (mapping.vlaIdx != -1) {
754 valueOp.typeSize = physicalField->type()->field(0)->type()->byte_width();
755 valueOp.listSize = -1;
756 // -1 is the current one, -2 is the one with for the offsets
757 valueOp.kind = ReadOpKind::VLA;
758 valueOp.targetBuffer = treeFragment->GetPlaceholderForOp(ops[ops.size() - 2].offsetCount * valueOp.typeSize);
759 } else if (listType) {
760 valueOp.kind = ReadOpKind::Values;
761 valueOp.listSize = listType->list_size();
762 valueOp.typeSize = physicalField->type()->field(0)->type()->byte_width();
763 valueOp.targetBuffer = treeFragment->GetPlaceholderForOp(valueOp.rootBranchEntries * valueOp.typeSize * valueOp.listSize);
764 } else {
765 valueOp.typeSize = physicalField->type()->byte_width();
766 valueOp.kind = ReadOpKind::Values;
767 valueOp.listSize = 1;
768 valueOp.targetBuffer = treeFragment->GetPlaceholderForOp(valueOp.rootBranchEntries * valueOp.typeSize);
769 }
770 arrow::Status status;
771 std::shared_ptr<arrow::Array> array;
772
773 if (listType) {
774 auto vdata = std::make_shared<arrow::ArrayData>(datasetField->type()->field(0)->type(), valueOp.rootBranchEntries * valueOp.listSize,
775 std::vector<std::shared_ptr<arrow::Buffer>>{nullptr, valueOp.targetBuffer});
776 array = std::make_shared<arrow::FixedSizeListArray>(datasetField->type(), valueOp.rootBranchEntries, arrow::MakeArray(vdata));
777 // This is a vla, there is also an offset op
778 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Op", "Created op for branch %{public}s with %lli entries, size of the buffer %lli.",
779 valueOp.branch->GetName(),
780 valueOp.rootBranchEntries,
781 valueOp.targetBuffer->size());
782 } else if (mapping.vlaIdx != -1) {
783 auto& offsetOp = ops[ops.size() - 2];
784 auto vdata = std::make_shared<arrow::ArrayData>(datasetField->type()->field(0)->type(), offsetOp.offsetCount,
785 std::vector<std::shared_ptr<arrow::Buffer>>{nullptr, valueOp.targetBuffer});
786 // We have pushed an offset op if this was the case.
787 array = std::make_shared<arrow::ListArray>(datasetField->type(), offsetOp.rootBranchEntries, offsetOp.targetBuffer, arrow::MakeArray(vdata));
788 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Op", "Created op for branch %{public}s with %lli entries, size of the buffer %lli.",
789 offsetOp.branch->GetName(), offsetOp.rootBranchEntries, offsetOp.targetBuffer->size());
790 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Op", "Created op for branch %{public}s with %lli entries, size of the buffer %lli.",
791 valueOp.branch->GetName(),
792 offsetOp.offsetCount,
793 valueOp.targetBuffer->size());
794 } else {
795 auto data = std::make_shared<arrow::ArrayData>(datasetField->type(), valueOp.rootBranchEntries,
796 std::vector<std::shared_ptr<arrow::Buffer>>{nullptr, valueOp.targetBuffer});
797 array = arrow::MakeArray(data);
798 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, tid, "Op", "Created op for branch %{public}s with %lli entries, size of the buffer %lli.",
799 valueOp.branch->GetName(),
800 valueOp.rootBranchEntries,
801 valueOp.targetBuffer->size());
802 }
803
804 columns.push_back(array);
805 }
806
807 // Do the actual filling of the buffers. This happens after we have created the whole structure
808 // so that we can read directly in shared memory.
809 int64_t rows = -1;
810 for (size_t i = 0; i < ops.size(); ++i) {
811 auto& op = ops[i];
812 if (rows == -1 && op.kind != ReadOpKind::VLA) {
813 rows = op.rootBranchEntries;
814 }
815 if (rows == -1 && op.kind == ReadOpKind::VLA) {
816 auto& offsetOp = ops[i - 1];
817 rows = offsetOp.rootBranchEntries;
818 }
819 if (op.kind != ReadOpKind::VLA && rows != op.rootBranchEntries) {
820 throw runtime_error_f("Unmatching number of rows for branch %s. Expected %lli, found %lli", op.branch->GetName(), rows, op.rootBranchEntries);
821 }
822 if (op.kind == ReadOpKind::VLA && rows != ops[i - 1].rootBranchEntries) {
823 throw runtime_error_f("Unmatching number of rows for branch %s. Expected %lli, found %lli", op.branch->GetName(), rows, ops[i - 1].offsetCount);
824 }
825 }
826
827 auto batch = arrow::RecordBatch::Make(dataset_schema, rows, columns);
828 totalCompressedSize += tree->GetZipBytes();
829 totalUncompressedSize += tree->GetTotBytes();
830 O2_SIGNPOST_END(root_arrow_fs, tid, "Generator", "Done creating batch compressed:%zu uncompressed:%zu", totalCompressedSize, totalUncompressedSize);
831 return batch;
832 };
833 return generator;
834}
835
836char const* rootSuffixFromArrow(arrow::Type::type id)
837{
838 switch (id) {
839 case arrow::Type::BOOL:
840 return "/O";
841 case arrow::Type::UINT8:
842 return "/b";
843 case arrow::Type::UINT16:
844 return "/s";
845 case arrow::Type::UINT32:
846 return "/i";
847 case arrow::Type::UINT64:
848 return "/l";
849 case arrow::Type::INT8:
850 return "/B";
851 case arrow::Type::INT16:
852 return "/S";
853 case arrow::Type::INT32:
854 return "/I";
855 case arrow::Type::INT64:
856 return "/L";
857 case arrow::Type::FLOAT:
858 return "/F";
859 case arrow::Type::DOUBLE:
860 return "/D";
861 default:
862 throw runtime_error("Unsupported arrow column type");
863 }
864}
865
866arrow::Result<std::shared_ptr<arrow::io::OutputStream>> TTreeFileSystem::OpenOutputStream(
867 const std::string& path,
868 const std::shared_ptr<const arrow::KeyValueMetadata>& metadata)
869{
870 arrow::dataset::FileSource source{path, shared_from_this()};
871 auto prefix = metadata->Get("branch_prefix");
872 if (prefix.ok()) {
873 return std::make_shared<TTreeOutputStream>(GetTree(source).get(), *prefix);
874 }
875 return std::make_shared<TTreeOutputStream>(GetTree(source).get(), "");
876}
877
878namespace
879{
880struct BranchInfo {
881 std::string name;
882 TBranch* ptr;
883 bool mVLA;
884};
885} // namespace
886
887auto arrowTypeFromROOT(EDataType type, int size)
888{
889 auto typeGenerator = [](std::shared_ptr<arrow::DataType> const& type, int size) -> std::shared_ptr<arrow::DataType> {
890 switch (size) {
891 case -1:
892 return arrow::list(type);
893 case 1:
894 return std::move(type);
895 default:
896 return arrow::fixed_size_list(type, size);
897 }
898 };
899
900 switch (type) {
901 case EDataType::kBool_t:
902 return typeGenerator(arrow::boolean(), size);
903 case EDataType::kUChar_t:
904 return typeGenerator(arrow::uint8(), size);
905 case EDataType::kUShort_t:
906 return typeGenerator(arrow::uint16(), size);
907 case EDataType::kUInt_t:
908 return typeGenerator(arrow::uint32(), size);
909 case EDataType::kULong64_t:
910 return typeGenerator(arrow::uint64(), size);
911 case EDataType::kChar_t:
912 return typeGenerator(arrow::int8(), size);
913 case EDataType::kShort_t:
914 return typeGenerator(arrow::int16(), size);
915 case EDataType::kInt_t:
916 return typeGenerator(arrow::int32(), size);
917 case EDataType::kLong64_t:
918 return typeGenerator(arrow::int64(), size);
919 case EDataType::kFloat_t:
920 return typeGenerator(arrow::float32(), size);
921 case EDataType::kDouble_t:
922 return typeGenerator(arrow::float64(), size);
923 default:
924 throw o2::framework::runtime_error_f("Unsupported branch type: %d", static_cast<int>(type));
925 }
926}
927
928// This is a datatype for branches which implies
929struct RootTransientIndexType : arrow::ExtensionType {
930};
931
932arrow::Result<std::shared_ptr<arrow::Schema>> TTreeFileFormat::Inspect(const arrow::dataset::FileSource& source) const
933{
934 auto fs = std::dynamic_pointer_cast<VirtualRootFileSystemBase>(source.filesystem());
935
936 if (!fs.get()) {
937 throw runtime_error_f("Unknown filesystem %s\n", source.filesystem()->type_name().c_str());
938 }
939 auto objectHandler = fs->GetObjectHandler(source);
940
941 if (!objectHandler->format->Equals(*this)) {
942 throw runtime_error_f("Unknown filesystem %s\n", source.filesystem()->type_name().c_str());
943 }
944
945 // Notice that we abuse of the API here and do not release the TTree,
946 // so that it's still managed by ROOT.
947 auto tree = objectHandler->GetObjectAsOwner<TTree>().release();
948 if (tree == nullptr) {
949 return arrow::Status::IOError("Unable to read tree ", source.path());
950 }
951
952 auto branches = tree->GetListOfBranches();
953 auto n = branches->GetEntries();
954
955 std::vector<std::shared_ptr<arrow::Field>> fields;
956
957 bool prevIsSize = false;
958 for (auto i = 0; i < n; ++i) {
959 auto branch = static_cast<TBranch*>(branches->At(i));
960 if (branch == nullptr || branch->GetListOfLeaves()->At(0) == nullptr) {
961 return arrow::Status::IOError("Invalid branch ", i, " in tree ", source.path());
962 }
963 std::string name = branch->GetName();
964 if (prevIsSize && fields.back()->name() != name + "_size") {
965 throw runtime_error_f("Unexpected layout for VLA container %s.", branch->GetName());
966 }
967
968 if (name.ends_with("_size")) {
969 fields.emplace_back(std::make_shared<arrow::Field>(name, arrow::int32()));
970 prevIsSize = true;
971 } else {
972 static TClass* cls;
973 EDataType type;
974 branch->GetExpectedType(cls, type);
975
976 if (prevIsSize) {
977 fields.emplace_back(std::make_shared<arrow::Field>(name, arrowTypeFromROOT(type, -1)));
978 } else {
979 auto listSize = static_cast<TLeaf*>(branch->GetListOfLeaves()->At(0))->GetLenStatic();
980 fields.emplace_back(std::make_shared<arrow::Field>(name, arrowTypeFromROOT(type, listSize)));
981 }
982 prevIsSize = false;
983 }
984 }
985
986 if (!fields.empty() && fields.back()->name().ends_with("_size")) {
987 throw runtime_error_f("Missing values for VLA indices %s.", fields.back()->name().c_str());
988 }
989 return std::make_shared<arrow::Schema>(fields);
990}
991
993arrow::Result<std::shared_ptr<arrow::dataset::FileFragment>> TTreeFileFormat::MakeFragment(
994 arrow::dataset::FileSource source, arrow::compute::Expression partition_expression,
995 std::shared_ptr<arrow::Schema> physical_schema)
996{
997
998 return std::make_shared<TTreeFileFragment>(source, std::dynamic_pointer_cast<arrow::dataset::FileFormat>(shared_from_this()),
999 std::move(partition_expression),
1000 physical_schema);
1001}
1002
1004{
1005 std::vector<TBranch*> branches;
1006 std::vector<TBranch*> sizesBranches;
1007 std::vector<std::shared_ptr<arrow::Array>> valueArrays;
1008 std::vector<std::shared_ptr<arrow::Array>> sizeArrays;
1009 std::vector<std::shared_ptr<arrow::DataType>> valueTypes;
1010
1011 std::vector<int64_t> valuesIdealBasketSize;
1012 std::vector<int64_t> sizeIdealBasketSize;
1013
1014 std::vector<int64_t> typeSizes;
1015 std::vector<int64_t> listSizes;
1016 bool firstBasket = true;
1017
1018 // This is to create a batsket size according to the first batch.
1019 void finaliseBasketSize(std::shared_ptr<arrow::RecordBatch> firstBatch)
1020 {
1021 O2_SIGNPOST_ID_FROM_POINTER(sid, root_arrow_fs, this);
1022 O2_SIGNPOST_START(root_arrow_fs, sid, "finaliseBasketSize", "First batch with %lli rows received and %zu columns",
1023 firstBatch->num_rows(), firstBatch->columns().size());
1024 for (size_t i = 0; i < branches.size(); i++) {
1025 auto* branch = branches[i];
1026 auto* sizeBranch = sizesBranches[i];
1027
1028 int valueSize = valueTypes[i]->byte_width();
1029 if (listSizes[i] == 1) {
1030 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, sid, "finaliseBasketSize", "Branch %s exists and uses %d bytes per entry for %lli entries.",
1031 branch->GetName(), valueSize, firstBatch->num_rows());
1032 assert(sizeBranch == nullptr);
1033 branch->SetBasketSize(1024 + firstBatch->num_rows() * valueSize);
1034 } else if (listSizes[i] == -1) {
1035 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, sid, "finaliseBasketSize", "Branch %s exists and uses %d bytes per entry.",
1036 branch->GetName(), valueSize);
1037 // This should probably lookup the
1038 auto column = firstBatch->GetColumnByName(schema_->field(i)->name());
1039 auto list = std::static_pointer_cast<arrow::ListArray>(column);
1040 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, sid, "finaliseBasketSize", "Branch %s needed. Associated size branch %s and there are %lli entries of size %d in that list.",
1041 branch->GetName(), sizeBranch->GetName(), list->length(), valueSize);
1042 branch->SetBasketSize(1024 + firstBatch->num_rows() * valueSize * list->length());
1043 sizeBranch->SetBasketSize(1024 + firstBatch->num_rows() * 4);
1044 } else {
1045 O2_SIGNPOST_EVENT_EMIT(root_arrow_fs, sid, "finaliseBasketSize", "Branch %s needed. There are %lli entries per array of size %d in that list.",
1046 branch->GetName(), listSizes[i], valueSize);
1047 assert(sizeBranch == nullptr);
1048 branch->SetBasketSize(1024 + firstBatch->num_rows() * valueSize * listSizes[i]);
1049 }
1050
1051 auto field = firstBatch->schema()->field(i);
1052 if (field->name().starts_with("fIndexArray")) {
1053 // One int per array to keep track of the size
1054 int idealBasketSize = 4 * firstBatch->num_rows() + 1024 + field->type()->byte_width() * firstBatch->num_rows(); // minimal additional size needed, otherwise we get 2 baskets
1055 int basketSize = std::max(32000, idealBasketSize); // keep a minimum value
1056 sizeBranch->SetBasketSize(basketSize);
1057 branch->SetBasketSize(basketSize);
1058 }
1059 }
1060 O2_SIGNPOST_END(root_arrow_fs, sid, "finaliseBasketSize", "Done");
1061 }
1062
1063 public:
1064 // Create the TTree based on the physical_schema, not the one in the batch.
1065 // The write method will have to reconcile the two schemas.
1066 TTreeFileWriter(std::shared_ptr<arrow::Schema> schema, std::shared_ptr<arrow::dataset::FileWriteOptions> options,
1067 std::shared_ptr<arrow::io::OutputStream> destination,
1068 arrow::fs::FileLocator destination_locator)
1069 : FileWriter(schema, options, destination, destination_locator)
1070 {
1071 // Batches have the same number of entries for each column.
1072 auto directoryStream = std::dynamic_pointer_cast<TDirectoryFileOutputStream>(destination_);
1073 auto treeStream = std::dynamic_pointer_cast<TTreeOutputStream>(destination_);
1074
1075 if (directoryStream.get()) {
1076 TDirectoryFile* dir = directoryStream->GetDirectory();
1077 dir->cd();
1078 auto* tree = new TTree(destination_locator_.path.c_str(), "");
1079 treeStream = std::make_shared<TTreeOutputStream>(tree, "");
1080 } else if (treeStream.get()) {
1081 // We already have a tree stream, let's derive a new one
1082 // with the destination_locator_.path as prefix for the branches
1083 // This way we can multiplex multiple tables in the same tree.
1084 auto* tree = treeStream->GetTree();
1085 treeStream = std::make_shared<TTreeOutputStream>(tree, destination_locator_.path);
1086 } else {
1087 // I could simply set a prefix here to merge to an already existing tree.
1088 throw std::runtime_error("Unsupported backend.");
1089 }
1090
1091 for (auto i = 0u; i < schema->fields().size(); ++i) {
1092 auto& field = schema->field(i);
1093 listSizes.push_back(1);
1094
1095 int valuesIdealBasketSize = 0;
1096 // Construct all the needed branches.
1097 switch (field->type()->id()) {
1098 case arrow::Type::FIXED_SIZE_LIST: {
1099 listSizes.back() = std::static_pointer_cast<arrow::FixedSizeListType>(field->type())->list_size();
1100 valuesIdealBasketSize = 1024 + valueTypes.back()->byte_width() * listSizes.back();
1101 valueTypes.push_back(field->type()->field(0)->type());
1102 sizesBranches.push_back(nullptr);
1103 std::string leafList = fmt::format("{}[{}]{}", field->name(), listSizes.back(), rootSuffixFromArrow(valueTypes.back()->id()));
1104 branches.push_back(treeStream->CreateBranch(field->name().c_str(), leafList.c_str()));
1105 } break;
1106 case arrow::Type::LIST: {
1107 valueTypes.push_back(field->type()->field(0)->type());
1108 std::string leafList = fmt::format("{}[{}_size]{}", field->name(), field->name(), rootSuffixFromArrow(valueTypes.back()->id()));
1109 listSizes.back() = -1; // VLA, we need to calculate it on the fly;
1110 std::string sizeLeafList = field->name() + "_size/I";
1111 sizesBranches.push_back(treeStream->CreateBranch((field->name() + "_size").c_str(), sizeLeafList.c_str()));
1112 branches.push_back(treeStream->CreateBranch(field->name().c_str(), leafList.c_str()));
1113 // Notice that this could be replaced by a better guess of the
1114 // average size of the list elements, but this is not trivial.
1115 } break;
1116 default: {
1117 valueTypes.push_back(field->type());
1118 std::string leafList = field->name() + rootSuffixFromArrow(valueTypes.back()->id());
1119 sizesBranches.push_back(nullptr);
1120 branches.push_back(treeStream->CreateBranch(field->name().c_str(), leafList.c_str()));
1121 } break;
1122 }
1123 }
1124 // We create the branches from the schema
1125 }
1126
1127 arrow::Status Write(const std::shared_ptr<arrow::RecordBatch>& batch) override
1128 {
1129 if (firstBasket) {
1130 firstBasket = false;
1131 finaliseBasketSize(batch);
1132 }
1133
1134 // Support writing empty tables
1135 if (batch->columns().empty() || batch->num_rows() == 0) {
1136 return arrow::Status::OK();
1137 }
1138
1139 // Batches have the same number of entries for each column.
1140 auto directoryStream = std::dynamic_pointer_cast<TDirectoryFileOutputStream>(destination_);
1141 TTree* tree = nullptr;
1142 if (directoryStream.get()) {
1143 TDirectoryFile* dir = directoryStream->GetDirectory();
1144 tree = (TTree*)dir->Get(destination_locator_.path.c_str());
1145 }
1146 auto treeStream = std::dynamic_pointer_cast<TTreeOutputStream>(destination_);
1147
1148 if (!tree) {
1149 // I could simply set a prefix here to merge to an already existing tree.
1150 throw std::runtime_error("Unsupported backend.");
1151 }
1152
1153 for (auto i = 0u; i < batch->columns().size(); ++i) {
1154 auto column = batch->column(i);
1155 auto& field = batch->schema()->field(i);
1156
1157 valueArrays.push_back(nullptr);
1158
1159 switch (field->type()->id()) {
1160 case arrow::Type::FIXED_SIZE_LIST: {
1161 auto list = std::static_pointer_cast<arrow::FixedSizeListArray>(column);
1162 if (list->list_type()->field(0)->type()->id() == arrow::Type::BOOL) {
1163 int64_t length = list->length() * list->list_type()->list_size();
1164 arrow::UInt8Builder builder;
1165 auto ok = builder.Reserve(length);
1166 // I need to build an array of uint8_t for the conversion to ROOT which uses
1167 // bytes for boolans.
1168 auto boolArray = std::static_pointer_cast<arrow::BooleanArray>(list->values());
1169 for (int64_t i = 0; i < length; ++i) {
1170 if (boolArray->IsValid(i)) {
1171 // Expand each boolean value (true/false) to uint8 (1/0)
1172 uint8_t value = boolArray->Value(i) ? 1 : 0;
1173 auto ok = builder.Append(value);
1174 } else {
1175 // Append null for invalid entries
1176 auto ok = builder.AppendNull();
1177 }
1178 }
1179 valueArrays.back() = *builder.Finish();
1180 } else {
1181 valueArrays.back() = list->values();
1182 }
1183 } break;
1184 case arrow::Type::LIST: {
1185 auto list = std::static_pointer_cast<arrow::ListArray>(column);
1186 valueArrays.back() = list->values();
1187 } break;
1188 case arrow::Type::BOOL: {
1189 // In case of arrays of booleans, we need to go back to their
1190 // char based representation for ROOT to save them.
1191 auto boolArray = std::static_pointer_cast<arrow::BooleanArray>(column);
1192
1193 int64_t length = boolArray->length();
1194 arrow::UInt8Builder builder;
1195 auto ok = builder.Reserve(length);
1196
1197 for (int64_t i = 0; i < length; ++i) {
1198 if (boolArray->IsValid(i)) {
1199 // Expand each boolean value (true/false) to uint8 (1/0)
1200 uint8_t value = boolArray->Value(i) ? 1 : 0;
1201 auto ok = builder.Append(value);
1202 } else {
1203 // Append null for invalid entries
1204 auto ok = builder.AppendNull();
1205 }
1206 }
1207 valueArrays.back() = *builder.Finish();
1208 } break;
1209 default:
1210 valueArrays.back() = column;
1211 }
1212 }
1213
1214 int64_t pos = 0;
1215 while (pos < batch->num_rows()) {
1216 for (size_t bi = 0; bi < branches.size(); ++bi) {
1217 auto* branch = branches[bi];
1218 auto* sizeBranch = sizesBranches[bi];
1219 auto array = batch->column(bi);
1220 auto& field = batch->schema()->field(bi);
1221 auto& listSize = listSizes[bi];
1222 auto valueType = valueTypes[bi];
1223 auto valueArray = valueArrays[bi];
1224
1225 switch (field->type()->id()) {
1226 case arrow::Type::LIST: {
1227 auto list = std::static_pointer_cast<arrow::ListArray>(array);
1228 listSize = list->value_length(pos);
1229 uint8_t const* buffer = std::static_pointer_cast<arrow::PrimitiveArray>(valueArray)->values()->data() + array->offset() + list->value_offset(pos) * valueType->byte_width();
1230 branch->SetAddress((void*)buffer);
1231 sizeBranch->SetAddress(&listSize);
1232 } break;
1233 case arrow::Type::FIXED_SIZE_LIST:
1234 default: {
1235 // needed for the boolean case, I should probably cache this.
1236 auto byteWidth = valueType->byte_width() ? valueType->byte_width() : 1;
1237 uint8_t const* buffer = std::static_pointer_cast<arrow::PrimitiveArray>(valueArray)->values()->data() + array->offset() + pos * listSize * byteWidth;
1238 branch->SetAddress((void*)buffer);
1239 };
1240 }
1241 }
1242 tree->Fill();
1243 ++pos;
1244 }
1245 return arrow::Status::OK();
1246 }
1247
1248 arrow::Future<> FinishInternal() override
1249 {
1250 auto treeStream = std::dynamic_pointer_cast<TTreeOutputStream>(destination_);
1251 auto* tree = treeStream->GetTree();
1252 tree->Write("", TObject::kOverwrite);
1253 tree->SetDirectory(nullptr);
1254
1255 return {};
1256 };
1257};
1258arrow::Result<std::shared_ptr<arrow::dataset::FileWriter>> TTreeFileFormat::MakeWriter(std::shared_ptr<arrow::io::OutputStream> destination, std::shared_ptr<arrow::Schema> schema, std::shared_ptr<arrow::dataset::FileWriteOptions> options, arrow::fs::FileLocator destination_locator) const
1259{
1260 auto writer = std::make_shared<TTreeFileWriter>(schema, options, destination, destination_locator);
1261 return std::dynamic_pointer_cast<arrow::dataset::FileWriter>(writer);
1262}
1263
1264std::shared_ptr<arrow::dataset::FileWriteOptions> TTreeFileFormat::DefaultWriteOptions()
1265{
1266 std::shared_ptr<TTreeFileWriteOptions> options(
1267 new TTreeFileWriteOptions(shared_from_this()));
1268 return options;
1269}
1270
1272
1276} // namespace o2::framework
std::shared_ptr< arrow::Schema > schema
std::vector< std::shared_ptr< arrow::Field > > fields
unsigned char uint8_t
unsigned int uint32_t
int32_t i
o2::raw::RawFileWriter * raw
uint32_t op
configStandalone Reconstruction rec_tpc multiplier for the combined cluster track error during track following area in cm for the search of for z only used if decrease removal by when above this local occupancy rowx Shift of TPC revert cluster flag edge bit distance to edge exceeds this ZOffset for secondary multiple of radius max P of electron K BB bands crossing max P of electron p BB bands crossing max P of electron d BB bands crossing max P of electron t BB bands crossing min accepted dEdxMaxTPC of the track Additive extra cluster error for Y2 if edge flag set Additive extra cluster error for Y2 if single set Additive extra cluster error for Y2 if or single set Additive extra cluster error for Z2 if or single set Extra errors to add to track when crossing depending on addErrorsCECrossing Inner portion of y range in sector that is not used in searching for extrapolated track candidates factor2YS for track merging factor2K for track merging Margin in cm of Y position when rejecting edge clusters based on uncorrected track Y Max fraction of shared clusters for tracklet Except pad from cluster finding if total number of charges in a fragment is above this Except pad from cluster finding if number of consecutive charges in a fragment is above this Threshold where a timebin is considered disabling the noisy pad check for that pad Treat signal above this minimum as saturated Smoothing factor for the exponential Highly Ionising Particle tail filter Number of timebins to use for the averaging Add additional custom track errors when crossing relative as fraction of Do N fit passes in final fit of< 0:reject at max -n hits") AddOptionRTC(rejectIFCLowRadiusCluster, uint8_t, 1, "", 0, "Reject clusters that get the IFC mask error during refit") AddOptionRTC(dEdxTruncLow, uint8_t, 2, "", 0, "Low truncation threshold, fraction of 128") AddOptionRTC(dEdxTruncHigh, uint8_t, 77, "", 0, "High truncation threshold, fraction of 128") AddOptionRTC(extrapolationTracking, int8_t, 1, "", 0, "Enable Extrapolation Tracking(prolong tracks to adjacent sectors to find short segments)") AddOptionRTC(disableRefitAttachment, uint8_t, 0, "", 0, "Bitmask to disable certain attachment steps during refit(1:attachment, 2:propagation, 4:loop following, 8:mirroring)") AddOptionRTC(rejectionStrategy, uint8_t, o2::gpu::GPUSettings::RejectionStrategyA, "", 0, "Enable rejection of TPC clusters for compression(0=no, 1=strategy A, 2=strategy B)") AddOptionRTC(mergeLoopersAfterburner, uint8_t, 1, "", 0, "Run afterburner for additional looper merging") AddOptionRTC(compressionTypeMask, uint8_t, o2::gpu::GPUSettings::CompressionFull, "", 0, "TPC Compression mode bits(1=truncate charge/width LSB, 2=differences, 4=track-model)") AddOptionRTC(compressionSortOrder, uint8_t, o2::gpu::GPUSettings::SortTime, "", 0, "Sort order of TPC compression(0=time, 1=pad, 2=Z-time-pad, 3=Z-pad-time, 4=no sorting(use incoming order))") AddOptionRTC(sigBitsCharge, uint8_t, 4, "", 0, "Number of significant bits for TPC cluster charge in compression mode 1") AddOptionRTC(sigBitsWidth, uint8_t, 3, "", 0, "Number of significant bits for TPC cluster width in compression mode 1") AddOptionRTC(dropLoopers, uint8_t, 0, "", 0, "Drop looping tracks starting from second loop") AddOptionRTC(mergerInterpolateErrors, uint8_t, 1, "", 0, "Use interpolation instead of extrapolation for chi2 based cluster rejection") AddOptionRTC(mergerInterpolateRejectAlsoOnCurrentPosition, uint8_t, 1, "", 0, "When using mergerInterpolateErrors, reject based on chi2 twice computed with interpolated and current track position starting from NDF > mergerNonInterpolateRejectMinNDF Merge tracks accross the central electrode Enable PID response Add sub threshold clusters in TPC dEdx computation OR mask of TPC flags that will reject the cluster in alternative dEdx Reject edge clusters based on uncorrected track Y during track fit Increase decrease removal by when on this number of pads from the edge h configStandalone Reconstruction rec_trd Max chi2 for TRD tracklets to be matched to a track Chi2 cut for strict matching mode Number of sigmas for ITS TPC track time error estimate Addition to search road around track prolongation along Y in cm Optimal tracklet rphi resolution in maximum chi2 per attached tracklet for TRD tracks take into account the full likelihood of the slope including exponential tails Tracks with less attached tracklets are discarded after the tracking Post trigger Pile up integration time in BCs fits all iterations but the last one in projections Use material lookup table for TPC refit Apply workarounds to avoid cyclic merge should not be needed tpc dyn configStandalone Processing proc_rtc Replace constant variables static by constexpr expressions false
configStandalone Reconstruction rec_tpc multiplier for the combined cluster track error during track following area in cm for the search of for z only used if decrease removal by when above this local occupancy rowx Shift of TPC revert cluster flag edge bit distance to edge exceeds this ZOffset for secondary multiple of radius max P of electron K BB bands crossing max P of electron p BB bands crossing max P of electron d BB bands crossing max P of electron t BB bands crossing min accepted dEdxMaxTPC of the track Additive extra cluster error for Y2 if edge flag set Additive extra cluster error for Y2 if single set Additive extra cluster error for Y2 if or single set Additive extra cluster error for Z2 if or single set Extra errors to add to track when crossing depending on addErrorsCECrossing Inner portion of y range in sector that is not used in searching for extrapolated track candidates factor2YS for track merging factor2K for track merging Margin in cm of Y position when rejecting edge clusters based on uncorrected track Y Max fraction of shared clusters for tracklet Except pad from cluster finding if total number of charges in a fragment is above this Except pad from cluster finding if number of consecutive charges in a fragment is above this Threshold where a timebin is considered disabling the noisy pad check for that pad Treat signal above this minimum as saturated Smoothing factor for the exponential Highly Ionising Particle tail filter Number of timebins to use for the averaging Add additional custom track errors when crossing relative as fraction of Do N fit passes in final fit of< 0:reject at max -n hits") AddOptionRTC(rejectIFCLowRadiusCluster, uint8_t, 1, "", 0, "Reject clusters that get the IFC mask error during refit") AddOptionRTC(dEdxTruncLow, uint8_t, 2, "", 0, "Low truncation threshold, fraction of 128") AddOptionRTC(dEdxTruncHigh, uint8_t, 77, "", 0, "High truncation threshold, fraction of 128") AddOptionRTC(extrapolationTracking, int8_t, 1, "", 0, "Enable Extrapolation Tracking(prolong tracks to adjacent sectors to find short segments)") AddOptionRTC(disableRefitAttachment, uint8_t, 0, "", 0, "Bitmask to disable certain attachment steps during refit(1:attachment, 2:propagation, 4:loop following, 8:mirroring)") AddOptionRTC(rejectionStrategy, uint8_t, o2::gpu::GPUSettings::RejectionStrategyA, "", 0, "Enable rejection of TPC clusters for compression(0=no, 1=strategy A, 2=strategy B)") AddOptionRTC(mergeLoopersAfterburner, uint8_t, 1, "", 0, "Run afterburner for additional looper merging") AddOptionRTC(compressionTypeMask, uint8_t, o2::gpu::GPUSettings::CompressionFull, "", 0, "TPC Compression mode bits(1=truncate charge/width LSB, 2=differences, 4=track-model)") AddOptionRTC(compressionSortOrder, uint8_t, o2::gpu::GPUSettings::SortTime, "", 0, "Sort order of TPC compression(0=time, 1=pad, 2=Z-time-pad, 3=Z-pad-time, 4=no sorting(use incoming order))") AddOptionRTC(sigBitsCharge, uint8_t, 4, "", 0, "Number of significant bits for TPC cluster charge in compression mode 1") AddOptionRTC(sigBitsWidth, uint8_t, 3, "", 0, "Number of significant bits for TPC cluster width in compression mode 1") AddOptionRTC(dropLoopers, uint8_t, 0, "", 0, "Drop looping tracks starting from second loop") AddOptionRTC(mergerInterpolateErrors, uint8_t, 1, "", 0, "Use interpolation instead of extrapolation for chi2 based cluster rejection") AddOptionRTC(mergerInterpolateRejectAlsoOnCurrentPosition, uint8_t, 1, "", 0, "When using mergerInterpolateErrors, reject based on chi2 twice computed with interpolated and current track position starting from NDF > mergerNonInterpolateRejectMinNDF Merge tracks accross the central electrode Enable PID response Add sub threshold clusters in TPC dEdx computation OR mask of TPC flags that will reject the cluster in alternative dEdx Reject edge clusters based on uncorrected track Y during track fit Increase decrease removal by when on this number of pads from the edge h configStandalone Reconstruction rec_trd Max chi2 for TRD tracklets to be matched to a track Chi2 cut for strict matching mode Number of sigmas for ITS TPC track time error estimate Addition to search road around track prolongation along Y in cm Optimal tracklet rphi resolution in maximum chi2 per attached tracklet for TRD tracks take into account the full likelihood of the slope including exponential tails Tracks with less attached tracklets are discarded after the tracking Post trigger Pile up integration time in BCs fits all iterations but the last one in projections Use material lookup table for TPC refit Apply workarounds to avoid cyclic merge should not be needed tpc dyn configStandalone Processing proc_rtc true
#define DEFINE_DPL_PLUGIN_INSTANCE(NAME, KIND)
Definition Plugins.h:112
#define DEFINE_DPL_PLUGINS_END
Definition Plugins.h:115
#define DEFINE_DPL_PLUGINS_BEGIN
Definition Plugins.h:107
uint16_t pos
Definition RawData.h:3
#define O2_DECLARE_DYNAMIC_LOG(name)
Definition Signpost.h:490
#define O2_SIGNPOST_ID_FROM_POINTER(name, log, pointer)
Definition Signpost.h:506
#define O2_SIGNPOST_END(log, id, name, format,...)
Definition Signpost.h:609
#define O2_SIGNPOST_EVENT_EMIT(log, id, name, format,...)
Definition Signpost.h:523
#define O2_SIGNPOST_START(log, id, name, format,...)
Definition Signpost.h:603
TBranch * ptr
bool mVLA
std::string type_name() const override
SingleTreeFileSystem(TTree *tree, size_t &totalCompressedSize, size_t &totalUncompressedSize)
arrow::Result< arrow::fs::FileInfo > GetFileInfo(std::string const &path) override
std::shared_ptr< RootObjectHandler > GetObjectHandler(arrow::dataset::FileSource source) override
std::unique_ptr< TTree > & GetTree(arrow::dataset::FileSource) override
arrow::Status Reset(std::vector< ReadOps > ops, int64_t initial_capacity, arrow::MemoryPool *pool)
Initialize state of OutputStream with newly allocated memory and set position to 0.
arrow::Status Write(const void *data, int64_t nbytes) override
arrow::Result< std::shared_ptr< arrow::Buffer > > Finish()
Close the stream and return the buffer.
arrow::Result< int64_t > Tell() const override
static arrow::Result< std::shared_ptr< TTreeDeferredReadOutputStream > > Create(std::vector< ReadOps > &ops, int64_t initial_capacity=4096, arrow::MemoryPool *pool=arrow::default_memory_pool())
Create in-memory output stream with indicated capacity using a memory pool.
arrow::Status Close() override
Close the stream, preserving the buffer (retrieve it with Finish()).
TTreeFileFormat(size_t &totalCompressedSize, size_t &totalUncompressedSize)
arrow::Result< arrow::RecordBatchGenerator > ScanBatchesAsync(const std::shared_ptr< arrow::dataset::ScanOptions > &options, const std::shared_ptr< arrow::dataset::FileFragment > &fragment) const override
std::shared_ptr< arrow::dataset::FileWriteOptions > DefaultWriteOptions() override
arrow::Result< bool > IsSupported(const arrow::dataset::FileSource &source) const override
arrow::Result< std::shared_ptr< arrow::Schema > > Inspect(const arrow::dataset::FileSource &source) const override
std::string type_name() const override
bool Equals(const FileFormat &other) const override
arrow::Result< std::shared_ptr< arrow::dataset::FileWriter > > MakeWriter(std::shared_ptr< arrow::io::OutputStream > destination, std::shared_ptr< arrow::Schema > schema, std::shared_ptr< arrow::dataset::FileWriteOptions > options, arrow::fs::FileLocator destination_locator) const override
~TTreeFileFormat() override=default
arrow::Result< std::shared_ptr< arrow::dataset::FileFragment > > MakeFragment(arrow::dataset::FileSource source, arrow::compute::Expression partition_expression, std::shared_ptr< arrow::Schema > physical_schema) override
Create a FileFragment for a FileSource.
TTreeFileFragment(arrow::dataset::FileSource source, std::shared_ptr< arrow::dataset::FileFormat > format, arrow::compute::Expression partition_expression, std::shared_ptr< arrow::Schema > physical_schema)
std::vector< ReadOps > & ops()
std::shared_ptr< arrow::Buffer > GetPlaceholderForOp(size_t size)
arrow::Result< std::shared_ptr< arrow::io::OutputStream > > OpenOutputStream(const std::string &path, const std::shared_ptr< const arrow::KeyValueMetadata > &metadata) override
virtual std::unique_ptr< TTree > & GetTree(arrow::dataset::FileSource source)=0
TTreeFileWriteOptions(std::shared_ptr< arrow::dataset::FileFormat > format)
arrow::Status Write(const std::shared_ptr< arrow::RecordBatch > &batch) override
arrow::Future FinishInternal() override
TTreeFileWriter(std::shared_ptr< arrow::Schema > schema, std::shared_ptr< arrow::dataset::FileWriteOptions > options, std::shared_ptr< arrow::io::OutputStream > destination, arrow::fs::FileLocator destination_locator)
arrow::Status Write(const void *data, int64_t nbytes) override
arrow::Result< int64_t > Tell() const override
arrow::Status Close() override
TBranch * CreateBranch(char const *branchName, char const *sizeBranch)
TTreeOutputStream(TTree *, std::string branchPrefix)
GLdouble n
Definition glcorearb.h:1982
GLint GLsizei count
Definition glcorearb.h:399
GLuint64EXT * result
Definition glcorearb.h:5662
GLuint buffer
Definition glcorearb.h:655
GLsizeiptr size
Definition glcorearb.h:659
GLuint GLsizei const GLuint const GLintptr * offsets
Definition glcorearb.h:2595
GLenum array
Definition glcorearb.h:4274
GLuint const GLchar * name
Definition glcorearb.h:781
GLdouble f
Definition glcorearb.h:310
GLsizei GLsizei GLchar * source
Definition glcorearb.h:798
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLenum target
Definition glcorearb.h:1641
GLint GLint GLsizei GLint GLenum GLenum type
Definition glcorearb.h:275
GLintptr offset
Definition glcorearb.h:660
GLuint GLsizei GLsizei * length
Definition glcorearb.h:790
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLint ref
Definition glcorearb.h:291
GLint GLint GLsizei GLint GLenum format
Definition glcorearb.h:275
Defining ITS Vertex explicitly as messageable.
Definition Cartesian.h:288
RuntimeErrorRef runtime_error(const char *)
TBufferFile & rootBuffer()
char const * rootSuffixFromArrow(arrow::Type::type id)
auto arrowTypeFromROOT(EDataType type, int size)
void bigEndianCopy(void *dest, const void *src, int count, size_t typeSize)
Definition BigEndian.h:26
RuntimeErrorRef runtime_error_f(const char *,...)
std::shared_ptr< arrow::Buffer > targetBuffer
std::function< std::shared_ptr< arrow::dataset::FileWriteOptions >()> options
std::shared_ptr< o2::framework::TTreeFileFormat > format
VectorOfTObjectPtrs other
ctfTree Write()
std::unique_ptr< TTree > tree((TTree *) flIn.Get(std::string(o2::base::NameConf::CTFTREENAME).c_str()))
std::vector< ReadoutWindowData > rows