From 7c1166bcf20c0ceff4c4194738939e6dbf80dec3 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 7 Jul 2026 15:12:28 +0200 Subject: [PATCH 1/2] First evtpool merger --- Generators/CMakeLists.txt | 8 ++ Generators/src/MergeEventPool.cxx | 203 ++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 Generators/src/MergeEventPool.cxx diff --git a/Generators/CMakeLists.txt b/Generators/CMakeLists.txt index 5624ce7df5f07..54a15ad05e943 100644 --- a/Generators/CMakeLists.txt +++ b/Generators/CMakeLists.txt @@ -165,6 +165,14 @@ o2_add_test_root_macro(share/egconfig/pythia8_userhooks_charm.C LABELS generators) endif() +o2_add_executable(merge-evtpool + COMPONENT_NAME generators + SOURCES src/MergeEventPool.cxx + PUBLIC_LINK_LIBRARIES O2::SimulationDataFormat + ROOT::Core + ROOT::Tree + Boost::program_options) + o2_data_file(COPY share/external DESTINATION Generators) o2_data_file(COPY share/egconfig DESTINATION Generators) o2_data_file(COPY share/TPCLoopers DESTINATION Generators) diff --git a/Generators/src/MergeEventPool.cxx b/Generators/src/MergeEventPool.cxx new file mode 100644 index 0000000000000..e34fae9dc8111 --- /dev/null +++ b/Generators/src/MergeEventPool.cxx @@ -0,0 +1,203 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \brief Merges multiple event-pool files (e.g. evtpool.root / genevents_Kine.root, +/// produced by o2-sim --noGeant) into a single "o2sim" tree. +/// +/// Unlike a naive TFileMerger-style concatenation (as done by O2DPG's root_merger.py), +/// this tool renumbers MCEventHeader's event ID so that it stays unique and monotonically +/// increasing across the merged output, instead of resetting per input file. No other +/// remapping is needed: each tree entry already holds one full, self-contained event, so +/// MCTrack mother/daughter indices (which are local to that entry) remain valid as-is. + +#include "SimulationDataFormat/MCTrack.h" +#include "SimulationDataFormat/MCEventHeader.h" +#include "SimulationDataFormat/TrackReference.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bpo = boost::program_options; +namespace fs = std::filesystem; + +namespace +{ +const char* kTrackBranch = "MCTrack"; +const char* kHeaderBranch = "MCEventHeader."; +const char* kTrackRefBranch = "TrackRefs"; + +// Checks that every input file exists, is readable, and has a tree with the branches +// this tool needs to merge (MCTrack + MCEventHeader are mandatory, TrackRefs optional +// but must be consistently present or absent across all files). Nothing is written +// until all inputs pass this check. +bool checkFiles(std::vector const& files, std::string const& treename, bool& hasTrackRefs) +{ + bool ok = true; + bool first = true; + for (auto const& f : files) { + if (!fs::exists(f)) { + LOG(error) << "Input file " << f << " does not exist"; + ok = false; + continue; + } + std::unique_ptr file(TFile::Open(f.c_str(), "READ")); + if (!file || file->IsZombie()) { + LOG(error) << "Cannot open " << f; + ok = false; + continue; + } + auto tree = (TTree*)file->Get(treename.c_str()); + if (!tree) { + LOG(error) << "No tree named '" << treename << "' found in " << f; + ok = false; + continue; + } + const bool hasTracks = tree->GetBranch(kTrackBranch) != nullptr; + const bool hasHeader = tree->GetBranch(kHeaderBranch) != nullptr; + const bool hasRefs = tree->GetBranch(kTrackRefBranch) != nullptr; + if (!hasTracks || !hasHeader) { + LOG(error) << "File " << f << " is missing the required '" << kTrackBranch << "' and/or '" << kHeaderBranch << "' branch"; + ok = false; + continue; + } + if (first) { + hasTrackRefs = hasRefs; + first = false; + } else if (hasTrackRefs != hasRefs) { + LOG(error) << "Inconsistent schema: file " << f << (hasRefs ? " has" : " lacks") << " a '" << kTrackRefBranch << "' branch, unlike previous input files"; + ok = false; + } + LOG(info) << " OK " << f << " (" << tree->GetEntries() << " events)"; + } + return ok; +} + +// Merges the already-validated input files into outfile, giving every event a fresh, +// globally unique, monotonically increasing event ID (starting at startId). +Long64_t mergeFiles(std::vector const& files, std::string const& treename, + std::string const& outfile, bool hasTrackRefs, UInt_t startId) +{ + TFile fout(outfile.c_str(), "RECREATE"); + if (fout.IsZombie()) { + LOG(fatal) << "Cannot create output file " << outfile; + return -1; + } + + auto tracks = std::make_unique>(); + auto header = std::make_unique(); + auto trackrefs = std::make_unique>(); + auto* tracksPtr = tracks.get(); + auto* headerPtr = header.get(); + auto* trackrefsPtr = trackrefs.get(); + + auto outTree = new TTree(treename.c_str(), treename.c_str()); + outTree->Branch(kTrackBranch, &tracksPtr); + outTree->Branch(kHeaderBranch, &headerPtr); + if (hasTrackRefs) { + outTree->Branch(kTrackRefBranch, &trackrefsPtr); + } + + UInt_t nextEventId = startId; + Long64_t totalEvents = 0; + + for (auto const& f : files) { + std::unique_ptr fin(TFile::Open(f.c_str(), "READ")); + auto tin = (TTree*)fin->Get(treename.c_str()); + + tin->SetBranchAddress(kTrackBranch, &tracksPtr); + tin->SetBranchAddress(kHeaderBranch, &headerPtr); + if (hasTrackRefs) { + tin->SetBranchAddress(kTrackRefBranch, &trackrefsPtr); + } + + const Long64_t nEntries = tin->GetEntries(); + LOG(info) << "Merging " << nEntries << " events from " << f << " (event ID " << nextEventId << ".." << (nextEventId + nEntries - 1) << ")"; + for (Long64_t i = 0; i < nEntries; ++i) { + tin->GetEntry(i); + header->SetEventID(nextEventId++); + outTree->Fill(); + } + totalEvents += nEntries; + } + + fout.cd(); + outTree->Write("", TObject::kWriteDelete); + fout.Close(); + + return totalEvents; +} +} // namespace + +int main(int argc, char* argv[]) +{ + bpo::options_description options("o2-generators-merge-evtpool options"); + options.add_options() + ("input,i", bpo::value()->required(), "comma-separated list of input event-pool ROOT files") + ("output,o", bpo::value()->required(), "output ROOT file with the merged event pool") + ("treename,t", bpo::value()->default_value("o2sim"), "name of the tree to merge") + ("start-id", bpo::value()->default_value(1), "event ID assigned to the first merged event") + ("help,h", "produce help message"); + + bpo::variables_map vm; + try { + bpo::store(bpo::parse_command_line(argc, argv, options), vm); + if (vm.count("help")) { + LOG(info) << options; + return 0; + } + bpo::notify(vm); + } catch (const bpo::error& e) { + LOG(fatal) << "Error parsing command-line arguments: " << e.what() << "\n\n" + << options; + return 1; + } + + std::vector infiles; + { + std::stringstream ss(vm["input"].as()); + std::string tok; + while (std::getline(ss, tok, ',')) { + if (!tok.empty()) { + infiles.push_back(tok); + } + } + } + if (infiles.empty()) { + LOG(fatal) << "No input files given"; + return 1; + } + + const std::string outfile = vm["output"].as(); + const std::string treename = vm["treename"].as(); + const UInt_t startId = vm["start-id"].as(); + + LOG(info) << "Validating " << infiles.size() << " input file(s) ..."; + bool hasTrackRefs = false; + if (!checkFiles(infiles, treename, hasTrackRefs)) { + LOG(fatal) << "Validation failed; not writing any output"; + return 1; + } + + LOG(info) << "Merging into " << outfile << " ..."; + const Long64_t total = mergeFiles(infiles, treename, outfile, hasTrackRefs, startId); + if (total < 0) { + return 1; + } + + LOG(info) << "Done: wrote " << total << " events (event ID " << startId << ".." << (startId + total - 1) << ") to " << outfile; + return 0; +} From 21b4e88083ede04e2de9ea8f9c094c89bf7b1db9 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Thu, 9 Jul 2026 15:58:12 +0200 Subject: [PATCH 2/2] Improved version: added reading parallelization + alien files reading --- Generators/CMakeLists.txt | 4 +- Generators/src/MergeEventPool.cxx | 364 +++++++++++++++++++++++++----- 2 files changed, 316 insertions(+), 52 deletions(-) diff --git a/Generators/CMakeLists.txt b/Generators/CMakeLists.txt index 54a15ad05e943..206a506bfb1e5 100644 --- a/Generators/CMakeLists.txt +++ b/Generators/CMakeLists.txt @@ -171,7 +171,9 @@ o2_add_executable(merge-evtpool PUBLIC_LINK_LIBRARIES O2::SimulationDataFormat ROOT::Core ROOT::Tree - Boost::program_options) + ROOT::Net + Boost::program_options + Threads::Threads) o2_data_file(COPY share/external DESTINATION Generators) o2_data_file(COPY share/egconfig DESTINATION Generators) diff --git a/Generators/src/MergeEventPool.cxx b/Generators/src/MergeEventPool.cxx index e34fae9dc8111..d346a3bd60240 100644 --- a/Generators/src/MergeEventPool.cxx +++ b/Generators/src/MergeEventPool.cxx @@ -12,11 +12,17 @@ /// \brief Merges multiple event-pool files (e.g. evtpool.root / genevents_Kine.root, /// produced by o2-sim --noGeant) into a single "o2sim" tree. /// -/// Unlike a naive TFileMerger-style concatenation (as done by O2DPG's root_merger.py), -/// this tool renumbers MCEventHeader's event ID so that it stays unique and monotonically -/// increasing across the merged output, instead of resetting per input file. No other -/// remapping is needed: each tree entry already holds one full, self-contained event, so -/// MCTrack mother/daughter indices (which are local to that entry) remain valid as-is. +/// This tool merges event pools renumbering MCEventHeader's event ID so that it stays unique and +/// increasing across the merged output, instead of resetting per input file. +/// +/// Input files can be given directly, or collected from text files listing further paths +/// (one per line, '#' comments allowed, resolved recursively). Both the pool files and the +/// list files themselves may live on AliEn (alien:// URLs); an AliEn list file is fetched +/// to a local temp copy via alien_cp (the same idiom used for dynamic configuration files +/// in GeneratorHybrid.cxx) and removed again once read. Reading of the input files is +/// parallelized: each file is deserialized by a worker thread into memory, while a single +/// writer thread fills the output tree strictly in input-file order, so the merged tree's +/// physical layout is unchanged with respect to a purely sequential merge. #include "SimulationDataFormat/MCTrack.h" #include "SimulationDataFormat/MCEventHeader.h" @@ -25,10 +31,24 @@ #include #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include #include +#include #include namespace bpo = boost::program_options; @@ -39,22 +59,145 @@ namespace const char* kTrackBranch = "MCTrack"; const char* kHeaderBranch = "MCEventHeader."; const char* kTrackRefBranch = "TrackRefs"; +const char* kProtocol = "alien://"; + +bool isAlienPath(std::string const& path) +{ + return path.rfind(kProtocol, 0) == 0; +} + +bool isRootFile(std::string const& path) +{ + std::string p = isAlienPath(path) ? path.substr(strlen(kProtocol)) : path; + const std::string suffix = ".root"; + return p.size() >= suffix.size() && p.compare(p.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +// TJAlienConnectionManager (behind TFile::Open("alien://...")) is not safe against +// concurrent Connect() calls: several worker threads opening AliEn files at once corrupt +// its shared websocket/TLS connection state and crash. The open/connect step is then serialized with a mutex +std::mutex gAlienOpenMutex; + +std::unique_ptr openInputFile(std::string const& path) +{ + if (isAlienPath(path)) { + std::lock_guard lk(gAlienOpenMutex); + return std::unique_ptr(TFile::Open(path.c_str(), "READ")); + } + return std::unique_ptr(TFile::Open(path.c_str(), "READ")); +} + +// Reads the lines of a local text file. nullopt if it could not be opened. +std::optional> readLocalListFileLines(std::string const& path) +{ + std::ifstream in(path); + if (!in.is_open()) { + return std::nullopt; + } + std::vector lines; + std::string line; + while (std::getline(in, line)) { + lines.push_back(line); + } + return lines; +} + +// Fetches an AliEn list file to a local temp copy and reads its lines; the temp copy +// is removed before returning. +std::optional> readAlienListFileLines(std::string const& entry) +{ + if (!gGrid) { + LOG(error) << "Not connected to AliEn; cannot read " << entry; + return std::nullopt; + } + static std::atomic counter{0}; + const auto tmpPath = fs::temp_directory_path() / + fs::path("merge-evtpool-list-" + std::to_string(::getpid()) + "-" + std::to_string(counter++) + ".txt"); + + TString cmd = Form("alien_cp %s file:%s", entry.c_str(), tmpPath.c_str()); + const bool fetched = gSystem->Exec(cmd.Data()) == 0; + + std::optional> result; + if (fetched) { + result = readLocalListFileLines(tmpPath.string()); + if (!result) { + LOG(error) << "Fetched " << entry << " from AliEn but could not read the local copy " << tmpPath.string(); + } + } else { + LOG(error) << "Failed to fetch list file " << entry << " from AliEn (alien_cp error)"; + } + + std::error_code ec; + fs::remove(tmpPath, ec); + return result; +} + +// Returns the lines of a files list +std::optional> readListFileLines(std::string const& entry) +{ + return isAlienPath(entry) ? readAlienListFileLines(entry) : readLocalListFileLines(entry); +} + +// Reads a text file listing input paths, one per line +// ('#' comments and blank lines ignored). Each listed path is either a .root file (local +// or alien://) or itself another list file. +void expandInputEntry(std::string const& entry, std::vector& out, std::vector& stack) +{ + if (isRootFile(entry)) { + out.push_back(entry); + return; + } + if (std::find(stack.begin(), stack.end(), entry) != stack.end()) { + LOG(error) << "Reference to an existing list " << entry << "; ignoring"; + return; + } + auto lines = readListFileLines(entry); + if (!lines) { + LOG(error) << "Cannot open " << entry << " (neither a .root file nor a readable list)"; + return; + } + stack.push_back(entry); + for (auto line : *lines) { + const auto b = line.find_first_not_of(" \t\r\n"); + if (b == std::string::npos) { + continue; + } + const auto e = line.find_last_not_of(" \t\r\n"); + line = line.substr(b, e - b + 1); + if (line.empty() || line[0] == '#') { + continue; + } + expandInputEntry(line, out, stack); + } + stack.pop_back(); +} + +// Expands a list of raw --input entries (each either a .root file or a list) into +// the flat list of .root files to merge. +std::vector expandInputs(std::vector const& rawEntries) +{ + std::vector result; + std::vector stack; + for (auto const& e : rawEntries) { + expandInputEntry(e, result, stack); + } + return result; +} // Checks that every input file exists, is readable, and has a tree with the branches -// this tool needs to merge (MCTrack + MCEventHeader are mandatory, TrackRefs optional -// but must be consistently present or absent across all files). Nothing is written -// until all inputs pass this check. -bool checkFiles(std::vector const& files, std::string const& treename, bool& hasTrackRefs) +// this tool needs to merge, as produced by the standard o2-sim simulation. +bool checkFiles(std::vector const& files, std::string const& treename, std::vector& entryCounts) { bool ok = true; - bool first = true; - for (auto const& f : files) { - if (!fs::exists(f)) { + entryCounts.assign(files.size(), 0); + for (size_t i = 0; i < files.size(); ++i) { + auto const& f = files[i]; + if (!isAlienPath(f) && !fs::exists(f)) { LOG(error) << "Input file " << f << " does not exist"; ok = false; continue; } - std::unique_ptr file(TFile::Open(f.c_str(), "READ")); + std::unique_ptr file = openInputFile(f); if (!file || file->IsZombie()) { LOG(error) << "Cannot open " << f; ok = false; @@ -69,27 +212,67 @@ bool checkFiles(std::vector const& files, std::string const& treena const bool hasTracks = tree->GetBranch(kTrackBranch) != nullptr; const bool hasHeader = tree->GetBranch(kHeaderBranch) != nullptr; const bool hasRefs = tree->GetBranch(kTrackRefBranch) != nullptr; - if (!hasTracks || !hasHeader) { - LOG(error) << "File " << f << " is missing the required '" << kTrackBranch << "' and/or '" << kHeaderBranch << "' branch"; + if (!hasTracks || !hasHeader || !hasRefs) { + LOG(error) << "File " << f << " is missing the required '" << kTrackBranch << "', '" << kHeaderBranch + << "' and/or '" << kTrackRefBranch << "' branch"; ok = false; continue; } - if (first) { - hasTrackRefs = hasRefs; - first = false; - } else if (hasTrackRefs != hasRefs) { - LOG(error) << "Inconsistent schema: file " << f << (hasRefs ? " has" : " lacks") << " a '" << kTrackRefBranch << "' branch, unlike previous input files"; - ok = false; - } - LOG(info) << " OK " << f << " (" << tree->GetEntries() << " events)"; + entryCounts[i] = tree->GetEntries(); + LOG(info) << " OK " << f << " (" << entryCounts[i] << " events)"; } return ok; } +// One deserialized event, buffered in memory between the reader thread that produced it +// and the writer thread that fills the output tree. +struct Event { + std::vector tracks; + o2::dataformats::MCEventHeader header; + std::vector trackrefs; +}; + +// Reads one whole input file into memory, assigning event IDs starting at startId. +std::vector readFile(std::string const& file, std::string const& treename, Long64_t nEntries, UInt_t startId) +{ + std::vector events; + events.reserve(nEntries); + + std::unique_ptr fin = openInputFile(file); + if (!fin || fin->IsZombie()) { + // the file passed validation earlier, so this only happens if it vanished in between + LOG(fatal) << "File " << file << " became unreadable after validation"; + } + auto tin = (TTree*)fin->Get(treename.c_str()); + + auto tracks = std::make_unique>(); + auto header = std::make_unique(); + auto trackrefs = std::make_unique>(); + auto* tracksPtr = tracks.get(); + auto* headerPtr = header.get(); + auto* trackrefsPtr = trackrefs.get(); + tin->SetBranchAddress(kTrackBranch, &tracksPtr); + tin->SetBranchAddress(kHeaderBranch, &headerPtr); + tin->SetBranchAddress(kTrackRefBranch, &trackrefsPtr); + + UInt_t id = startId; + for (Long64_t i = 0; i < nEntries; ++i) { + tin->GetEntry(i); + headerPtr->SetEventID(id++); + events.push_back(Event{*tracksPtr, *headerPtr, *trackrefsPtr}); + } + return events; +} + // Merges the already-validated input files into outfile, giving every event a fresh, -// globally unique, monotonically increasing event ID (starting at startId). +// globally unique event ID (starting at startId). +// +// Reading is parallelized across up to `jobs` worker threads (one input file at a time +// per thread); a single writer (this thread) fills the output tree strictly in input-file +// order as soon as each file's buffer is ready. Long64_t mergeFiles(std::vector const& files, std::string const& treename, - std::string const& outfile, bool hasTrackRefs, UInt_t startId) + std::string const& outfile, std::vector const& entryCounts, + UInt_t startId, unsigned int jobs) { TFile fout(outfile.c_str(), "RECREATE"); if (fout.IsZombie()) { @@ -107,31 +290,86 @@ Long64_t mergeFiles(std::vector const& files, std::string const& tr auto outTree = new TTree(treename.c_str(), treename.c_str()); outTree->Branch(kTrackBranch, &tracksPtr); outTree->Branch(kHeaderBranch, &headerPtr); - if (hasTrackRefs) { - outTree->Branch(kTrackRefBranch, &trackrefsPtr); + outTree->Branch(kTrackRefBranch, &trackrefsPtr); + + // per-file starting event ID, computed upfront from the validated entry counts + std::vector startIds(files.size()); + { + UInt_t next = startId; + for (size_t i = 0; i < files.size(); ++i) { + startIds[i] = next; + next += static_cast(entryCounts[i]); + } } - UInt_t nextEventId = startId; - Long64_t totalEvents = 0; + std::vector> fileBuffers(files.size()); + std::vector> fileReady(files.size()); + for (auto& r : fileReady) { + r.store(false); + } + std::atomic nextFileToRead{0}; + std::atomic nextFileToWrite{0}; + std::mutex readyMutex; + std::condition_variable readyCv; - for (auto const& f : files) { - std::unique_ptr fin(TFile::Open(f.c_str(), "READ")); - auto tin = (TTree*)fin->Get(treename.c_str()); + const unsigned int nWorkers = std::max(1u, std::min(jobs, files.size() > 0 ? files.size() : 1)); + // bound on how far readers may run ahead of the writer, so buffered-but-unwritten + // files cannot pile up in memory when reading outpaces writing/compression + const size_t maxFilesAhead = 2 * static_cast(nWorkers); - tin->SetBranchAddress(kTrackBranch, &tracksPtr); - tin->SetBranchAddress(kHeaderBranch, &headerPtr); - if (hasTrackRefs) { - tin->SetBranchAddress(kTrackRefBranch, &trackrefsPtr); + auto worker = [&]() { + while (true) { + const size_t idx = nextFileToRead.fetch_add(1); + if (idx >= files.size()) { + break; + } + { + std::unique_lock lk(readyMutex); + readyCv.wait(lk, [&] { return idx < nextFileToWrite.load(std::memory_order_acquire) + maxFilesAhead; }); + } + LOG(info) << "Reading " << entryCounts[idx] << " events from " << files[idx] + << " (event ID " << startIds[idx] << ".." << (startIds[idx] + entryCounts[idx] - 1) << ")"; + fileBuffers[idx] = readFile(files[idx], treename, entryCounts[idx], startIds[idx]); + { + // the store must happen under the mutex: otherwise it can race with the writer's + // check and the notification is lost (deadlock on the last file) + std::lock_guard lk(readyMutex); + fileReady[idx].store(true, std::memory_order_release); + } + readyCv.notify_all(); } + }; + std::vector workers; + workers.reserve(nWorkers); + for (unsigned int w = 0; w < nWorkers; ++w) { + workers.emplace_back(worker); + } - const Long64_t nEntries = tin->GetEntries(); - LOG(info) << "Merging " << nEntries << " events from " << f << " (event ID " << nextEventId << ".." << (nextEventId + nEntries - 1) << ")"; - for (Long64_t i = 0; i < nEntries; ++i) { - tin->GetEntry(i); - header->SetEventID(nextEventId++); + Long64_t totalEvents = 0; + for (size_t idx = 0; idx < files.size(); ++idx) { + { + std::unique_lock lk(readyMutex); + readyCv.wait(lk, [&] { return fileReady[idx].load(std::memory_order_acquire); }); + } + for (auto& ev : fileBuffers[idx]) { + *tracksPtr = std::move(ev.tracks); + *headerPtr = std::move(ev.header); + *trackrefsPtr = std::move(ev.trackrefs); outTree->Fill(); } - totalEvents += nEntries; + totalEvents += static_cast(fileBuffers[idx].size()); + // free the buffer as soon as it has been written out and let waiting readers advance + fileBuffers[idx].clear(); + fileBuffers[idx].shrink_to_fit(); + { + std::lock_guard lk(readyMutex); + nextFileToWrite.store(idx + 1, std::memory_order_release); + } + readyCv.notify_all(); + } + + for (auto& t : workers) { + t.join(); } fout.cd(); @@ -146,10 +384,13 @@ int main(int argc, char* argv[]) { bpo::options_description options("o2-generators-merge-evtpool options"); options.add_options() - ("input,i", bpo::value()->required(), "comma-separated list of input event-pool ROOT files") - ("output,o", bpo::value()->required(), "output ROOT file with the merged event pool") + ("input,i", bpo::value()->required(), "comma-separated list of inputs: event-pool ROOT files " + ", and/or text files (local or alien://, fetched via alien_cp) listing more " + "paths (one per line, '#' comments allowed)") + ("output,o", bpo::value()->default_value("evtpool.root"), "output ROOT file with the merged event pool") ("treename,t", bpo::value()->default_value("o2sim"), "name of the tree to merge") ("start-id", bpo::value()->default_value(1), "event ID assigned to the first merged event") + ("jobs,j", bpo::value()->default_value(8), "number of worker threads used to read input files in parallel (0 = auto-detect)") ("help,h", "produce help message"); bpo::variables_map vm; @@ -166,34 +407,55 @@ int main(int argc, char* argv[]) return 1; } - std::vector infiles; + std::vector rawEntries; { std::stringstream ss(vm["input"].as()); std::string tok; while (std::getline(ss, tok, ',')) { if (!tok.empty()) { - infiles.push_back(tok); + rawEntries.push_back(tok); } } } - if (infiles.empty()) { + if (rawEntries.empty()) { LOG(fatal) << "No input files given"; return 1; } + ROOT::EnableThreadSafety(); + + const bool needsAlien = std::any_of(rawEntries.begin(), rawEntries.end(), isAlienPath); + if (needsAlien && !gGrid) { + LOG(info) << "Connecting to AliEn ..."; + if (!TGrid::Connect("alien:") || !gGrid) { + LOG(fatal) << "Could not connect to AliEn; check your alien token"; + return 1; + } + } + + const std::vector infiles = expandInputs(rawEntries); + if (infiles.empty()) { + LOG(fatal) << "No input files resolved from the given --input entries"; + return 1; + } + const std::string outfile = vm["output"].as(); const std::string treename = vm["treename"].as(); const UInt_t startId = vm["start-id"].as(); + unsigned int jobs = vm["jobs"].as(); + if (jobs == 0) { + jobs = std::max(1u, std::thread::hardware_concurrency()); + } LOG(info) << "Validating " << infiles.size() << " input file(s) ..."; - bool hasTrackRefs = false; - if (!checkFiles(infiles, treename, hasTrackRefs)) { + std::vector entryCounts; + if (!checkFiles(infiles, treename, entryCounts)) { LOG(fatal) << "Validation failed; not writing any output"; return 1; } - LOG(info) << "Merging into " << outfile << " ..."; - const Long64_t total = mergeFiles(infiles, treename, outfile, hasTrackRefs, startId); + LOG(info) << "Merging into " << outfile << " using up to " << jobs << " reader thread(s) ..."; + const Long64_t total = mergeFiles(infiles, treename, outfile, entryCounts, startId, jobs); if (total < 0) { return 1; }