Skip to content

Conversation

@JasMehta08
Copy link
Contributor

This Pull request:

improves the existing Performance Metrics in RNTuple.

Changes or fixes:

This PR addresses the issue that had been raised for the improvements of metrics. It implements,

  • GetSparseness(): Calculates the ratio of explicit payload bytes read to the total file size.
  • GetRandomness(): Calculates the ratio of seek distance to bytes read (identifies inefficient access patterns).
  • GetTransactions(): Tracks the total number of physical I/O operations (now using a dedicated fTransactions atomic counter).

Implementation Details:

These have been implemented through the introduction of transient, thread-safe members in RNTupleMetrics,

  • std::atomicstd::uint64_t fSumSkip: To track total seek distance.

  • std::atomicstd::uint64_t fExplicitBytesRead: To track payload bytes.

  • std::atomicstd::uint64_t fTransactions: To track I/O operations.

I have changed RPageStorageFile to update these counters during ReadBuffer, ReadV, and LoadClusters operations. The use of std::atomic ensures thread safety with negligible performance overhead on the I/O path.

Reset Functionality & Accuracy:
I added a Reset() method to RNTupleMetrics. This is needed for obtaining accurate Randomness metrics for the analysis loop.

  • Without Reset(): The metric is dominated by the initial file seek (Header → Footer → Header), resulting in a Randomness score > 2.0.

  • With Reset(): Users can clear the counters after initialization, isolating the steady-state performance of their event loop.

Verification:

Added a new unit test TEST(Metrics, IOMetrics) in tree/ntuple/test/ntuple_metrics.cxx to verify the logic and the Reset() behavior.

Also tested locally using this code

Click to view Manual Verification Code & Output
#include <ROOT/RNTupleModel.hxx>
#include <ROOT/RNTupleWriter.hxx>
#include <ROOT/RNTupleReader.hxx>
#include <ROOT/RNTupleMetrics.hxx>
#include <iostream>

int main()
{
   using namespace ROOT::Experimental;
   using namespace ROOT;


   std::string ntupleName = "ntuple";
   std::string fileName = "verify_metrics.root";

   // Create a dummy RNTuple
   std::cout << "[INFO] Creating dummy RNTuple..." << std::endl;
   {
      auto model = RNTupleModel::Create();
      auto field = model->MakeField<int>("val");
      auto writer = RNTupleWriter::Recreate(std::move(model), ntupleName, fileName);
      for (int i = 0; i < 1000; ++i) {
         *field = i;
         writer->Fill();
      }
   }

   // Read it back
   std::cout << "[INFO] Reading RNTuple and enabling metrics..." << std::endl;
   auto reader = RNTupleReader::Open(ntupleName, fileName);
   reader->EnableMetrics();

   // Read everything to force I/O
   for (auto entry : *reader) {
      (void)entry;
   }

   // Print Metrics
   auto& metrics = reader->GetMetrics();
   
   std::cout << "\n===== RNTuple Performance Metrics Report =====" << std::endl;
   std::cout << "Transactions: " << metrics.GetTransactions() << " (Expected > 0)" << std::endl;
   std::cout << "Sparseness:   " << metrics.GetSparseness() << " (Expected > 0.0)" << std::endl;
   std::cout << "Randomness:   " << metrics.GetRandomness() << " (Expected > 0.0)" << std::endl;
   std::cout << "==============================================\n" << std::endl;

   // Verification Logic
   bool passed = true;
   if (metrics.GetTransactions() <= 0) {
       std::cerr << "[FAIL] Transactions should be > 0" << std::endl;
       passed = false;
   }
   if (metrics.GetSparseness() <= 0.0 || metrics.GetSparseness() > 1.0) {
       std::cerr << "[FAIL] Sparseness should be in (0.0, 1.0]" << std::endl;
       passed = false;
   }
   if (metrics.GetRandomness() <= 0.0) {
       std::cerr << "[FAIL] Randomness should be > 0.0" << std::endl;
       passed = false;
   }

   // Verify Reset functionality
   std::cout << "[INFO] Testing Reset()..." << std::endl;
   reader->GetMetrics().Reset();
   
   // We expect Randomness to go from ~2.13 down to 0.0
   if (metrics.GetSparseness() == 0.0 && metrics.GetRandomness() == 0.0) {
       std::cout << "[SUCCESS] Reset() worked! Randomness dropped to 0.0" << std::endl;
   } else {
       std::cerr << "[FAIL] Reset() failed! Counters are not zero." << std::endl;
       passed = false;
   }
   
   if (passed) {
       std::cout << "[SUCCESS] All metrics logic verified!" << std::endl;
       return 0;
   } else {
       std::cout << "[FAILURE] Some metrics checks failed." << std::endl;
       return 1;
   }
}
[INFO] Reading RNTuple and enabling metrics...

===== RNTuple Performance Metrics Report =====
Transactions: 2 (Expected > 0)
Sparseness:   0.181648 (Expected > 0.0)
Randomness:   2.13746 (Expected > 0.0)
==============================================

[INFO] Testing Reset()...
[SUCCESS] Reset() worked! Randomness dropped to 0.0
[SUCCESS] All metrics logic verified!
#include <ROOT/RNTupleModel.hxx>
#include <ROOT/RNTupleWriter.hxx>
#include <ROOT/RNTupleReader.hxx>
#include <ROOT/RNTupleMetrics.hxx>
#include <iostream>
#include <cstdlib>

