Project
Loading...
Searching...
No Matches
MergeEventPool.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
42
45#include <fairlogger/Logger.h>
46#include <TFile.h>
47#include <TFileMerger.h>
48#include <TGrid.h>
49#include <TMap.h>
50#include <TObjString.h>
51#include <TTree.h>
52#include <boost/program_options.hpp>
53#include <algorithm>
54#include <filesystem>
55#include <fstream>
56#include <memory>
57#include <optional>
58#include <set>
59#include <string>
60#include <vector>
61
62namespace bpo = boost::program_options;
63namespace fs = std::filesystem;
64
65namespace
66{
67const char* kTrackBranch = "MCTrack";
68const char* kHeaderBranch = "MCEventHeader.";
69const char* kTrackRefBranch = "TrackRefs";
70const char* kProtocol = "alien://";
71
72bool isAlienPath(std::string const& path)
73{
74 return o2::utils::Str::beginsWith(path, kProtocol);
75}
76
77// Connects to AliEn if that has not happened yet
78bool GridOn()
79{
80 if (gGrid) {
81 return true;
82 }
83 LOG(info) << "Connecting to AliEn ...";
84 if (!TGrid::Connect("alien:") || !gGrid) {
85 LOG(error) << "Could not connect to AliEn; check your alien token";
86 return false;
87 }
88 return true;
89}
90
91// Reads the lines of a local text file. nullopt if it could not be opened.
92std::optional<std::vector<std::string>> readLocalListFileLines(std::string const& path)
93{
94 std::ifstream in(path);
95 if (!in.is_open()) {
96 return std::nullopt;
97 }
98 std::vector<std::string> lines;
99 std::string line;
100 while (std::getline(in, line)) {
101 lines.push_back(line);
102 }
103 return lines;
104}
105
106// Reads a text file listing input paths, one per line ('#' comments and blank lines
107// ignored). Each listed path is either a .root file (local or alien://) or itself
108// another list file. The lists themselves are always read locally.
109// Returns how many entries, here or in a nested list, could not be resolved.
110size_t expandInputEntry(std::string const& rawEntry, std::vector<std::string>& out, std::vector<std::string>& stack)
111{
112 // done here so that the expansion works also when the variables appear in a list file
113 const auto entry = o2::utils::expandShellVarsInFileName(rawEntry);
114 if (o2::utils::Str::endsWith(entry, ".root")) {
115 out.push_back(entry);
116 return 0;
117 }
118 if (std::find(stack.begin(), stack.end(), entry) != stack.end()) {
119 LOG(error) << "Reference to an existing list " << entry << "; ignoring";
120 return 1;
121 }
122 auto lines = readLocalListFileLines(entry);
123 if (!lines) {
124 LOG(error) << "Cannot open " << entry << " (neither a .root file nor a readable local list)";
125 return 1;
126 }
127 stack.push_back(entry);
128 size_t unresolved = 0;
129 for (auto line : *lines) {
131 if (line.empty() || line[0] == '#') {
132 continue;
133 }
134 unresolved += expandInputEntry(line, out, stack);
135 }
136 stack.pop_back();
137 return unresolved;
138}
139
140// Expands a list of raw --input entries (each either a .root file or a list) into the flat
141// list of .root files to merge, dropping repetitions. Returns how many entries did not resolve.
142size_t expandInputs(std::vector<std::string> const& rawEntries, std::vector<std::string>& infiles)
143{
144 std::vector<std::string> resolved;
145 std::vector<std::string> stack;
146 size_t unresolved = 0;
147 for (auto const& e : rawEntries) {
148 unresolved += expandInputEntry(e, resolved, stack);
149 }
150 std::set<std::string> seen;
151 for (auto const& f : resolved) {
152 if (seen.insert(f).second) {
153 infiles.push_back(f);
154 } else {
155 LOG(warning) << "Input " << f << " is listed more than once; merging it only once";
156 }
157 }
158 return unresolved;
159}
160
161// Checks that a file is readable and holds a tree with the branches expected from a
162// standard o2-sim event pool, reporting its event count and compression settings.
163// Returns an empty string when the file is usable, the reason otherwise.
164std::string inspectFile(std::string const& path, std::string const& treename,
165 Long64_t& entries, int& compression)
166{
167 std::unique_ptr<TFile> file(TFile::Open(path.c_str(), "READ"));
168 if (!file || file->IsZombie()) {
169 return "file does not exist or cannot be opened";
170 }
171 auto tree = (TTree*)file->Get(treename.c_str());
172 if (!tree) {
173 return "no tree named '" + treename + "' in the file";
174 }
175 if (tree->GetBranch(kTrackBranch) == nullptr || tree->GetBranch(kHeaderBranch) == nullptr ||
176 tree->GetBranch(kTrackRefBranch) == nullptr) {
177 return std::string("missing the required '") + kTrackBranch + "', '" + kHeaderBranch + "' and/or '" +
178 kTrackRefBranch + "' branch";
179 }
180 entries = tree->GetEntries();
181 compression = file->GetCompressionSettings();
182 return {};
183}
184
185// Checks every input before anything is written, collecting the usable ones and reporting
186// the total number of events and the compression settings of the first usable input.
187// Returns true when every input passed.
188bool checkFiles(std::vector<std::string> const& files, std::string const& treename,
189 std::vector<std::string>& usable, Long64_t& totalEvents, int& compression)
190{
191 bool ok = true;
192 totalEvents = 0;
193 compression = -1;
194 for (auto const& f : files) {
195 Long64_t entries = 0;
196 int fileCompression = -1;
197 const auto issue = inspectFile(f, treename, entries, fileCompression);
198 if (!issue.empty()) {
199 LOG(error) << "Input file " << f << ": " << issue;
200 ok = false;
201 continue;
202 }
203 if (compression < 0) {
204 compression = fileCompression;
205 }
206 totalEvents += entries;
207 usable.push_back(f);
208 LOG(info) << " OK " << f << " (" << entries << " events)";
209 }
210 return ok;
211}
212
213// Records what the merge was asked for and what actually went into it.
214void writeMergeInfo(std::string const& outfile, std::vector<std::string> const& requested,
215 std::vector<std::string> const& merged, size_t unresolved, Long64_t events)
216{
217 std::unique_ptr<TFile> file(TFile::Open(outfile.c_str(), "UPDATE"));
218 if (!file || file->IsZombie()) {
219 LOG(warning) << "Cannot add the merge information to " << outfile;
220 return;
221 }
222 // the files that were asked for but did not make it, so that the gap can be named from
223 // the file alone and not just counted
224 std::string mergedList, skippedList;
225 for (auto const& f : requested) {
226 if (std::find(merged.begin(), merged.end(), f) != merged.end()) {
227 mergedList += f + "\n";
228 } else {
229 skippedList += f + "\n";
230 }
231 }
232 TMap info;
233 info.SetOwnerKeyValue();
234 info.Add(new TObjString("inputsRequested"), new TObjString(std::to_string(requested.size()).c_str()));
235 info.Add(new TObjString("inputsMerged"), new TObjString(std::to_string(merged.size()).c_str()));
236 info.Add(new TObjString("inputsUnresolved"), new TObjString(std::to_string(unresolved).c_str()));
237 info.Add(new TObjString("events"), new TObjString(std::to_string(events).c_str()));
238 info.Add(new TObjString("mergedFiles"), new TObjString(mergedList.c_str()));
239 info.Add(new TObjString("skippedFiles"), new TObjString(skippedList.c_str()));
240 file->cd();
241 info.Write("mergeInfo", TObject::kSingleKey);
242}
243
244// Re-opens the merged output and checks that it holds the expected tree, branches and
245// number of events, so that a truncated or half-written pool does not pass unnoticed.
246bool validateOutput(std::string const& outfile, std::string const& treename, Long64_t expected)
247{
248 Long64_t entries = 0;
249 int compression = -1;
250 const auto issue = inspectFile(outfile, treename, entries, compression);
251 if (!issue.empty()) {
252 LOG(error) << "Merged file " << outfile << " is not usable: " << issue;
253 return false;
254 }
255 if (entries != expected) {
256 LOG(error) << "Merged file " << outfile << " has " << entries << " events, but " << expected
257 << " were merged into it";
258 return false;
259 }
260 return true;
261}
262} // namespace
263
264int main(int argc, char* argv[])
265{
266 bpo::options_description options("o2-generators-merge-evtpool options");
267 auto add = options.add_options();
268 add("input,i", bpo::value<std::string>()->required(),
269 "comma-separated list of inputs: event-pool ROOT files (local or alien://), and/or "
270 "local text files listing more paths (one per line, '#' comments allowed)");
271 add("output,o", bpo::value<std::string>()->default_value("evtpool.root"),
272 "output ROOT file with the merged event pool");
273 add("check-tree,t", bpo::value<std::string>()->default_value("o2sim"),
274 "name of the tree the inputs and the merged pool are checked against; everything the "
275 "input files contain is merged regardless");
276 add("skip-non-existing-files", bpo::bool_switch(),
277 "skip inputs that cannot be resolved or opened instead of aborting the merge");
278 add("help,h", "produce help message");
279 bpo::variables_map vm;
280 try {
281 bpo::store(bpo::parse_command_line(argc, argv, options), vm);
282 if (vm.count("help")) {
283 LOG(info) << options;
284 return 0;
285 }
286 bpo::notify(vm);
287 } catch (const bpo::error& e) {
288 LOG(error) << "Error parsing command-line arguments: " << e.what() << "\n\n"
289 << options;
290 return 1;
291 }
292 const auto rawEntries = o2::utils::Str::tokenize(vm["input"].as<std::string>(), ',');
293 if (rawEntries.empty()) {
294 LOG(error) << "No input files given";
295 return 1;
296 }
297 // option similar in aodMerger
298 const bool skipMissing = vm["skip-non-existing-files"].as<bool>();
299 std::vector<std::string> infiles;
300 const size_t unresolved = expandInputs(rawEntries, infiles);
301 if (unresolved > 0 && !skipMissing) {
302 LOG(error) << "Some --input entries could not be resolved; "
303 "pass --skip-non-existing-files to merge the rest anyway";
304 return 1;
305 }
306 if (infiles.empty()) {
307 LOG(error) << "No input files resolved from the given --input entries";
308 return 1;
309 }
310 // Check Grid connection if any input is on AliEn
311 if (std::any_of(infiles.begin(), infiles.end(), isAlienPath) && !GridOn()) {
312 LOG(error) << "Some inputs live on AliEn but the grid is not available";
313 return 1;
314 }
315 const std::string outfile = vm["output"].as<std::string>();
316 const std::string treename = vm["check-tree"].as<std::string>();
317 LOG(info) << "Validating " << infiles.size() << " input file(s) ...";
318 std::vector<std::string> usable;
319 Long64_t totalEvents = 0;
320 int compression = -1;
321 if (!checkFiles(infiles, treename, usable, totalEvents, compression) && !skipMissing) {
322 LOG(error) << "Validation failed; not writing any output "
323 "(pass --skip-non-existing-files to merge the rest anyway)";
324 return 1;
325 }
326 if (usable.empty()) {
327 LOG(error) << "None of the input files could be used; not writing any output";
328 return 1;
329 }
330
331 // merged into a temporary name and renamed only once the result has been checked, so that
332 // a failed job never leaves something behind that looks like a finished pool
333 const std::string partfile = outfile + ".part";
334 auto discardPart = [&partfile]() {
335 std::error_code ec;
336 fs::remove(partfile, ec);
337 return 1;
338 };
339
340 LOG(info) << "Merging " << totalEvents << " events from " << usable.size() << " file(s) into "
341 << outfile << " ...";
342 {
343 TFileMerger merger(/*isLocal*/ false, /*histoOneGo*/ false);
344 merger.SetPrintLevel(0);
345 if (!merger.OutputFile(partfile.c_str(), "RECREATE", compression)) {
346 LOG(error) << "Cannot create output file " << partfile;
347 return discardPart();
348 }
349 for (auto const& f : usable) {
350 if (!merger.AddFile(f.c_str())) {
351 LOG(error) << "Cannot add " << f << " to the merge";
352 return discardPart();
353 }
354 }
355 if (!merger.Merge()) {
356 LOG(error) << "Merging failed; no output written";
357 return discardPart();
358 }
359 }
360
361 writeMergeInfo(partfile, infiles, usable, unresolved, totalEvents);
362 if (!validateOutput(partfile, treename, totalEvents)) {
363 LOG(error) << "The merged pool did not pass the final check; no output written";
364 return discardPart();
365 }
366
367 std::error_code ec;
368 fs::rename(partfile, outfile, ec);
369 if (ec) {
370 LOG(error) << "Cannot move " << partfile << " to " << outfile << ": " << ec.message();
371 return discardPart();
372 }
373
374 LOG(info) << "Done: wrote " << totalEvents << " events from " << usable.size() << " of "
375 << infiles.size() << " input file(s) to " << outfile;
376 return 0;
377}
uint32_t stack
Definition RawData.h:1
bool checkFiles(std::vector< std::string > const &filenames, std::string const &treename)
GLuint entry
Definition glcorearb.h:5735
GLdouble f
Definition glcorearb.h:310
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
int32_t const char * file
int32_t const char int32_t line
std::string expandShellVarsInFileName(std::string const &input)
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
static void trim(std::string &s)
Definition StringUtils.h:70
static bool beginsWith(const std::string &s, const std::string &start)
static std::vector< std::string > tokenize(const std::string &src, char delim, bool trimToken=true, bool skipEmpty=true)
static bool endsWith(const std::string &s, const std::string &ending)
std::map< std::string, ID > expected
#define main
LOG(info)<< "Compressed in "<< sw.CpuTime()<< " s"
std::unique_ptr< TTree > tree((TTree *) flIn.Get(std::string(o2::base::NameConf::CTFTREENAME).c_str()))