Refactor/monitoring single core - #453
Conversation
2563187 to
0e6ec88
Compare
0030a28 to
81ab16e
Compare
Store each measure in either vector<int> or vector<double> according to its output type. This halves memory and checkpoint storage for integer measures while keeping one shared routing and output implementation.
Check output files after opening and after an explicit close. This reports invalid paths and delayed write failures such as a full disk with the existing FileIO exit code instead of returning success.
Negative output IDs were accepted by the schema but mistaken for hidden condition stores and omitted from the output. Reject them during monitoring initialization with a scenario error.
Choose text or binary serialization independently from gzip compression. Compressed binary output retains the versioned binary format after decompression and uses the usual .gz suffix.
Make measure() consteval and fail constant evaluation for unknown names. All call sites use literals, so misspellings now fail the build instead of returning an invalid index at runtime.
gzstream can report a good stream state when its file did not open. Check the underlying buffer's open state so compressed output path failures are reported before serialization begins.
Open plain text output in binary mode so Windows does not translate LF line endings, and update the monitoring schema documentation to reference the current OutMeasures.h file.
b-s-code
left a comment
There was a problem hiding this comment.
This looks like a strong improvement!
My only blocking query is: what would be involved in adding test coverage of the binary output feature? I assume some change to test/run.py would be needed.
General items:
- Adding binary output is perhaps a bit more than a refactor, and maybe could have been its own PR. No issue including it this time though.
- It looks like continuous output can only be text, not binary? Is this correct?
- Providing an example of how to read a binary output file in R might be useful, either on the PR description, on the wiki, or as an example R script in this repo.
| ctsOStream << "##\t##\n"; // live-graph needs a delimiter specifier when it is not a comma | ||
| if( duringInit ) ctsOStream << "simulation time\t"; | ||
| ctsOStream << "timestep"; // TODO: change to days or remove or leave? |
There was a problem hiding this comment.
This code is a candidate for some cleanup (I assume we don't support live graph any more and the TODO is pre-existing).
Optional.
|
|
||
| namespace { | ||
|
|
||
| bool notPowerOfTwo(uint32_t num) |
There was a problem hiding this comment.
I think this function ought to be renamed.
E.g. notPowerOfTwo(std::pow(2, 30)) returns 1, where I'd expect 0, if I didn't read the implementation.
I can see it doesn't matter much given the enclosing anonymous namespace and context at the call site but I think it would be good to address this while we're already making changes here.
| const char magic[8] = {'O', 'M', 'O', 'U', 'T', 'B', '2', '\0'}; | ||
| const uint32_t version = 2; |
There was a problem hiding this comment.
Unless the R reader benefits from it, I'd prefer to avoid writing the version number twice.
If writing it twice is useful, I think it would be better to derive one value from the other here, rather than having two literal values.
| }; | ||
|
|
||
| struct OutMeasure { | ||
| const char* userName = nullptr; // defs[] only |
There was a problem hiding this comment.
Would userFacingName better describe this?
| string name = parseNextArg (argc, argv, i); | ||
| (scenarioFile = "scenario").append(name).append(".xml"); | ||
| (outputName = "output").append(name).append(".txt"); | ||
| (outputName = "output").append(name); |
Monitoring refactor
Summary
This PR replaces the fragmented monitoring subsystem with a smaller implementation that is easier to maintain, trace, and debug. It removes 858 net lines across 55 files.
A measure now has one identity throughout the model:
<option name="nHost">->mon::measure("nHost")-> onedefs[]entry -> its stable output numberThe main changes are:
Consolidated architecture
Seven legacy monitoring files and their overlapping responsibilities were replaced by:
Monitoring.{h,cpp}for runtime state, recording, checkpointing, and output.OutMeasures.hfor measure definitions.init.{h,cpp}for scenario-driven initialization.This removes the former
AgeGroup,OutputMeasures,info,management,reporting,mon.cpp, andmisc.cpplayers.The old measure enums, such as
MHR_HOSTSandMHF_LOG_DENSITY, were removed. Call sites now use the exact XML name:Measureis a compact index into oneconstexpr defs[]table. That table is the source of truth for the measure name, stable output number, numeric formatting, and supported dimensions. C++ measure literals are resolved withconsteval, so an unknown name fails at compile time.The
internalNameproxy,measureKey()indirection, and deployment-method metadata were removed from measure definitions. Deployment variants are recorded directly under their final measure names.Five category booleans were replaced by a
Dimbitmask for age, cohort, species, genotype, and drug. Each configured output uses one flattened buffer selected fromstd::vector<int>orstd::vector<double>, preserving compact integer storage without duplicating routing or output code.Recording and survey semantics
Fifteen type- and dimension-specific reporting functions were replaced by three semantic names:
recordStat()records values for the current reported survey.recordEvent()accumulates events toward the next reported survey.recordDeploy()selects the final deployment measure.All dimensional routing and accumulation pass through one private
recordValue()implementation, and all in-tree call sites were migrated.The survey timeline retains both reported and unreported dates because both affect event attribution. Runtime state separately tracks the current survey and the next reported survey receiving events.
This also fixes
nCMDTReport: custom decision-tree reports now use event semantics and are no longer lost when an unreported survey boundary occurs before the next report.Text and binary output
Text and binary output use the same logical row generation. The new
--output-format txt|binoption selects exactly one format:--compress-output..txt,.txt.gz,.bin, or.bin.gzaccording to the selected options.Binary output uses a versioned, little-endian columnar layout. Its header stores separate integer and floating-point row counts, followed by contiguous
int32_tsurvey, category, and measure columns. Integer measure values are written asint32_t; floating-point measure values remain IEEE-754 doubles. Dimensionless measures use category0, as in text output.Grouping identifiers and values by column improves both direct reads and gzip compression. It also preserves the compact integer representation from monitoring buffers through serialization instead of converting every value to a double.
Binary format
The header is 28 bytes. Let
Ibe the number of integer rows,Dthe number of floating-point rows, andN = I + D. All fields are little-endian, integer rows precede floating-point rows, and.bin.gzis the same payload wrapped in gzip.OMOUTB2\0uint32(2)uint64uint64int32NageGroup) columnint32Nint32Nint32IdoubleDPython reader
This reader requires NumPy and pandas. Plain files are memory-mapped; gzip files are decompressed once in memory.
R reader
This reader uses
data.table, bulkreadBin()calls, and streamed gzip input.Output files are checked after opening and explicit close. Invalid paths and delayed write failures such as a full disk now terminate with the existing
FileIOstatus instead of returning success.Checkpointing and continuous output
Monitoring report buffers are allocated from scenario-derived dimensions before checkpoint loading. Serialized lengths must exactly match those allocations, so monitoring does not need an arbitrary container limit and malformed checkpoints cannot control allocation size.
The empty
ContinuousTypesingleton was replaced bymon::Continuousnamespace functions. Fresh and checkpoint initialization now share option resolution, and continuous checkpointing stores the absolute output position directly.The monitoring checkpoint layout changed, so cross-version checkpoint compatibility should not be assumed.
Compatibility
Validation
R 4.6 warm-cache medians for the complete extraction of 189,800 rows from
ModelNameNoOverrides(15 batches of 20 reads, data.table 1.18.4 using four threads):fread()fread()readBin()gzfile()andreadBin()The R readers preallocate the per-scenario result list and add the scenario index by reference with
set(). The binary reader uses onereadBin()call per contiguous column and converts the resulting named list to adata.tableby reference withsetDT(). Gzip input is streamed throughgzfile()without an additional uncompressed-file-sized raw buffer. Plain binary was about twice as fast as plain text in this test; gzip binary was much smaller and substantially faster to read than gzip text.Monitoring Refactor Changes
Continuous Outputs
ContinuousTypeclass and global singleton withmon::Continuousnamespace functions:init(),update(),checkpoint(), andregisterCallback(); all initialization, simulation, and checkpoint callers now use these functions directly.period,duringInit, and generated values.streamOff/streamStartpair was replaced by one absolutestreamOffset.'\n'replaced index loops and the dependency onmon::lineEnd.Normal Timed Outputs
AgeGroup.h,OutputMeasures.h,info.h,management.h,misc.cpp,mon.cpp, andreporting.hwithMonitoring.{h,cpp},OutMeasures.h, andinit.{h,cpp}.Measure, a compactuint16_tindex into the singleconstexpr defs[]table;consteval measure(std::string_view)resolves literals at compile time and rejects unknown names during compilation.defs[]; theinternalNameproxy,measureKey()indirection, and old names such asMHR_HOSTSwere removed.OutMeasure.OutMeasurenow contains only the final name, output number, numeric formatting flag, dimensions, and resolved measure index.Dimbitmask for age, cohort, species, genotype, and drug; the temporary dimension operators andhasDim()/clearDim()helpers were removed in favor of direct bit operations.std::variant<std::vector<int>, std::vector<double>>per configured output; integer measures retain compact memory and checkpoint storage while routing and output remain shared.storesByMeasurerouting so one recording reaches every configured store for that measure, including output-number overrides; deployment conditions create an uncategorized, non-output store only when their measure is otherwise disabled.mon::AgeGroupwrapper; humans now store a plainsize_t monitoringAgeGroup.recordStat()for the current reported survey,recordEvent()for accumulated events, andrecordDeploy()for deployment-method selection.recordValue()function;isUsed()checks the direct measure-to-store routing table.allCauseIMRretains only its special calculation and output path.Timed Measures: new Binary Output
--output-format txt|bin; text remains the default, exactly one format is written, and either format may be gzip-compressed with--compress-output..txt,.txt.gz,.bin, or.bin.gz; explicit extensions that conflict with the format or compression options are rejected.int32_tsurvey, category, and measure columns, followed byint32_tinteger values and IEEE-754doublefloating-point values.FileIOerror status.Rest Of The Code
Humanand stored survey-period age and cohort indices directly inEpisodewhere historical event attribution is required.recordStat()for survey statistics andrecordEvent()for accumulated events.nCMDTReporttorecordEvent(), fixing reports before unreported survey markers so they accumulate into the next reported survey; its output-number filtering remains supported.innoculationsPerAgeGroupandinnoculationsPerVectorcalls, including species and genotype dimensions where applicable.nMassIRS/nCtsIRS,nMassGVI/nCtsGVI,nMassITNs/nEPI_ITNs,nMassVaccinations/nEPIVaccinations,nMDAs/nCtsMDA,nMassScreenings/nCtsScreenings, andnMassRecruitOnly/nCtsRecruitOnly; treatment deployments usenTreatDeployments.Deploy::Methodremains only in the intervention recording API to select timed, continuous, or treatment deployment behavior; it is no longer stored in measure definitions.mon::Continuous::registerCallback().OutMeasures.h.