Testing Guide for DR_EVT Scheduler
Overview
DR_EVT’s expected test outputs are generated by running
scripts/python_reference_scheduler.py - a from-scratch, independently written
EASY backfilling implementation - and recording what it produces.
Agreement between the C++ simulator and the expected files means that the C++
implementation matches the Python reference implementation’s behavior. If both
share the same misunderstanding of EASY backfilling, both would agree with each
other and with the expected files while both being wrong in the same way.
What this means in practice: matching the expected output is a regression check (did this change alter scheduling behavior?), not proof of algorithmic correctness. For the smallest tests (2-5 jobs), it’s practical to hand-verify a computed schedule against the algorithm’s rules yourself if you want actual independent confirmation - see “Manual Verification” below. For larger tests, that’s not practical, and the expected file’s authority rests entirely on the Python reference being correct.
How to Run Tests
cd tests
# Run comprehensive test suite (34 tests)
./test_all_dr_evt.sh
# Run unit tests
./run_unit_tests.sh
# Run feature tests
./run_feature_tests.sh
# Run scale tests (manually, no wrapper script yet)
${CMAKE_INSTALL_PREFIX}/bin/simulator test_traces/scale/small_10jobs.csv --total_nodes 795 --run_time_mode limit
# Run replay tests
./run_replay_tests.sh
# Run streaming API tests (compiled binaries, installed under bin/tests/ - not shell scripts, so not in tests/)
${CMAKE_INSTALL_PREFIX}/bin/tests/test_streaming_api
${CMAKE_INSTALL_PREFIX}/bin/tests/test_batch_vs_streaming
# Run queue implementation differential tests (circular/deque/multimap/block)
./test_fcfs_comprehensive.sh --correctness
# Run column alias tests (time_limit/actual_run_time accepted column-name variants)
./test_column_aliases.sh
./test_run_time_modes.sh
Installed Test Binaries
make install installs the C++ test binaries (test_streaming_api,
test_batch_vs_streaming, test_block_queue, test_mpi_streaming,
test_grpc_multi_client_server, t_state, t_state_cereal,
t_state_rngen, t_rngen) under ${CMAKE_INSTALL_PREFIX}/bin/tests/,
separate from the production binaries (simulator, tracer,
dr_evt_server, dr_evt_client) directly under
${CMAKE_INSTALL_PREFIX}/bin/:
${CMAKE_INSTALL_PREFIX}/bin/tests/test_streaming_api
${CMAKE_INSTALL_PREFIX}/bin/tests/t_state
Comprehensive Tests
Location: tests/test_traces/comprehensive/
Total: 34 tests organized in 9 tiers
Node count: 100
Purpose: Verify EASY backfilling scheduler correctness
Expected outputs generated by: Python reference implementation (scripts/python_reference_scheduler.py)
How to run a single test:
cd build
${CMAKE_INSTALL_PREFIX}/bin/simulator ../tests/test_traces/comprehensive/01_backfill_allowed.csv \
--total_nodes 100 \
--run_time_mode limit \
--outfile /tmp/output.csv
# Compare with expected
diff /tmp/output.csv ../tests/test_traces/comprehensive/01_backfill_allowed.expected_output.csv
Test artifacts for each test:
[test_name].csv- Input trace (job arrivals)[test_name].expected_output.csv- Expected job schedule[test_name].expected_resources.csv- Expected resource timeline[test_name].construction.md- Scenario description[test_name].answer.json- Test constraints (not validated by scripts)
Tier 1-2: FCFS & Basic Backfilling (10 tests)
Test |
Description |
Jobs |
Key Feature |
Artifacts |
|---|---|---|---|---|
|
Small job backfills while large job waits |
3 |
Basic backfill |
|
|
Job too long for backfill window |
3 |
Time constraint |
|
|
Job too large for available nodes |
3 |
Resource constraint |
|
|
Multiple small jobs compete for backfill |
4 |
FCFS among backfillers |
|
|
Two jobs backfill simultaneously |
4 |
Concurrent backfills |
|
|
Later job backfills before earlier |
4 |
Out-of-order execution |
|
|
Jobs arrive at same time |
3 |
Tie-breaking |
|
|
(no verified description) |
5 |
- |
|
|
(no verified description) |
2 |
- |
|
|
System goes idle between jobs |
3 |
Idle periods |
Tier 3-4: Event Timing & Competition (9 tests)
Tests where job completions and arrivals interact in complex ways.
Note: the descriptions below are inferred from each test’s name and job
count only - the earlier implementation of this table had invented, unverified
descriptions that didn’t match the actual trace files. Where a
.construction.md file exists, it’s linked as the authoritative
description; where it doesn’t, treat the name as the only claim being made.
Test |
Jobs |
Artifacts |
|---|---|---|
|
3 |
|
|
3 |
|
|
3 |
|
|
3 |
|
|
3 |
|
|
31 |
|
|
21 |
|
|
12 |
|
|
4 |
Tier 5: Fragmentation & Sustained Load (5 tests)
Test |
Jobs |
Artifacts |
|---|---|---|
|
4 |
|
|
50 |
|
|
20 |
|
|
30 |
|
|
4 |
Tier 6-9: Completion Interactions & Backfill Edge Cases (10 tests)
Jobs completing early (actual_run_time < time_limit) and other backfill
edge cases - see Run Time Mode Tests
above for the run_time_mode mechanics these traces exercise.
Test |
Jobs |
Artifacts |
|---|---|---|
|
3 |
|
|
4 |
|
|
4 |
|
|
6 |
|
|
5 |
|
|
3 |
|
|
4 |
|
|
4 |
|
|
6 |
|
|
4 |
Status: 34/34 passing
Unit Tests
Location: tests/test_traces/unit/
Total: 7 tests
Purpose: Basic I/O, trace format parsing, simple scenarios
How to run:
cd build
../tests/run_unit_tests.sh
Test |
Description |
Key Feature |
Status |
Artifacts |
|---|---|---|---|---|
|
2-job sequential execution |
Basic CSV parsing |
✅ Working |
|
|
3-job replay format |
Tab-separated format |
✅ Working |
|
|
Epoch timestamp format |
Unix timestamps |
✅ Working |
|
|
ISO 8601 timestamps |
ISO format parsing |
✅ Working |
|
|
Timezone handling |
Timezone conversion |
✅ Working |
|
|
3-job sequential |
Simple scenario |
✅ Working |
|
|
Basic 2-job test |
Format test |
✅ Working |
Status: 7/7 passing (fixed Aug 31, 2026 in commit e6da09c)
Known Issue: queue field must be pbatch or pbatch0-3. Other values (e.g., "batch") are silently dropped during load.
Feature Tests
Location: tests/test_traces/feature/
Total: 6 tests
Purpose: Compare different scheduling policies and modes
How to run:
cd build
../tests/run_feature_tests.sh
Test |
Description |
Comparison |
Status |
Artifacts |
|---|---|---|---|---|
|
Conservative vs EASY |
Backfill policies |
✅ Passing |
|
|
Millisecond output |
Whole-second timestamps |
✅ Passing |
|
|
Millisecond output |
Fractional timestamps |
✅ Passing |
|
|
Oversized-job rejection |
Rejected job is excluded from output |
✅ Passing |
|
|
Replay mode validation |
Replay vs simulation |
✅ Passing |
|
|
Long-running simulation |
Sustained load |
✅ Passing |
Status: 6/6 passing
Note: easy_vs_conservative_test.csv is run separately by
tests/test_easy_vs_conservative_correctness.sh; it validates against
algorithm-specific expected outputs rather than a shared oracle.
Scale Tests
Location: tests/test_traces/scale/
Total: 7 tests
Node count: 795 (not 100 - matches default in src/dr_evt_types.hpp)
Purpose: Large-scale performance testing
How to run:
cd build
${CMAKE_INSTALL_PREFIX}/bin/simulator ../tests/test_traces/scale/small_10jobs.csv \
--total_nodes 795 \
--run_time_mode limit \
--outfile /tmp/output.csv
# Compare with expected
diff /tmp/output.csv ../tests/test_traces/scale/small_10jobs.expected_output.csv
Test |
Jobs |
Status |
Description |
Artifacts |
|---|---|---|---|---|
|
10 |
✅ Passing |
Small baseline |
|
|
50 |
✅ Passing |
Medium scale |
|
|
100 |
✅ Passing |
Large scale |
|
|
200 |
✅ Passing |
Large scale |
|
|
500 |
✅ Passing |
Extra large scale |
|
|
2000 |
✅ Passing |
Huge scale |
|
|
10000 |
✅ Passing |
Huge scale |
Status: 7/7 passing
Generated via scripts/generators/generate_scale_expected_outputs.py.
Replay Tests
Location: tests/run_replay_tests.sh
Purpose: Verify replay mode reproduces resource usage from simulation
How it works:
Run simulation mode → generates job schedule + resource trace
Feed schedule back as replay mode input
Compare resource traces → must match exactly
How to run:
cd build
../tests/run_replay_tests.sh
Tests hardcoded in script:
comprehensive/01_backfill_allowed.csvcomprehensive/05_multiple_backfills.csvcomprehensive/13_consecutive_fcfs.csvcomprehensive/21_sustained_high_load.csv
Verified to pass on: All 34 comprehensive tests + 4 scale tests
Resource History Tests
Location: tests/run_resource_history_tests.sh
Purpose: Verify the resource-history circular buffer (--resource_history_capacity) produces identical output under forced reclaiming, that invalid input is rejected cleanly, and measure the wall-clock cost of repeated flushing at a too-small capacity
Note: unlike job-store’s m_data, resource-history genuinely accumulates incrementally during the run (not preloaded), and has no grow option - so a too-small capacity here causes real, repeated flush-and-discard for the whole run, not a one-time reallocation. Sufficient capacity is 2x the job count (each job contributes at most 2 events - start, end), not 1x.
How it works:
Run each input twice: once with the default (auto-sized) capacity, once with a small forced capacity (5) that triggers reclaiming on nearly every insert
Compare resource traces → must match exactly, for both
simulatorandtracerSeparately, feed
tracersimulation-format input directly (never valid forrun_job_trace(), which only replays begin_time/end_time that’s already set) → must fail cleanly with an actionable error, not crashBenchmark (informational, not pass/fail): wall-clock time for a tiny (5) vs. sufficient (2x job count) capacity on a 10,000-job trace - confirms repeated flushing is cheap and output-identical
How to run:
cd build
../tests/run_resource_history_tests.sh
Tests hardcoded in script:
feature/05_multiple_backfills.csv(simulator)feature/21_sustained_high_load.csv(simulator)feature/huge_2000jobs.csv(tracer, via simulator’s own replay-format output)feature/huge_2000jobs.csv(tracer, fed directly - misuse-rejection check)feature/huge_10000jobs.csv(flush-overhead benchmark)
Job Store Tests
Location: tests/run_job_store_tests.sh
Purpose: Verify the job-store circular buffer (Trace::m_data, --job_store_capacity) produces identical output and stats regardless of the requested initial capacity, correctly excludes rejected jobs without stalling, aborts cleanly when capacity can’t be satisfied, and measures the wall-clock cost of a too-small initial capacity
Batch mode note: loading a whole trace file (the only mode that exists today) sizes capacity to fit the entire trace before the run starts - a too-small --job_store_capacity triggers repeated reallocation during loading (falling back to growing), not repeated reclaiming during the run, which essentially can’t happen more than once in batch mode - see docs/dev/design-decisions/OUT_TRACE_STREAMING.md.
How it works:
Run each input twice: once with the default (already-sufficient) capacity, once with a tiny requested capacity (2) that forces several grow reallocations during loading
Compare job output and simulation stats → must match exactly
Separately, run a small trace at default capacity → confirm no job is reclaimed prematurely (guards against a real bug found during development: an earlier version reclaimed unconditionally whenever a job finished, rather than only when the buffer was actually full)
Feed a trace with one job that requests more nodes than exist → confirm it’s rejected, excluded from output/stats, and doesn’t stall completion of the jobs behind it
Force
--job_store_overflow abortwith a capacity too small to hold the trace even during loading → must fail cleanly with an actionable error, not crashBenchmark (informational, not pass/fail): wall-clock time for a tiny vs. sufficient initial capacity on a 10,000-job trace - confirms grow-during-load is cheap and output-identical
How to run:
cd build
../tests/run_job_store_tests.sh
Tests hardcoded in script:
feature/05_multiple_backfills.csvfeature/21_sustained_high_load.csvfeature/01_backfill_allowed.csv(default-capacity and abort checks)feature/rejected_job.csvfeature/huge_10000jobs.csv(grow-overhead benchmark)
Append-Job Tests
Location: tests/test_append_job_api.cpp (C++), tests/test_append_job_grpc.cpp (gRPC)
Purpose: Verify Trace::append_job()/Simulation::append_job() (single-job) and Trace::append_jobs()/Simulation::append_jobs() (batch) - the real streaming insertion points, for jobs the trace has never seen before - together with submit_job()/advance_to()’s general correctness (online scheduling loops, exclusive-vs-inclusive advance, resource-leak checks, idle-gap handling), all driven via append_job()/append_jobs() rather than a preloaded trace file. This file used to be two (a separate test_streaming_api.cpp covered the submit_job()/advance_to() half by loading a small, hand-written CSV first) - consolidated once it became clear none of those tests actually depended on a preloaded file (each one’s submit times were hand-written to match the CSV exactly, never diverging from it), so the same coverage is achievable via append_job() with no file needed at all.
How it works:
(C++) A trace with zero preloaded jobs (empty-of-rows CSV), then jobs appended one at a time via
append_job()and run to completion → output/stats must be correct(C++) Force
--job_store_capacity 1; confirm a secondappend_job()call reclaims the first (already-finished) job’s slot rather than growing - the actual point this ordering matters, unlikeload_data()(seedocs/dev/design-decisions/OUT_TRACE_STREAMING.md’s “Reclaim at the point of need” section for whyload_data()itself never needs this)(C++)
append_job()enforces the samesubmit_time >= current_timepreconditionsubmit_job()already does → must throw on a past submit_time(C++)
append_jobs()batch call - several jobs in one call, run to completion → output/stats must be correct(C++)
append_jobs()rejects a batch not sorted bysubmit_time, non-decreasing - and, being all-or-nothing on input validation, leavesm_datacompletely untouched(C++)
append_jobs()rejects a batch containing anysubmit_time < current_time- same all-or-nothing input validation, nothing from the batch is added(C++) Reclaim-before-grow (test 2’s guarantee) holds the same way within a batch call - each request goes through the same per-job order, not a single capacity computation for the whole batch upfront
(C++)
append_jobs()over an empty request vector is a valid no-op(C++) Basic append+submit+advance sequencing, exclusive-vs-inclusive
advance_to()/run_until_exclusive(), an online-scheduling loop that appends jobs only as they “arrive,” sequential resource-leak detection, andadvance_to()’s idle-gap postcondition across a long gap with no pending events(gRPC, if built with
-DDR_EVT_ENABLE_GRPC=ON) Same append scenario as (1), but over the actual network wire viaAppendJobRequest, against a real runningdr_evt_server, plus the batch case viaAppendJobsRequest(C++)
submit_job()recordsbusy_nodes(other jobs’ node occupancy at arrival) for a streaming-arrived job, same asload_data()’s own submission loop already does for a batch-loaded one(C++)
append_jobs()honors--check_memory_pressurethe same wayload_next_file()(progressive loading) does, since both shareensure_batch_capacity()- forced deterministically via theDR_EVT_TEST_AVAILABLE_MEMORY_BYTEStest seam
How to run:
cd build
../tests/run_append_job_tests.sh
Tests hardcoded in script:
feature/empty_trace.csv(gRPC test - header-only, zero data rows)
Progressive Loading Tests
Location: tests/test_progressive_load.cpp (C++), tests/run_progressive_load_tests.sh (both wraps the C++ binary and runs CLI-level checks)
Purpose: Verify --infile_list/Trace::load_next_file()/Simulation::run_progressive() - loading a trace as a sequence of separate, pre-sorted files instead of one big one, so --job_store_capacity can actually bound memory (single-file mode always grows to fit the whole trace regardless of this setting; see docs/dev/design-decisions/OUT_TRACE_STREAMING.md)
How it works:
(C++) The same 6 jobs split across 3 files vs. one combined file must produce byte-identical output - splitting the input shouldn’t change the schedule
(C++) With a small
--job_store_capacityand job durations short enough that earlier jobs finish before later files load, progressive mode’s peakm_datacapacity must stay well below single-file mode’s (confirmed as an honest baseline, not assumed - single-file mode always grows to fit the whole trace)(C++)
--job_store_overflow=abortmust throw cleanly, not crash, when a file’s jobs can’t fit even after reclaiming(C++) An empty file (header only) in the middle of the list must be skipped gracefully, not treated as an error
(C++) A later file whose earliest
submit_timeprecedes the previous file’s latest must be rejected (cross-file continuity)(C++) REPLAY-format input must be rejected outright for
--infile_list- there’s no scheduling decision for progressive loading to plug into for replay at all(C++)
--check_memory_pressuremust refuse to load the next file when doing so would push projected job-store usage past the configured fraction of actual available memory - forced deterministically via theDR_EVT_TEST_AVAILABLE_MEMORY_BYTEStest seam, not by depending on the test machine’s real memory state(C++) The same forced-low-memory condition must NOT affect a run that never enables
--check_memory_pressure- the check is opt-in, not a background limit(C++) With
--check_memory_pressureenabled but no artificially low memory forced, a normal run must still succeed - no false positive against real, plentiful memory(C++) The fraction itself is what’s compared against, not a fixed threshold hiding behind a configurable-looking argument - under the exact same forced available-memory condition, a tight fraction must refuse while a loose fraction succeeds
(CLI) The same split-vs-combined comparison as (1), but through the actual
simulator --infile_listinvocation, exercisingSim_Params::getopt()’s real parsing path(CLI) The same small-capacity run as (2), confirmed to still produce the correct schedule end-to-end
(CLI) An empty
--infile_listfile is rejected with a clear error(CLI)
--infile_listtogether with a positional trace-file argument is rejected (mutually exclusive)
How to run:
cd build
../tests/run_progressive_load_tests.sh
Tests hardcoded in script:
progressive/part1.csv,part2.csv,part3.csv,combined.csv,file_list.txt
Streaming API Tests
Location: tests/
Purpose: Test online job submission API
Test |
Language |
Description |
How to Run |
|---|---|---|---|
|
C++ |
Batch vs streaming equivalence |
|
|
Python |
Python bindings |
|
|
C++ (MPI) |
Parallel job submission |
|
Configuration Tests
Location: tests/test_configs/ (hand-crafted fixtures), tests/test_protobuf_config_doc_examples.py (documentation’s own examples)
Purpose: Verify protobuf config files match CLI options, and separately, that every example shown in docs/user-guide/protobuf-config.md actually parses and runs - not just the hand-verified fixtures below, which by construction can never expose a bug in the documentation’s own prose (this caught two real ones: every doc example wrapping fields in a fictional sim_setup { ... } block, and several “Run with” instructions omitting the required positional trace-file argument - see the script’s own docstring for the full story)
How to run:
cd build
cmake .. -DDR_EVT_ENABLE_PROTOBUF=ON
make
../tests/run_configs_tests.sh
Config files tested:
minimal_config.pbconservative_config.pbfull_config.pbdistribution_config.pbresource_trace_config.pbinfile_list_config.pb(progressive loading via a protobuf config, not just the CLI)memory_pressure_config.pb(memory_pressure_fractionvia a protobuf config, forced to trigger via theDR_EVT_TEST_AVAILABLE_MEMORY_BYTEStest seam)
Queue Implementation Testing
Location: tests/test_traces/comprehensive/ (same 34 traces as the Comprehensive Tests above)
Purpose: Verify all four FCFS wait-queue data structure implementations
(--queue_impl circular/deque/multimap/block) produce byte-for-byte identical
output to each other
This checks something different from the Python-reference comparison the rest of this guide describes: not whether the C++ simulator’s scheduling decisions are correct, but whether swapping the underlying queue data structure changes the result. It won’t catch a bug shared by all four implementations, but it will catch a bug introduced in one specific implementation while the others stay correct - something the Python-reference comparison above wouldn’t specifically localize.
circular is the default (--queue_impl unset uses it); deque,
multimap, and block all still need to be requested explicitly.
How to run:
cd build
../tests/test_fcfs_comprehensive.sh --correctness
Performance comparison (not correctness - separate script):
../tests/benchmark_block_sizes.sh
Runs in CI (.github/workflows/tests.yml, “Run Queue Implementation
Differential Tests”). See
dev/design-decisions/CIRCULAR_QUEUE.md
for the default implementation and
dev/design-decisions/BLOCK_QUEUE.md
for the block-based one.
Column Alias Tests
Location: tests/test_column_aliases.sh (generates its own small traces under /tmp/)
Purpose: Verify time_limit and actual_run_time are each recognized
under multiple accepted column-name aliases, so an existing trace can be
reused without editing its header - slow to do by hand on a large file.
time_limitaccepts:time_limit,timelimit,walltimeactual_run_timeaccepts:actual_run_time,duration,actual_duration,run_time
8 checks: one per alias (confirming both that it’s recognized and that it
drives the correct execution time), plus one confirming a trace missing
time_limit under all of its aliases is rejected with a clear error
rather than silently defaulting.
How to run:
cd build
../tests/test_column_aliases.sh
Runs in CI (.github/workflows/tests.yml, “Run Column Alias Tests”). See
user-guide/trace-formats.md
for the full column reference.
Run Time Mode Tests
Location: tests/test_run_time_modes.sh
Purpose: Verify two specific behavioral contracts not exercised by any
other test suite:
Replay mode uses the trace’s own real, historical begin/end times - confirmed using
test_traces/comprehensive/25_early_completion_basic.csv, wheretime_limitandactual_run_timegenuinely differ (200s vs 50s).run_time_mode=distribution’snormal/lognormalsamples are capped attime_limit- a real HPC scheduler kills a job at its stated limit, so the simulator must respect the same constraint. Checked against 10,000 jobs for a statistically reliable result.
How to run:
cd build
../tests/test_run_time_modes.sh
Runs in CI (.github/workflows/tests.yml, “Run Duration/Run Time Mode
Tests”). See reference/terminology.md for
Test Summary
Category |
Total |
Passing |
Broken |
Purpose |
|---|---|---|---|---|
Comprehensive |
34 |
34 |
0 |
Scheduler correctness |
Unit |
7 |
7 |
0 |
Basic I/O & formats |
Feature |
6 |
6 |
0 |
Policy comparisons, output formats, and rejection handling |
Conservative |
2 |
2 |
0 |
CONSERVATIVE backfilling |
Scale |
7 |
7 |
0 |
Performance testing |
Replay |
4 |
4 |
0 |
Resource verification |
Resource History |
5 |
5 |
0 |
Resource-history circular buffer, flush overhead |
Job Store |
6 |
6 |
0 |
Job-record circular buffer, capacity sizing |
Append-Job |
17 |
17 |
0 |
Streaming insertion (append_job/append_jobs) + submit_job()/advance_to() |
Progressive Loading |
14 |
14 |
0 |
–infile_list, bounding job-store memory across a multi-file trace |
Streaming |
4 |
4 |
0 |
Online API |
Config |
7 |
7 |
0 |
Protobuf validation |
Queue Impl |
34 |
34 |
0 |
Wait-queue data structure consistency (circular/deque/multimap/block) |
Column Aliases |
8 |
8 |
0 |
time_limit/actual_run_time accepted column-name variants |
TOTAL |
158+ |
158+ |
0 |
Complete test suite |
All tests passing as of Sept 3, 2026
Test File Formats
Input Trace (.csv)
job_submit_time,num_nodes,exit_status,queue,time_limit[,actual_run_time]
0,70,0,pbatch,200
10,50,0,pbatch,300
20,20,0,pbatch,50
actual_run_timeoptional (defaults totime_limit); used for early completion tests. Also accepted under the namesduration,actual_duration, orrun_time;time_limitis also accepted undertimelimitorwalltime- useful for reusing an existing trace without editing its header.queuemust bepbatch(orpbatch0-pbatch3) - other values are silently dropped during load (seesrc/trace/job_io.cpp), producing zero loaded jobs with no error. This is not a cosmetic requirement.
Expected Job Output (.expected_output.csv)
job_id,start_time,end_time
0,0,200
1,200,500
2,20,70
job_id corresponds to input row order (0-indexed), which is safe even
when multiple jobs share a submit time: the simulator internally
stable-sorts by submit time at load, and all comprehensive//scale/
input files are already submit-time-sorted on disk, so this sort is a
no-op and row order is preserved.
Expected Resource Timeline (.expected_resources.csv)
Two conventions exist in this repo - check which one a given test uses before writing tooling against it:
time,nodes_used,nodes_free,running_jobs
0,70,30,0
20,90,10,"0,2"
(comprehensive/, scale/ - via the Python reference generators)
time,free_nodes,allocated_nodes
0,30,70
20,10,90
(the C++ simulator’s own --resource_trace output - note the swapped
column order and no running_jobs column)
Answer File (.answer.json)
{
"test_id": "01_backfill_allowed",
"exact_schedule": [[0,0,200], [1,200,500], [2,20,70]],
"constraints": [...]
}
exact_schedule: null marks tests where multiple schedules could be
valid (e.g. 21_sustained_high_load, deliberately dense/high-contention
scenarios). No script in this repo currently validates these
.answer.json constraint files - they document intent but aren’t
enforced. Don’t assume a test with a well-formed .answer.json is
actually being checked against it.
Generating Expected Outputs
All expected outputs are generated by the Python reference implementation:
# Comprehensive tests (34 tests)
python scripts/generators/generate_all_expected_outputs.py
# Scale tests (7 tests)
python scripts/generators/generate_scale_expected_outputs.py
Python reference: scripts/python_reference_scheduler.py
Verification: 34/34 comprehensive tests match Python reference
Adding New Tests
Create the input trace under the appropriate directory (
comprehensive/,scale/,unit/, orfeature/), usingpbatchas the queue name.Run the relevant generator script (
generate_all_expected_outputs.pyorgenerate_scale_expected_outputs.py) to produce.expected_output.csv/.expected_resources.csv. Don’t hand-write these - see the Overview above for why that would just duplicate the Python reference’s own computation, badly.For a genuinely independent check on a small test (2-5 jobs), trace the EASY algorithm by hand yourself and compare - this is the only way to get real independent verification in this suite, and it doesn’t scale to larger tests. If reference implementation passes all of such verifications, the comparison against the reference becomes more useful.
If adding to
comprehensive/, add the test name to theTESTSlist ingenerate_all_expected_outputs.py; same forscale/and its generator.
Common Test Patterns
3-Job Backfill Pattern
Job 0: Running (R0 nodes, duration D0)
Job 1: FCFS head, CANNOT fit (R1 > free)
Job 2: Backfiller, CAN fit (R2 <= free)
Question: Can Job 2 backfill? Check: does Job 2 complete strictly
before Job 1’s reservation time? (Strict <, not <= - resources don’t
free up instantly when a job completes; the completion event has to be
processed first.)
Multiple Running Jobs Pattern
Job 0: R0 nodes, ends at T0
Job 1: R1 nodes, ends at T1 > T0
Job 2: FCFS head, needs more than either alone frees
Reservation is whichever completion time first provides enough cumulative freed capacity - not necessarily T0 or T1 alone.
Early Completion Pattern
Job 0: time_limit=200, actual_run_time=50
Job 1: FCFS head, reservation calculated using 200 (the estimate), not 50
Result: Job 1’s reservation is pessimistic (based on the estimate), but Job 0 actually frees resources at 50 - so anything backfilling against Job 0’s completion benefits from the earlier, actual time once it happens.
Troubleshooting
Test Fails: Schedule Mismatch
Read the test’s
construction.md(if present) to understand the intended scenario - but verify it against the actual current behavior first; at least one (29_large_completion_multiple_backfills) has a stale narrative that doesn’t match its own correct, validated schedule.Manually trace through the EASY algorithm’s rules for the specific jobs involved.
Check whether the expected file might be the one that’s wrong, especially for
scale/-style tests that lack a generator history - this has happened before (seetests/test_traces/README.md).
All Tests Pass but Behavior Seems Wrong
Passing tests here means “matches the Python reference,” not “definitely correct” - see the Overview. If you suspect a specific scenario is mishandled, trace it by hand rather than trusting a passing test to rule it out.
Conservative Backfilling Tests
DR_EVT implements both EASY and CONSERVATIVE backfilling algorithms. Conservative backfilling provides stronger fairness guarantees by protecting ALL waiting jobs’ reservations, not just the first job.
Algorithm Comparison
Aspect |
EASY |
CONSERVATIVE |
|---|---|---|
Reservation |
Only first job in queue |
ALL jobs in queue |
Backfill Check |
Complete before first job’s reservation? |
Complete before ANY job’s reservation? |
Complexity |
O(n) per scheduling event |
O(n²) per scheduling event |
Fairness |
Lower - deep queue jobs can be delayed |
Higher - all jobs protected |
Utilization |
Higher (~95% on 2000-job test) |
Lower (~87% on 2000-job test) |
Makespan |
Shorter |
Longer (+9% on 2000-job test) |
Performance |
Fast (0.05s for 2000 jobs) |
Slow (144s for 2000 jobs, 2715x slower) |
Test 1: Behavioral Correctness (EASY vs CONSERVATIVE)
Purpose: Demonstrates the key behavioral difference between algorithms
Script: tests/test_easy_vs_conservative_correctness.sh
Duration: ~1 second
Jobs: 6 jobs, 100 nodes
Key Scenario:
Running at t=0:
Job 0: 60 nodes, ends at t=50
Job 1: 20 nodes, ends at t=100
Free: 20 nodes
Waiting queue (FCFS order):
Job 2: 50 nodes, duration 200 → reservation at t=50
Job 3: 30 nodes, duration 50 → reservation at t=50
Job 4: 15 nodes, duration 100 → reservation at t=0 (could start now!)
Job 5: 10 nodes, duration 40 → BACKFILL CANDIDATE
Expected Behavior:
EASY: Job 5 backfills at t=0 (only checks Job 2’s reservation, 40 < 50 ✓)
CONSERVATIVE: Job 5 delayed to t=100 (checks Job 4’s reservation, 40 > 0 ✗)
Job 4 could start immediately but is behind Jobs 2-3 in the queue. EASY ignores Job 4’s reservation and backfills Job 5, delaying Job 4. CONSERVATIVE protects Job 4’s reservation and rejects Job 5.
Run:
./tests/test_easy_vs_conservative_correctness.sh
Success Criteria:
✅ Job 5 starts at t=0 with EASY
✅ Job 5 starts at t=100 with CONSERVATIVE
✅ All job schedules match expected output
✅ Resource traces match expected utilization
Test 2: Implementation Equivalence (C++ vs Python)
Purpose: Verifies C++ and Python CONSERVATIVE implementations produce identical schedules
Script: tests/compare_cpp_python_conservative.sh
Duration: ~8 minutes
Jobs: 2000 jobs, 400 nodes (default)
What it tests:
Both implementations follow the same algorithm
Schedules match exactly (start/end times for all jobs)
No divergence on complex, long-running simulations
Performance comparison (C++ vs Python)
Run:
# Default: 2000 jobs, 400 nodes
./tests/compare_cpp_python_conservative.sh
# Custom trace and node count
./tests/compare_cpp_python_conservative.sh <trace.csv> <nodes> <output_dir>
Success Criteria:
✅ Same number of jobs completed
✅ All job start times match (within 0.001s)
✅ All job end times match (within 0.001s)
✅ Makespan is identical
Verified Results (2000 jobs, 400 nodes):
Jobs compared: 2000
Mismatches: 0
Python time: 504s (8m24s)
C++ time: 143.9s (2m24s)
Speedup: 3.5x
Makespan: 993,969 seconds (identical)
Utilization: 87.13% (identical)
Conservative Backfilling Implementation
C++ Implementation:
Location:
src/sim/scheduler_fcfs_conservative.cppKey method:
try_backfill()Complexity: O(n²) - calculates reservations for all waiting jobs
Optimization: Uses pre-built resource timeline
Queue: Currently only
std::dequeimplementation (circular buffer variant not yet implemented for ~10-15% potential speedup)
Python Reference:
Location:
scripts/python_conservative_scheduler.pyKey method:
try_start_jobs()Purpose: Reference implementation for verification
Note: ~3.5x slower than C++ due to interpreter overhead
Critical Implementation Details: Both implementations must:
Track effective running jobs (actual running + backfilled this cycle)
Calculate reservations using effective running state
Exclude already-backfilled jobs from window calculation
Update effective running as jobs backfill within the same scheduling event
Test Data Files
Behavioral test:
easy_vs_conservative_test.csv - Input trace (6 jobs)
easy_vs_conservative_expected_easy.csv - Expected EASY schedule
easy_vs_conservative_expected_conservative.csv - Expected CONSERVATIVE schedule
easy_vs_conservative_expected_easy_resources.csv - Expected EASY resources
easy_vs_conservative_expected_conservative_resources.csv - Expected CONSERVATIVE resources
Equivalence test:
huge_2000jobs.csv - 2000 job trace
large_100jobs.csv - 100 job trace
small_10jobs.csv - 10 job trace
References
Algorithm: BACKFILLING_ALGORITHMS.md (EASY and CONSERVATIVE)
Python Reference: scripts/python_reference_scheduler.py
Python Conservative Reference: scripts/python_conservative_scheduler.py
Test overview: tests/README.md
Test trace details: tests/test_traces/README.md
Queue implementation differential testing: dev/design-decisions/CIRCULAR_QUEUE.md (default), dev/design-decisions/BLOCK_QUEUE.md
Key Principles
No independent ground truth: expected outputs come from the Python reference implementation, not from first-principles calculation.
Passing tests demonstrate consistency, not correctness: a shared bug between implementations would still pass.
Hand-verification is only practical for small tests: use it when you need actual confidence beyond “matches the reference.”
Some scenarios genuinely have no unique correct schedule: see
exact_schedule: nullin.answer.jsonfiles.Not everything documented here is validated by running code: the
.answer.jsonconstraints and the config tests both need protobuf/ manual verification this document didn’t perform - check before assuming.