From 3c7089d424934fa840bc114b7cdf20c2ca49dee6 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Fri, 4 Sep 2026 08:39:07 +0200 Subject: [PATCH 1/2] [ntuple] always load streamer info on file open Populate the streamer infos of the file on LoadStructure(). This will make sure it is loaded together with the header and footer. We don't load the streamer info directly on file open in order to prevent computational work (decompressing the record) taking place when we open the file asynchronously. --- tree/ntuple/inc/ROOT/RMiniFile.hxx | 6 +- tree/ntuple/src/RMiniFile.cxx | 17 ++-- tree/ntuple/src/RNTupleMerger.cxx | 4 - tree/ntuple/src/RPageStorageFile.cxx | 9 ++- tree/ntuple/test/ntuple_emulated.cxx | 60 ++++---------- tree/ntuple/test/ntuple_evolution_shape.cxx | 88 ++++++--------------- tree/ntuple/test/ntuple_test.hxx | 7 ++ 7 files changed, 65 insertions(+), 126 deletions(-) diff --git a/tree/ntuple/inc/ROOT/RMiniFile.hxx b/tree/ntuple/inc/ROOT/RMiniFile.hxx index f7614695b0067..8c1205416d09b 100644 --- a/tree/ntuple/inc/ROOT/RMiniFile.hxx +++ b/tree/ntuple/inc/ROOT/RMiniFile.hxx @@ -68,6 +68,10 @@ private: /// what happens in `RNTupleFileWriter::WriteBlob()`. std::uint64_t fMaxKeySize = 0; + /// Information about the streamer info record cache on file open from the TFile header. Used in LoadStreamerInfo. + std::uint64_t fSeekKeyInfo = 0; + std::uint64_t fNbytesKeyAndInfo = 0; + /// Used when the file container turns out to be a bare file RResult GetNTupleBare(std::string_view ntupleName); /// Used when the file turns out to be a TFile container. The ntuplePath variable is either the ntuple name @@ -96,7 +100,7 @@ public: void ReadBuffer(void *buffer, size_t nbytes, std::uint64_t offset); /// Like ReadBuffer but returns a RResult instead of throwing. ROOT::RResult TryReadBuffer(void *buffer, size_t nbytes, std::uint64_t offset); - /// Attempts to load the streamer info from the file. + /// Load the streamer info from the file into the global list of streamer infos void LoadStreamerInfo(); std::uint64_t GetMaxKeySize() const { return fMaxKeySize; } diff --git a/tree/ntuple/src/RMiniFile.cxx b/tree/ntuple/src/RMiniFile.cxx index 79048d0a9f016..bd4e95fd7bfd2 100644 --- a/tree/ntuple/src/RMiniFile.cxx +++ b/tree/ntuple/src/RMiniFile.cxx @@ -723,16 +723,15 @@ std::uint64_t ROOT::Internal::RMiniFileReader::SearchInDirectory(std::uint64_t & void ROOT::Internal::RMiniFileReader::LoadStreamerInfo() { - RTFHeader fileHeader; - ReadBuffer(&fileHeader, sizeof(fileHeader), 0); - - const std::uint64_t seekKeyInfo = fileHeader.GetSeekInfo(); + if (fIsBare == 0) + return; RTFKey key; - ReadBuffer(&key, sizeof(key), seekKeyInfo); + ReadBuffer(&key, sizeof(key), fSeekKeyInfo); - const std::uint64_t nbytesInfo = fileHeader.GetNbytesInfo() - key.fKeyLen; - const std::uint64_t seekInfo = seekKeyInfo + key.fKeyLen; + R__ASSERT(fNbytesKeyAndInfo >= key.fKeyLen); + const std::uint64_t nbytesInfo = fNbytesKeyAndInfo - key.fKeyLen; + const std::uint64_t seekInfo = fSeekKeyInfo + key.fKeyLen; const std::uint32_t uncompLenInfo = key.fObjLen; auto streamerInfo = MakeUninitArray(uncompLenInfo); if (nbytesInfo == uncompLenInfo) { @@ -770,6 +769,9 @@ ROOT::RResult ROOT::Internal::RMiniFileReader::GetNTupleProper(st RTFHeader fileHeader; ReadBuffer(&fileHeader, sizeof(fileHeader), 0); + fSeekKeyInfo = fileHeader.GetSeekInfo(); + fNbytesKeyAndInfo = fileHeader.GetNbytesInfo(); + RTFKey key; RTFString name; ReadBuffer(&key, sizeof(key), fileHeader.fBEGIN); @@ -817,6 +819,7 @@ ROOT::RResult ROOT::Internal::RMiniFileReader::GetNTupleProper(st const auto objNbytes = key.GetSize() - key.fKeyLen; auto res = GetNTupleProperAtOffset(offset, objNbytes, key.fObjLen); + return res; } diff --git a/tree/ntuple/src/RNTupleMerger.cxx b/tree/ntuple/src/RNTupleMerger.cxx index 3b54445c39287..7d88c4070fc3c 100644 --- a/tree/ntuple/src/RNTupleMerger.cxx +++ b/tree/ntuple/src/RNTupleMerger.cxx @@ -1393,10 +1393,6 @@ ROOT::RResult RNTupleMerger::Merge(std::span sources, const // Merge main loop for (RPageSource *source : sources) { - // We need to make sure the streamer info from the source files is loaded otherwise we may not be able - // to build the streamer info of user-defined types unless we have their dictionaries available. - source->LoadStreamerInfo(); - source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting); auto srcDescriptor = source->GetSharedDescriptorGuard(); mergeData.fSrcDescriptor = &srcDescriptor.GetRef(); diff --git a/tree/ntuple/src/RPageStorageFile.cxx b/tree/ntuple/src/RPageStorageFile.cxx index 3e7ffd15eba40..019f826a7a7ea 100644 --- a/tree/ntuple/src/RPageStorageFile.cxx +++ b/tree/ntuple/src/RPageStorageFile.cxx @@ -425,6 +425,10 @@ void ROOT::Internal::RPageSourceFile::LoadStructureImpl() // Otherwise, the page source was created by OpenFromAnchor() if (!fAnchor) { fAnchor = fReader.GetNTuple(fNTupleName).Unwrap(); + // We couple finding the RNTuple anchor to loading the streamer infos. + // If we already have the anchor, we must have opened the file before (either through TFile or by the source of + // OpenWithDifferentAnchor(), in which case we already loaded the streamer info) . + fReader.LoadStreamerInfo(); } fReader.SetMaxKeySize(fAnchor->GetMaxKeySize()); @@ -712,7 +716,4 @@ ROOT::Internal::RPageSourceFile::LoadClusters(std::span clusterK return clusters; } -void ROOT::Internal::RPageSourceFile::LoadStreamerInfo() -{ - fReader.LoadStreamerInfo(); -} +void ROOT::Internal::RPageSourceFile::LoadStreamerInfo() {} diff --git a/tree/ntuple/test/ntuple_emulated.cxx b/tree/ntuple/test/ntuple_emulated.cxx index 013a8f1fd30bf..d61f7a5d6c84f 100644 --- a/tree/ntuple/test/ntuple_emulated.cxx +++ b/tree/ntuple/test/ntuple_emulated.cxx @@ -9,6 +9,8 @@ TEST(RNTupleEmulated, EmulatedFields_Simple) FileRaii fileGuard("test_ntuple_emulated_fields.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -32,12 +34,6 @@ TEST(RNTupleEmulated, EmulatedFields_Simple) auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "Outer_Simple").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -93,9 +89,6 @@ TEST(RNTupleEmulated, EmulatedFields_Simple) RNTupleDescriptor::RCreateModelOptions cmOpts; cmOpts.SetEmulateUnknownTypes(true); - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TClass::Init", "no dictionary for class", - /*matchFullMessage=*/false); std::unique_ptr file(TFile::Open(fileGuard.GetPath().c_str())); std::unique_ptr ntpl(file->Get("ntpl")); reader = RNTupleReader::Open(cmOpts, *ntpl); @@ -121,6 +114,8 @@ TEST(RNTupleEmulated, EmulatedFields_Vecs) FileRaii fileGuard("test_ntuple_emulated_fields_vecs.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -144,12 +139,6 @@ TEST(RNTupleEmulated, EmulatedFields_Vecs) auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("outers", "std::vector").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -222,6 +211,8 @@ TEST(RNTupleEmulated, EmulatedFields_VecsTemplatedWrapper) { FileRaii fileGuard("test_ntuple_emulated_fields_vecs_templated_wrapper.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -298,6 +289,8 @@ TEST(RNTupleEmulated, EmulatedFields_EmptyStruct) FileRaii fileGuard("test_ntuple_emulated_emptystruct.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -316,12 +309,6 @@ TEST(RNTupleEmulated, EmulatedFields_EmptyStruct) auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "Outer_EmptyStruct").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -373,6 +360,8 @@ TEST(RNTupleEmulated, EmulatedFields_EmptyVec) FileRaii fileGuard("test_ntuple_emulated_emptyvec.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -396,11 +385,6 @@ TEST(RNTupleEmulated, EmulatedFields_EmptyVec) ProcessLine("ptrInners->push_back(Inner_EmptyVec{});"); ProcessLine("ptrInners->push_back(Inner_EmptyVec{});"); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); writer.reset(); }); @@ -453,6 +437,8 @@ TEST(RNTupleEmulated, EmulatedFields_Write) FileRaii fileGuard("test_ntuple_emulated_write.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { fileGuard.PreserveFile(); @@ -476,11 +462,6 @@ TEST(RNTupleEmulated, EmulatedFields_Write) auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); writer.reset(); }); @@ -504,6 +485,8 @@ TEST(RNTupleEmulated, CollectionProxy) { FileRaii fileGuard("test_ntuple_emulated_collproxy.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + // Declare a custom type in a separate process, then write it through a custom collection proxy. // In the main process we load the generated RNTuple without having its dictionary available and we verify // we can read it (only) via field emulation (as an untyped VectorField). @@ -648,11 +631,6 @@ TEST(RNTupleEmulated, CollectionProxy) ProcessLine("pProxyC->v.clear();"); writer->Fill(); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); writer.reset(); }); @@ -692,6 +670,8 @@ TEST(RNTupleEmulated, MergeEmulated) FileRaii fileGuard2("test_ntuple_merge_emulated2.root"); FileRaii fileGuardOut("test_ntuple_merge_emulated_out.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { fileGuard1.PreserveFile(); fileGuard2.PreserveFile(); @@ -726,11 +706,6 @@ TEST(RNTupleEmulated, MergeEmulated) ProcessLine("ptr2->fInt2 = 66;"); writer2->Fill(); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); writer.reset(); writer2.reset(); }); @@ -745,9 +720,6 @@ TEST(RNTupleEmulated, MergeEmulated) } { - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.requiredDiag(kWarning, "TClass::Init", "no dictionary", /*matchFullMessage=*/false); - auto destination = std::make_unique("ntuple", fileGuardOut.GetPath(), RNTupleWriteOptions()); RNTupleMerger merger{std::move(destination)}; auto res = merger.Merge(sourcePtrs); diff --git a/tree/ntuple/test/ntuple_evolution_shape.cxx b/tree/ntuple/test/ntuple_evolution_shape.cxx index e06a6c46acc42..e0befde54ec88 100644 --- a/tree/ntuple/test/ntuple_evolution_shape.cxx +++ b/tree/ntuple/test/ntuple_evolution_shape.cxx @@ -114,6 +114,8 @@ TEST(RNTupleEvolution, AddedMemberObject) { FileRaii fileGuard("test_ntuple_evolution_added_member_object.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -131,12 +133,6 @@ struct AddedMemberObject { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "AddedMemberObject").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -357,6 +353,8 @@ TEST(RNTupleEvolution, RenamedMemberClass) // RNTuple currently does not support automatic schema evolution when a class is renamed. FileRaii fileGuard("test_ntuple_evolution_renamed_member_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -373,12 +371,6 @@ struct RenamedMemberClass { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RenamedMemberClass").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for members). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -448,6 +440,8 @@ TEST(RNTupleEvolution, AddedBaseClass) { FileRaii fileGuard("test_ntuple_evolution_added_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -464,12 +458,6 @@ struct AddedBaseDerived : public AddedBaseIntermediate { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "AddedBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -513,6 +501,8 @@ TEST(RNTupleEvolution, AddedSecondBaseClass) { FileRaii fileGuard("test_ntuple_evolution_added_second_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -529,12 +519,6 @@ struct AddedSecondBaseDerived : public AddedSecondBaseFirst { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "AddedSecondBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -572,6 +556,8 @@ TEST(RNTupleEvolution, PrependSecondBaseClass) // automatically evolve this case, even if the member fields and on-disk columns are compatible. FileRaii fileGuard("test_ntuple_evolution_prepend_second_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -588,12 +574,6 @@ struct PrependSecondBaseDerived : public PrependSecondBaseFirst { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "PrependSecondBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -623,6 +603,8 @@ TEST(RNTupleEvolution, AddedIntermediateClass) { FileRaii fileGuard("test_ntuple_evolution_added_intermediate_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -639,12 +621,6 @@ struct AddedIntermediateDerived : public AddedIntermediateBase { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "AddedIntermediateDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -674,6 +650,8 @@ TEST(RNTupleEvolution, RemovedBaseClass) { FileRaii fileGuard("test_ntuple_evolution_removed_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -693,12 +671,6 @@ struct RemovedBaseDerived : public RemovedBaseIntermediate { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RemovedBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -738,6 +710,8 @@ TEST(RNTupleEvolution, RemovedIntermediateClass) { FileRaii fileGuard("test_ntuple_evolution_removed_intermediate_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -757,12 +731,6 @@ struct RemovedIntermediateDerived : public RemovedIntermediate { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RemovedIntermediateDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -789,6 +757,8 @@ TEST(RNTupleEvolution, RemovedSecondBaseClass) { FileRaii fileGuard("test_ntuple_evolution_removed_second_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -808,12 +778,6 @@ struct RemovedSecondBaseDerived : public RemovedSecondBaseFirst, public RemovedS auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RemovedSecondBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -841,6 +805,8 @@ TEST(RNTupleEvolution, RenamedBaseClass) // RNTuple currently does not support automatic schema evolution when a class is renamed. FileRaii fileGuard("test_ntuple_evolution_renamed_base_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -857,12 +823,6 @@ struct RenamedBaseDerived : public RenamedBase1 { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RenamedBaseDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); @@ -890,6 +850,8 @@ TEST(RNTupleEvolution, RenamedIntermediateClass) // RNTuple currently does not support automatic schema evolution when a class is renamed. FileRaii fileGuard("test_ntuple_evolution_renamed_intermediate_class.root"); + EXPECT_NO_STREAMER_OR_DICTIONARY(); + ExecInFork([&] { // The child process writes the file and exits, but the file must be preserved to be read by the parent. fileGuard.PreserveFile(); @@ -909,12 +871,6 @@ struct RenamedIntermediateDerived : public RenamedIntermediate1 { auto model = RNTupleModel::Create(); model->AddField(RFieldBase::Create("f", "RenamedIntermediateDerived").Unwrap()); - // TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes). - // See also https://github.com/root-project/root/issues/9371 - ROOT::TestSupport::CheckDiagsRAII diagRAII; - diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", - /*matchFullMessage=*/false); - auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); }); diff --git a/tree/ntuple/test/ntuple_test.hxx b/tree/ntuple/test/ntuple_test.hxx index 32ea44985503e..1a12aa84bdd0a 100644 --- a/tree/ntuple/test/ntuple_test.hxx +++ b/tree/ntuple/test/ntuple_test.hxx @@ -150,4 +150,11 @@ void PatchRNTupleSection(std::string_view filePath, std::uint64_t sectionSeek, s std::uint64_t patchedOffsetIntoSection, const std::byte *bytesToWrite, std::size_t bytesToWriteLen, EEndianness sectionEndianness); +// TStreamerInfo::Build will report a warning for interpreted classes (but only for members). +// See also https://github.com/root-project/root/issues/9371 +#define EXPECT_NO_STREAMER_OR_DICTIONARY() \ + ROOT::TestSupport::CheckDiagsRAII diagNoStreamerOrDictRAII; \ + diagNoStreamerOrDictRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary", false); \ + diagNoStreamerOrDictRAII.optionalDiag(kWarning, "TClass::Init", "no dictionary for class", false) + #endif From 1110602075a411744e31366d9b7cc2918a328edb Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Fri, 4 Sep 2026 15:26:10 +0200 Subject: [PATCH 2/2] [ntuple] remove RPageSource::LoadStreamerInfo() --- tree/ntuple/inc/ROOT/RPageStorage.hxx | 4 ---- tree/ntuple/inc/ROOT/RPageStorageDaos.hxx | 2 -- tree/ntuple/inc/ROOT/RPageStorageFile.hxx | 2 -- tree/ntuple/src/RPageStorageDaos.cxx | 5 ----- tree/ntuple/src/RPageStorageFile.cxx | 2 -- tree/ntuple/test/ntuple_cluster.cxx | 1 - tree/ntuple/test/ntuple_endian.cxx | 1 - tree/ntuple/test/ntuple_pages.cxx | 1 - 8 files changed, 18 deletions(-) diff --git a/tree/ntuple/inc/ROOT/RPageStorage.hxx b/tree/ntuple/inc/ROOT/RPageStorage.hxx index b670e5989a447..dfcf2d11d411c 100644 --- a/tree/ntuple/inc/ROOT/RPageStorage.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorage.hxx @@ -906,10 +906,6 @@ public: /// connecting streamer fields so that emulated classes can be read. void RegisterStreamerInfos(); - /// Forces the loading of ROOT StreamerInfo from the underlying file. This currently only has an effect for - /// TFile-backed sources. - virtual void LoadStreamerInfo() = 0; - /// Creates a new PageSource using the same underlying file as this but referring to a different RNTuple, /// described by `anchorLink`. virtual std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, diff --git a/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx b/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx index d2b017516d2ab..b5ff59bddd0e6 100644 --- a/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx @@ -176,8 +176,6 @@ public: /// Return the object class used for user data OIDs in this ntuple. std::string GetObjectClass() const; - void LoadStreamerInfo() final; - std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, const ROOT::RNTupleReadOptions &options = {}) final; }; // class RPageSourceDaos diff --git a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx index c1881042c4ee3..d94feff68f1e7 100644 --- a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx @@ -189,8 +189,6 @@ public: std::vector> LoadClusters(std::span clusterKeys) final; - - void LoadStreamerInfo() final; }; // class RPageSourceFile } // namespace Internal diff --git a/tree/ntuple/src/RPageStorageDaos.cxx b/tree/ntuple/src/RPageStorageDaos.cxx index 7e5651f8d6761..0e3bffc8d0c6c 100644 --- a/tree/ntuple/src/RPageStorageDaos.cxx +++ b/tree/ntuple/src/RPageStorageDaos.cxx @@ -596,11 +596,6 @@ ROOT::Experimental::Internal::RPageSourceDaos::LoadClusters(std::span ROOT::Experimental::Internal::RPageSourceDaos::OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) diff --git a/tree/ntuple/src/RPageStorageFile.cxx b/tree/ntuple/src/RPageStorageFile.cxx index 019f826a7a7ea..effcdd4bd9a06 100644 --- a/tree/ntuple/src/RPageStorageFile.cxx +++ b/tree/ntuple/src/RPageStorageFile.cxx @@ -715,5 +715,3 @@ ROOT::Internal::RPageSourceFile::LoadClusters(std::span clusterK return clusters; } - -void ROOT::Internal::RPageSourceFile::LoadStreamerInfo() {} diff --git a/tree/ntuple/test/ntuple_cluster.cxx b/tree/ntuple/test/ntuple_cluster.cxx index 32808d8a0830e..6121a09d61e1e 100644 --- a/tree/ntuple/test/ntuple_cluster.cxx +++ b/tree/ntuple/test/ntuple_cluster.cxx @@ -43,7 +43,6 @@ class RPageSourceMock : public RPageSource { std::unique_ptr CloneImpl() const final { return nullptr; } void LoadPageListImpl(const ROOT::RNTupleLocator &, unsigned char *) final {} void LoadSealedPageImpl(const ROOT::RNTupleLocator &, RSealedPage &) final {} - void LoadStreamerInfo() final {} std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) final { diff --git a/tree/ntuple/test/ntuple_endian.cxx b/tree/ntuple/test/ntuple_endian.cxx index a1b22baf4adfe..9255a5d5c46ff 100644 --- a/tree/ntuple/test/ntuple_endian.cxx +++ b/tree/ntuple/test/ntuple_endian.cxx @@ -93,7 +93,6 @@ class RPageSourceMock : public RPageSource { std::unique_ptr CloneImpl() const final { return nullptr; } void LoadPageListImpl(const ROOT::RNTupleLocator &, unsigned char *) final {} void LoadSealedPageImpl(const ROOT::RNTupleLocator &, RSealedPage &) final {} - void LoadStreamerInfo() final {} std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) final diff --git a/tree/ntuple/test/ntuple_pages.cxx b/tree/ntuple/test/ntuple_pages.cxx index 2cfea078846b7..909f091072887 100644 --- a/tree/ntuple/test/ntuple_pages.cxx +++ b/tree/ntuple/test/ntuple_pages.cxx @@ -13,7 +13,6 @@ class RPageSourceMock : public RPageSource { std::unique_ptr CloneImpl() const final { return nullptr; } void LoadPageListImpl(const ROOT::RNTupleLocator &, unsigned char *) final {} void LoadSealedPageImpl(const ROOT::RNTupleLocator &, RSealedPage &) final {} - void LoadStreamerInfo() final {} std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) final {