diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97374155..df1b4e04 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,13 +5,14 @@ set(rscore_srcs arch/mem.c arch/thread.c core/core.c - init.c core/sync.c + core/output.c datatypes/msg_queue.c distributed/control_msg.c gvt/fossil.c gvt/gvt.c gvt/termination.c + init.c log/file.c log/log.c log/stats.c diff --git a/src/ROOT-Sim.h b/src/ROOT-Sim.h index 8b861e55..6fe08231 100644 --- a/src/ROOT-Sim.h +++ b/src/ROOT-Sim.h @@ -66,6 +66,23 @@ typedef void (*ProcessEvent_t)(lp_id_t me, simtime_t now, unsigned event_type, c */ typedef bool (*CanEnd_t)(lp_id_t me, const void *snapshot); +/** + * @brief Perform output operations. + * @param me The logical process ID of the LP performing the output + * @param output_type Numerical output type + * @param output_content The output content + * @param output_size The size (in bytes) of the output content + * + * This function is called by the simulation kernel whenever an event is committed, during the handling of which + * an output operation had been scheduled by the simulation model using the ScheduleOutput() function. + * This function will receive the same data that was passed to the ScheduleOutput() function. + * + * @warning The memory pointed by output_content is not a copy, and is owned by the simulation kernel. It will be + * freed by the simulation kernel after the function returns. + * @warning The function shall not perform any memory-managed allocation (e.g. using rs_malloc). + */ +typedef void (*OutputCallback_t)(lp_id_t me, unsigned output_type, const void *output_content, unsigned output_size); + /// @brief Internal event types used by the simulation kernel. /// /// These event types are automatically scheduled to the model according to the following logic: @@ -91,6 +108,19 @@ extern void ScheduleNewEvent(lp_id_t receiver, simtime_t timestamp, unsigned eve extern void SetState(void *new_state); +/** + * @brief API to schedule a new output event to execute once the event being currently executed is committed + * + * This function makes a copy of the data in @p output_content , which stays owned by the caller. + * + * @param output_type Numerical output type to be passed to the output handling function + * @param output_content The output content + * @param output_size The size (in bytes) of the output content + * + * @warning This function shall not be called during the handling of LP_FINI events, as it will produce no output. + */ +extern void ScheduleOutput(unsigned output_type, const void *output_content, unsigned output_size); + /** * @brief Allocates rollbackable memory * @@ -206,6 +236,8 @@ struct simulation_configuration { ProcessEvent_t dispatcher; /// Function pointer to the termination detection function CanEnd_t committed; + /// Function pointer to the output handling function + OutputCallback_t output_callback; }; extern int RootsimInit(const struct simulation_configuration *conf); diff --git a/src/core/output.c b/src/core/output.c new file mode 100644 index 00000000..be965e6f --- /dev/null +++ b/src/core/output.c @@ -0,0 +1,88 @@ +/** + * @file core/output.c + * + * @brief Committed output management functions + * + * This module implements the facilities for committed output + * + * SPDX-FileCopyrightText: 2008-2023 HPDCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include +#include +#include +#include +#include +#include + +void ScheduleOutput(unsigned output_type, const void *output_content, unsigned output_size) +{ + if(unlikely(silent_processing)) + return; + + if(unlikely(global_config.synchronization == SERIAL)) { + global_config.output_callback(current_msg->dest, output_type, output_content, output_size); + return; + } + + char *content = mm_alloc(output_size); + memcpy(content, output_content, output_size); + + struct output_data data = {.type = output_type, .content = content, .size = output_size}; + + output_array_t *outputs = current_msg->outputs; + if(!outputs) { + outputs = mm_alloc(sizeof(output_array_t)); + array_init(*outputs); + current_msg->outputs = outputs; + } + + array_push(*outputs, data); +} + +void execute_outputs(struct lp_msg *msg) +{ + output_array_t *outputs = msg->outputs; + if(!outputs) + return; + + for(array_count_t i = 0; i < array_count(*outputs); ++i) { + struct output_data data = array_get_at(*outputs, i); + global_config.output_callback(msg->dest, data.type, data.content, data.size); + mm_free(data.content); + } + + /* Use array_clear() rather than array_fini(): the message buffer remains alive in the + * system and will eventually reach free_msg_outputs(), which performs the final + * array_fini() + mm_free(). When called right before msg_allocator_free() (e.g. from + * fossil_lp_collect()), the array_clear() is redundant but harmless. */ + array_clear(*outputs); +} + +void free_msg_outputs(output_array_t *output_array) +{ + if(!output_array) + return; + + for(array_count_t i = 0; i < array_count(*output_array); ++i) { + struct output_data data = array_get_at(*output_array, i); + mm_free(data.content); + } + + array_fini(*output_array); + mm_free(output_array); +} + +void committed_output_on_rollback(struct lp_msg *msg) +{ + output_array_t *outputs = msg->outputs; + if(!outputs) + return; + + for(array_count_t i = 0; i < array_count(*outputs); ++i) { + struct output_data data = array_get_at(*outputs, i); + mm_free(data.content); + } + + array_clear(*outputs); +} diff --git a/src/core/output.h b/src/core/output.h new file mode 100644 index 00000000..5c6fa97c --- /dev/null +++ b/src/core/output.h @@ -0,0 +1,45 @@ +/** + * @file core/output.h + * + * @brief Committed output management functions + * + * Committed output management functions + * + * SPDX-FileCopyrightText: 2008-2023 HPDCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#pragma once + +#include + +struct output_data { + unsigned type; + void *content; + unsigned size; +}; + +typedef array_declare(struct output_data) output_array_t; + +/** + * @brief Free the outputs stored for later from a message + * @param output_array the output_data from the message + */ +void free_msg_outputs(output_array_t *output_array); + +struct lp_msg; + +/** + * @brief Invoke the output callback on all outputs stored in the message. + * @param msg the message to execute outputs from + */ +void execute_outputs(struct lp_msg *msg); + +/** + * @brief Handle rolling-back of a message + * @param msg the rolled-back + * + * This function is called when a message is rolled back. It frees the output data + * stored in the message, as they are no longer valid. + * The output array is not freed, as it can still be used in the future. + */ +void committed_output_on_rollback(struct lp_msg *msg); diff --git a/src/gvt/fossil.c b/src/gvt/fossil.c index a2df2bfb..dd5fae34 100644 --- a/src/gvt/fossil.c +++ b/src/gvt/fossil.c @@ -10,6 +10,7 @@ #include #include +#include _Thread_local unsigned fossil_epoch_current; /// The value of the last GVT, kept here for easier fossil collection operations @@ -50,12 +51,14 @@ void fossil_lp_collect(struct lp_ctx *lp) past_i = model_allocator_fossil_lp_collect(&lp->mm_state, past_i + 1); - for(array_count_t k = past_i; k;) { - struct pes_entry e = array_get_at(proc_ctx->pes, --k); + for(array_count_t k = 0; k < past_i; ++k) { + struct pes_entry e = array_get_at(proc_ctx->pes, k); + if(pes_entry_is_sent_local(e)) continue; struct lp_msg *m = pes_entry_msg(e); + execute_outputs(m); msg_allocator_free(m); } array_truncate_first(proc_ctx->pes, past_i); diff --git a/src/lp/common.c b/src/lp/common.c index deda14ef..eca109fa 100644 --- a/src/lp/common.c +++ b/src/lp/common.c @@ -1,21 +1,19 @@ /** -* @file lp/common.c -* -* @brief Common LP and message functionalities -* -* SPDX-FileCopyrightText: 2008-2025 HPCS Group -* SPDX-License-Identifier: GPL-3.0-only -*/ + * @file lp/common.c + * + * @brief Common LP and message functionalities + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ #include #include -#ifndef NDEBUG -_Thread_local const struct lp_msg *current_msg; -#endif +_Thread_local struct lp_msg *current_msg; -void ScheduleNewEvent(const lp_id_t receiver, const simtime_t timestamp, const unsigned event_type, - const void *payload, const unsigned payload_size) +void ScheduleNewEvent(const lp_id_t receiver, const simtime_t timestamp, const unsigned event_type, const void *payload, + const unsigned payload_size) { #ifndef NDEBUG if(unlikely(event_type >= LP_INIT)) { diff --git a/src/lp/common.h b/src/lp/common.h index eba7e01f..9ac32987 100644 --- a/src/lp/common.h +++ b/src/lp/common.h @@ -15,11 +15,8 @@ #include #include -#ifndef NDEBUG /// The currently processed message -/** This is not necessary for normal operation, but it's useful in debug */ -extern __thread const struct lp_msg *current_msg; -#endif +extern _Thread_local struct lp_msg *current_msg; /** * @brief Process a message for a given LP (Logical Process) @@ -30,16 +27,12 @@ extern __thread const struct lp_msg *current_msg; * @param lp A pointer to the LP associated with the message. * @param msg A pointer to the message to be processed. */ -static inline void common_msg_process(const struct lp_ctx *lp, const struct lp_msg *msg) +static inline void common_msg_process(const struct lp_ctx *lp, struct lp_msg *msg) { timer_uint t = timer_hr_new(); -#ifndef NDEBUG current_msg = msg; -#endif global_config.dispatcher(msg->dest, msg->dest_t, msg->m_type, msg->pl, msg->pl_size, lp->state_pointer); -#ifndef NDEBUG current_msg = NULL; -#endif stats_take(STATS_MSG_PROCESSED_TIME, timer_hr_value(t)); stats_take(STATS_MSG_PROCESSED, 1); } diff --git a/src/lp/msg.h b/src/lp/msg.h index 63e0d3a4..9bdcd1fe 100644 --- a/src/lp/msg.h +++ b/src/lp/msg.h @@ -11,6 +11,7 @@ #pragma once #include +#include #include #include @@ -59,6 +60,8 @@ struct lp_msg { /// The next element in the message list (used in the message queue) struct lp_msg *next; + /// Data for committed output — kept in the preamble so it is not transmitted over MPI + output_array_t *outputs; /// The id of the recipient LP lp_id_t dest; /// The intended destination logical time of this message diff --git a/src/lp/process.c b/src/lp/process.c index 817b9b42..dc10133f 100644 --- a/src/lp/process.c +++ b/src/lp/process.c @@ -21,10 +21,9 @@ #include #include #include -#include /// The flag used in ScheduleNewEvent() to keep track of silent execution -static _Thread_local bool silent_processing = false; +_Thread_local bool silent_processing = false; /** * @brief Schedule a new event. Parallel (Time Warp) version. @@ -96,6 +95,8 @@ void process_lp_fini(struct lp_ctx *lp) if(pes_entry_is_sent_local(e)) continue; + execute_outputs(pes_entry_msg(e)); + if(pes_entry_is_sent_remote(e) || !(atomic_load_explicit(&pes_entry_msg_received(e)->flags, memory_order_relaxed) & MSG_FLAG_ANTI)) msg_allocator_free(pes_entry_msg(e)); @@ -166,6 +167,7 @@ static inline void send_anti_messages(struct process_ctx *msg_processing, const struct lp_msg *msg = pes_entry_msg_received(e); const uint64_t f = atomic_fetch_add_explicit(&msg->flags, -MSG_FLAG_PROCESSED, memory_order_relaxed); + committed_output_on_rollback(msg); if(!(f & MSG_FLAG_ANTI)) msg_queue_insert_self(msg); diff --git a/src/lp/process.h b/src/lp/process.h index 9570cd66..8195e8e9 100644 --- a/src/lp/process.h +++ b/src/lp/process.h @@ -97,6 +97,8 @@ struct process_ctx { struct lp_ctx; // forward declaration +extern _Thread_local bool silent_processing; + extern void process_lp_init(struct lp_ctx *lp); extern void process_lp_fini(struct lp_ctx *lp); diff --git a/src/mm/msg_allocator.c b/src/mm/msg_allocator.c index 8bdf0c05..b6fa8b9d 100644 --- a/src/mm/msg_allocator.c +++ b/src/mm/msg_allocator.c @@ -67,6 +67,7 @@ struct lp_msg *msg_allocator_alloc(const unsigned payload_size) ret = array_pop(free_list); } ret->pl_size = payload_size; + ret->outputs = NULL; return ret; } @@ -76,6 +77,7 @@ struct lp_msg *msg_allocator_alloc(const unsigned payload_size) */ void msg_allocator_free(struct lp_msg *msg) { + free_msg_outputs(msg->outputs); if(likely(msg->pl_size <= MSG_PAYLOAD_BASE_SIZE)) array_push(free_list, msg); else diff --git a/src/serial/serial.c b/src/serial/serial.c index c3487a30..6cf31089 100644 --- a/src/serial/serial.c +++ b/src/serial/serial.c @@ -82,7 +82,7 @@ static int serial_simulation_run(void) lp_id_t to_terminate = global_config.lps; while(likely(!heap_is_empty(queue))) { - const struct lp_msg *msg = heap_min(queue); + struct lp_msg *msg = heap_min(queue); struct lp_ctx *lp = &lps[msg->dest]; current_lp = lp; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ac3ad43..410727cc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -45,3 +45,5 @@ test_program(correctness_parallel integration/correctness/timewarp.c integration test_program_link_libraries(correctness_parallel rscore) test_program(phold integration/phold.c) test_program_link_libraries(phold rscore) +test_program(committed_output core/output.c) +test_program_link_libraries(committed_output rscore) diff --git a/test/core/output.c b/test/core/output.c new file mode 100644 index 00000000..7b77986f --- /dev/null +++ b/test/core/output.c @@ -0,0 +1,449 @@ +/** + * @file test/core/output.c + * + * @brief Test: committed output correctness + * + * This file contains two test cases for the committed output mechanism: + * + * 1. **Time Warp (parallel) test**: Uses two LPs where LP 1 busy-waits until + * LP 0 has processed an event, then sends a straggler that causes LP 0 to + * roll back. The busy-waiting plus core_binding=true force a deterministic + * interleaving, yielding a fixed and verifiable committed output sequence. + * Both the exact output content (per-entry) and the total output count are + * checked; the latter catches any phantom outputs from rolled-back events. + * + * 2. **Serial test**: Verifies that ScheduleOutput() fires the output callback + * immediately and in causal order during a fully sequential simulation. + * + * @note Run with AddressSanitizer (-fsanitize=address) to detect memory leaks + * from the output content buffers. + * + * SPDX-FileCopyrightText: 2008-2025 HPDCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include +#include + +#include +#include +#include +#include +#include +#include + +/* ========================================================================= + * Shared event/output type constants and payload struct + * ========================================================================= */ + +#define EVENT 1 +#define STRAGGLER_EVENT 2 + +#define OUTPUT_TYPE 0 +#define OUTPUT_FROM_STRAGGLER 1 + +#define OUT_SLOTS 100 +#define OUT_SZ 64 + +struct output_data { + lp_id_t id; + unsigned long count; +}; + +/* ========================================================================= + * Time Warp (parallel) test + * ========================================================================= */ + +#define TW_NUM_LPS 2 +#define TW_NUM_THREADS 2 + +struct lp_state { + unsigned long count; +}; + +#define lp0_max_count 20 +static simtime_t lp0_times[lp0_max_count] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; +#define lp1_max_count 10 +static simtime_t lp1_times[lp1_max_count] = {1.5, 3.5, 5.5, 7.5, 9.5, 11.5, 13.5, 15.5, 17.5, 19.5}; +static simtime_t lp1_send_delay = 0.2; + +/* Atomic flag used to coordinate the deterministic interleaving between the + * two LPs. lp1_turn==true means LP 0 is paused and LP 1 may proceed. */ +static atomic_bool lp1_turn = false; + +/* Output storage for the Time Warp test */ +static char tw_lp_outs[TW_NUM_LPS][OUT_SLOTS][OUT_SZ]; +static size_t tw_lp_outs_count[TW_NUM_LPS]; +/* Total number of callback invocations, used to detect phantom rollback outputs */ +static atomic_size_t tw_total_output_count; + +static void TW_PerformOutput(lp_id_t me, unsigned output_type, const void *output_content, unsigned output_size) +{ + /* Verify the size is always exactly what was scheduled */ + if(output_size != sizeof(struct output_data)) { + fprintf(stderr, "PerformOutput: unexpected output_size %u (expected %zu)\n", output_size, + sizeof(struct output_data)); + test_fail(); + } + + /* Bounds guard: unexpected extra outputs would silently corrupt memory otherwise */ + size_t idx = tw_lp_outs_count[me]; + if(idx >= OUT_SLOTS) { + fprintf(stderr, "PerformOutput: too many outputs for LP %llu (>= %d)\n", (unsigned long long)me, + OUT_SLOTS); + test_fail(); + } + + const struct output_data *data = output_content; + lp_id_t id = data->id; + unsigned long count = data->count; + + switch(output_type) { + case OUTPUT_TYPE: + snprintf(tw_lp_outs[me][idx], OUT_SZ, "N%llu,%llu,%lu", (unsigned long long)me, + (unsigned long long)id, count); + break; + case OUTPUT_FROM_STRAGGLER: + snprintf(tw_lp_outs[me][idx], OUT_SZ, "S%llu,%llu,%lu", (unsigned long long)me, + (unsigned long long)id, count); + break; + default: + fprintf(stderr, "PerformOutput: unknown output_type %u\n", output_type); + test_fail(); + } + + tw_lp_outs_count[me]++; + atomic_fetch_add_explicit(&tw_total_output_count, 1, memory_order_relaxed); +} + +static void TW_Handler0(lp_id_t me, simtime_t now, unsigned event_type, const void *content, unsigned size, void *s) +{ + (void)now; + (void)content; + (void)size; + + struct lp_state *state = (struct lp_state *)s; + + if(state->count >= lp0_max_count || event_type == LP_FINI) { + atomic_store(&lp1_turn, true); + return; + } + + while(atomic_load(&lp1_turn)) { + /* Spin until LP 1 has scheduled the straggler for this round */ + } + + if(event_type == EVENT) { + ScheduleOutput(OUTPUT_TYPE, &(struct output_data){.id = me, .count = state->count}, + sizeof(struct output_data)); + ScheduleNewEvent(0, lp0_times[state->count], EVENT, NULL, 0); + state->count++; + } else if(event_type == STRAGGLER_EVENT) { + ScheduleOutput(OUTPUT_FROM_STRAGGLER, &(struct output_data){.id = me, .count = state->count}, + sizeof(struct output_data)); + } +} + +static void TW_Handler1(lp_id_t me, simtime_t now, unsigned event_type, const void *content, unsigned size, void *s) +{ + (void)content; + (void)size; + + struct lp_state *state = s; + + if(state->count >= lp1_max_count || event_type == LP_FINI) + return; + + while(!atomic_load(&lp1_turn)) { + /* Spin until LP 0 has finished its current event */ + } + + if(event_type == EVENT) { + ScheduleNewEvent(1, lp1_times[state->count], EVENT, NULL, 0); + ScheduleNewEvent(0, now + lp1_send_delay, STRAGGLER_EVENT, NULL, 0); + ScheduleOutput(OUTPUT_TYPE, &(struct output_data){.id = me, .count = state->count}, + sizeof(struct output_data)); + state->count++; + } + + atomic_store(&lp1_turn, false); +} + +static void TW_ProcessEvent(lp_id_t me, simtime_t now, unsigned event_type, const void *content, unsigned size, void *s) +{ + struct lp_state *state = s; + + if(event_type != LP_FINI && now > 25.0) + return; + + if(event_type == LP_INIT) { + state = rs_malloc(sizeof(*state)); + if(state == NULL) + abort(); + SetState(state); + state->count = 0; + ScheduleNewEvent(me, 0, EVENT, NULL, 0); + return; + } + + if(me == 0) + TW_Handler0(me, now, event_type, content, size, state); + else if(me == 1) + TW_Handler1(me, now, event_type, content, size, state); + else { + fprintf(stderr, "TW_ProcessEvent: unknown LP ID %llu\n", (unsigned long long)me); + abort(); + } +} + +static bool TW_CanEnd(lp_id_t me, const void *snapshot) +{ + (void)me; + (void)snapshot; + return false; +} + +static struct simulation_configuration tw_conf = { + .lps = TW_NUM_LPS, + .n_threads = TW_NUM_THREADS, + .termination_time = 25, + .gvt_period = 10, + .log_level = LOG_WARN, + .stats_file = "output_test_tw", + .ckpt_interval = 0, + .core_binding = true, + .synchronization = TIME_WARP, + .dispatcher = TW_ProcessEvent, + .committed = TW_CanEnd, + .output_callback = TW_PerformOutput, +}; + +static bool chk_out(const char *actual, const char *expected, const char *file, int line) +{ + if(strcmp(actual, expected) != 0) { + fprintf(stderr, "%s:%d: output mismatch: got \"%s\", expected \"%s\"\n", file, line, actual, expected); + test_fail(); + } + return true; +} + +#define CHK_OUT(actual, expected) chk_out(actual, expected, __FILE__, __LINE__) + +static int perform_tw_exec(void *arg) +{ + (void)arg; + + /* Reset shared state between runs */ + atomic_store(&lp1_turn, false); + memset(tw_lp_outs, 0, sizeof(tw_lp_outs)); + memset(tw_lp_outs_count, 0, sizeof(tw_lp_outs_count)); + atomic_store(&tw_total_output_count, 0); + + RootsimInit(&tw_conf); + RootsimRun(); + + /* --- Verify exact committed output sequence for LP 0 --- + * + * The busy-wait synchronization plus core_binding=true force a deterministic + * execution order. Each round: LP 0 processes an EVENT at time T (output N), + * then LP 1 sends a STRAGGLER_EVENT at time T+0.2 to LP 0. This causes LP 0 + * to roll back and process the STRAGGLER_EVENT first (output S), then replay + * its EVENT (output N again). Some rounds don't trigger a straggler due to + * the interleaving; those appear as consecutive N outputs. */ + CHK_OUT(tw_lp_outs[0][0], "N0,0,0"); + CHK_OUT(tw_lp_outs[0][1], "S0,0,1"); + CHK_OUT(tw_lp_outs[0][2], "N0,0,1"); + CHK_OUT(tw_lp_outs[0][3], "S0,0,2"); + CHK_OUT(tw_lp_outs[0][4], "N0,0,2"); + CHK_OUT(tw_lp_outs[0][5], "N0,0,3"); + CHK_OUT(tw_lp_outs[0][6], "S0,0,4"); + CHK_OUT(tw_lp_outs[0][7], "N0,0,4"); + CHK_OUT(tw_lp_outs[0][8], "N0,0,5"); + CHK_OUT(tw_lp_outs[0][9], "S0,0,6"); + CHK_OUT(tw_lp_outs[0][10], "N0,0,6"); + CHK_OUT(tw_lp_outs[0][11], "N0,0,7"); + CHK_OUT(tw_lp_outs[0][12], "S0,0,8"); + CHK_OUT(tw_lp_outs[0][13], "N0,0,8"); + CHK_OUT(tw_lp_outs[0][14], "N0,0,9"); + CHK_OUT(tw_lp_outs[0][15], "S0,0,10"); + CHK_OUT(tw_lp_outs[0][16], "N0,0,10"); + CHK_OUT(tw_lp_outs[0][17], "N0,0,11"); + CHK_OUT(tw_lp_outs[0][18], "S0,0,12"); + CHK_OUT(tw_lp_outs[0][19], "N0,0,12"); + CHK_OUT(tw_lp_outs[0][20], "N0,0,13"); + CHK_OUT(tw_lp_outs[0][21], "S0,0,14"); + CHK_OUT(tw_lp_outs[0][22], "N0,0,14"); + CHK_OUT(tw_lp_outs[0][23], "N0,0,15"); + CHK_OUT(tw_lp_outs[0][24], "S0,0,16"); + CHK_OUT(tw_lp_outs[0][25], "N0,0,16"); + CHK_OUT(tw_lp_outs[0][26], "N0,0,17"); + CHK_OUT(tw_lp_outs[0][27], "S0,0,18"); + CHK_OUT(tw_lp_outs[0][28], "N0,0,18"); + CHK_OUT(tw_lp_outs[0][29], "N0,0,19"); + + /* --- Verify exact committed output sequence for LP 1 --- */ + CHK_OUT(tw_lp_outs[1][0], "N1,1,0"); + CHK_OUT(tw_lp_outs[1][1], "N1,1,1"); + CHK_OUT(tw_lp_outs[1][2], "N1,1,2"); + CHK_OUT(tw_lp_outs[1][3], "N1,1,3"); + CHK_OUT(tw_lp_outs[1][4], "N1,1,4"); + CHK_OUT(tw_lp_outs[1][5], "N1,1,5"); + CHK_OUT(tw_lp_outs[1][6], "N1,1,6"); + CHK_OUT(tw_lp_outs[1][7], "N1,1,7"); + CHK_OUT(tw_lp_outs[1][8], "N1,1,8"); + CHK_OUT(tw_lp_outs[1][9], "N1,1,9"); + + /* --- Verify total count: detects phantom outputs from rolled-back events --- + * + * LP 0: 20 N-outputs + 10 S-outputs = 30 + * LP 1: 10 N-outputs + * Total expected: 40 + * + * If committed_output_on_rollback() fails to suppress outputs, rolled-back + * EVENT handlers on LP 0 would fire extra N-outputs, inflating this count. */ + size_t total = atomic_load(&tw_total_output_count); + if(total != 40) { + fprintf(stderr, "Total output count mismatch: got %zu, expected 40\n", total); + test_fail(); + } + + /* Also verify the per-LP counts individually */ + if(tw_lp_outs_count[0] != 30) { + fprintf(stderr, "LP 0 output count mismatch: got %zu, expected 30\n", tw_lp_outs_count[0]); + test_fail(); + } + if(tw_lp_outs_count[1] != 10) { + fprintf(stderr, "LP 1 output count mismatch: got %zu, expected 10\n", tw_lp_outs_count[1]); + test_fail(); + } + + return 0; +} + +/* ========================================================================= + * Serial test + * ========================================================================= */ + +#define SERIAL_NUM_LPS 4 +#define SERIAL_EVENTS_PER_LP 5 + +/* Output storage for the serial test */ +static char serial_outs[SERIAL_NUM_LPS][SERIAL_EVENTS_PER_LP][OUT_SZ]; +static size_t serial_outs_count[SERIAL_NUM_LPS]; + +static void Serial_PerformOutput(lp_id_t me, unsigned output_type, const void *output_content, unsigned output_size) +{ + if(output_type != OUTPUT_TYPE) { + fprintf(stderr, "Serial_PerformOutput: unexpected output_type %u\n", output_type); + test_fail(); + } + if(output_size != sizeof(struct output_data)) { + fprintf(stderr, "Serial_PerformOutput: unexpected output_size %u (expected %zu)\n", output_size, + sizeof(struct output_data)); + test_fail(); + } + + size_t idx = serial_outs_count[me]; + if(idx >= SERIAL_EVENTS_PER_LP) { + fprintf(stderr, "Serial_PerformOutput: too many outputs for LP %llu\n", (unsigned long long)me); + test_fail(); + } + + const struct output_data *data = output_content; + snprintf(serial_outs[me][idx], OUT_SZ, "N%llu,%llu,%lu", (unsigned long long)me, (unsigned long long)data->id, + data->count); + serial_outs_count[me]++; +} + +static void Serial_ProcessEvent(lp_id_t me, simtime_t now, unsigned event_type, const void *content, unsigned size, + void *s) +{ + (void)now; + (void)content; + (void)size; + + struct lp_state *state = s; + + if(event_type == LP_INIT) { + state = rs_malloc(sizeof(*state)); + if(state == NULL) + abort(); + SetState(state); + state->count = 0; + ScheduleNewEvent(me, 1.0, EVENT, NULL, 0); + return; + } + + if(event_type == LP_FINI) + return; + + if(event_type == EVENT && state->count < SERIAL_EVENTS_PER_LP) { + ScheduleOutput(OUTPUT_TYPE, &(struct output_data){.id = me, .count = state->count}, + sizeof(struct output_data)); + state->count++; + if(state->count < SERIAL_EVENTS_PER_LP) + ScheduleNewEvent(me, now + 1.0, EVENT, NULL, 0); + } +} + +static bool Serial_CanEnd(lp_id_t me, const void *snapshot) +{ + (void)snapshot; + const struct lp_state *state = snapshot; + (void)state; + (void)me; + return false; +} + +static struct simulation_configuration serial_conf = { + .lps = SERIAL_NUM_LPS, + .n_threads = 1, + .termination_time = 100, + .log_level = LOG_WARN, + .stats_file = "output_test_serial", + .synchronization = SERIAL, + .dispatcher = Serial_ProcessEvent, + .committed = Serial_CanEnd, + .output_callback = Serial_PerformOutput, +}; + +static int perform_serial_exec(void *arg) +{ + (void)arg; + + memset(serial_outs, 0, sizeof(serial_outs)); + memset(serial_outs_count, 0, sizeof(serial_outs_count)); + + RootsimInit(&serial_conf); + RootsimRun(); + + /* Each LP should have produced exactly SERIAL_EVENTS_PER_LP outputs, + * one per EVENT, in causal order (count 0,1,2,...). */ + for(lp_id_t lp = 0; lp < SERIAL_NUM_LPS; lp++) { + if(serial_outs_count[lp] != SERIAL_EVENTS_PER_LP) { + fprintf(stderr, "Serial: LP %llu produced %zu outputs, expected %d\n", (unsigned long long)lp, + serial_outs_count[lp], SERIAL_EVENTS_PER_LP); + test_fail(); + } + for(unsigned i = 0; i < SERIAL_EVENTS_PER_LP; i++) { + char expected[OUT_SZ]; + snprintf(expected, OUT_SZ, "N%llu,%llu,%u", (unsigned long long)lp, (unsigned long long)lp, i); + if(strcmp(serial_outs[lp][i], expected) != 0) { + fprintf(stderr, "Serial: LP %llu output[%u] = \"%s\", expected \"%s\"\n", + (unsigned long long)lp, i, serial_outs[lp][i], expected); + test_fail(); + } + } + } + + return 0; +} + +/* ========================================================================= + * Entry point + * ========================================================================= */ + +int main(void) +{ + test("Testing committed output — serial mode", perform_serial_exec, NULL); + test("Testing committed output — Time Warp with stragglers", perform_tw_exec, NULL); +}