int main()
{
   using namespace ROOT::Experimental;
   using namespace ROOT;

   std::string ntupleName = "ntuple";
   std::string fileName = "verify_metrics_v2.root";

   // Create Data (50MB analysis dataset)
   {
      auto model = RNTupleModel::Create();
      auto fieldPt = model->MakeField<float>("pt");
      auto fieldEta = model->MakeField<float>("eta");
      auto fieldPhi = model->MakeField<float>("phi");

      // Realistic cluster size for analysis (1MB)
      RNTupleWriteOptions options;
      options.SetApproxZippedClusterSize(1024 * 1024); 
      
      auto writer = RNTupleWriter::Recreate(std::move(model), ntupleName, fileName, options);
      
      std::cout << "[INFO] Creating 50MB analysis dataset..." << std::endl;
      // ~4.2M events × 12 bytes/event = ~50MB
      for (int i = 0; i < 4200000; ++i) {
         *fieldPt = 20.0f + static_cast<float>(std::rand() % 100);
         *fieldEta = -2.5f + static_cast<float>(std::rand() % 500) / 100.0f;
         *fieldPhi = -3.14f + static_cast<float>(std::rand() % 628) / 100.0f;
         writer->Fill();
         
         if (i % 500000 == 0) {
            std::cout << "  Written " << (i / 1000) << "k events..." << std::endl;
         }
      }
      std::cout << "[INFO] Dataset created successfully." << std::endl;
   }

   // Open and IMMEDIATE RESET
   std::cout << "[INFO] Opening RNTuple..." << std::endl;
   auto reader = RNTupleReader::Open(ntupleName, fileName);
   reader->EnableMetrics();
   
   // FORCE the first seek (Tail -> Head) so we can clear it
   try {
        // LoadEntry is void but might throw if out of range, 0 is safe here
        reader->LoadEntry(0); 
   } catch (...) {}

   auto& m_pre = reader->GetMetrics();
   std::cout << "\n[DEBUG] Pre-Reset Metrics:" << std::endl;
   std::cout << "  Transactions: " << m_pre.GetTransactions() << std::endl;
   std::cout << "  Randomness:   " << m_pre.GetRandomness() << std::endl;
   std::cout << "  Total File Size: " << m_pre.GetTotalFileSize() << " bytes" << std::endl;
   std::cout << "  N Entries: " << reader->GetNEntries() << std::endl;

   std::cout << "[INFO] Resetting Metrics to isolate analysis phase..." << std::endl;
   reader->GetMetrics().Reset();

   // Analysis Loop (actually access the data)
   std::cout << "[INFO] Running analysis on " << reader->GetNEntries() << " events..." << std::endl;
   
   auto viewPt = reader->GetView<float>("pt");
   auto viewEta = reader->GetView<float>("eta");
   auto viewPhi = reader->GetView<float>("phi");
   
   long selectedEvents = 0;
   double sumPt = 0.0;
   
   for (auto i : reader->GetEntryRange()) {
      float pt = viewPt(i);
      float eta = viewEta(i);
      float phi = viewPhi(i);
      
      if (pt > 25.0 && std::abs(eta) < 2.4) {
         selectedEvents++;
         sumPt += pt;
      }
      
      if (i % 500000 == 0 && i > 0) {
         std::cout << "  Processed " << (i / 1000) << "k events..." << std::endl;
      }
   }
   
   std::cout << "[INFO] Analysis complete. Selected " << selectedEvents 
             << " events, <pT> = " << (sumPt / selectedEvents) << " GeV" << std::endl;

   auto& metrics = reader->GetMetrics();

   // Report Analysis Metrics
   std::cout << "\n===== Analysis Performance Report =====" << std::endl;
   std::cout << "Transactions: " << metrics.GetTransactions() << " (Expected > 0)" << std::endl;
   std::cout << "Sparseness:   " << metrics.GetSparseness() << std::endl;
   std::cout << "Randomness:   " << metrics.GetRandomness() << " (Expected low for sequential)" << std::endl;
   std::cout << "======================================\n" << std::endl;

   if (metrics.GetTransactions() > 0 && metrics.GetRandomness() < 1.0) {
       std::cout << "[SUCCESS] Analysis metrics look good!" << std::endl;
       std::cout << "          Sequential reading achieved (low randomness)." << std::endl;
       return 0;
   } else {
       std::cerr << "[FAIL] Unexpected metrics pattern." << std::endl;
       std::cerr << "       Transactions: " << metrics.GetTransactions() << std::endl;
       std::cerr << "       Randomness: " << metrics.GetRandomness() << std::endl;
       return 1;
   }
}
[INFO] Creating 50MB analysis dataset...
  Written 0k events...
  Written 500k events...
  Written 1000k events...
  Written 1500k events...
  Written 2000k events...
  Written 2500k events...
  Written 3000k events...
  Written 3500k events...
  Written 4000k events...
[INFO] Dataset created successfully.
[INFO] Opening RNTuple...

[DEBUG] Pre-Reset Metrics:
  Transactions: 4
  Randomness:   24.7884
  Total File Size: 0 bytes
  N Entries: 4200000
[INFO] Resetting Metrics to isolate analysis phase...
[INFO] Running analysis on 4200000 events...
  Processed 500k events...
  Processed 1000k events...
  Processed 1500k events...
  Processed 2000k events...
  Processed 2500k events...
  Processed 3000k events...
  Processed 3500k events...
  Processed 4000k events...
[INFO] Analysis complete. Selected 3782202 events, <pT> = 72.4999 GeV

===== Analysis Performance Report =====
Transactions: 24 (Expected > 0)
Sparseness:   0.919237
Randomness:   4.08388e-05 (Expected low for sequential)
======================================

[SUCCESS] Analysis metrics look good!
          Sequential reading achieved (low randomness).
## Checklist:
  • tested changes locally
  • updated the docs (if necessary)

This PR fixes #20853

@JasMehta08 JasMehta08 requested a review from jblomer as a code owner January 22, 2026 19:47
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.

[ntuple] add several useful performance metrics

1 participant