Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions tree/ntuple/inc/ROOT/RNTupleDescriptor.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,7 @@ public:
/// * Fields have valid parents
/// * Number of columns is constant across column representations
RResult<void> EnsureValidDescriptor() const;
RNTupleDescriptor &GetDescriptor() { return fDescriptor; }
const RNTupleDescriptor &GetDescriptor() const { return fDescriptor; }
RStringPool &GetStringPool() { return *fDescriptor.fStringPool; }
RNTupleDescriptor MoveDescriptor();
Expand Down
17 changes: 17 additions & 0 deletions tree/ntuple/inc/ROOT/RNTupleWriteOptions.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ The compression settings for this RNTuple
</td>
</tr>

<tr>
<td>`fClusterGroupPageThreshold`</td>
<td>`std::size_t`</td>
<td>500'000</td>
<td>
Commit a new cluster group when the number of pages in the current cluster group surpasses the given limit.
Reading and writing will typically pin page locations of one to two cluster groups in memory concurrently.
This limit typically does not need to be adjusted except, perhaps, for very wide data models (>100k columns)
or very tight memory budgets.
Set to zero to disable automatic cluster group creation.
</td>
</tr>

<tr>
<td>`ApproxZippedClusterSize`</td>
<td>`std::size_t`</td>
Expand Down Expand Up @@ -194,6 +207,7 @@ public:

protected:
std::uint32_t fCompression{RCompressionSetting::EDefaults::kUseGeneralPurpose};
std::size_t fClusterGroupPageThreshold = 500'000;
std::size_t fApproxZippedClusterSize = 128 * 1024 * 1024;
std::size_t fMaxUnzippedClusterSize = 10 * fApproxZippedClusterSize;
std::size_t fInitialUnzippedPageSize = 16;
Expand Down Expand Up @@ -221,6 +235,9 @@ public:
fCompression = CompressionSettings(algorithm, compressionLevel);
}

std::size_t GetClusterGroupPageThreshold() const { return fClusterGroupPageThreshold; }
void SetClusterGroupPageThreshold(std::size_t val) { fClusterGroupPageThreshold = val; }

std::size_t GetApproxZippedClusterSize() const { return fApproxZippedClusterSize; }
void SetApproxZippedClusterSize(std::size_t val);

Expand Down
14 changes: 7 additions & 7 deletions tree/ntuple/inc/ROOT/RNTupleWriter.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ private:
std::vector<std::shared_ptr<RNTupleAttrSetWriter>> fAttributeSets;

ROOT::NTupleSize_t fLastCommittedClusterGroup = 0;
/// If CommitCluster(false) is used together with a non-zero cluster group commit page threshold
/// (i.e., with the option to automatically do cluster group commits), warn the caller once.
bool fFlagHasWarnedAboutClusterGroupCommitConflict = false;

RNTupleWriter(std::unique_ptr<ROOT::RNTupleModel> model, std::unique_ptr<Internal::RPageSink> sink);

Expand Down Expand Up @@ -200,13 +203,10 @@ public:
void FlushColumns() { fFillContext.FlushColumns(); }
/// Flush so far filled entries to storage
void FlushCluster() { fFillContext.FlushCluster(); }
/// Ensure that the data from the so far seen Fill calls has been written to storage
void CommitCluster(bool commitClusterGroup = false)
{
fFillContext.FlushCluster();
if (commitClusterGroup)
CommitClusterGroup();
}
/// Ensure that the data from the so far seen Fill calls has been written to storage.
void CommitCluster();
/// Same as CommitCluster() but explicitly create (or avoid creation of) a cluster group.
void CommitCluster(bool commitClusterGroup);
/// Closes the underlying file (page sink) and expires the model. Automatically called on destruct.
/// Once the dataset is committed, calls to Fill(), [Commit|Flush]Cluster(), FlushColumns(), CreateEntry(),
/// and model updating fail.
Expand Down
9 changes: 7 additions & 2 deletions tree/ntuple/inc/ROOT/RPageStorage.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,13 @@ private:
/// Used to map the IDs of the descriptor to the physical IDs issued during header/footer serialization
ROOT::Internal::RNTupleSerializer::RContext fSerializationContext;

