Skip to content

[df] Fix global RSampleInfo::EntryRange when processing chains - #23548

Open
ravindra-RKB wants to merge 3 commits into
root-project:masterfrom
ravindra-RKB:feature-issue-22741
Open

ravindra-RKB wants to merge 3 commits into
root-project:masterfrom
ravindra-RKB:feature-issue-22741

Conversation

@ravindra-RKB

Copy link
Copy Markdown
Contributor

Fixes #22741

When iterating over a TChain using global clusters (e.g., when friend trees are present and ImplicitMT is enabled via RDatasetSpec), TTreeReader::GetEntriesRange() returns a global entry range instead of local ones. However, RSampleInfo::EntryRange() expects the range to be local to the current sample (file).

This PR adjusts the extracted range using TChain::GetTreeOffset so it is always converted back to local indices for the current sample. A unit test has been added to dataframe_datasetspec.cxx to verify this behavior explicitly.

Comment thread tree/dataframe/src/RLoopManager.cxx Outdated
Comment on lines +743 to +750
if (range.second == -1) {
range.second = tree->GetEntries(); // convert '-1', i.e. 'until the end', to the actual entry number
} else if (auto chain = dynamic_cast<TChain*>(r.GetTree())) {
// The reader is iterating over a TChain (e.g. from TTreeProcessorMT global clusters).
// The entry range from the reader is global, but RSampleInfo expects local indices.
Long64_t treeOffset = chain->GetTreeOffset()[chain->GetTreeNumber()];
range.first -= treeOffset;
range.second -= treeOffset;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't range.first need to be updated whether .second is -1 or not?

Is there structural guarantee that range.first and .second are greater than treeOffset? If not what is the semantic and how should it be handled?

@ravindra-RKB

Copy link
Copy Markdown
Contributor Author

Hi @pcanal,

If .second == -1, it means SetEntriesRange() was never called on the TTreeReader, so fBeginEntry retains its default value of 0. In this case, the reader is configured to read the entire dataset. When UpdateSampleInfo is called for a specific tree in the chain, the intended local range for this tree is exactly [0, tree->GetEntries()]. If we were to subtract treeOffset from range.first (which is 0) when treeOffset > 0 (e.g., for the 2nd tree in a chain), range.first would become negative and break the semantics. Thus, if .second == -1, .first is already correct (0) and doesn't need the global-to-local offset adjustment.

Regarding the structural guarantee: Yes, TTreeProcessorMT ensures that task clusters never cross file (tree) boundaries. Therefore, the global range.first and range.second assigned to a TTreeReader by TTreeProcessorMT will always fall within [treeOffset, treeOffset + tree->GetEntries()].

To make this logic more robust and self-documenting, I have pushed a new commit that adds a safety check (if (range.first >= treeOffset)) and makes range.first = 0 explicit when .second == -1.

Comment on lines +1095 to +1105
auto makeTree = [](const std::string& fname, int offset) {
TFile f(fname.c_str(), "RECREATE");
TTree t("t", "t");
int x; t.Branch("x", &x);
x = offset; t.Fill(); x = offset+1; t.Fill();
f.Write(); f.Close();
};
makeTree("test_issue22741_main1.root", 0);
makeTree("test_issue22741_main2.root", 2);
makeTree("test_issue22741_friend1.root", 0);
makeTree("test_issue22741_friend2.root", 2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part should be rewritten to use RAII to ensure file deletion after the test.

Comment on lines +1096 to +1097
TFile f(fname.c_str(), "RECREATE");
TTree t("t", "t");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use std::unique_ptr rather than allocating TFile and TTree on the stack

TTree t("t", "t");
int x; t.Branch("x", &x);
x = offset; t.Fill(); x = offset+1; t.Fill();
f.Write(); f.Close();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write is enough, Close is not necessary

Comment thread tree/dataframe/src/RLoopManager.cxx Outdated
Comment on lines 741 to 756
std::pair<Long64_t, Long64_t> range = r.GetEntriesRange();
R__ASSERT(range.first >= 0);
if (range.second == -1) {
// If no explicit range was set, fBeginEntry is 0. The local range is the entire tree.
range.first = 0;
range.second = tree->GetEntries(); // convert '-1', i.e. 'until the end', to the actual entry number
} else if (auto chain = dynamic_cast<TChain*>(r.GetTree())) {
// The reader is iterating over a TChain (e.g. from TTreeProcessorMT global clusters).
// The entry range from the reader is global, but RSampleInfo expects local indices.
// TTreeProcessorMT guarantees that clusters do not cross tree boundaries.
Long64_t treeOffset = chain->GetTreeOffset()[chain->GetTreeNumber()];
if (range.first >= treeOffset) {
range.first -= treeOffset;
range.second -= treeOffset;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should somehow be moved to RTTreeDS.cxx so we don't pollute RLoopManager.cxx with more TTree-specific logic (an effort that I've started some time ago, but not completed yet).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Confusing (wrong? buggy?) behavior of sample ranges with chained trees when using dataframes

3 participants