Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TTree] when merging, ignore trees without branches #17652

Merged
merged 4 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions io/io/test/TFileMergerTests.cxx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "ROOT/TestSupport.hxx"

#include "TFileMerger.h"
#include "TFileMergeInfo.h"

#include "TMemFile.h"
#include "TTree.h"
Expand Down Expand Up @@ -84,12 +85,12 @@ TEST(TFileMerger, MergeSingleOnlyListed)
hist3->Fill(1); hist3->Fill(1); hist3->Fill(1);
hist4->Fill(1); hist4->Fill(1); hist4->Fill(1); hist4->Fill(1);
a.Write();

TFileMerger merger;
auto output = std::unique_ptr<TFile>(new TFile("SingleOnlyListed.root", "RECREATE"));
bool success = merger.OutputFile(std::move(output));
ASSERT_TRUE(success);

merger.AddObjectNames("hist1");
merger.AddObjectNames("hist2");
merger.AddFile(&a, false);
Expand All @@ -101,3 +102,60 @@ TEST(TFileMerger, MergeSingleOnlyListed)
ASSERT_TRUE(output.get() && output->GetListOfKeys());
EXPECT_EQ(output->GetListOfKeys()->GetSize(), 2);
}

// https://github.com/root-project/root/issues/14558 aka https://its.cern.ch/jira/browse/ROOT-4716
TEST(TFileMerger, MergeBranches)
{
TTree atree("atree", "atitle");
int value;
atree.Branch("a", &value);

TTree abtree("abtree", "abtitle");
abtree.Branch("a", &value);
abtree.Branch("b", &value);
value = 11;
abtree.Fill();
value = 42;
abtree.Fill();

TTree dummy("emptytree", "emptytitle");
TList treelist;

// Case 1 - Static: NoBranch + NoEntries + 2 entries
treelist.Add(&dummy);
treelist.Add(&atree);
treelist.Add(&abtree);
std::unique_ptr<TFile> file1(TFile::Open("b_4716.root", "RECREATE"));
auto rtree = TTree::MergeTrees(&treelist);
ASSERT_TRUE(rtree->FindBranch("a") != nullptr);
EXPECT_EQ(rtree->FindBranch("a")->GetEntries(), 2);
ASSERT_TRUE(rtree->FindBranch("b") == nullptr);
file1->Write();

// Case 2 - This (NoBranch) + NoEntries + 2 entries
treelist.Clear();
treelist.Add(&atree);
treelist.Add(&abtree);
std::unique_ptr<TFile> file2(TFile::Open("c_4716.root", "RECREATE"));
TFileMergeInfo info2(file2.get());
dummy.Merge(&treelist, &info2);
ASSERT_TRUE(dummy.FindBranch("a") != nullptr);
ASSERT_TRUE(dummy.FindBranch("b") == nullptr);
EXPECT_EQ(dummy.FindBranch("a")->GetEntries(), 2);
EXPECT_EQ(atree.FindBranch("a")->GetEntries(), 2);
// atree has now 2 entries instead of zero since it was used as skeleton for dummy
file2->Write();

// Case 3 - This (NoEntries) + 2 entries
TTree a0tree("a0tree", "a0title"); // We cannot reuse atree since it was cannibalized by dummy
a0tree.Branch("a", &value);
treelist.Clear();
treelist.Add(&abtree);
std::unique_ptr<TFile> file3(TFile::Open("d_4716.root", "RECREATE"));
TFileMergeInfo info3(file3.get());
a0tree.Merge(&treelist, &info3);
ASSERT_TRUE(a0tree.FindBranch("a") != nullptr);
ASSERT_TRUE(a0tree.FindBranch("b") == nullptr);
EXPECT_EQ(a0tree.FindBranch("a")->GetEntries(), 2);
file3->Write();
}
104 changes: 90 additions & 14 deletions tree/tree/src/TTree.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
/** \class TTree
\ingroup tree

A TTree represents a columnar dataset. Any C++ type can be stored in its columns. The modern
A TTree represents a columnar dataset. Any C++ type can be stored in its columns. The modern
version of TTree is RNTuple: please consider using it before opting for TTree.

A TTree, often called in jargon *tree*, consists of a list of independent columns or *branches*,
Expand Down Expand Up @@ -141,19 +141,19 @@
~~~ {.cpp}
auto branch = tree.Branch( branchname, STLcollection, buffsize, splitlevel);
~~~
`STLcollection` is the address of a pointer to a container of the standard
library such as `std::vector`, `std::list`, containing pointers, fundamental types
`STLcollection` is the address of a pointer to a container of the standard
library such as `std::vector`, `std::list`, containing pointers, fundamental types
or objects.
If the splitlevel is a value bigger than 100 (`TTree::kSplitCollectionOfPointers`)
then the collection will be written in split mode, i.e. transparently storing
then the collection will be written in split mode, i.e. transparently storing
individual data members as arrays, therewith potentially increasing compression ratio.

### Note
In case of dynamic structures changing with each entry, see e.g.
### Note
In case of dynamic structures changing with each entry, see e.g.
~~~ {.cpp}
branch->SetAddress(void *address)
~~~
one must redefine the branch address before filling the branch
one must redefine the branch address before filling the branch
again. This is done via the `TBranch::SetAddress` member function.

\anchor addingacolumnofobjs
Expand Down Expand Up @@ -186,8 +186,8 @@
- `p_object` is a pointer to an object.
- If `className` is not specified, the `Branch` method uses the type of `p_object`
to determine the type of the object.
- If `className` is used to specify explicitly the object type, the `className`
must be of a type related to the one pointed to by the pointer. It should be
- If `className` is used to specify explicitly the object type, the `className`
must be of a type related to the one pointed to by the pointer. It should be
either a parent or derived class.

Note: The pointer whose address is passed to `TTree::Branch` must not
Expand All @@ -205,13 +205,13 @@
tree.Branch(branchname, &p_object);
~~~
In either case, the ownership of the object is not taken over by the `TTree`.
Even though in the first case an object is be allocated by `TTree::Branch`,
Even though in the first case an object is be allocated by `TTree::Branch`,
the object will <b>not</b> be deleted when the `TTree` is deleted.

\anchor addingacolumnoftclonesarray
## Add a column holding TClonesArray instances

*The usage of `TClonesArray` should be abandoned in favour of `std::vector`,
*The usage of `TClonesArray` should be abandoned in favour of `std::vector`,
for which `TTree` has been heavily optimised, as well as `RNTuple`.*

~~~ {.cpp}
Expand Down Expand Up @@ -347,7 +347,7 @@
~~~
## TTree Diagram

The following diagram shows the organisation of the federation of classes related to TTree.
The following diagram shows the organisation of the federation of classes related to TTree.

Begin_Macro
../../../tutorials/legacy/tree/tree.C
Expand Down Expand Up @@ -6841,6 +6841,8 @@
///
/// Trees in the list can be memory or disk-resident trees.
/// The new tree is created in the current directory (memory if gROOT).
/// Trees with no branches will be skipped, the branch structure
/// will be taken from the first non-zero-branch Tree of {li}

TTree* TTree::MergeTrees(TList* li, Option_t* options)
{
Expand All @@ -6852,8 +6854,15 @@
while ((obj=next())) {
if (!obj->InheritsFrom(TTree::Class())) continue;
TTree *tree = (TTree*)obj;
if (tree->GetListOfBranches()->IsEmpty()) {
if (gDebug > 2) {
Warning("MergeTrees","TTree %s has no branches, skipping.", tree->GetName());

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / alma9 arm64 CMAKE_BUILD_TYPE=RelWithDebInfo

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / mac15 ARM64 LLVM_ENABLE_ASSERTIONS=On, CMAKE_CXX_STANDARD=20

call to non-static member function without an object argument

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / mac13 ARM64 LLVM_ENABLE_ASSERTIONS=On, builtin_zlib=ON

call to non-static member function without an object argument

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / fedora41 LLVM_ENABLE_ASSERTIONS=On

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / mac-beta ARM64 LLVM_ENABLE_ASSERTIONS=On, CMAKE_CXX_STANDARD=20

call to non-static member function without an object argument

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / ubuntu22 imt=Off, LLVM_ENABLE_ASSERTIONS=On, CMAKE_BUILD_TYPE=Debug

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / alma9-clang clang LLVM_ENABLE_ASSERTIONS=On, CMAKE_C_COMPILER=clang, CMAKE_CXX_COMPILER=clang++

call to non-static member function without an object argument

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / alma8 LLVM_ENABLE_ASSERTIONS=On

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / debian125 LLVM_ENABLE_ASSERTIONS=On, CMAKE_CXX_STANDARD=20

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / ubuntu2410 LLVM_ENABLE_ASSERTIONS=On, CMAKE_BUILD_TYPE=Debug

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / alma9 LLVM_ENABLE_ASSERTIONS=On, CMAKE_BUILD_TYPE=Debug

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object

Check failure on line 6859 in tree/tree/src/TTree.cxx

View workflow job for this annotation

GitHub Actions / alma9 march_native CMAKE_BUILD_TYPE=RelWithDebInfo, CMAKE_CXX_FLAGS=-march=native, CMAKE_C_FLAGS=-march=native, fortran=OFF

cannot call member function ‘virtual void TObject::Warning(const char*, const char*, ...) const’ without object
}
continue; // Completely ignore the empty trees.
}
Long64_t nentries = tree->GetEntries();
if (nentries == 0) continue;
if (newtree && nentries == 0)
continue; // If we already have the structure and we have no entry, save time and skip
if (!newtree) {
newtree = (TTree*)tree->CloneTree(-1, options);
if (!newtree) continue;
Expand All @@ -6867,7 +6876,8 @@
newtree->ResetBranchAddresses();
continue;
}

if (nentries == 0)
continue;
newtree->CopyEntries(tree, -1, options, true);
}
if (newtree && newtree->GetTreeIndex()) {
Expand All @@ -6880,9 +6890,43 @@
/// Merge the trees in the TList into this tree.
///
/// Returns the total number of entries in the merged tree.
/// Trees with no branches will be skipped, the branch structure
/// will be taken from the first non-zero-branch Tree of {this+li}

Long64_t TTree::Merge(TCollection* li, Option_t *options)
{
if (fBranches.IsEmpty()) {
if (!li || li->IsEmpty())
return 0; // Nothing to do ....
// Let's find the first non-empty
TIter next(li);
TTree *tree;
while ((tree = (TTree *)next())) {
if (tree == this || tree->GetListOfBranches()->IsEmpty()) {
if (gDebug > 2) {
Warning("Merge","TTree %s has no branches, skipping.", tree->GetName());
}
continue;
}
// We could come from a list made up of different names, the first one still wins
tree->SetName(this->GetName());
auto prevEntries = tree->GetEntries();
auto result = tree->Merge(li, options);
if (result != prevEntries) {
// If there is no additional entries, the first write was enough.
tree->Write();
}
// Make sure things are really written out to disk before attempting any reading.
if (tree->GetCurrentFile()) {
tree->GetCurrentFile()->Flush();
// Read back the complete info in this TTree, so that caller does not
// inadvertently write the empty tree.
tree->GetDirectory()->ReadTObject(this, this->GetName());
}
return result;
}
return 0; // All trees have empty branches
}
if (!li) return 0;
Long64_t storeAutoSave = fAutoSave;
// Disable the autosave as the TFileMerge keeps a list of key and deleting the underlying
Expand Down Expand Up @@ -6915,11 +6959,43 @@
/// info->fOutputDirectory and then overlay the new TTree information onto
/// this TTree object (so that this TTree object is now the appropriate to
/// use for further merging).
/// Trees with no branches will be skipped, the branch structure
/// will be taken from the first non-zero-branch Tree of {this+li}
///
/// Returns the total number of entries in the merged tree.

Long64_t TTree::Merge(TCollection* li, TFileMergeInfo *info)
{
if (fBranches.IsEmpty()) {
if (!li || li->IsEmpty())
return 0; // Nothing to do ....
// Let's find the first non-empty
TIter next(li);
TTree *tree;
while ((tree = (TTree *)next())) {
if (tree == this || tree->GetListOfBranches()->IsEmpty()) {
if (gDebug > 2) {
Warning("Merge","TTree %s has no branches, skipping.", tree->GetName());
}
continue;
}
// We could come from a list made up of different names, the first one still wins
tree->SetName(this->GetName());
auto prevEntries = tree->GetEntries();
auto result = tree->Merge(li, info);
if (result != prevEntries) {
// If there is no additional entries, the first write was enough.
tree->Write();
}
// Make sure things are really written out to disk before attempting any reading.
info->fOutputDirectory->GetFile()->Flush();
// Read back the complete info in this TTree, so that TFileMerge does not
// inadvertently write the empty tree.
info->fOutputDirectory->ReadTObject(this, this->GetName());
return result;
}
return 0; // All trees have empty branches
}
const char *options = info ? info->fOptions.Data() : "";
if (info && info->fIsFirst && info->fOutputDirectory && info->fOutputDirectory->GetFile() != GetCurrentFile()) {
if (GetCurrentFile() == nullptr) {
Expand Down
Loading