Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-25 09:28:47

0001 
0002 #include "JExecutionEngine.h"
0003 #include <JANA/Utils/JApplicationInspector.h>
0004 #include <JANA/JVersion.h>
0005 
0006 #if JANA2_HAVE_PERFETTO
0007 #include <JANA/Services/JPerfettoService.h>
0008 #endif
0009 
0010 #include <chrono>
0011 #include <cstddef>
0012 #include <cstdio>
0013 #include <ctime>
0014 #include <exception>
0015 #include <mutex>
0016 #include <csignal>
0017 #include <ostream>
0018 #include <sstream>
0019 #include <sys/stat.h>
0020 #include <fcntl.h>
0021 #include <unistd.h>
0022 
0023 thread_local int jana2_worker_id = -1;
0024 thread_local JBacktrace* jana2_worker_backtrace = nullptr;
0025 
0026 void JExecutionEngine::Init() {
0027     auto params = GetApplication()->GetJParameterManager();
0028 
0029     params->SetDefaultParameter("jana:timeout", m_timeout_s, 
0030         "Max time (in seconds) JANA will wait for a thread to update its heartbeat before hard-exiting. 0 to disable timeout completely.");
0031 
0032     params->SetDefaultParameter("jana:warmup_timeout", m_warmup_timeout_s, 
0033         "Max time (in seconds) JANA will wait for 'initial' events to complete before hard-exiting.");
0034 
0035     params->SetDefaultParameter("jana:backoff_interval", m_backoff_ms, 
0036         "Max time (in seconds) JANA will wait for 'initial' events to complete before hard-exiting.");
0037 
0038     params->SetDefaultParameter("jana:show_ticker", m_show_ticker, "Controls whether the ticker is visible");
0039 
0040     params->SetDefaultParameter("jana:ticker_interval", m_ticker_ms, "Controls the ticker interval (in ms)");
0041 
0042     auto p = params->SetDefaultParameter("jana:status_fname", m_path_to_named_pipe,
0043         "Filename of named pipe for retrieving instantaneous status info");
0044 
0045     size_t pid = getpid();
0046     mkfifo(m_path_to_named_pipe.c_str(), 0666);
0047 
0048     LOG_WARN(GetLogger()) << "To pause processing and inspect, press Ctrl-C." << LOG_END;
0049     LOG_WARN(GetLogger()) << "For a clean shutdown, press Ctrl-C twice." << LOG_END;
0050     LOG_WARN(GetLogger()) << "For a hard shutdown, press Ctrl-C three times." << LOG_END;
0051 
0052     if (p->IsDefault()) {
0053         LOG_WARN(GetLogger()) << "For worker status information, press Ctrl-Z, or run `jana-status " << pid << "`" << LOG_END;
0054     }
0055     else {
0056         LOG_WARN(GetLogger()) << "For worker status information, press Ctrl-Z, or run `jana-status " << pid << " " << m_path_to_named_pipe << "`" << LOG_END;
0057     }
0058 
0059 
0060     // Not sure how I feel about putting this here yet, but I think it will at least work in both cases it needs to.
0061     // The reason this works is because JTopologyBuilder::create_topology() has already been called before 
0062     // JApplication::ProvideService<JExecutionEngine>().
0063     for (JArrow* arrow : m_topology->GetArrows()) {
0064 
0065         arrow->Initialize();
0066 
0067         m_arrow_states.emplace_back();
0068         auto& arrow_state = m_arrow_states.back();
0069         arrow_state.is_source = arrow->IsSource();
0070         arrow_state.is_sink = arrow->IsSink();
0071         arrow_state.is_parallel = arrow->IsParallel();
0072         arrow_state.next_input = arrow->GetNextPortIndex();
0073     }
0074 }
0075 
0076 void JExecutionEngine::RequestInspector() {
0077     std::unique_lock<std::mutex> lock(m_mutex);
0078     m_interrupt_status = InterruptStatus::InspectRequested;
0079 }
0080 
0081 void JExecutionEngine::RunTopology() {
0082     std::unique_lock<std::mutex> lock(m_mutex);
0083 
0084     if (m_runstatus == RunStatus::Failed) {
0085         throw JException("Cannot switch topology runstatus to Running because it is already Failed");
0086     }
0087     if (m_runstatus == RunStatus::Finished) {
0088         throw JException("Cannot switch topology runstatus to Running because it is already Finished");
0089     }
0090     if (m_arrow_states.size() == 0) {
0091         throw JException("Cannot execute an empty topology! Hint: Have you provided an event source?");
0092     }
0093 
0094     // Set start time and event count
0095     m_time_at_start = clock_t::now();
0096     m_event_count_at_start = m_event_count_at_finish;
0097 
0098     // Reactivate topology
0099     for (auto& arrow: m_arrow_states) {
0100         if (arrow.status == ArrowState::Status::Paused) {
0101             arrow.status = ArrowState::Status::Running;
0102         }
0103     }
0104 
0105     m_runstatus = RunStatus::Running;
0106 
0107     lock.unlock();
0108     m_condvar.notify_one();
0109 }
0110 
0111 void JExecutionEngine::ScaleWorkers(size_t nthreads) {
0112     // We both create and destroy the pool of workers here. They all sleep until they 
0113     // receive work from the scheduler, which won't happen until the runstatus <- {Running, 
0114     // Pausing, Draining} and there is a task ready to execute. This way worker creation/destruction
0115     // is decoupled from topology execution.
0116 
0117     // If we scale to zero, no workers will run. This is useful for testing, and also for using
0118     // an external thread team, should the need arise.
0119 
0120     std::unique_lock<std::mutex> lock(m_mutex);
0121 
0122     if (nthreads != 0 && m_arrow_states.size() == 0) {
0123         // We check that (nthreads != 0) because this gets called at shutdown even if the topology wasn't run
0124         // Remember, we want JApplication::Initialize() to succeed and JMain to shut down cleanly even when the topology is empty
0125         throw JException("Cannot execute an empty topology! Hint: Have you provided an event source?");
0126     }
0127 
0128     auto prev_nthreads = m_worker_states.size();
0129 
0130     if (prev_nthreads < nthreads) {
0131         // We are launching additional worker threads
0132         LOG_DEBUG(GetLogger()) << "Scaling up to " << nthreads << " worker threads" << LOG_END;
0133         auto mapping = m_topology->GetProcessorMapping();
0134         for (size_t worker_id=prev_nthreads; worker_id < nthreads; ++worker_id) {
0135             auto worker = std::make_unique<WorkerState>();
0136             worker->worker_id = worker_id;
0137             worker->is_stop_requested = false;
0138             worker->cpu_id = mapping.get_cpu_id(worker_id);
0139             worker->location_id = mapping.get_loc_id(worker_id);
0140             worker->thread = new std::thread(&JExecutionEngine::RunWorker, this, Worker{worker_id, &worker->backtrace});
0141             LOG_DEBUG(GetLogger()) << "Launching worker thread " << worker_id << " on cpu=" << worker->cpu_id << ", location=" << worker->location_id << LOG_END;
0142             m_worker_states.push_back(std::move(worker));
0143 
0144             bool pin_to_cpu = (mapping.get_affinity() != JProcessorMapping::AffinityStrategy::None);
0145             if (pin_to_cpu) {
0146                 JCpuInfo::PinThreadToCpu(worker->thread, worker->cpu_id);
0147             }
0148         }
0149     }
0150 
0151     else if (prev_nthreads > nthreads) {
0152         // We are destroying existing worker threads
0153         LOG_DEBUG(GetLogger()) << "Scaling down to " << nthreads << " worker threads" << LOG_END;
0154 
0155         // Signal to threads that they need to terminate.
0156         for (int worker_id=prev_nthreads-1; worker_id >= (int)nthreads; --worker_id) {
0157             LOG_DEBUG(GetLogger()) << "Stopping worker " << worker_id << LOG_END;
0158             m_worker_states[worker_id]->is_stop_requested = true;
0159         }
0160         m_condvar.notify_all(); // Wake up all threads so that they can exit the condvar wait loop
0161         lock.unlock();
0162 
0163         // We join all (eligible) threads _outside_ of the mutex
0164         for (int worker_id=prev_nthreads-1; worker_id >= (int) nthreads; --worker_id) {
0165             if (m_worker_states[worker_id]->thread != nullptr) {
0166                 if (m_worker_states[worker_id]->is_timed_out) {
0167                     // Thread has timed out. Rather than non-cooperatively killing it,
0168                     // we relinquish ownership of it but remember that it was ours once and
0169                     // is still out there, somewhere, biding its time
0170                     m_worker_states[worker_id]->thread->detach();
0171                     LOG_DEBUG(GetLogger()) << "Detached worker " << worker_id << LOG_END;
0172                 }
0173                 else {
0174                     LOG_DEBUG(GetLogger()) << "Joining worker " << worker_id << LOG_END;
0175                     m_worker_states[worker_id]->thread->join();
0176                     LOG_DEBUG(GetLogger()) << "Joined worker " << worker_id << LOG_END;
0177                 }
0178             }
0179             else {
0180                 LOG_DEBUG(GetLogger()) << "Skipping worker " << worker_id << LOG_END;
0181             }
0182         }
0183 
0184         lock.lock();
0185         // We retake the mutex so we can safely modify m_worker_states
0186         for (int worker_id=prev_nthreads-1; worker_id >= (int)nthreads; --worker_id) {
0187             if (m_worker_states.back()->thread != nullptr) {
0188                 delete m_worker_states.back()->thread;
0189             }
0190             m_worker_states.pop_back();
0191         }
0192     }
0193 }
0194 
0195 void JExecutionEngine::PauseTopology() {
0196     std::unique_lock<std::mutex> lock(m_mutex);
0197     if (m_runstatus != RunStatus::Running) return;
0198     m_runstatus = RunStatus::Pausing;
0199     for (auto& arrow: m_arrow_states) {
0200         if (arrow.status == ArrowState::Status::Running) {
0201             arrow.status = ArrowState::Status::Paused;
0202         }
0203     }
0204     LOG_WARN(GetLogger()) << "Requested pause" << LOG_END;
0205     lock.unlock();
0206     m_condvar.notify_all();
0207 }
0208 
0209 void JExecutionEngine::DrainTopology() {
0210     std::unique_lock<std::mutex> lock(m_mutex);
0211     if (m_runstatus != RunStatus::Running) return;
0212     m_runstatus = RunStatus::Draining;
0213     for (auto& arrow: m_arrow_states) {
0214         if (arrow.is_source) {
0215             if (arrow.status == ArrowState::Status::Running) {
0216                 arrow.status = ArrowState::Status::Paused;
0217             }
0218         }
0219     }
0220     LOG_WARN(GetLogger()) << "Requested drain" << LOG_END;
0221     lock.unlock();
0222     m_condvar.notify_all();
0223 }
0224 
0225 void JExecutionEngine::RunSupervisor() {
0226 
0227     if (m_interrupt_status == InterruptStatus::NoInterruptsUnsupervised) {
0228         m_interrupt_status = InterruptStatus::NoInterruptsSupervised;
0229     }
0230     size_t last_event_count = 0;
0231     clock_t::time_point last_measurement_time = clock_t::now();
0232 
0233     Perf perf;
0234     while (true) {
0235 
0236         if (m_enable_timeout && m_timeout_s > 0) {
0237             CheckTimeout();
0238         }
0239 
0240         if (m_print_worker_report_requested) {
0241             PrintWorkerReport(false);
0242             m_print_worker_report_requested = false;
0243         }
0244 
0245         if (m_send_worker_report_requested) {
0246             PrintWorkerReport(true);
0247             m_send_worker_report_requested = false;
0248         }
0249 
0250         perf = GetPerf();
0251         if ((perf.runstatus == RunStatus::Paused && m_interrupt_status != InterruptStatus::InspectRequested) || 
0252             perf.runstatus == RunStatus::Finished || 
0253             perf.runstatus == RunStatus::Failed) {
0254             break;
0255         }
0256 
0257         if (m_interrupt_status == InterruptStatus::InspectRequested) {
0258             if (perf.runstatus == RunStatus::Paused) {
0259                 LOG_INFO(GetLogger()) << "Entering inspector" << LOG_END;
0260                 m_enable_timeout = false;
0261                 m_interrupt_status = InterruptStatus::InspectInProgress;
0262                 InspectApplication(GetApplication());
0263                 m_interrupt_status = InterruptStatus::NoInterruptsSupervised;
0264 
0265                 // Jump back to the top of the loop so that we have fresh event count data
0266                 last_measurement_time = clock_t::now();
0267                 last_event_count = 0;
0268                 continue; 
0269             }
0270             else if (perf.runstatus == RunStatus::Running) {
0271                 PauseTopology();
0272             }
0273         }
0274         else if (m_interrupt_status == InterruptStatus::PauseAndQuit) {
0275             PauseTopology();
0276         }
0277 
0278         if (m_show_ticker) {
0279             auto next_measurement_time = clock_t::now();
0280             auto last_measurement_duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(next_measurement_time - last_measurement_time).count();
0281             float latest_throughput_hz = (last_measurement_duration_ms == 0) ? 0 : (perf.event_count - last_event_count) * 1000.0 / last_measurement_duration_ms;
0282             last_measurement_time = next_measurement_time;
0283             last_event_count = perf.event_count;
0284 
0285             // Print rates
0286             LOG_INFO(m_logger) << "Status: " << perf.event_count << " events processed at "
0287                             << JTypeInfo::to_string_with_si_prefix(latest_throughput_hz) << "Hz ("
0288                             << JTypeInfo::to_string_with_si_prefix(perf.throughput_hz) << "Hz avg)" << LOG_END;
0289         }
0290 
0291         std::this_thread::sleep_for(std::chrono::milliseconds(m_ticker_ms));
0292     }
0293     LOG_INFO(GetLogger()) << "Processing paused." << LOG_END;
0294 
0295     if (perf.runstatus == RunStatus::Failed) {
0296         HandleFailures();
0297     }
0298 
0299     PrintFinalReport();
0300 }
0301 
0302 bool JExecutionEngine::CheckTimeout() {
0303     std::unique_lock<std::mutex> lock(m_mutex);
0304     auto now = clock_t::now();
0305     bool timeout_detected = false;
0306     for (auto& worker: m_worker_states) {
0307         auto timeout_s = (worker->is_event_warmed_up) ? m_timeout_s : m_warmup_timeout_s;
0308         auto duration_s = std::chrono::duration_cast<std::chrono::seconds>(now - worker->last_checkout_time).count();
0309         if (duration_s > timeout_s && worker->last_arrow_id != static_cast<uint64_t>(-1)) {
0310             worker->is_timed_out = true;
0311             timeout_detected = true;
0312             m_runstatus = RunStatus::Failed;
0313         }
0314     }
0315     return timeout_detected;
0316 }
0317 
0318 void JExecutionEngine::HandleFailures() {
0319 
0320     std::unique_lock<std::mutex> lock(m_mutex);
0321 
0322     // First, we log all of the failures we've found
0323     for (auto& worker: m_worker_states) {
0324         if (worker->is_timed_out) {
0325             std::string arrow_name = (worker->last_arrow_id == static_cast<uint64_t>(-1)) ? "(none)" : m_topology->GetArrows()[worker->last_arrow_id]->GetName();
0326             LOG_FATAL(GetLogger()) << "Timeout in worker thread " << worker->worker_id << " while executing " << arrow_name << " on event #" << worker->last_event_nr << LOG_END;
0327             pthread_kill(worker->thread->native_handle(), SIGUSR2);
0328             LOG_INFO(GetLogger()) << "Worker thread signalled; waiting for backtrace capture." << LOG_END;
0329             worker->backtrace.WaitForCapture();
0330         }
0331         if (worker->stored_exception != nullptr) {
0332             std::string arrow_name = (worker->last_arrow_id == static_cast<uint64_t>(-1)) ? "(none)" : m_topology->GetArrows()[worker->last_arrow_id]->GetName();
0333             LOG_FATAL(GetLogger()) << "Exception in worker thread " << worker->worker_id << " while executing " << arrow_name << " on event #" << worker->last_event_nr << LOG_END;
0334         }
0335     }
0336 
0337     // Now we throw each of these exceptions in order, in case the caller is going to attempt to catch them.
0338     // In reality all callers are going to print everything they can about the exception and exit.
0339     for (auto& worker: m_worker_states) {
0340         if (worker->stored_exception != nullptr) {
0341             GetApplication()->SetExitCode((int) JApplication::ExitCode::UnhandledException);
0342             std::rethrow_exception(worker->stored_exception);
0343         }
0344         if (worker->is_timed_out) {
0345             GetApplication()->SetExitCode((int) JApplication::ExitCode::Timeout);
0346             auto ex = JException("Timeout in worker thread");
0347             ex.backtrace = worker->backtrace;
0348             throw ex;
0349         }
0350     }
0351 }
0352 
0353 void JExecutionEngine::FinishTopology() {
0354     std::unique_lock<std::mutex> lock(m_mutex);
0355     assert(m_runstatus == RunStatus::Paused);
0356 
0357     LOG_DEBUG(GetLogger()) << "Finishing processing..." << LOG_END;
0358     for (auto* arrow : m_topology->GetArrows()) {
0359         arrow->Finalize();
0360     }
0361     for (auto* pool: m_topology->GetPools()) {
0362         pool->Finalize();
0363     }
0364     m_runstatus = RunStatus::Finished;
0365     LOG_INFO(GetLogger()) << "Finished processing." << LOG_END;
0366 }
0367 
0368 JExecutionEngine::RunStatus JExecutionEngine::GetRunStatus() {
0369     std::unique_lock<std::mutex> lock(m_mutex);
0370     return m_runstatus;
0371 }
0372 
0373 JExecutionEngine::Perf JExecutionEngine::GetPerf() {
0374     std::unique_lock<std::mutex> lock(m_mutex);
0375     Perf result;
0376     if (m_runstatus == RunStatus::Paused || m_runstatus == RunStatus::Failed) {
0377         result.event_count = m_event_count_at_finish - m_event_count_at_start;
0378         result.uptime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(m_time_at_finish - m_time_at_start).count();
0379     }
0380     else {
0381         // Obtain current event count
0382         size_t current_event_count = 0;
0383         for (auto& state : m_arrow_states) {
0384             if (state.is_sink) {
0385                 current_event_count += state.events_processed;
0386             }
0387         }
0388         result.event_count = current_event_count - m_event_count_at_start;
0389         result.uptime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(clock_t::now() - m_time_at_start).count();
0390     }
0391     result.runstatus = m_runstatus;
0392     result.thread_count = m_worker_states.size();
0393     result.throughput_hz = (result.uptime_ms == 0) ? 0 : (result.event_count * 1000.0) / result.uptime_ms;
0394     result.event_level = JEventLevel::PhysicsEvent;
0395     return result;
0396 }
0397 
0398 JExecutionEngine::Worker JExecutionEngine::RegisterWorker() {
0399     std::unique_lock<std::mutex> lock(m_mutex);
0400     auto mapping = m_topology->GetProcessorMapping();
0401     auto worker_id = m_worker_states.size();
0402     auto worker = std::make_unique<WorkerState>();
0403     worker->worker_id = worker_id;
0404     worker->is_stop_requested = false;
0405     worker->cpu_id = mapping.get_cpu_id(worker_id);
0406     worker->location_id = mapping.get_loc_id(worker_id);
0407     worker->thread = nullptr;
0408     m_worker_states.push_back(std::move(worker));
0409 
0410     bool pin_to_cpu = (mapping.get_affinity() != JProcessorMapping::AffinityStrategy::None);
0411     if (pin_to_cpu) {
0412         JCpuInfo::PinThreadToCpu(worker->thread, worker->cpu_id);
0413     }
0414     return {worker_id, &worker->backtrace};
0415 
0416 }
0417 
0418 
0419 void JExecutionEngine::RunWorker(Worker worker) {
0420 
0421     LOG_DEBUG(GetLogger()) << "Launched worker thread " << worker.worker_id << LOG_END;
0422     jana2_worker_id = worker.worker_id;
0423     jana2_worker_backtrace = worker.backtrace;
0424 #if JANA2_HAVE_PERFETTO
0425     JPerfettoService::RegisterCurrentThread(worker.worker_id);
0426 #endif
0427     try {
0428         Task task;
0429         while (true) {
0430             ExchangeTask(task, worker.worker_id);
0431             if (task.arrow == nullptr) break; // Exit as soon as ExchangeTask() stops blocking
0432             {
0433 #if JANA2_HAVE_PERFETTO
0434                 TRACE_EVENT("jana", perfetto::DynamicString{task.arrow->GetName()},
0435                     "worker_id", (uint64_t)worker.worker_id);
0436 #endif
0437                 task.arrow->Fire(task.input_event, task.outputs, task.output_count, task.status);
0438             }
0439         }
0440         LOG_DEBUG(GetLogger()) << "Stopped worker thread " << worker.worker_id << LOG_END;
0441     }
0442     catch(JException& ex) {
0443         LOG_ERROR(GetLogger()) << "Exception on worker thread " << worker.worker_id << ": " << ex.GetMessage();
0444         std::unique_lock<std::mutex> lock(m_mutex);
0445         m_runstatus = RunStatus::Failed;
0446         m_worker_states.at(worker.worker_id)->stored_exception = std::current_exception();
0447     }
0448     catch (...) {
0449         LOG_ERROR(GetLogger()) << "Exception on worker thread " << worker.worker_id << LOG_END;
0450         std::unique_lock<std::mutex> lock(m_mutex);
0451         m_runstatus = RunStatus::Failed;
0452         m_worker_states.at(worker.worker_id)->stored_exception = std::current_exception();
0453     }
0454 }
0455 
0456 
0457 void JExecutionEngine::ExchangeTask(Task& task, size_t worker_id, bool nonblocking) {
0458 
0459     auto checkin_time = std::chrono::steady_clock::now();
0460     // It's important to start measuring this _before_ acquiring the lock because acquiring the lock
0461     // may be a big part of the scheduler overhead
0462 
0463     std::unique_lock<std::mutex> lock(m_mutex);
0464     
0465     auto& worker = *m_worker_states.at(worker_id);
0466 
0467     if (task.arrow != nullptr) {
0468         CheckinCompletedTask_Unsafe(task, worker, checkin_time);
0469     }
0470 
0471     if (worker.is_stop_requested) {
0472         return;
0473     }
0474 
0475     FindNextReadyTask_Unsafe(task, worker);
0476 
0477     if (nonblocking) { return; }
0478     auto idle_time_start = clock_t::now();
0479     m_total_scheduler_duration += (idle_time_start - checkin_time);
0480 
0481     while (task.arrow == nullptr && !worker.is_stop_requested) {
0482         m_condvar.wait(lock);
0483         FindNextReadyTask_Unsafe(task, worker);
0484     }
0485     worker.last_checkout_time = clock_t::now();
0486 
0487     if (task.input_event != nullptr) {
0488         worker.last_event_nr = task.input_event->GetEventNumber();
0489     }
0490     else {
0491         worker.last_event_nr = 0;
0492     }
0493     m_total_idle_duration += (worker.last_checkout_time - idle_time_start);
0494 
0495     // Notify one worker, who will notify the next, etc, as long as FindNextReadyTaskUnsafe() succeeds.
0496     // After FindNextReadyTaskUnsafe fails, all threads block until the next returning worker reactivates the
0497     // notification chain.
0498     if (task.arrow != nullptr && task.arrow->IsParallel()) {
0499         m_condvar.notify_one();
0500     }
0501 }
0502 
0503 
0504 void JExecutionEngine::CheckinCompletedTask_Unsafe(Task& task, WorkerState& worker, clock_t::time_point checkin_time) {
0505 
0506     auto processing_duration = checkin_time - worker.last_checkout_time;
0507 
0508     ArrowState& arrow_state = m_arrow_states.at(worker.last_arrow_id);
0509 
0510     arrow_state.active_tasks -= 1;
0511     arrow_state.total_processing_duration += processing_duration;
0512 
0513     for (size_t output=0; output<task.output_count; ++output) {
0514         if (!task.arrow->GetPort(task.outputs[output].second).GetSkipFinishEvent()) {
0515             arrow_state.events_processed++;
0516         }
0517     }
0518 
0519     // Put each output in its correct queue or pool
0520     task.arrow->Push(task.outputs, task.output_count, worker.location_id);
0521 
0522     if (task.status == JArrow::FireResult::Finished) {
0523         // If this is an eventsource self-terminating (the only thing that returns Status::Finished right now) it will
0524         // have already called DoClose(). I'm tempted to always call DoClose() as part of JExecutionEngine::Finish() instead, however.
0525 
0526         // Mark arrow as finished
0527         arrow_state.status = ArrowState::Status::Finished;
0528 
0529         // Check if this switches the topology to Draining()
0530         if (m_runstatus == RunStatus::Running) {
0531             bool draining = true;
0532             for (auto& arrow: m_arrow_states) {
0533                 if (arrow.is_source && arrow.status == ArrowState::Status::Running) {
0534                     draining = false;
0535                 }
0536             }
0537             if (draining) {
0538                 m_runstatus = RunStatus::Draining;
0539             }
0540         }
0541     }
0542     worker.last_arrow_id = -1;
0543     worker.last_event_nr = 0;
0544 
0545     task.arrow = nullptr;
0546     task.input_event = nullptr;
0547     task.output_count = 0;
0548     task.status = JArrow::FireResult::NotRunYet;
0549 };
0550 
0551 
0552 void JExecutionEngine::FindNextReadyTask_Unsafe(Task& task, WorkerState& worker) {
0553 
0554     if (m_runstatus == RunStatus::Running || m_runstatus == RunStatus::Draining) {
0555         // We only pick up a new task if the topology is running or draining.
0556 
0557         // Each call to FindNextReadyTask_Unsafe() starts with a different m_next_arrow_id to ensure balanced arrow assignments
0558         size_t arrow_count = m_arrow_states.size();
0559         m_next_arrow_id += 1;
0560         m_next_arrow_id %= arrow_count;
0561 
0562         for (size_t i=m_next_arrow_id; i<(m_next_arrow_id+arrow_count); ++i) {
0563             size_t arrow_id = i % arrow_count;
0564 
0565             auto& state = m_arrow_states[arrow_id];
0566             if (!state.is_parallel && (state.active_tasks != 0)) {
0567                 // We've found a sequential arrow that is already active. Nothing we can do here.
0568                 LOG_TRACE(GetLogger()) << "Scheduler: Arrow with id " << arrow_id << " is unready: Sequential and already active." << LOG_END;
0569                 continue;
0570             }
0571 
0572             if (state.status != ArrowState::Status::Running) {
0573                 LOG_TRACE(GetLogger()) << "Scheduler: Arrow with id " << arrow_id << " is unready: Arrow is either paused or finished." << LOG_END;
0574                 continue;
0575             }
0576             // TODO: Support next_visit_time so that we don't hammer blocked event sources
0577 
0578             // See if we can obtain an input event (this is silly)
0579             JArrow* arrow = m_topology->GetArrows()[arrow_id];
0580             // TODO: consider setting state.next_input, retrieving via Fire()
0581             auto port = arrow->GetNextPortIndex();
0582             JEvent* event = (port == -1) ? nullptr : arrow->Pull(port, worker.location_id);
0583             if (event != nullptr || port == -1) {
0584                 LOG_TRACE(GetLogger()) << "Scheduler: Found next ready arrow with id " << arrow_id << LOG_END;
0585                 // We've found a task that is ready!
0586                 state.active_tasks += 1;
0587 
0588                 task.arrow = arrow;
0589                 task.input_port = port;
0590                 task.input_event = event;
0591                 task.output_count = 0;
0592                 task.status = JArrow::FireResult::NotRunYet;
0593 
0594                 worker.last_arrow_id = arrow_id;
0595                 if (event != nullptr) {
0596                     worker.is_event_warmed_up = event->IsWarmedUp();
0597                     worker.last_event_nr = event->GetEventNumber();
0598                 }
0599                 else {
0600                     worker.is_event_warmed_up = true; // Use shorter timeout
0601                     worker.last_event_nr = 0;
0602                 }
0603                 return;
0604             }
0605             else {
0606                 LOG_TRACE(GetLogger()) << "Scheduler: Arrow with id " << arrow_id << " is unready: Input event is needed but not on queue yet." << LOG_END;
0607             }
0608         }
0609     }
0610 
0611     // Because we reached this point, we know that there aren't any tasks ready,
0612     // so we check whether more are potentially coming. If not, we can pause the topology.
0613     // Note that our worker threads will still wait at ExchangeTask() until they get
0614     // shut down separately during Scale().
0615     
0616     if (m_runstatus == RunStatus::Pausing || m_runstatus == RunStatus::Draining) {
0617         // We want to avoid scenarios such as where the topology already Finished but then gets reset to Paused
0618         // This also leaves a cleaner narrative in the logs. 
0619 
0620         bool any_active_source_found = false;
0621         bool any_active_task_found = false;
0622         
0623         LOG_DEBUG(GetLogger()) << "Scheduler: No tasks ready" << LOG_END;
0624 
0625         for (size_t arrow_id = 0; arrow_id < m_arrow_states.size(); ++arrow_id) {
0626             auto& state = m_arrow_states[arrow_id];
0627             any_active_source_found |= (state.status == ArrowState::Status::Running && state.is_source);
0628             any_active_task_found |= (state.active_tasks != 0);
0629             // A source might have been deactivated by RequestPause, Ctrl-C, etc, and might be inactive even though it still has active tasks
0630         }
0631 
0632         if (!any_active_source_found && !any_active_task_found) {
0633             // Pause the topology
0634             m_time_at_finish = clock_t::now();
0635             m_event_count_at_finish = 0;
0636             for (auto& arrow_state : m_arrow_states) {
0637                 if (arrow_state.is_sink) {
0638                     m_event_count_at_finish += arrow_state.events_processed;
0639                 }
0640             }
0641             LOG_DEBUG(GetLogger()) << "Scheduler: Processing paused" << LOG_END;
0642             m_runstatus = RunStatus::Paused;
0643             // I think this is the ONLY site where the topology gets paused. Verify this?
0644         }
0645     }
0646 
0647     worker.last_arrow_id = -1;
0648 
0649     task.arrow = nullptr;
0650     task.input_port = -1;
0651     task.input_event = nullptr;
0652     task.output_count = 0;
0653     task.status = JArrow::FireResult::NotRunYet;
0654 }
0655 
0656 
0657 void JExecutionEngine::PrintFinalReport() {
0658 
0659     std::unique_lock<std::mutex> lock(m_mutex);
0660     auto event_count = m_event_count_at_finish - m_event_count_at_start;
0661     auto uptime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(m_time_at_finish - m_time_at_start).count();
0662     auto thread_count = m_worker_states.size();
0663     auto throughput_hz = (event_count * 1000.0) / uptime_ms;
0664 
0665     LOG_INFO(GetLogger()) << "Detailed report:" << LOG_END;
0666     LOG_INFO(GetLogger()) << LOG_END;
0667     LOG_INFO(GetLogger()) << "  Avg throughput [Hz]:         " << std::setprecision(3) << throughput_hz << LOG_END;
0668     LOG_INFO(GetLogger()) << "  Completed events [count]:    " << event_count << LOG_END;
0669     LOG_INFO(GetLogger()) << "  Total uptime [s]:            " << std::setprecision(4) << uptime_ms/1000.0 << LOG_END;
0670     LOG_INFO(GetLogger()) << "  Thread team size [count]:    " << thread_count << LOG_END;
0671     LOG_INFO(GetLogger()) << LOG_END;
0672     LOG_INFO(GetLogger()) << "  Arrow-level metrics:" << LOG_END;
0673     LOG_INFO(GetLogger()) << LOG_END;
0674 
0675     size_t total_useful_ms = 0;
0676 
0677     for (size_t arrow_id=0; arrow_id < m_arrow_states.size(); ++arrow_id) {
0678         auto* arrow = m_topology->GetArrows()[arrow_id];
0679         auto& arrow_state = m_arrow_states[arrow_id];
0680         auto useful_ms = std::chrono::duration_cast<std::chrono::milliseconds>(arrow_state.total_processing_duration).count();
0681         total_useful_ms += useful_ms;
0682         auto avg_latency = useful_ms*1.0/arrow_state.events_processed;
0683         auto throughput_bottleneck = 1000.0 / avg_latency;
0684         if (arrow->IsParallel()) {
0685             throughput_bottleneck *= thread_count;
0686         }
0687 
0688         LOG_INFO(GetLogger()) << "  - Arrow name:                 " << arrow->GetName() << LOG_END;
0689         LOG_INFO(GetLogger()) << "    Parallel:                   " << arrow->IsParallel() << LOG_END;
0690         LOG_INFO(GetLogger()) << "    Events completed:           " << arrow_state.events_processed << LOG_END;
0691         LOG_INFO(GetLogger()) << "    Avg latency [ms/event]:     " << avg_latency << LOG_END;
0692         LOG_INFO(GetLogger()) << "    Throughput bottleneck [Hz]: " << throughput_bottleneck << LOG_END;
0693         LOG_INFO(GetLogger()) << LOG_END;
0694     }
0695 
0696     auto total_scheduler_ms = std::chrono::duration_cast<std::chrono::milliseconds>(m_total_scheduler_duration).count();
0697     auto total_idle_ms = std::chrono::duration_cast<std::chrono::milliseconds>(m_total_idle_duration).count();
0698 
0699     LOG_INFO(GetLogger()) << "  Total useful time [s]:     " << std::setprecision(6) << total_useful_ms/1000.0 << LOG_END;
0700     LOG_INFO(GetLogger()) << "  Total scheduler time [s]:  " << std::setprecision(6) << total_scheduler_ms/1000.0 << LOG_END;
0701     LOG_INFO(GetLogger()) << "  Total idle time [s]:       " << std::setprecision(6) << total_idle_ms/1000.0 << LOG_END;
0702 
0703     LOG_INFO(GetLogger()) << LOG_END;
0704 
0705     LOG_INFO(GetLogger()) << "Final report: " << event_count << " events processed at "
0706                           << JTypeInfo::to_string_with_si_prefix(throughput_hz) << "Hz" << LOG_END;
0707 
0708 }
0709 
0710 void JExecutionEngine::SetTickerEnabled(bool show_ticker) {
0711     m_show_ticker = show_ticker;
0712 }
0713 
0714 bool JExecutionEngine::IsTickerEnabled() const {
0715     return m_show_ticker;
0716 }
0717 
0718 void JExecutionEngine::SetTimeoutEnabled(bool timeout_enabled) {
0719     m_enable_timeout = timeout_enabled;
0720 }
0721 
0722 bool JExecutionEngine::IsTimeoutEnabled() const {
0723     return m_enable_timeout;
0724 }
0725 
0726 JArrow::FireResult JExecutionEngine::Fire(size_t arrow_id, size_t location_id) {
0727 
0728     std::unique_lock<std::mutex> lock(m_mutex);
0729     if (arrow_id >= m_topology->GetArrows().size()) {
0730         LOG_WARN(GetLogger()) << "Firing unsuccessful: No arrow exists with id=" << arrow_id << LOG_END;
0731         return JArrow::FireResult::NotRunYet;
0732     }
0733     JArrow* arrow = m_topology->GetArrows()[arrow_id];
0734     LOG_WARN(GetLogger()) << "Attempting to fire arrow with name=" << arrow->GetName() 
0735                           << ", index=" << arrow_id << ", location=" << location_id << LOG_END;
0736 
0737     ArrowState& arrow_state = m_arrow_states[arrow_id];
0738     if (arrow_state.status == ArrowState::Status::Finished) {
0739         LOG_WARN(GetLogger()) << "Firing unsuccessful: Arrow status is Finished." << arrow_id << LOG_END;
0740         return JArrow::FireResult::Finished;
0741     }
0742     if (!arrow_state.is_parallel && arrow_state.active_tasks != 0) {
0743         LOG_WARN(GetLogger()) << "Firing unsuccessful: Arrow is sequential and already has an active task." << arrow_id << LOG_END;
0744         return JArrow::FireResult::NotRunYet;
0745     }
0746     arrow_state.active_tasks += 1;
0747 
0748     auto port = arrow->GetNextPortIndex();
0749     JEvent* event = nullptr;
0750     if (port != -1) {
0751         event = arrow->Pull(port, location_id);
0752         if (event == nullptr) {
0753             LOG_WARN(GetLogger()) << "Firing unsuccessful: Arrow needs an input event from port " << port << ", but the queue or pool is empty." << LOG_END;
0754             arrow_state.active_tasks -= 1;
0755             return JArrow::FireResult::NotRunYet;
0756         }
0757         else {
0758             LOG_WARN(GetLogger()) << "Input event #" << event->GetEventNumber() << " from port " << port << LOG_END;
0759         }
0760     }
0761     else {
0762         LOG_WARN(GetLogger()) << "No input events" << LOG_END;
0763     }
0764     lock.unlock();
0765 
0766     size_t output_count;
0767     JArrow::OutputData outputs;
0768     JArrow::FireResult result = JArrow::FireResult::NotRunYet;
0769 
0770     LOG_WARN(GetLogger()) << "Firing arrow" << LOG_END;
0771     arrow->Fire(event, outputs, output_count, result);
0772     LOG_WARN(GetLogger()) << "Fired arrow with result " << ToString(result) << LOG_END;
0773     if (output_count == 0) {
0774         LOG_WARN(GetLogger()) << "No output events" << LOG_END;
0775     }
0776     else {
0777         for (size_t i=0; i<output_count; ++i) {
0778             LOG_WARN(GetLogger()) << "Output event #" << outputs.at(i).first->GetEventNumber() << " on port " << outputs.at(i).second << LOG_END;
0779         }
0780     }
0781 
0782     lock.lock();
0783     arrow->Push(outputs, output_count, location_id);
0784     arrow_state.active_tasks -= 1;
0785     lock.unlock();
0786     return result;
0787 }
0788 
0789 
0790 void JExecutionEngine::HandleSIGINT() {
0791     InterruptStatus status = m_interrupt_status;
0792     std::cout << std::endl;
0793     switch (status) {
0794         case InterruptStatus::NoInterruptsSupervised: m_interrupt_status = InterruptStatus::InspectRequested; break;
0795         case InterruptStatus::InspectRequested: m_interrupt_status = InterruptStatus::PauseAndQuit; break;
0796         case InterruptStatus::NoInterruptsUnsupervised:
0797         case InterruptStatus::PauseAndQuit:
0798         case InterruptStatus::InspectInProgress: 
0799             _exit(-2);
0800     }
0801 }
0802 
0803 void JExecutionEngine::HandleSIGUSR1() {
0804     m_send_worker_report_requested = true;
0805 }
0806 
0807 void JExecutionEngine::HandleSIGUSR2() {
0808     if (jana2_worker_backtrace != nullptr) {
0809         jana2_worker_backtrace->Capture(3);
0810     }
0811 }
0812 
0813 void JExecutionEngine::HandleSIGTSTP() {
0814     std::cout << std::endl;
0815     m_print_worker_report_requested = true;
0816 }
0817 
0818 void JExecutionEngine::PrintWorkerReport(bool send_to_pipe) {
0819 
0820     std::unique_lock<std::mutex> lock(m_mutex);
0821     LOG_INFO(GetLogger()) << "Generating worker report. It may take some time to retrieve each symbol's debug information." << LOG_END;
0822     for (auto& worker: m_worker_states) {
0823         worker->backtrace.Reset();
0824         pthread_kill(worker->thread->native_handle(), SIGUSR2);
0825     }
0826     for (auto& worker: m_worker_states) {
0827         worker->backtrace.WaitForCapture();
0828     }
0829     std::ostringstream oss;
0830     oss << "Worker report" << std::endl;
0831     for (auto& worker: m_worker_states) {
0832         oss << "------------------------------" << std::endl 
0833             << "  Worker:        " << worker->worker_id << std::endl
0834             << "  Current arrow: " << worker->last_arrow_id << std::endl
0835             << "  Current event: " << worker->last_event_nr << std::endl
0836             << "  Backtrace:" << std::endl << std::endl
0837             << worker->backtrace.ToString();
0838     }
0839     auto s = oss.str();
0840     LOG_WARN(GetLogger()) << s << LOG_END;
0841 
0842     if (send_to_pipe) {
0843 
0844         int fd = open(m_path_to_named_pipe.c_str(), O_WRONLY);
0845         if (fd >= 0) {
0846             write(fd, s.c_str(), s.length()+1);
0847             close(fd);
0848         }
0849         else {
0850             LOG_ERROR(GetLogger()) << "Unable to open named pipe '" << m_path_to_named_pipe << "' for writing. \n"
0851             << "  You can use a different named pipe for status info by setting the parameter `jana:status_fname`.\n"
0852             << "  The status report will still show up in the log." << LOG_END;
0853         }
0854     }
0855 }
0856 
0857 
0858 std::string ToString(JExecutionEngine::RunStatus runstatus) {
0859     switch(runstatus) {
0860         case JExecutionEngine::RunStatus::Running: return "Running";
0861         case JExecutionEngine::RunStatus::Paused: return "Paused";
0862         case JExecutionEngine::RunStatus::Failed: return "Failed";
0863         case JExecutionEngine::RunStatus::Pausing: return "Pausing";
0864         case JExecutionEngine::RunStatus::Draining: return "Draining";
0865         case JExecutionEngine::RunStatus::Finished: return "Finished";
0866         default: return "CorruptedRunStatus";
0867     }
0868 }
0869