Global Health Policy Simulation model
| Home | Quick Start | User Guide | Schemas | Models | Architecture | Data Model | Developer Guide | Technical docs | API |
| Related: Individual ID tracking plan | Technical index | Documentation index |
Title: Parallelize output writes and is_active
Overview: Parallelize result/tracking file writes by using two writer threads (main result vs individual tracking) and a second queue, then reduce redundant is_active() calls in hot paths (e.g. analysis module) by caching or filtering once per population iteration.
EventType::result and EventType::individual_tracking use result_event_handler, which pushes every message into one results_queue_. One thread (result_dispatch_thread) pops and calls accept() so that:
ResultEventMessage -> result_writer_.write() (JSON + main CSV + income CSVs)IndividualTrackingEventMessage -> individual_tracking_writer_->write()
All writes are therefore serialized on one thread.ResultFileWriter::write() holds a single mutex, then writes JSON fragment, main CSV rows, and (if enabled) income CSV rows. Different files (stream_, csvstream_, income_csvstreams_) but same lock.IndividualIDTrackingWriter::write() has its own mutex and writes only to the tracking CSV.is_alive_ && !has_emigrated_), but in tight loops over the full population it can add up.Goal: Let main result writes and individual-tracking writes run on different threads so they can proceed in parallel (different files, no shared state).
Approach: two queues, two dispatch threads.
tbb::concurrent_queue<std::shared_ptr<hgps::EventMessage>> tracking_results_queue_ and a tracking_dispatch_thread() that loops popping from this queue and calling m->accept(*this) (same visitor; only IndividualTrackingEventMessage will be pushed here).results_queue_ and result_dispatch_thread() for ResultEventMessage only.results_queue_ (unchanged).tracking_results_queue_ instead of results_queue_.tg_.run([this] { result_dispatch_thread(); }), add tg_.run([this] { tracking_dispatch_thread(); }).stop() / dtor: tg_context_.cancel_group_execution() and tg_.wait() already wait for all tasks, so both threads will finish.parallel_invoke) before taking the lock and writing; this reduces CPU before I/O but does not parallelize I/O itself. Can be a follow-up.Result: Main result file(s) and individual-tracking file are written by two threads in parallel; ordering within each file is unchanged.
Goal: Avoid calling is_active() repeatedly on the same person in the same logical “iteration” (e.g. same year, same module).
Where it’s used (examples):
context.population() that do if (!entity.is_active()) continue; or similar (e.g. lines 100, 224, 312, 418, 625, 685, 746, 1002, 1171, 1452, 1553, 1760, 1979).is_active().Options:
std::vector<bool> or bit set) of “active” by index, then in subsequent loops over the same population snapshot use that cache instead of calling entity.is_active(). Downside: population can change during a year (deaths, births); so the cache is only valid if all uses in that pass see the same snapshot. In publish_result_message and the various calculate_* paths, the population is not modified during the same call, so a cache per function scope is valid.std::vector<std::size_t> active_indices, then later loops iterate for (auto i : active_indices) { auto& entity = population[i]; ... }. This replaces many is_active() checks with one pass and then direct iteration. Same caveat: use within a single logical pass where population is not changing.Recommendation: Start with analysis_module only. In functions that do multiple passes over context.population() in one go (e.g. calculate_historical_statistics, or the block that does DALYs + risk-factor sums + comorbidity + prevalence), add one initial pass that fills std::vector<bool> is_active(pop.size()) (or an index list), then use that in subsequent loops instead of calling entity.is_active(). Measure before/after if needed.
Scope for plan: Implement Phase 1 (two writer threads) first; then implement Phase 2 in analysis_module (one or two key functions) and leave a short comment for extending to other modules later.
| File | Change |
|---|---|
| event_monitor.h | Add tracking_results_queue_, declare tracking_dispatch_thread(), and second tg_.run() for it. |
| event_monitor.cpp | individual_tracking subscriber pushes to tracking_results_queue_; implement tracking_dispatch_thread() (same loop as result_dispatch_thread but pop from tracking queue); start tracking thread in ctor. |
No changes to result_file_writer.cpp or individual_id_tracking_writer.cpp for Phase 1; each writer stays single-threaded from its own dispatch thread.
| File | Change |
|---|---|
| analysis_module.cpp | In one or two hot functions that do multiple population passes (e.g. calculate_historical_statistics), add a single initial pass that builds a cache of active flags (or active indices), then use that cache in later loops instead of calling person.is_active(). |
is_active() checks with the cache.Author: Mahima Ghosh