/// Remembers the starting cluster id for the next cluster group
std::uint64_t fNextClusterInGroup = 0;
/// Remembers the starting cluster id for the next group. Used also to check if there are new clusters to commit
/// for a cluster group (otherwise committing a cluster group is a noop)
ROOT::NTupleSize_t fPrevGroupNClusters = 0;
/// Keeps track of the number of pages so far committed in all the clusters of the current cluster group.
/// Used to automatically create a new cluster group once the user-defined threshold of pages per cluster is
/// surpassed.
ROOT::NTupleSize_t fNPagesInGroup = 0;
/// Used to calculate the number of entries in the current cluster
ROOT::NTupleSize_t fPrevClusterNEntries = 0;
/// Keeps track of the number of elements in the currently open cluster. Indexed by column id.
Expand Down
3 changes: 1 addition & 2 deletions tree/ntuple/src/RNTupleAttrWriting.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ void ROOT::RNTupleAttrSetWriter::CommitRange(ROOT::RNTupleAttrPendingRange pendi
ROOT::Internal::RNTupleLink ROOT::RNTupleAttrSetWriter::Commit()
{
fFillContext.FlushCluster();
if (fFillContext.GetNEntries() > 0)
fFillContext.fSink->CommitClusterGroup();
fFillContext.fSink->CommitClusterGroup();
return fFillContext.fSink->CommitDataset();
}
2 changes: 2 additions & 0 deletions tree/ntuple/src/RNTupleDescriptor.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,8 @@ ROOT::RResult<ROOT::RClusterGroupDescriptor> ROOT::Internal::RClusterGroupDescri
{
if (fClusterGroup.fClusterGroupId == ROOT::kInvalidDescriptorId)
return R__FAIL("unset cluster group ID");
if (fClusterGroup.fEntrySpan == 0 || fClusterGroup.fNClusters == 0)
return R__FAIL("invalid empty cluster group");
RClusterGroupDescriptor result;
std::swap(result, fClusterGroup);
return result;
Expand Down
7 changes: 2 additions & 5 deletions tree/ntuple/src/RNTupleMerger.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -2026,9 +2026,7 @@ ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const
// Commit the output
for (auto &[attrSetName, attrSetData] : attrSetsData) {
attrSetData.fStrategy.BeforeCommit();
if (attrSetData.fSink->GetNEntries() > 0) {
attrSetData.fSink->CommitClusterGroup();
}
attrSetData.fSink->CommitClusterGroup();
auto anchorLink = attrSetData.fSink->CommitDataset();
fDestination->CommitAttributeSet(attrSetName, anchorLink);
}
Expand All @@ -2038,9 +2036,8 @@ ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const
// Commit the output
if (fDestination->GetNEntries() == 0)
R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
else
fDestination->CommitClusterGroup();

fDestination->CommitClusterGroup();
fDestination->CommitDataset();

return RResult<void>::Success();
Expand Down
6 changes: 6 additions & 0 deletions tree/ntuple/src/RNTupleSerialize.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,8 @@ ROOT::Internal::RNTupleSerializer::SerializeClusterSummary(const RClusterSummary
{
if (clusterSummary.fNEntries >= (static_cast<std::uint64_t>(1) << 56)) {
return R__FAIL("number of entries in cluster exceeds maximum of 2^56");
} else if (clusterSummary.fNEntries == 0) {
return R__FAIL("invalid serialization of empty cluster");
}

auto base = reinterpret_cast<unsigned char *>(buffer);
Expand Down Expand Up @@ -1317,6 +1319,10 @@ ROOT::Internal::RNTupleSerializer::DeserializeClusterSummary(const void *buffer,
ROOT::RResult<std::uint32_t>
ROOT::Internal::RNTupleSerializer::SerializeClusterGroup(const RClusterGroup &clusterGroup, void *buffer)
{
if (clusterGroup.fEntrySpan == 0 || clusterGroup.fNClusters == 0) {
return R__FAIL("invalid serialization of empty cluster group");
}

auto base = reinterpret_cast<unsigned char *>(buffer);
auto pos = base;
void **where = (buffer == nullptr) ? &buffer : reinterpret_cast<void **>(&pos);
Expand Down
21 changes: 19 additions & 2 deletions tree/ntuple/src/RNTupleWriter.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,27 @@ ROOT::RNTupleWriter::Append(std::unique_ptr<ROOT::RNTupleModel> model, std::stri
return Create(std::move(model), std::move(sink), options);
}

void ROOT::RNTupleWriter::CommitCluster()
{
FlushCluster();
}

void ROOT::RNTupleWriter::CommitCluster(bool commitClusterGroup)
{
FlushCluster();
if (commitClusterGroup) {
CommitClusterGroup();
} else if (fFillContext.fSink->GetWriteOptions().GetClusterGroupPageThreshold() > 0 &&
!fFlagHasWarnedAboutClusterGroupCommitConflict) {
R__LOG_WARNING(ROOT::Internal::NTupleLog())
<< "CommitCluster(false) is used while automatically committing cluster groups is active "
"(cluster group page threshold > 0). A cluster group may still be committed.";
fFlagHasWarnedAboutClusterGroupCommitConflict = true;
}
}

void ROOT::RNTupleWriter::CommitClusterGroup()
{
if (GetNEntries() == fLastCommittedClusterGroup)
return;
fFillContext.fSink->CommitClusterGroup();
fLastCommittedClusterGroup = GetNEntries();
}
Expand Down
39 changes: 23 additions & 16 deletions tree/ntuple/src/RPageStorage.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1841,11 +1841,12 @@ void ROOT::Internal::RPagePersistentSink::CommitStagedClusters(std::span<RStaged
{
for (const auto &cluster : clusters) {
RClusterDescriptorBuilder clusterBuilder;
clusterBuilder.ClusterId(fDescriptorBuilder.GetDescriptor().GetNActiveClusters())
clusterBuilder.ClusterId(fPrevGroupNClusters + fDescriptorBuilder.GetDescriptor().GetNActiveClusters())
.FirstEntryIndex(fPrevClusterNEntries)
.NEntries(cluster.fNEntries);
for (const auto &columnInfo : cluster.fColumnInfos) {
const auto colId = columnInfo.fPageRange.GetPhysicalColumnId();
fNPagesInGroup += columnInfo.fPageRange.GetPageInfos().size();
if (columnInfo.fIsSuppressed) {
assert(columnInfo.fPageRange.GetPageInfos().empty());
clusterBuilder.MarkSuppressedColumnRange(colId);
Expand All @@ -1871,6 +1872,9 @@ void ROOT::Internal::RPagePersistentSink::CommitStagedClusters(std::span<RStaged

fDescriptorBuilder.AddCluster(clusterBuilder.MoveDescriptor().Unwrap());
fPrevClusterNEntries += cluster.fNEntries;
if (fNPagesInGroup > fOptions->GetClusterGroupPageThreshold() && fOptions->GetClusterGroupPageThreshold() > 0) {
CommitClusterGroup();
}
}
}

Expand All @@ -1879,10 +1883,15 @@ void ROOT::Internal::RPagePersistentSink::CommitClusterGroup()
const auto &descriptor = fDescriptorBuilder.GetDescriptor();

const auto nClusters = descriptor.GetNActiveClusters();
if (nClusters == 0) {
assert(fNPagesInGroup == 0);
return;
}

std::vector<ROOT::DescriptorId_t> physClusterIDs;
physClusterIDs.reserve(nClusters);
for (auto i = fNextClusterInGroup; i < nClusters; ++i) {
physClusterIDs.emplace_back(fSerializationContext.MapClusterId(i));
for (ROOT::NTupleSize_t i = 0; i < nClusters; ++i) {
physClusterIDs.emplace_back(fSerializationContext.MapClusterId(fPrevGroupNClusters + i));
}

auto szPageList =
Expand All @@ -1894,26 +1903,24 @@ void ROOT::Internal::RPagePersistentSink::CommitClusterGroup()
const auto locator = CommitClusterGroupImpl(bufPageList.get(), szPageList);
RClusterGroupDescriptorBuilder cgBuilder;
cgBuilder.ClusterGroupId(clusterGroupId).PageListLocator(locator).PageListLength(szPageList);
if (fNextClusterInGroup == nClusters) {
cgBuilder.MinEntry(0).EntrySpan(0).NClusters(0);
} else {
const auto &firstClusterDesc = descriptor.GetClusterDescriptor(fNextClusterInGroup);
const auto &lastClusterDesc = descriptor.GetClusterDescriptor(nClusters - 1);
cgBuilder.MinEntry(firstClusterDesc.GetFirstEntryIndex())
.EntrySpan(lastClusterDesc.GetFirstEntryIndex() + lastClusterDesc.GetNEntries() -
firstClusterDesc.GetFirstEntryIndex())
.NClusters(nClusters - fNextClusterInGroup);
}
const auto &firstClusterDesc = descriptor.GetClusterDescriptor(fPrevGroupNClusters);
const auto &lastClusterDesc = descriptor.GetClusterDescriptor(fPrevGroupNClusters + nClusters - 1);
cgBuilder.MinEntry(firstClusterDesc.GetFirstEntryIndex())
.EntrySpan(lastClusterDesc.GetFirstEntryIndex() + lastClusterDesc.GetNEntries() -
firstClusterDesc.GetFirstEntryIndex())
.NClusters(nClusters);
std::vector<ROOT::DescriptorId_t> clusterIds;
clusterIds.reserve(nClusters);
for (auto i = fNextClusterInGroup; i < nClusters; ++i) {
clusterIds.emplace_back(i);
for (ROOT::NTupleSize_t i = 0; i < nClusters; ++i) {
clusterIds.emplace_back(fPrevGroupNClusters + i);
}
cgBuilder.AddSortedClusters(clusterIds);
fDescriptorBuilder.AddClusterGroup(cgBuilder.MoveDescriptor().Unwrap());
fSerializationContext.MapClusterGroupId(clusterGroupId);
fDescriptorBuilder.GetDescriptor().DropClusterGroupDetails(clusterGroupId);

fNextClusterInGroup = nClusters;
fPrevGroupNClusters += nClusters;
fNPagesInGroup = 0;
}

void ROOT::Internal::RPagePersistentSink::CommitAttributeSet(std::string_view attrSetName,
Expand Down
25 changes: 23 additions & 2 deletions tree/ntuple/test/ntuple_serialize.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -559,19 +559,40 @@ TEST(RNTuple, SerializeClusterSummary)
} catch (const ROOT::RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("exceed"));
}
summary.fNEntries = 0;
try {
RNTupleSerializer::SerializeClusterSummary(summary, buffer);
FAIL() << "empty cluster should fail";
} catch (const ROOT::RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("invalid serialization of empty cluster"));
}
}

TEST(RNTuple, SerializeClusterGroup)
{
RNTupleSerializer::RClusterGroup group;
group.fMinEntry = 7;
group.fEntrySpan = 84600;
Comment thread
jblomer marked this conversation as resolved.
Resolved
group.fNClusters = 42;
group.fEntrySpan = 0;
group.fNClusters = 0;
group.fPageListEnvelopeLink.fLength = 42;
group.fPageListEnvelopeLink.fLocator.SetPosition(137U);
group.fPageListEnvelopeLink.fLocator.SetNBytesOnStorage(7);

unsigned char buffer[52];
try {
RNTupleSerializer::SerializeClusterGroup(group, nullptr).Unwrap();
FAIL() << "empty cluster group should fail";
} catch (const ROOT::RException &e) {
EXPECT_THAT(e.what(), testing::HasSubstr("invalid serialization of empty cluster group"));
}
group.fEntrySpan = 84600;
try {
RNTupleSerializer::SerializeClusterGroup(group, nullptr).Unwrap();
FAIL() << "empty cluster group should fail";
} catch (const ROOT::RException &e) {
EXPECT_THAT(e.what(), testing::HasSubstr("invalid serialization of empty cluster group"));
}
group.fNClusters = 42;
ASSERT_EQ(48u, RNTupleSerializer::SerializeClusterGroup(group, nullptr).Unwrap());
EXPECT_EQ(48u, RNTupleSerializer::SerializeClusterGroup(group, buffer).Unwrap());
RNTupleSerializer::RClusterGroup reco;
Expand Down
35 changes: 34 additions & 1 deletion tree/ntuple/test/ntuple_storage.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,8 @@ TEST(RPageSink, MultipleClusterGroups)
// This pattern should work: CommitCluster(false) followed by CommitCluster(true) and
// be identical to a single call to CommitCluster(true)
ntuple->CommitCluster(true);
// Calling this a second time should be a noop
ntuple->CommitCluster(true);
*wrPt = 24.0;
ntuple->Fill();
ntuple->CommitCluster();
Expand All @@ -825,6 +827,35 @@ TEST(RPageSink, MultipleClusterGroups)
EXPECT_EQ(12.0, *rdPt);
}

TEST(RPageSink, MultipleClusterGroupsAutoCommit)
{
FileRaii fileGuard("test_ntuple_multi_cluster_groups_auto_commit.ntuple");

{
auto model = RNTupleModel::Create();
auto wrPt = model->MakeField<float>("pt");
ROOT::RNTupleWriteOptions opts;
opts.SetClusterGroupPageThreshold(2);
auto writer = RNTupleWriter::Recreate(std::move(model), "f", fileGuard.GetPath(), opts);
writer->Fill();
writer->CommitCluster();
writer->Fill();
writer->CommitCluster();
writer->Fill();
writer->CommitCluster(); // auto commit cluster group
writer->Fill();
ROOT::TestSupport::CheckDiagsRAII diagRAII;
diagRAII.requiredDiag(kWarning, "ROOT.NTuple",
"CommitCluster(false) is used while automatically committing cluster groups is active",
false);
writer->CommitCluster(false);
}

auto ntuple = RNTupleReader::Open("f", fileGuard.GetPath());
EXPECT_EQ(2U, ntuple->GetDescriptor().GetNClusterGroups());
EXPECT_EQ(4U, ntuple->GetDescriptor().GetNClusters());
}

TEST(RPageNullSink, Basics)
{
auto model = RNTupleModel::Create();
Expand Down Expand Up @@ -1424,7 +1455,9 @@ TEST(RPageSource, ClusterGroupBudget)
auto ptrCtr = model->MakeField<std::vector<int>>("ctr");
ptrCtr->resize(1);
{
auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath());
ROOT::RNTupleWriteOptions opts;
opts.SetClusterGroupPageThreshold(0);
auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath(), opts);
for (int c = 0; c < 10; ++c) {
for (int i = 0; i < 10; ++i) {
(*ptrCtr)[0] = 10 * c + i;
Expand Down
Loading