Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Sequencer.cpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file Sequencer.cpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2017-2019 CERN for the benefit of the Acts project
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
10 
24 
25 #include <algorithm>
26 #include <atomic>
27 #include <cctype>
28 #include <chrono>
29 #include <cstdint>
30 #include <cstdlib>
31 #include <exception>
32 #include <functional>
33 #include <iterator>
34 #include <limits>
35 #include <numeric>
36 #include <ostream>
37 #include <ratio>
38 #include <regex>
39 #include <stdexcept>
40 #include <string>
41 #include <string_view>
42 #include <typeinfo>
43 
44 #include <boost/stacktrace/stacktrace.hpp>
45 
46 #ifndef ACTS_EXAMPLES_NO_TBB
47 #include <TROOT.h>
48 #endif
49 
50 #include <boost/algorithm/string.hpp>
51 #include <boost/algorithm/string/predicate.hpp>
52 #include <boost/core/demangle.hpp>
53 #include <dfe/dfe_io_dsv.hpp>
54 #include <dfe/dfe_namedtuple.hpp>
55 
56 namespace ActsExamples {
57 
58 namespace {
59 
60 std::string_view getAlgorithmType(const SequenceElement& element) {
61  if (dynamic_cast<const IWriter*>(&element) != nullptr) {
62  return "Writer";
63  }
64  if (dynamic_cast<const IReader*>(&element) != nullptr) {
65  return "Reader";
66  }
67  return "Algorithm";
68 }
69 
70 // Saturated addition that does not overflow and exceed SIZE_MAX.
71 //
72 // From http://locklessinc.com/articles/sat_arithmetic/
73 size_t saturatedAdd(size_t a, size_t b) {
74  size_t res = a + b;
75  res |= -static_cast<int>(res < a);
76  return res;
77 }
78 
80 std::string demangleAndShorten(std::string name) {
81  name = boost::core::demangle(name.c_str());
82 
83  // Remove std::allocator from vector
84  const static std::regex vector_pattern(
85  R"??(std::vector<(.*), std::allocator<(\1\s*)>\s*>)??");
86  name = std::regex_replace(name, vector_pattern, "std::vector<$1>");
87 
88  // Shorten Acts::BoundVariantMeasurement
89  const static std::regex variant_pattern(
90  R"??(std::variant<(Acts::Measurement<Acts::BoundIndices, [0-9]ul>(,|)\s+)+>)??");
91  name = std::regex_replace(name, variant_pattern,
92  "Acts::BoundVariantMeasurement");
93 
94  // strip namespaces
95  boost::algorithm::replace_all(name, "std::", "");
96  boost::algorithm::replace_all(name, "boost::container::", "");
97  boost::algorithm::replace_all(name, "Acts::", "");
98  boost::algorithm::replace_all(name, "ActsExamples::", "");
99  boost::algorithm::replace_all(name, "ActsFatras::", "");
100 
101  return name;
102 }
103 
104 } // namespace
105 
107  : m_cfg(cfg),
108  m_taskArena((m_cfg.numThreads < 0) ? tbb::task_arena::automatic
109  : m_cfg.numThreads),
110  m_logger(Acts::getDefaultLogger("Sequencer", m_cfg.logLevel)) {
111 #ifndef ACTS_EXAMPLES_NO_TBB
112  if (m_cfg.numThreads == 1) {
113 #endif
114  ACTS_INFO("Create Sequencer (single-threaded)");
115 #ifndef ACTS_EXAMPLES_NO_TBB
116  } else {
117  ROOT::EnableThreadSafety();
118  ACTS_INFO("Create Sequencer with " << m_cfg.numThreads << " threads");
119  }
120 #endif
121 
122  const char* envvar = std::getenv("ACTS_SEQUENCER_DISABLE_FPEMON");
123  if (envvar != nullptr) {
124  ACTS_INFO(
125  "Overriding FPE tracking Sequencer based on environment variable "
126  "ACTS_SEQUENCER_DISABLE_FPEMON");
127  m_cfg.trackFpes = false;
128  }
129 }
130 
132  std::shared_ptr<IContextDecorator> decorator) {
133  if (not decorator) {
134  throw std::invalid_argument("Can not add empty/NULL context decorator");
135  }
136  m_decorators.push_back(std::move(decorator));
137  ACTS_INFO("Added context decarator '" << m_decorators.back()->name() << "'");
138 }
139 
140 void Sequencer::addReader(std::shared_ptr<IReader> reader) {
141  if (not reader) {
142  throw std::invalid_argument("Can not add empty/NULL reader");
143  }
144  m_readers.push_back(reader);
145  addElement(std::move(reader));
146 }
147 
148 void Sequencer::addAlgorithm(std::shared_ptr<IAlgorithm> algorithm) {
149  if (not algorithm) {
150  throw std::invalid_argument("Can not add empty/NULL algorithm");
151  }
152 
153  addElement(std::move(algorithm));
154 }
155 
156 void Sequencer::addWriter(std::shared_ptr<IWriter> writer) {
157  if (not writer) {
158  throw std::invalid_argument("Can not add empty/NULL writer");
159  }
160  addElement(std::move(writer));
161 }
162 
163 void Sequencer::addElement(const std::shared_ptr<SequenceElement>& element) {
164  if (not element) {
165  throw std::invalid_argument("Can not add empty/NULL element");
166  }
167 
168  m_sequenceElements.push_back({element});
169 
170  std::string elementType{getAlgorithmType(*element)};
171  std::string elementTypeCapitalized = elementType;
172  elementTypeCapitalized[0] = std::toupper(elementTypeCapitalized[0]);
173  ACTS_INFO("Add " << elementType << " '" << element->name() << "'");
174 
175  if (!m_cfg.runDataFlowChecks) {
176  return;
177  }
178 
179  auto symbol = [&](const char* in) {
180  std::string s = demangleAndShorten(in);
181  size_t pos = 0;
182  while (pos + 80 < s.size()) {
183  ACTS_INFO(" " + s.substr(pos, pos + 80));
184  pos += 80;
185  }
186  ACTS_INFO(" " + s.substr(pos));
187  };
188 
189  bool valid = true;
190 
191  for (const auto* handle : element->readHandles()) {
192  if (!handle->isInitialized()) {
193  continue;
194  }
195 
196  ACTS_INFO("<- " << handle->name() << " '" << handle->key() << "':");
197  symbol(handle->typeInfo().name());
198 
199  if (auto it = m_whiteBoardState.find(handle->key());
200  it != m_whiteBoardState.end()) {
201  const auto& source = *it->second;
202  if (!source.isCompatible(*handle)) {
203  ACTS_ERROR("Adding "
204  << elementType << " " << element->name() << ":"
205  << "\n-> white board will contain key '" << handle->key()
206  << "'"
207  << "\nat this point in the sequence (source: "
208  << source.fullName() << "),"
209  << "\nbut the type will be\n"
210  << "'" << demangleAndShorten(source.typeInfo().name()) << "'"
211  << "\nand not\n"
212  << "'" << demangleAndShorten(handle->typeInfo().name())
213  << "'");
214  valid = false;
215  }
216  } else {
217  ACTS_ERROR("Adding " << elementType << " " << element->name() << ":"
218  << "\n-> white board will not contain key"
219  << " '" << handle->key()
220  << "' at this point in the sequence."
221  << "\n Needed for read data handle '"
222  << handle->name() << "'")
223  valid = false;
224  }
225  }
226 
227  if (valid) { // only record outputs this if we're valid until here
228  for (const auto* handle : element->writeHandles()) {
229  if (!handle->isInitialized()) {
230  continue;
231  }
232 
233  ACTS_INFO("-> " << handle->name() << " '" << handle->key() << "':");
234  symbol(handle->typeInfo().name());
235 
236  if (auto it = m_whiteBoardState.find(handle->key());
237  it != m_whiteBoardState.end()) {
238  const auto& source = *it->second;
239  ACTS_ERROR("White board will already contain key '"
240  << handle->key() << "'. Source: '" << source.fullName()
241  << "' (cannot overwrite)");
242  valid = false;
243  break;
244  }
245 
246  m_whiteBoardState.emplace(std::pair{handle->key(), handle});
247 
248  if (auto it = m_whiteboardObjectAliases.find(handle->key());
249  it != m_whiteboardObjectAliases.end()) {
250  ACTS_DEBUG("Key '" << handle->key() << "' aliased to '" << it->second
251  << "'");
252  m_whiteBoardState[it->second] = handle;
253  }
254  }
255  }
256 
257  if (!valid) {
259  }
260 }
261 
263  const std::string& objectName) {
264  auto [it, success] =
265  m_whiteboardObjectAliases.insert({objectName, aliasName});
266  if (!success) {
267  throw std::invalid_argument("Alias to '" + aliasName + "' -> '" +
268  objectName + "' already set");
269  }
270 
271  if (auto oit = m_whiteBoardState.find(objectName);
272  oit != m_whiteBoardState.end()) {
273  m_whiteBoardState[aliasName] = oit->second;
274  }
275 }
276 
277 std::vector<std::string> Sequencer::listAlgorithmNames() const {
278  std::vector<std::string> names;
279 
280  // WARNING this must be done in the same order as in the processing
281  for (const auto& decorator : m_decorators) {
282  names.push_back("Decorator:" + decorator->name());
283  }
284  for (const auto& [algorithm, fpe] : m_sequenceElements) {
285  names.push_back(std::string(getAlgorithmType(*algorithm)) + ":" +
286  algorithm->name());
287  }
288 
289  return names;
290 }
291 
292 std::pair<std::size_t, std::size_t> Sequencer::determineEventsRange() const {
293  constexpr auto kInvalidEventsRange = std::make_pair(SIZE_MAX, SIZE_MAX);
294 
295  // Note on skipping events:
296  //
297  // Previously, skipping events was only allowed when readers where
298  // available, since only readers had a `.skip()` functionality. The
299  // `.skip()` interface has been removed in favour of telling the readers the
300  // event they are requested to read via the algorithm context. Skipping can
301  // now also be used when no readers are configured, e.g. for generating only
302  // a few specific events in a simulation setup.
303 
304  // determine intersection of event ranges available from readers
305  size_t beg = 0u;
306  size_t end = SIZE_MAX;
307  for (const auto& reader : m_readers) {
308  auto available = reader->availableEvents();
309  beg = std::max(beg, available.first);
310  end = std::min(end, available.second);
311  }
312 
313  // since we use event ranges (and not just num events) they might not
314  // overlap
315  if (end < beg) {
316  ACTS_ERROR("Available events ranges from readers do not overlap");
317  return kInvalidEventsRange;
318  }
319  // configured readers without available events makes no sense
320  // TODO could there be a use-case for zero events? run only setup functions?
321  if (beg == end) {
322  ACTS_ERROR("No events available");
323  return kInvalidEventsRange;
324  }
325  // trying to skip too many events must be an error
326  if (end <= saturatedAdd(beg, m_cfg.skip)) {
327  ACTS_ERROR("Less events available than requested to skip");
328  return kInvalidEventsRange;
329  }
330  // events range was not defined by either the readers or user command line.
331  if ((beg == 0u) and (end == SIZE_MAX) and (!m_cfg.events.has_value())) {
332  ACTS_ERROR("Could not determine number of events");
333  return kInvalidEventsRange;
334  }
335 
336  // take user selection into account
337  auto begSelected = saturatedAdd(beg, m_cfg.skip);
338  auto endSelected = end;
339  if (m_cfg.events.has_value()) {
340  auto endRequested = saturatedAdd(begSelected, m_cfg.events.value());
341  endSelected = std::min(end, endRequested);
342  if (end < endRequested) {
343  ACTS_INFO("Restrict requested number of events to available ones");
344  }
345  }
346 
347  return {begSelected, endSelected};
348 }
349 
350 // helpers for per-algorithm timing information
351 namespace {
352 using Clock = std::chrono::high_resolution_clock;
353 using Duration = Clock::duration;
354 using Timepoint = Clock::time_point;
355 using Seconds = std::chrono::duration<double>;
356 using NanoSeconds = std::chrono::duration<double, std::nano>;
357 
358 // RAII-based stopwatch to time execution within a block
359 struct StopWatch {
360  Timepoint start;
361  Duration& store;
362 
363  StopWatch(Duration& s) : start(Clock::now()), store(s) {}
364  ~StopWatch() { store += Clock::now() - start; }
365 };
366 
367 // Convert duration to a printable string w/ reasonable unit.
368 template <typename D>
369 inline std::string asString(D duration) {
370  double ns = std::chrono::duration_cast<NanoSeconds>(duration).count();
371  if (1e9 < std::abs(ns)) {
372  return std::to_string(ns / 1e9) + " s";
373  } else if (1e6 < std::abs(ns)) {
374  return std::to_string(ns / 1e6) + " ms";
375  } else if (1e3 < std::abs(ns)) {
376  return std::to_string(ns / 1e3) + " us";
377  } else {
378  return std::to_string(ns) + " ns";
379  }
380 }
381 
382 // Convert duration scaled to one event to a printable string.
383 template <typename D>
384 inline std::string perEvent(D duration, size_t numEvents) {
385  return asString(duration / numEvents) + "/event";
386 }
387 
388 // Store timing data
389 struct TimingInfo {
391  double time_total_s = 0;
392  double time_perevent_s = 0;
393 
394  DFE_NAMEDTUPLE(TimingInfo, identifier, time_total_s, time_perevent_s);
395 };
396 
397 void storeTiming(const std::vector<std::string>& identifiers,
398  const std::vector<Duration>& durations, std::size_t numEvents,
399  const std::string& path) {
400  dfe::NamedTupleTsvWriter<TimingInfo> writer(path, 4);
401  for (size_t i = 0; i < identifiers.size(); ++i) {
402  TimingInfo info;
403  info.identifier = identifiers[i];
404  info.time_total_s =
405  std::chrono::duration_cast<Seconds>(durations[i]).count();
406  info.time_perevent_s = info.time_total_s / numEvents;
407  writer.append(info);
408  }
409 }
410 } // namespace
411 
413  // measure overall wall clock
414  Timepoint clockWallStart = Clock::now();
415  // per-algorithm time measures
416  std::vector<std::string> names = listAlgorithmNames();
417  std::vector<Duration> clocksAlgorithms(names.size(), Duration::zero());
418  tbbWrap::queuing_mutex clocksAlgorithmsMutex;
419 
420  // processing only works w/ a well-known number of events
421  // error message is already handled by the helper function
422  std::pair<size_t, size_t> eventsRange = determineEventsRange();
423  if ((eventsRange.first == SIZE_MAX) and (eventsRange.second == SIZE_MAX)) {
424  return EXIT_FAILURE;
425  }
426 
427  ACTS_INFO("Processing events [" << eventsRange.first << ", "
428  << eventsRange.second << ")");
429  ACTS_INFO("Starting event loop with " << m_cfg.numThreads << " threads");
430  ACTS_INFO(" " << m_decorators.size() << " context decorators");
431  ACTS_INFO(" " << m_sequenceElements.size() << " sequence elements");
432 
433  size_t nWriters = 0;
434  size_t nReaders = 0;
435  size_t nAlgorithms = 0;
436  for (const auto& [alg, fpe] : m_sequenceElements) {
437  if (dynamic_cast<const IWriter*>(alg.get()) != nullptr) {
438  nWriters++;
439  } else if (dynamic_cast<const IReader*>(alg.get()) != nullptr) {
440  nReaders++;
441  } else if (dynamic_cast<const IAlgorithm*>(alg.get()) != nullptr) {
442  nAlgorithms++;
443  } else {
444  throw std::runtime_error{"Unknown sequence element type"};
445  }
446  }
447 
448  ACTS_INFO(" " << nReaders << " readers");
449  ACTS_INFO(" " << nAlgorithms << " algorithms");
450  ACTS_INFO(" " << nWriters << " writers");
451 
452  ACTS_VERBOSE("Initialize sequence elements");
453  for (auto& [alg, fpe] : m_sequenceElements) {
454  ACTS_VERBOSE("Initialize " << getAlgorithmType(*alg) << ": "
455  << alg->name());
456  if (alg->initialize() != ProcessCode::SUCCESS) {
457  ACTS_FATAL("Failed to initialize " << getAlgorithmType(*alg) << ": "
458  << alg->name());
459  throw std::runtime_error("Failed to process event data");
460  }
461  }
462 
463  // execute the parallel event loop
464  std::atomic<size_t> nProcessedEvents = 0;
465  size_t nTotalEvents = eventsRange.second - eventsRange.first;
466  m_taskArena.execute([&] {
468  tbb::blocked_range<size_t>(eventsRange.first, eventsRange.second),
469  [&](const tbb::blocked_range<size_t>& r) {
470  std::vector<Duration> localClocksAlgorithms(names.size(),
471  Duration::zero());
472 
473  for (size_t event = r.begin(); event != r.end(); ++event) {
474  ACTS_DEBUG("start processing event " << event);
476  // Use per-event store
477  WhiteBoard eventStore(
479  m_cfg.logLevel),
481  // If we ever wanted to run algorithms in parallel, this needs to
482  // be changed to Algorithm context copies
483  AlgorithmContext context(0, event, eventStore);
484  size_t ialgo = 0;
485 
487  for (auto& cdr : m_decorators) {
488  StopWatch sw(localClocksAlgorithms[ialgo++]);
489  ACTS_VERBOSE("Execute context decorator: " << cdr->name());
490  if (cdr->decorate(++context) != ProcessCode::SUCCESS) {
491  throw std::runtime_error("Failed to decorate event context");
492  }
493  }
494 
495  ACTS_VERBOSE("Execute sequence elements");
496 
497  for (auto& [alg, fpe] : m_sequenceElements) {
498  std::optional<Acts::FpeMonitor> mon;
499  if (m_cfg.trackFpes) {
500  mon.emplace();
501  context.fpeMonitor = &mon.value();
502  }
503  StopWatch sw(localClocksAlgorithms[ialgo++]);
504  ACTS_VERBOSE("Execute " << getAlgorithmType(*alg) << ": "
505  << alg->name());
506  if (alg->internalExecute(++context) != ProcessCode::SUCCESS) {
507  ACTS_FATAL("Failed to execute " << getAlgorithmType(*alg)
508  << ": " << alg->name());
509  throw std::runtime_error("Failed to process event data");
510  }
511 
512  if (mon) {
513  auto& local = fpe.local();
514 
515  for (const auto& [count, type, st] :
516  mon->result().stackTraces()) {
517  auto [maskLoc, nMasked] = fpeMaskCount(*st, type);
518  if (nMasked < count) {
519  std::stringstream ss;
520  ss << "FPE of type " << type
521  << " exceeded configured per-event threshold of "
522  << nMasked << " (mask: " << maskLoc
523  << ") (seen: " << count << " FPEs)\n"
526 
527  m_nUnmaskedFpe += (count - nMasked);
528 
529  if (m_cfg.failOnFirstFpe) {
530  ACTS_ERROR(ss.str());
531  local.merge(mon->result()); // merge so we get correct
532  // results after throwing
533  throw FpeFailure{ss.str()};
534  } else if (!local.contains(type, *st)) {
535  ACTS_INFO(ss.str());
536  }
537  }
538  }
539 
540  local.merge(mon->result());
541  }
542  context.fpeMonitor = nullptr;
543  }
544 
545  nProcessedEvents++;
546  if (logger().level() <= Acts::Logging::DEBUG) {
547  ACTS_DEBUG("finished event " << event);
548  } else if (nTotalEvents <= 100) {
549  ACTS_INFO("finished event " << event);
550  } else if (nProcessedEvents % 100 == 0) {
551  ACTS_INFO(nProcessedEvents << " / " << nTotalEvents
552  << " events processed");
553  }
554  }
555 
556  // add timing info to global information
557  {
558  tbbWrap::queuing_mutex::scoped_lock lock(clocksAlgorithmsMutex);
559  for (size_t i = 0; i < clocksAlgorithms.size(); ++i) {
560  clocksAlgorithms[i] += localClocksAlgorithms[i];
561  }
562  }
563  });
564  });
565 
566  ACTS_VERBOSE("Finalize sequence elements");
567  for (auto& [alg, fpe] : m_sequenceElements) {
568  ACTS_VERBOSE("Finalize " << getAlgorithmType(*alg) << ": " << alg->name());
569  if (alg->finalize() != ProcessCode::SUCCESS) {
570  ACTS_FATAL("Failed to finalize " << getAlgorithmType(*alg) << ": "
571  << alg->name());
572  throw std::runtime_error("Failed to process event data");
573  }
574  }
575 
576  fpeReport();
577 
578  // summarize timing
579  Duration totalWall = Clock::now() - clockWallStart;
580  Duration totalReal = std::accumulate(
581  clocksAlgorithms.begin(), clocksAlgorithms.end(), Duration::zero());
582  size_t numEvents = eventsRange.second - eventsRange.first;
583  ACTS_INFO("Processed " << numEvents << " events in " << asString(totalWall)
584  << " (wall clock)");
585  ACTS_INFO("Average time per event: " << perEvent(totalReal, numEvents));
586  ACTS_DEBUG("Average time per algorithm:");
587  for (size_t i = 0; i < names.size(); ++i) {
588  ACTS_DEBUG(" " << names[i] << ": "
589  << perEvent(clocksAlgorithms[i], numEvents));
590  }
591 
592  if (!m_cfg.outputDir.empty()) {
593  storeTiming(names, clocksAlgorithms, numEvents,
594  joinPaths(m_cfg.outputDir, m_cfg.outputTimingFile));
595  }
596 
597  if (m_nUnmaskedFpe > 0) {
598  return EXIT_FAILURE;
599  }
600 
601  return EXIT_SUCCESS;
602 }
603 
604 void Sequencer::fpeReport() const {
605  if (!m_cfg.trackFpes) {
606  return;
607  }
608 
609  for (auto& [alg, fpe] : m_sequenceElements) {
610  auto merged = std::accumulate(
611  fpe.begin(), fpe.end(), Acts::FpeMonitor::Result{},
612  [](const auto& lhs, const auto& rhs) { return lhs.merged(rhs); });
613  if (!merged) {
614  // no FPEs to report
615  continue;
616  }
617  ACTS_INFO("-----------------------------------");
618  ACTS_INFO("FPE summary for " << getAlgorithmType(*alg) << ": "
619  << alg->name());
620  ACTS_INFO("-----------------------------------");
621 
622  std::vector<std::reference_wrapper<const Acts::FpeMonitor::Result::FpeInfo>>
623  sorted;
625  merged.stackTraces().begin(), merged.stackTraces().end(),
626  std::back_inserter(sorted),
627  [](const auto& f) -> const auto& { return f; });
628  std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) {
629  return a.get().count > b.get().count;
630  });
631 
632  std::vector<std::reference_wrapper<const Acts::FpeMonitor::Result::FpeInfo>>
633  remaining;
634 
635  for (const auto& el : sorted) {
636  const auto& [count, type, st] = el.get();
637  auto [maskLoc, nMasked] = fpeMaskCount(*st, type);
638  ACTS_INFO("- " << type << ": (" << count << " times) "
639  << (nMasked > 0 ? "[MASKED: " + std::to_string(nMasked) +
640  " per event by " + maskLoc + "]"
641  : "")
642  << "\n"
644  *st, m_cfg.fpeStackTraceLength));
645  }
646  }
647 
648  if (m_nUnmaskedFpe > 0) {
649  ACTS_ERROR("Encountered " << m_nUnmaskedFpe << " unmasked FPEs");
650  } else {
651  ACTS_INFO("No unmasked FPEs encountered");
652  }
653 }
654 
655 std::pair<std::string, std::size_t> Sequencer::fpeMaskCount(
656  const boost::stacktrace::stacktrace& st, Acts::FpeType type) const {
657  for (const auto& frame : st) {
659  auto it = loc.find_last_of(':');
660  std::string locFile = loc.substr(0, it);
661  unsigned int locLine = std::stoi(loc.substr(it + 1));
662  for (const auto& [file, lines, fType, count] : m_cfg.fpeMasks) {
663  const auto [start, end] = lines;
664  if (boost::algorithm::ends_with(locFile, file) &&
665  (start <= locLine && locLine < end) && fType == type) {
666  std::string ls = start + 1 == end ? std::to_string(start)
667  : "(" + std::to_string(start) + ", " +
668  std::to_string(end) + "]";
669  return {file + ":" + ls, count};
670  }
671  }
672  }
673  return {"NONE", 0};
674 }
675 
676 Acts::FpeMonitor::Result Sequencer::fpeResult() const {
678  for (auto& [alg, fpe] : m_sequenceElements) {
679  merged.merge(std::accumulate(
680  fpe.begin(), fpe.end(), Acts::FpeMonitor::Result{},
681  [](const auto& lhs, const auto& rhs) { return lhs.merged(rhs); }));
682  }
683  return merged;
684 }
685 
686 std::ostream& operator<<(std::ostream& os,
688  os << "FpeMask(" << m.file << ":";
689 
690  if (m.lines.first + 1 == m.lines.second) {
691  os << m.lines.first;
692  } else {
693  os << "(" << m.lines.first << ", " << m.lines.second << "]";
694  }
695  os << ", " << m.type << " <= " << m.count << ")";
696  return os;
697 }
698 
699 } // namespace ActsExamples