diff --git a/src/ROOT-Sim.h b/src/ROOT-Sim.h index 8b861e55..4244d5c1 100644 --- a/src/ROOT-Sim.h +++ b/src/ROOT-Sim.h @@ -91,6 +91,24 @@ extern void ScheduleNewEvent(lp_id_t receiver, simtime_t timestamp, unsigned eve extern void SetState(void *new_state); +/** + * @brief Marks a memory region as dirty for incremental checkpointing. + * + * This function is injected at compile time by the software instrumentation + * tool before every memory-write instruction in the model code. It marks the + * corresponding blocks in the buddy system's dirty bitmap, so that only the + * dirtied blocks are saved in the next incremental checkpoint. + * + * LP-visible memory lives in the buddy system's base_mem[] buffer; writes to + * the longest[] allocation tree are tracked separately by buddy_malloc() and + * buddy_free() via direct bitmap_set() calls. Therefore, this function only + * needs to handle writes whose addresses fall within base_mem[]. + * + * @param ptr A pointer to the start of the memory region being written to. + * @param size The size of the memory region being written to, in bytes. + */ +extern void WriteMemory(const void *ptr, const size_t size); + /** * @brief Allocates rollbackable memory * @@ -193,6 +211,11 @@ struct simulation_configuration { const char *stats_file; /// The checkpointing interval unsigned ckpt_interval; + /// If set, incremental checkpointing is enabled + bool incremental_ckpt; + /// Period (in number of checkpoints) at which a full checkpoint is forced when incremental + /// checkpointing is enabled. If zero, full checkpoints are only taken at LP initialization. + unsigned full_ckpt_period; /// If set, worker threads are bound to physical cores bool core_binding; /// Specify what synchronization algorithm we are using diff --git a/src/log/stats.c b/src/log/stats.c index e3047833..c0295709 100644 --- a/src/log/stats.c +++ b/src/log/stats.c @@ -64,6 +64,7 @@ const char *const stats_names[] = { [STATS_CKPT] = "checkpoints", [STATS_CKPT_TIME] = "checkpoints time", [STATS_CKPT_SIZE] = "checkpoints size", + [STATS_CKPT_INCR_SIZE] = "incremental checkpoints size", [STATS_MSG_SILENT] = "silent messages", [STATS_MSG_SILENT_TIME] = "silent messages time", [STATS_MSG_ANTI] = "anti messages", diff --git a/src/log/stats.h b/src/log/stats.h index 33f86d3d..16b48707 100644 --- a/src/log/stats.h +++ b/src/log/stats.h @@ -51,6 +51,8 @@ enum stats_thread_type { STATS_CKPT_TIME, /// The size of LPs checkpoints STATS_CKPT_SIZE, + /// The actual size of incremental checkpoints (smaller than full_ckpt_size) + STATS_CKPT_INCR_SIZE, /// The count of messages processed in coasting forward, i.e. silently executed messages STATS_MSG_SILENT, /// The time taken to carry out silent processing activities diff --git a/src/lp/process.c b/src/lp/process.c index 817b9b42..d62f2676 100644 --- a/src/lp/process.c +++ b/src/lp/process.c @@ -64,6 +64,11 @@ static inline void checkpoint_take(struct lp_ctx *lp) const timer_uint t = timer_hr_new(); model_allocator_checkpoint_take(&lp->mm_state, array_count(lp->p.pes)); stats_take(STATS_CKPT_SIZE, lp->mm_state.full_ckpt_size); + if(global_config.incremental_ckpt) { + struct mm_log last = array_peek(lp->mm_state.logs); + if(is_log_incremental(last)) + stats_take(STATS_CKPT_INCR_SIZE, log_get_ckpt(last)->incr_ckpt_size); + } stats_take(STATS_CKPT, 1); stats_take(STATS_CKPT_TIME, timer_hr_value(t)); } diff --git a/src/mm/buddy/buddy.c b/src/mm/buddy/buddy.c index 2abd84b8..5b3ad01d 100644 --- a/src/mm/buddy/buddy.c +++ b/src/mm/buddy/buddy.c @@ -29,6 +29,7 @@ void buddy_init(struct buddy_state *self) self->longest[idx] = node_size; node_size -= is_power_of_2(idx + 2); } + memset(self->dirty, 0, sizeof(self->dirty)); } @@ -61,19 +62,16 @@ void *buddy_malloc(struct buddy_state *self, const uint_fast8_t req_blks_exp) /* update the *longest* value back */ self->longest[idx] = 0; -#ifdef ROOTSIM_INCREMENTAL - bitmap_set(self->dirty, idx >> B_BLOCK_EXP); -#endif + if(global_config.incremental_ckpt) + bitmap_set(self->dirty, idx >> B_BLOCK_EXP); const uint_fast32_t offset = ((idx + 1) << node_size) - (1 << B_TOTAL_EXP); while(idx) { idx = buddy_parent(idx); - self->longest[idx] = - max(self->longest[buddy_left_child(idx)], self->longest[buddy_right_child(idx)]); -#ifdef ROOTSIM_INCREMENTAL - bitmap_set(self->dirty, idx >> B_BLOCK_EXP); -#endif + self->longest[idx] = max(self->longest[buddy_left_child(idx)], self->longest[buddy_right_child(idx)]); + if(global_config.incremental_ckpt) + bitmap_set(self->dirty, idx >> B_BLOCK_EXP); } return ((char *)self->base_mem) + offset; @@ -102,16 +100,16 @@ uint_fast32_t buddy_free(struct buddy_state *self, void *ptr) self->longest[idx] = node_size; const uint_fast32_t ret = (uint_fast32_t)1U << node_size; -#ifdef ROOTSIM_INCREMENTAL - bitmap_set(self->dirty, idx >> B_BLOCK_EXP); + if(global_config.incremental_ckpt) { + bitmap_set(self->dirty, idx >> B_BLOCK_EXP); - uint_fast32_t bitmap_idx = (1 << (node_size - B_BLOCK_EXP)) - 1; - offset += (1 << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); - // Track freed blocks content because full checkpoints don't - do { - bitmap_set(self->dirty, offset + bitmap_idx); - } while(bitmap_idx--); -#endif + uint_fast32_t bitmap_idx = (1 << (node_size - B_BLOCK_EXP)) - 1; + offset += (1 << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); + // Track freed blocks content because full checkpoints don't + do { + bitmap_set(self->dirty, offset + bitmap_idx); + } while(bitmap_idx--); + } while(idx) { idx = buddy_parent(idx); @@ -124,9 +122,8 @@ uint_fast32_t buddy_free(struct buddy_state *self, void *ptr) } else { self->longest[idx] = max(left_long, right_long); } -#ifdef ROOTSIM_INCREMENTAL - bitmap_set(self->dirty, i >> B_BLOCK_EXP); -#endif + if(global_config.incremental_ckpt) + bitmap_set(self->dirty, idx >> B_BLOCK_EXP); ++node_size; } return ret; @@ -178,6 +175,21 @@ struct buddy_realloc_res buddy_best_effort_realloc(const struct buddy_state *sel return ret; } +/** + * @brief Resets the dirty bitmap of a buddy system. + * + * Clears all dirty-tracking bits so that the next incremental checkpoint + * will only record blocks dirtied after this call. This should be called + * after every checkpoint (full or incremental) when incremental checkpointing + * is enabled. + * + * @param self Pointer to the `buddy_state` structure. + */ +void buddy_dirty_reset(struct buddy_state *self) +{ + memset(self->dirty, 0, sizeof(self->dirty)); +} + /** * @brief Marks a memory region as dirty for incremental checkpointing. * diff --git a/src/mm/buddy/buddy.h b/src/mm/buddy/buddy.h index 14fb23a1..99ce2c6e 100644 --- a/src/mm/buddy/buddy.h +++ b/src/mm/buddy/buddy.h @@ -48,6 +48,8 @@ static_assert(offsetof(struct buddy_state, longest) == extern void buddy_init(struct buddy_state *self); extern void *buddy_malloc(struct buddy_state *self, uint_fast8_t req_blks_exp); extern uint_fast32_t buddy_free(struct buddy_state *self, void *ptr); +extern void buddy_dirty_reset(struct buddy_state *self); +extern void buddy_dirty_mark(const struct buddy_state *self, const void *ptr, size_t size); /** * @brief Represents the result of a best-effort reallocation in the buddy system. diff --git a/src/mm/buddy/checkpoint.c b/src/mm/buddy/checkpoint.c index f3cb9124..bf3808ad 100644 --- a/src/mm/buddy/checkpoint.c +++ b/src/mm/buddy/checkpoint.c @@ -3,6 +3,12 @@ * * @brief Buddy system checkpointing capabilities * + * Provides both full and incremental checkpointing of individual buddy systems. + * Full checkpoints save the entire allocation tree and all allocated memory blocks. + * Incremental checkpoints save only blocks that have been dirtied since + * the last checkpoint, as tracked by the dirty bitmap in @a buddy_state. + * Building-block restore functions support chain-walking during incremental restore. + * * SPDX-FileCopyrightText: 2008-2025 HPCS Group * SPDX-License-Identifier: GPL-3.0-only */ @@ -52,93 +58,181 @@ } \ }) -// TODO: fix incremental checkpointing -#ifdef ROOTSIM_INCREMENTAL - -struct buddy_checkpoint *checkpoint_incremental_take(const struct buddy_state *self) +/** + * @brief Computes the size in bytes required for an incremental checkpoint of this buddy system. + * + * The incremental checkpoint stores: + * - the buddy_checkpoint header fields (orig, dirty[]) + * - one block of (1 << B_BLOCK_EXP) bytes per dirty bit in the bitmap + * + * @param self A pointer to the buddy system state. + * @return The number of bytes needed. + */ +size_t buddy_checkpoint_incremental_size(const struct buddy_state *self) { - uint_fast32_t bset = bitmap_count_set(self->dirty, sizeof(self->dirty)); + uint_fast32_t dirty_count = bitmap_count_set(self->dirty, sizeof(self->dirty)); + return offsetof(struct buddy_checkpoint, longest) + ((size_t)dirty_count << B_BLOCK_EXP); +} - struct buddy_checkpoint *ret = mm_alloc(offsetof(struct buddy_checkpoint, longest) + bset * (1 << B_BLOCK_EXP)); +/** + * @brief Takes an incremental checkpoint of a buddy system. + * + * Saves only the dirty blocks (tracked via the dirty bitmap) into the checkpoint buffer. + * Dirty blocks from the contiguous longest[]+base_mem[] region are packed sequentially + * into the space normally occupied by longest[] and base_mem[]. + * After saving, the dirty bitmap is cleared. + * + * @param self A pointer to the buddy system state. + * @param ret A pointer to the destination checkpoint buffer (caller-allocated). + * @return A pointer past the last written byte (for chaining multiple buddy checkpoints). + */ +struct buddy_checkpoint *buddy_checkpoint_incremental_take(const struct buddy_state *self, struct buddy_checkpoint *ret) +{ + ret->orig = self; + memcpy(ret->dirty, self->dirty, sizeof(self->dirty)); - unsigned char *ptr = ret->longest; - const unsigned char *src = self->longest; + /* Pack dirty blocks sequentially into ret->longest (longest and base_mem are contiguous). */ + unsigned char *dst = ret->longest; + const unsigned char *src = self->longest; /* longest[] and base_mem[] are contiguous */ -#define copy_block_to_ckp(i) \ +#define incr_copy_block_to_ckp(i) \ __extension__({ \ - memcpy(ptr, src + ((i) << B_BLOCK_EXP), 1 << B_BLOCK_EXP); \ - ptr += 1 << B_BLOCK_EXP; \ + memcpy(dst, src + ((size_t)(i) << B_BLOCK_EXP), 1U << B_BLOCK_EXP); \ + dst += 1U << B_BLOCK_EXP; \ }) - bitmap_foreach_set(self->dirty, sizeof(self->dirty), copy_block_to_ckp); -#undef copy_block_to_ckp + bitmap_foreach_set(self->dirty, sizeof(self->dirty), incr_copy_block_to_ckp); +#undef incr_copy_block_to_ckp - memcpy(ret->dirty, self->dirty, sizeof(self->dirty)); - return ret; + // Reset dirty bitmap: blocks saved, start fresh for next checkpoint. + buddy_dirty_reset((struct buddy_state *)self); + + return (struct buddy_checkpoint *)dst; } -void checkpoint_incremental_restore(struct buddy_state *self, const struct buddy_checkpoint *ckp) +/** + * @brief Restores dirty blocks from an incremental checkpoint (full restore, single checkpoint). + * + * Intended for the simple case of restoring exactly one incremental checkpoint when + * no chain walk is needed. For chain-based restore, use + * buddy_checkpoint_incremental_restore_partial() instead. + * + * @param self A pointer to the buddy system state to restore into. + * @param ckp A pointer to the incremental checkpoint. + * @return A pointer past the consumed checkpoint data, or NULL if ckp doesn't match self. + */ +const struct buddy_checkpoint *buddy_checkpoint_incremental_restore(struct buddy_state *self, + const struct buddy_checkpoint *ckp) { - array_count_t i = array_count(self->logs) - 1; - const struct buddy_checkpoint *cur_ckp = array_get_at(self->logs, i).c; + if(ckp->orig != self) + return NULL; - while(cur_ckp != ckp) { - bitmap_merge_or(self->dirty, cur_ckp->dirty, sizeof(self->dirty)); - cur_ckp = array_get_at(self->logs, --i).c; - } + const unsigned char *src = ckp->longest; + unsigned char *dst = self->longest; /* longest[] and base_mem[] are contiguous */ -#define copy_dirty_block(i) \ +#define incr_copy_block_from_ckp(i) \ __extension__({ \ - if(bitmap_check(self->dirty, i)) { \ - memcpy(self->longest + (i << B_BLOCK_EXP), ptr, 1 << B_BLOCK_EXP); \ - bitmap_reset(self->dirty, i); \ - --r; \ - } \ - ptr += 1 << B_BLOCK_EXP; \ + memcpy(dst + ((size_t)(i) << B_BLOCK_EXP), src, 1U << B_BLOCK_EXP); \ + src += 1U << B_BLOCK_EXP; \ }) -#define copy_block_from_ckp(i) \ - __extension__({ \ - memcpy(self->longest + (i << B_BLOCK_EXP), cur_ckp->longest + (i << B_BLOCK_EXP), 1 << B_BLOCK_EXP); \ - --r; \ - }) + bitmap_foreach_set(ckp->dirty, sizeof(ckp->dirty), incr_copy_block_from_ckp); +#undef incr_copy_block_from_ckp -#define buddy_block_dirty_from_ckp(offset, len) \ + return (const struct buddy_checkpoint *)src; +} + +/** + * @brief Restores dirty blocks from an incremental checkpoint for blocks still in `remaining`. + * + * Used during backward chain traversal for incremental restore. For each dirty block in `ckp` + * that is also set in `remaining`, the block is copied into the live buddy state and cleared + * from `remaining`. Blocks already restored from more recent logs are skipped. + * + * @param self Live buddy system to restore into. + * @param ckp Incremental checkpoint to restore from. + * @param remaining Bitmap of blocks still needing restoration (modified in place). + * @return Pointer past the consumed checkpoint data, or NULL if ckp doesn't match self. + */ +const struct buddy_checkpoint *buddy_checkpoint_incremental_restore_partial(struct buddy_state *self, + const struct buddy_checkpoint *ckp, block_bitmap *remaining) +{ + if(ckp->orig != self) + return NULL; + + const unsigned char *src = ckp->longest; + unsigned char *dst = self->longest; + uint_fast32_t block_count = bitmap_count_set(ckp->dirty, sizeof(ckp->dirty)); + +#define incr_partial_restore(i) \ __extension__({ \ - uint_fast32_t i = (offset >> B_BLOCK_EXP) + (1 << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); \ - uint_fast32_t b_len = len; \ - do { \ - copy_dirty_block(i); \ - i++; \ - b_len -= 1U << B_BLOCK_EXP; \ - } while(b_len); \ + if(bitmap_check(remaining, (i))) { \ + memcpy(dst + ((size_t)(i) << B_BLOCK_EXP), src, 1U << B_BLOCK_EXP); \ + bitmap_reset(remaining, (i)); \ + } \ + src += 1U << B_BLOCK_EXP; \ }) - uint_fast32_t r = bitmap_count_set(self->dirty, sizeof(self->dirty)); - const unsigned char *ptr = cur_ckp->longest; + bitmap_foreach_set(ckp->dirty, sizeof(ckp->dirty), incr_partial_restore); + (void)block_count; +#undef incr_partial_restore - bitmap_foreach_set(cur_ckp->dirty, sizeof(cur_ckp->dirty), copy_dirty_block); + return (const struct buddy_checkpoint *)src; +} - const unsigned tree_bit_size = bitmap_required_size(1 << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); +/** + * @brief Restores blocks still needed (`remaining`) from a full checkpoint. + * + * Used as the final step of backward chain traversal. Restores only the blocks + * still set in `remaining` — blocks already restored from incremental logs are skipped. + * + * For the tree portion (longest[]), blocks are copied directly by index. + * For the base_mem portion, the full checkpoint uses buddy_tree_visit to find + * allocated blocks; only those also set in `remaining` are copied. + * + * @param self Live buddy system to restore into. + * @param ckp Full checkpoint to restore from. + * @param remaining Bitmap of blocks still needing restoration (read-only). + * @return Pointer past the consumed checkpoint data, or NULL if ckp doesn't match self. + */ +const struct buddy_checkpoint *buddy_checkpoint_full_restore_remaining(struct buddy_state *self, + const struct buddy_checkpoint *ckp, const block_bitmap *remaining) +{ + if(ckp->orig != self) + return NULL; + + const unsigned int tree_blocks = (1U << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); - while(r) { - cur_ckp = array_get_at(self->logs, --i).c; - if(cur_ckp->is_incremental) { - ptr = cur_ckp->longest; - bitmap_foreach_set(cur_ckp->dirty, sizeof(cur_ckp->dirty), copy_dirty_block); - } else { - bitmap_foreach_set(self->dirty, tree_bit_size, copy_block_from_ckp); - ptr = cur_ckp->base_mem; - buddy_tree_visit(cur_ckp->longest, buddy_block_dirty_from_ckp); + // Restore tree portion: copy only blocks set in remaining + for(unsigned int i = 0; i < tree_blocks; i++) { + if(bitmap_check(remaining, i)) { + memcpy(self->longest + ((size_t)i << B_BLOCK_EXP), ckp->longest + ((size_t)i << B_BLOCK_EXP), + 1U << B_BLOCK_EXP); } } -#undef copy_dirty_block -#undef copy_block_from_ckp -#undef buddy_block_dirty_from_ckp -} + // Restore base_mem portion via tree visit (full ckp stores only allocated blocks) + const unsigned char *ptr = ckp->base_mem; -#endif +#define full_restore_remaining_block(offset, len) \ + __extension__({ \ + uint_fast32_t _off = (offset); \ + uint_fast32_t _len = (len); \ + do { \ + uint_fast32_t _bi = (_off >> B_BLOCK_EXP) + tree_blocks; \ + if(bitmap_check(remaining, _bi)) \ + memcpy(self->base_mem + _off, ptr, 1U << B_BLOCK_EXP); \ + ptr += 1U << B_BLOCK_EXP; \ + _off += 1U << B_BLOCK_EXP; \ + _len -= 1U << B_BLOCK_EXP; \ + } while(_len); \ + }) + + buddy_tree_visit(ckp->longest, full_restore_remaining_block); +#undef full_restore_remaining_block + + return (const struct buddy_checkpoint *)ptr; +} /** * @brief Takes a full checkpoint. @@ -153,9 +247,8 @@ void checkpoint_incremental_restore(struct buddy_state *self, const struct buddy struct buddy_checkpoint *buddy_checkpoint_full_take(const struct buddy_state *self, struct buddy_checkpoint *ret) { ret->orig = self; -#ifdef ROOTSIM_INCREMENTAL - memcpy(ret->dirty, self->dirty, sizeof(self->dirty)); -#endif + if(global_config.incremental_ckpt) + memcpy(ret->dirty, self->dirty, sizeof(self->dirty)); memcpy(ret->longest, self->longest, sizeof(ret->longest)); #define buddy_block_copy_to_ckp(offset, len) \ diff --git a/src/mm/buddy/checkpoint.h b/src/mm/buddy/checkpoint.h index 48c0fafe..8bfb44e0 100644 --- a/src/mm/buddy/checkpoint.h +++ b/src/mm/buddy/checkpoint.h @@ -10,9 +10,12 @@ #include -/// A restorable checkpoint of the memory context of a single buddy system -struct buddy_checkpoint { // todo only log longest[] if changed, or incrementally - /// The buddy system to which this checkpoint applies. TODO: reengineer the multi-checkpointing approach +/// A restorable checkpoint of the memory context of a single buddy system. +/// For a full checkpoint, both `longest[]` and `base_mem[]` are populated. +/// For an incremental checkpoint, only dirty blocks (as tracked by `dirty[]`) are stored +/// in packed form starting at `longest[]`. +struct buddy_checkpoint { + /// The buddy system to which this checkpoint applies const struct buddy_state *orig; /// The checkpoint of the dirty bitmap block_bitmap dirty[bitmap_required_size( @@ -34,9 +37,20 @@ extern struct buddy_checkpoint *buddy_checkpoint_full_take(const struct buddy_st extern const struct buddy_checkpoint *buddy_checkpoint_full_restore(struct buddy_state *self, const struct buddy_checkpoint *data); -#ifdef ROOTSIM_INCREMENTAL -extern struct buddy_checkpoint *checkpoint_incremental_take(const struct buddy_state *self, +extern struct buddy_checkpoint *buddy_checkpoint_incremental_take(const struct buddy_state *self, struct buddy_checkpoint *data); -extern const struct buddy_checkpoint *checkpoint_incremental_restore(struct buddy_state *self, +extern const struct buddy_checkpoint *buddy_checkpoint_incremental_restore(struct buddy_state *self, const struct buddy_checkpoint *ckp); -#endif + +/// Restores dirty blocks from an incremental checkpoint for blocks still set in `remaining`. +/// Clears restored bits from `remaining`. Returns pointer past consumed data, or NULL if no match. +extern const struct buddy_checkpoint *buddy_checkpoint_incremental_restore_partial(struct buddy_state *self, + const struct buddy_checkpoint *ckp, block_bitmap *remaining); + +/// Restores blocks still set in `remaining` from a full checkpoint. +/// Returns pointer past consumed data, or NULL if no match. +extern const struct buddy_checkpoint *buddy_checkpoint_full_restore_remaining(struct buddy_state *self, + const struct buddy_checkpoint *ckp, const block_bitmap *remaining); + +/// Computes the size in bytes needed for an incremental checkpoint of this buddy system. +extern size_t buddy_checkpoint_incremental_size(const struct buddy_state *self); diff --git a/src/mm/checkpoint/checkpoint.h b/src/mm/checkpoint/checkpoint.h index 71e06752..846d943d 100644 --- a/src/mm/checkpoint/checkpoint.h +++ b/src/mm/checkpoint/checkpoint.h @@ -11,19 +11,26 @@ #include #include -#ifdef ROOTSIM_INCREMENTAL -/// Tells whether a checkpoint is incremental or not. -#define is_log_incremental(l) ((uintptr_t)(l).c & 0x1) -#else -/// Tells whether a checkpoint is incremental or not. -#define is_log_incremental(l) false -#endif +/// Tells whether a log entry holds an incremental checkpoint (pointer tag on bit 0). +#define is_log_incremental(l) ((uintptr_t)(l).ckpt & 0x1) + +/// Tag a checkpoint pointer as incremental. +#define log_mark_incremental(ptr) ((struct mm_checkpoint *)((uintptr_t)(ptr) | 0x1)) + +/// Strip the incremental tag from a log entry and return the real checkpoint pointer. +#define log_get_ckpt(l) ((struct mm_checkpoint *)((uintptr_t)(l).ckpt & ~(uintptr_t)0x1)) /// The checkpoint for the multiple buddy system allocator struct mm_checkpoint { - /// The total count of allocated bytes at the moment of the checkpoint + /// The total count of allocated bytes at the moment of the checkpoint. + /// For both full and incremental checkpoints this stores the full (uncompressed) + /// state size, used to track full_ckpt_size across restores. uint_fast32_t ckpt_size; + /// The actual number of bytes written for this checkpoint. + /// For full checkpoints this equals ckpt_size; for incremental checkpoints it is + /// smaller (only dirty blocks are saved). + uint_fast32_t incr_ckpt_size; /// The sequence of checkpoints of the allocated buddy systems (see @a buddy_checkpoint) unsigned char chkps[]; }; @@ -86,6 +93,6 @@ extern void auto_ckpt_init(void); extern void auto_ckpt_lp_init(struct auto_ckpt *auto_ckpt); extern void auto_ckpt_on_gvt(void); extern void auto_ckpt_recompute(struct auto_ckpt *auto_ckpt, uint_fast32_t state_size); -extern void model_allocator_checkpoint_next_force_full(const struct mm_state *self); +extern void model_allocator_checkpoint_next_force_full(struct mm_state *self); extern void model_allocator_checkpoint_take(struct mm_state *self, array_count_t ref_idx); extern array_count_t model_allocator_checkpoint_restore(struct mm_state *self, array_count_t ref_idx); diff --git a/src/mm/checkpoint/full.c b/src/mm/checkpoint/full.c index 9e1830e7..238b58ef 100644 --- a/src/mm/checkpoint/full.c +++ b/src/mm/checkpoint/full.c @@ -3,29 +3,35 @@ * * @brief Full checkpointing routines * - * This unit contains the implementation of full checkpointing routines for the LP memory management + * This unit contains the implementation of full checkpointing routines for the LP memory management. + * The model_allocator_checkpoint_take() and model_allocator_checkpoint_restore() entry points + * dispatch to full or incremental logic based on the runtime configuration and the force_full flag. * * SPDX-FileCopyrightText: 2008-2025 HPCS Group * SPDX-License-Identifier: GPL-3.0-only */ #include +#include #include #include +#include + +extern void model_allocator_checkpoint_take_incremental(struct mm_state *self, array_count_t ref_idx); /** * @brief Takes a full checkpoint of the memory management state. * - * This function creates a full checkpoint of the memory management state for a logical process. - * It allocates memory for the checkpoint, records its size, and stores it in the logs. Each buddy - * system's state is saved into the checkpoint structure. + * Allocates a checkpoint buffer sized to hold the complete state of all buddy systems, + * saves each buddy's state, and resets dirty bitmaps when incremental mode is active. * - * @param self A pointer to the `mm_state` structure representing the memory management state. - * @param ref_idx The reference index associated with the checkpoint. + * @param self A pointer to the `mm_state` structure. + * @param ref_idx The reference index (PES position) for this checkpoint. */ -void model_allocator_checkpoint_take(struct mm_state *self, array_count_t ref_idx) +static void checkpoint_take_full(struct mm_state *self, array_count_t ref_idx) { struct mm_checkpoint *ckpt = mm_alloc(self->full_ckpt_size); ckpt->ckpt_size = self->full_ckpt_size; + ckpt->incr_ckpt_size = self->full_ckpt_size; const struct mm_log mm_log = {.ref_idx = ref_idx, .ckpt = ckpt}; array_push(self->logs, mm_log); @@ -35,20 +41,76 @@ void model_allocator_checkpoint_take(struct mm_state *self, array_count_t ref_id while(i--) buddy_ckp = buddy_checkpoint_full_take(array_get_at(self->buddies, i), buddy_ckp); buddy_ckp->orig = NULL; + + if(global_config.incremental_ckpt) { + self->force_full = false; + self->ckpt_since_last_full = 0; + i = array_count(self->buddies); + while(i--) + buddy_dirty_reset(array_get_at(self->buddies, i)); + } } +/** + * @brief Entry point for taking a checkpoint. + * + * Dispatches to a full or incremental checkpoint based on the configuration and state: + * - If incremental mode is disabled, always takes a full checkpoint. + * - If force_full is set, takes a full checkpoint and resets the flag. + * - If full_ckpt_period is set and ckpt_since_last_full has reached it, forces a full checkpoint. + * - Otherwise, takes an incremental checkpoint. + * + * @param self A pointer to the `mm_state` structure. + * @param ref_idx The reference index (PES position) for this checkpoint. + */ +void model_allocator_checkpoint_take(struct mm_state *self, array_count_t ref_idx) +{ + if(!global_config.incremental_ckpt || self->force_full) { + checkpoint_take_full(self, ref_idx); + return; + } + + if(global_config.full_ckpt_period && ++self->ckpt_since_last_full >= global_config.full_ckpt_period) { + self->force_full = true; /* will be cleared inside checkpoint_take_full */ + checkpoint_take_full(self, ref_idx); + return; + } + + model_allocator_checkpoint_take_incremental(self, ref_idx); +} + +/** + * @brief Restores the memory state of all buddy systems from a full checkpoint. + * + * @param self A pointer to the `mm_state` structure. + * @param ckp Pointer to the full checkpoint data. + */ +static void restore_from_full(struct mm_state *self, const struct mm_checkpoint *ckp) +{ + self->full_ckpt_size = ckp->ckpt_size; + const struct buddy_checkpoint *buddy_ckpt = (const struct buddy_checkpoint *)ckp->chkps; + + array_count_t k = array_count(self->buddies); + while(k--) { + struct buddy_state *b = array_get_at(self->buddies, k); + const struct buddy_checkpoint *next = buddy_checkpoint_full_restore(b, buddy_ckpt); + if(unlikely(next == NULL)) { + buddy_init(b); + self->full_ckpt_size += offsetof(struct buddy_checkpoint, base_mem); + } else { + buddy_ckpt = next; + } + } +} /** * @brief Restores the memory management state to a specific checkpoint. * - * This function restores the memory management state of a logical process to the state - * recorded in the checkpoint corresponding to the given reference index. It ensures - * that all buddy systems are restored to their respective states as recorded in the - * checkpoint. If a checkpoint is missing for a buddy system, it reinitializes the buddy - * system to a default state. + * Finds the checkpoint at or before `ref_idx`. If it is a full checkpoint, restores + * directly. If it is an incremental checkpoint, walks backward through the log chain + * until a full checkpoint is found, restoring dirty blocks along the way. * - * @param self A pointer to the `mm_state` structure representing the memory - * management state of the logical process. + * @param self A pointer to the `mm_state` structure. * @param ref_idx The reference index of the checkpoint to restore. * @return The reference index of the restored checkpoint. */ @@ -58,26 +120,81 @@ array_count_t model_allocator_checkpoint_restore(struct mm_state *self, const ar while(array_get_at(self->logs, index).ref_idx > ref_idx) index--; - const struct mm_checkpoint *ckp = array_get_at(self->logs, index).ckpt; - self->full_ckpt_size = ckp->ckpt_size; - const struct buddy_checkpoint *buddy_ckpt = (struct buddy_checkpoint *)ckp->chkps; + if(!is_log_incremental(array_get_at(self->logs, index))) { + // Simple case: target is a full checkpoint. + restore_from_full(self, log_get_ckpt(array_get_at(self->logs, index))); + } else { + /* + * Incremental restore: forward apply approach. + * + * 1. Find the full checkpoint at or before the target in the log chain. + * 2. Restore the full checkpoint state via restore_from_full(). + * 3. Apply each incremental log forward from full+1 to target (inclusive), + * restoring only the dirty blocks recorded in each incremental log. + * + * This is correct because each incremental log records the state of dirty + * blocks AT the time of that checkpoint. Applying them in forward order + * produces the state at the target checkpoint. Blocks that were never dirtied + * between the full checkpoint and the target come from the full checkpoint. + */ - array_count_t k = array_count(self->buddies); - while(k--) { - struct buddy_state *b = array_get_at(self->buddies, k); - const struct buddy_checkpoint *ckpt = - buddy_checkpoint_full_restore(array_get_at(self->buddies, k), buddy_ckpt); - if(unlikely(ckpt == NULL)) { - buddy_init(b); - self->full_ckpt_size += offsetof(struct buddy_checkpoint, base_mem); - } else { - buddy_ckpt = ckpt; + // Find full checkpoint at bottom of chain. + array_count_t full_i = index; + while(full_i > 0 && is_log_incremental(array_get_at(self->logs, full_i))) + full_i--; + + // Restore baseline from full checkpoint. + restore_from_full(self, log_get_ckpt(array_get_at(self->logs, full_i))); + + // Apply incremental logs forward to target. + for(array_count_t chain_i = full_i + 1; chain_i <= index; chain_i++) { + const struct mm_checkpoint *cur_ckp = log_get_ckpt(array_get_at(self->logs, chain_i)); + const struct buddy_checkpoint *bckp = (const struct buddy_checkpoint *)cur_ckp->chkps; + + // Apply each buddy's incremental patch in this log. + while(bckp->orig != NULL) { + const struct buddy_checkpoint *next = + buddy_checkpoint_incremental_restore((struct buddy_state *)bckp->orig, bckp); + if(unlikely(next == NULL)) { + // This buddy is no longer in our state — skip using size. + const uint_fast32_t dirty_count = + bitmap_count_set(bckp->dirty, sizeof(bckp->dirty)); + bckp = (const struct buddy_checkpoint *)((const unsigned char *)bckp->longest + + ((size_t)dirty_count << B_BLOCK_EXP)); + } else { + bckp = next; + } + } } + + /* + * Adjust full_ckpt_size by the net change in live allocation between + * the full baseline and the target incremental checkpoint. + * restore_from_full() already set full_ckpt_size to the full checkpoint's + * ckpt_size and may have added offsetof(buddy_checkpoint, base_mem) for + * each buddy that exists now but was absent from the full checkpoint (those + * buddies are still live but empty, and they DO occupy space in the next + * checkpoint). Adding the delta between the target and the full preserves + * that per-buddy overhead while correctly reflecting the net alloc/free + * activity that occurred between the full checkpoint and the target. + */ + self->full_ckpt_size += log_get_ckpt(array_get_at(self->logs, index))->ckpt_size - + log_get_ckpt(array_get_at(self->logs, full_i))->ckpt_size; } for(array_count_t j = array_count(self->logs) - 1; j > index; --j) - mm_free(array_get_at(self->logs, j).ckpt); + mm_free(log_get_ckpt(array_get_at(self->logs, j))); array_count(self->logs) = index + 1; + + if(global_config.incremental_ckpt) { + // Reset dirty bitmaps: the state is now clean at the restored checkpoint. + array_count_t i = array_count(self->buddies); + while(i--) + buddy_dirty_reset(array_get_at(self->buddies, i)); + self->ckpt_since_last_full = 0; + self->last_dirty_buddy = NULL; + } + return array_get_at(self->logs, index).ref_idx; } diff --git a/src/mm/checkpoint/incremental.c b/src/mm/checkpoint/incremental.c index cf21f54f..32d51ac1 100644 --- a/src/mm/checkpoint/incremental.c +++ b/src/mm/checkpoint/incremental.c @@ -3,7 +3,9 @@ * * @brief Incremental checkpointing routines * - * This unit contains the implementation of incremental checkpointing routines for the LP memory management + * This unit contains the implementation of incremental checkpointing routines for the LP memory management. + * Incremental checkpointing saves only the memory blocks that have been dirtied since the last checkpoint, + * using the dirty bitmap maintained in each buddy system. * * SPDX-FileCopyrightText: 2008-2025 HPCS Group * SPDX-License-Identifier: GPL-3.0-only @@ -11,48 +13,91 @@ #include #include #include +#include #include +#include /** * @brief Forces the next checkpoint to be a full checkpoint. * - * This function is a placeholder for enabling full checkpointing when - * incremental state saving is active. Currently, it does nothing as - * incremental checkpointing is disabled. + * Sets the force_full flag on the mm_state so that the next call to + * model_allocator_checkpoint_take() will take a full checkpoint regardless + * of whether incremental mode is enabled. The flag is reset by the full + * checkpoint routine after it completes. * * @param self A pointer to the `mm_state` structure representing the memory * management state of the logical process. */ -void model_allocator_checkpoint_next_force_full(const struct mm_state *self) +void model_allocator_checkpoint_next_force_full(struct mm_state *self) { - (void)self; - // TODO: force full checkpointing when incremental state saving is enabled + self->force_full = true; } -/** - * @brief Marks a memory region as dirty for incremental checkpointing. - * - * This function is intended to be an entry point for the model's code, injected at compile time, - * to mark a memory region as dirty whenever a write operation is performed. It updates the - * corresponding buddy system to track the modified memory region. - * - * @note This function is currently unused because the incremental checkpointing subsystem - * is disabled. - * - * @param ptr A pointer to the start of the memory region being written to. - * @param size The size of the memory region being written to, in bytes. - */ -void __write_mem(const void *ptr, const size_t size) +void WriteMemory(const void *ptr, const size_t size) { + if(unlikely(!global_config.incremental_ckpt)) + return; + struct mm_state *self = ¤t_lp->mm_state; if(unlikely(!size || array_is_empty(self->buddies))) return; - if(unlikely(ptr < (void *)array_get_at(self->buddies, 0) || ptr > (void *)(array_peek(self->buddies) + 1))) + /* + * Fast path: check cached buddy first. This avoids the binary search in + * buddy_find_by_address() for consecutive writes to the same buddy, which + * is the common case (e.g., repeated rng_state writes within one LP event). + */ + struct buddy_state *buddy = self->last_dirty_buddy; + if(likely(buddy != NULL && ptr >= (void *)buddy && ptr < (void *)(buddy + 1))) { + buddy_dirty_mark(buddy, ptr, size); return; + } - struct buddy_state *buddy = buddy_find_by_address(self, ptr); + /* + * Preliminary bounds check: is ptr within any buddy at all? Maybe this could be unneeded if + * we relax the requirements in the contract, but I cannot let this check go. + */ + if(unlikely(ptr < (void *)array_get_at(self->buddies, 0) || ptr >= (void *)(array_peek(self->buddies) + 1))) + return; + buddy = buddy_find_by_address(self, ptr); + self->last_dirty_buddy = buddy; buddy_dirty_mark(buddy, ptr, size); } + +/** + * @brief Takes an incremental checkpoint of the memory management state. + * + * Computes the total size needed to store only the dirty blocks across all buddy + * systems. Allocates one contiguous mm_checkpoint buffer, fills it by calling + * buddy_checkpoint_incremental_take() for each buddy, tags the pointer as + * incremental (bit 0 set), and pushes it onto the log. + * + * @param self A pointer to the mm_state structure. + * @param ref_idx The reference index (PES position) for this checkpoint. + */ +void model_allocator_checkpoint_take_incremental(struct mm_state *self, array_count_t ref_idx) +{ + // Compute total size needed. + size_t total = offsetof(struct mm_checkpoint, chkps); + array_count_t n = array_count(self->buddies); + for(array_count_t i = 0; i < n; i++) + total += buddy_checkpoint_incremental_size(array_get_at(self->buddies, i)); + + // Sentinel buddy_checkpoint with orig == NULL. + total += offsetof(struct buddy_checkpoint, longest); + + struct mm_checkpoint *ckpt = mm_alloc(total); + ckpt->ckpt_size = self->full_ckpt_size; + ckpt->incr_ckpt_size = (uint_fast32_t)total; + + struct buddy_checkpoint *buddy_ckp = (struct buddy_checkpoint *)ckpt->chkps; + for(array_count_t i = n; i--;) + buddy_ckp = buddy_checkpoint_incremental_take(array_get_at(self->buddies, i), buddy_ckp); + buddy_ckp->orig = NULL; // sentinel + + // Tag as incremental and push to log + const struct mm_log entry = {.ref_idx = ref_idx, .ckpt = log_mark_incremental(ckpt)}; + array_push(self->logs, entry); +} diff --git a/src/mm/model_allocator.c b/src/mm/model_allocator.c index 6941003b..ac0d9c51 100644 --- a/src/mm/model_allocator.c +++ b/src/mm/model_allocator.c @@ -10,12 +10,16 @@ #include #include +#include #include #include #include #include +/* Declared in mm/checkpoint/incremental.c — marks a model-memory region dirty. */ +extern void WriteMemory(const void *ptr, size_t size); + /** * @brief Initializes the memory management state for a logical process. * @@ -32,6 +36,9 @@ void model_allocator_lp_init(struct mm_state *self) array_init(self->buddies); array_init(self->logs); self->full_ckpt_size = offsetof(struct mm_checkpoint, chkps) + sizeof(struct buddy_state *); + self->force_full = false; + self->ckpt_since_last_full = 0; + self->last_dirty_buddy = NULL; } @@ -49,7 +56,7 @@ void model_allocator_lp_fini(const struct mm_state *self) { array_count_t index = array_count(self->logs); while(index--) - mm_free(array_get_at(self->logs, index).ckpt); + mm_free(log_get_ckpt(array_get_at(self->logs, index))); array_fini(self->logs); @@ -131,8 +138,11 @@ void *rs_calloc(const size_t nmemb, const size_t size) const size_t tot = nmemb * size; void *ret = rs_malloc(tot); - if(likely(ret)) + if(likely(ret)) { + /* Mark base_mem dirty before the bulk write. */ + WriteMemory(ret, tot); memset(ret, 0, tot); + } return ret; } @@ -171,6 +181,8 @@ void *rs_realloc(void *ptr, size_t req_size) if(unlikely(new_buffer == NULL)) return NULL; + /* Mark the destination dirty before copying into model-allocated memory. */ + WriteMemory(new_buffer, min(req_size, ret.original)); memcpy(new_buffer, ptr, min(req_size, ret.original)); rs_free(ptr); @@ -211,7 +223,7 @@ array_count_t model_allocator_fossil_lp_collect(struct mm_state *self, const arr } while(j--) - mm_free(array_get_at(self->logs, j).ckpt); + mm_free(log_get_ckpt(array_get_at(self->logs, j))); array_truncate_first(self->logs, log_i); return ref_i; diff --git a/src/mm/model_allocator.h b/src/mm/model_allocator.h index bd1c9783..1f086c6c 100644 --- a/src/mm/model_allocator.h +++ b/src/mm/model_allocator.h @@ -20,6 +20,12 @@ struct mm_state { array_declare(struct mm_log) logs; /// The total count of allocated bytes uint_fast32_t full_ckpt_size; + /// When set, the next checkpoint will be forced to be a full one + bool force_full; + /// Counter of checkpoints taken since the last full checkpoint (for full_ckpt_period) + unsigned ckpt_since_last_full; + /// Cache of the last buddy found by WriteMemory() to avoid repeated binary searches + struct buddy_state *last_dirty_buddy; }; extern struct buddy_state *buddy_find_by_address(const struct mm_state *self, const void *ptr); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ac3ad43..5e9bd5cb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -16,7 +16,7 @@ test_program_link_libraries(load rscore) # Test data structures and subsystems test_program(bitmap datatypes/bitmap.c) -test_program(mm mm/buddy.c mm/buddy_hard.c mm/parallel.c mm/main.c mock.c) +test_program(mm mm/buddy.c mm/buddy_hard.c mm/parallel.c mm/incremental.c mm/model_allocator.c mm/main.c mock.c) target_include_directories(test_mm PRIVATE .) test_program_link_libraries(mm rscore) test_program(termination gvt/termination.c) @@ -43,5 +43,9 @@ test_program(correctness_serial integration/correctness/serial.c integration/cor test_program_link_libraries(correctness_serial rscore) test_program(correctness_parallel integration/correctness/timewarp.c integration/correctness/application.c integration/correctness/functions.c integration/correctness/output_256.c) test_program_link_libraries(correctness_parallel rscore) +test_program(correctness_incremental integration/correctness/timewarp_incremental.c integration/correctness/application_incremental.c integration/correctness/output_256.c) +test_program_link_libraries(correctness_incremental rscore) test_program(phold integration/phold.c) test_program_link_libraries(phold rscore) +test_program(phold_incremental integration/phold_incremental.c) +test_program_link_libraries(phold_incremental rscore) diff --git a/test/integration/correctness/application_incremental.c b/test/integration/correctness/application_incremental.c new file mode 100644 index 00000000..6816eb04 --- /dev/null +++ b/test/integration/correctness/application_incremental.c @@ -0,0 +1,260 @@ +/** + * @file test/integration/correctness/application_incremental.c + * + * @brief Instrumented model for incremental checkpointing correctness tests + * + * This file provides the same model logic as application.c + functions.c combined, + * but with explicit WriteMemory() calls before every write to model-allocated memory. + * This simulates the effect of compiler-level software instrumentation (which would + * normally be injected by a source-to-source transformation pass before every store + * to model-allocated memory). + * + * The WRMEM(lval) macro encapsulates the pattern: call WriteMemory() with the address + * and size of the destination, then perform the assignment. For bulk writes (memcpy, + * memset, loops), WriteMemory() is called with the full region before the operation. + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include "application.h" + +#include +#include +#include +#include + +/** + * WRMEM(lval) — instrument a scalar write. + * Usage: WRMEM(ptr->field) = value; + * Expands to: notify dirty-tracking, then yield lval as an lvalue. + */ +#define WRMEM(lval) (*(WriteMemory(&(lval), sizeof(lval)), &(lval))) + +/** + * WRMEM_BUF(ptr, size) — instrument a bulk write (memcpy/memset/loop). + * Call once before performing any bulk write to model-allocated memory. + */ +#define WRMEM_BUF(ptr, size) (WriteMemory((ptr), (size))) + +/* ----------------------------------------------------------------------- + * Instrumented helper functions. Replaces functions.c for this test. + * A refactor may probably be required here, but I am now happy with this. + * -------------------------------------------------------------------- */ + +uint32_t crc_update(const uint64_t *buf, size_t n, uint32_t crc); + +buffer *get_buffer(buffer *head, unsigned i) +{ + while(i--) + head = head->next; + return head; +} + +uint32_t read_buffer(buffer *head, const unsigned i, const uint32_t old_crc) +{ + head = get_buffer(head, i); + return crc_update(head->data, head->count, old_crc); +} + +buffer *allocate_buffer(lp_state *state, const unsigned *data, const unsigned count) +{ + buffer *buf = rs_malloc(sizeof(buffer) + count * sizeof(uint64_t)); + + /* + * buf->next, buf->count, and buf->data[] are freshly allocated memory. + * They will be tracked automatically if instrumentation marks the entire + * allocated block; but since we are explicit, call WriteMemory for each + * field we write before writing it. + */ + WRMEM(buf->next) = state->head; + WRMEM(buf->count) = count; + + if(data != NULL) { + WRMEM_BUF(buf->data, count * sizeof(uint64_t)); + memcpy(buf->data, data, count * sizeof(uint64_t)); + } else { + for(unsigned i = 0; i < count; i++) { + /* + * rng_random_u modifies state->rng_state in place — + * that write is to model-allocated memory too. + */ + WRMEM(state->rng_state); + WRMEM(buf->data[i]) = rng_random_u(&state->rng_state); + } + } + + return buf; +} + +buffer *deallocate_buffer(buffer *head, const unsigned i) +{ + buffer *prev = NULL; + buffer *to_free = head; + + for(unsigned j = 0; j < i; j++) { + prev = to_free; + to_free = to_free->next; + } + + if(prev != NULL) { + WRMEM(prev->next) = to_free->next; + rs_free(to_free); + return head; + } + + prev = head->next; + rs_free(head); + return prev; +} + +// CRC table, in static storage, not model-allocated, no WriteMemory needed. +static uint32_t crc_table[256]; + +void crc_table_init(void) +{ + uint32_t n = 256; + while(n--) { + uint32_t c = n; + int k = 8; + while(k--) { + if(c & 1) + c = 0xedb88320UL ^ (c >> 1); + else + c = c >> 1; + } + crc_table[n] = c; + } +} + +uint32_t crc_update(const uint64_t *buf, size_t n, const uint32_t crc) +{ + uint32_t c = crc ^ 0xffffffffUL; + while(n--) { + unsigned k = 64; + do { + k -= 8; + c = crc_table[(c ^ (buf[n] >> k)) & 0xff] ^ (c >> 8); + } while(k); + } + return c ^ 0xffffffffUL; +} + +/* ----------------------------------------------------------------------- + * Instrumented ProcessEvent. Replaces application.c for this test. + * Maybe here a refactor will be useful. + * -------------------------------------------------------------------- */ + +#define do_random() (WriteMemory(&state->rng_state, sizeof(state->rng_state)), rng_random(&state->rng_state)) + +void ProcessEvent(const lp_id_t me, const simtime_t now, const unsigned event_type, const void *event_content, + const unsigned event_size, void *const st) +{ + lp_state *state = st; + if(state && state->events >= COMPLETE_EVENTS) { + if(event_type == LP_FINI) { + if(model_expected_output[me] != state->total_checksum) { + puts("[ERROR] Incorrect output!"); + abort(); + } + while(state->head) + state->head = deallocate_buffer(state->head, 0); + rs_free(state); + } + return; + } + + if(!state && event_type != LP_INIT) { + puts("[ERROR] Requested to process a weird event!"); + abort(); + } + + switch(event_type) { + case LP_INIT: + state = rs_malloc(sizeof(lp_state)); + if(state == NULL) + exit(-1); + + /* + * memset writes the entire lp_state, notify WriteMemory first. + * After this, rng_init writes only rng_state (also inside state). + */ + WRMEM_BUF(state, sizeof(lp_state)); + memset(state, 0, sizeof(lp_state)); + + WRMEM(state->rng_state); + rng_init(&state->rng_state, ((test_rng_state)me + 1) * 4390023366657240769ULL); + SetState(state); + + { + const unsigned buffers_to_allocate = do_random() * MAX_BUFFERS; + for(unsigned i = 0; i < buffers_to_allocate; ++i) { + const unsigned c = do_random() * MAX_BUFFER_SIZE / sizeof(uint64_t); + WRMEM(state->head) = allocate_buffer(state, NULL, c); + WRMEM(state->buffer_count)++; + } + } + + ScheduleNewEvent(me, 20 * do_random(), LOOP, NULL, 0); + break; + + case LOOP: + if(do_random() < NULLING_PROBABILITY) + return; + WRMEM(state->events)++; + ScheduleNewEvent(me, now + do_random() * 10, LOOP, NULL, 0); + { + lp_id_t dest = do_random() * N_LPS; + if(do_random() < DOUBLING_PROBABILITY && dest != me) + ScheduleNewEvent(dest, now + do_random() * 10, LOOP, NULL, 0); + + if(state->buffer_count) + WRMEM(state->total_checksum) = read_buffer(state->head, + do_random() * state->buffer_count, state->total_checksum); + + if(state->buffer_count < MAX_BUFFERS && do_random() < ALLOC_PROBABILITY) { + const unsigned c = do_random() * MAX_BUFFER_SIZE / sizeof(uint64_t); + WRMEM(state->head) = allocate_buffer(state, NULL, c); + WRMEM(state->buffer_count)++; + } + + if(state->buffer_count && do_random() < DEALLOC_PROBABILITY) { + WRMEM(state->head) = + deallocate_buffer(state->head, do_random() * state->buffer_count); + WRMEM(state->buffer_count)--; + } + + if(state->buffer_count && do_random() < SEND_PROBABILITY) { + const unsigned i = do_random() * state->buffer_count; + const buffer *to_send = get_buffer(state->head, i); + + dest = do_random() * N_LPS; + ScheduleNewEvent(dest, now + do_random() * 10, RECEIVE, to_send->data, + to_send->count * sizeof(uint64_t)); + + WRMEM(state->head) = deallocate_buffer(state->head, i); + WRMEM(state->buffer_count)--; + } + } + break; + + case RECEIVE: + if(do_random() < NULLING_PROBABILITY) + return; + if(state->buffer_count >= MAX_BUFFERS) + break; + WRMEM(state->head) = allocate_buffer(state, event_content, event_size / sizeof(uint64_t)); + WRMEM(state->buffer_count)++; + break; + + default: + puts("[ERROR] Requested to process an unknown event!"); + abort(); + } +} + +bool CanEnd(lp_id_t me, const void *snapshot) +{ + (void)me; + const lp_state *state = snapshot; + return state->events >= COMPLETE_EVENTS; +} diff --git a/test/integration/correctness/timewarp_incremental.c b/test/integration/correctness/timewarp_incremental.c new file mode 100644 index 00000000..f5bb22d4 --- /dev/null +++ b/test/integration/correctness/timewarp_incremental.c @@ -0,0 +1,46 @@ +/** + * @file test/integration/correctness/timewarp_incremental.c + * + * @brief Test: correctness of the Time Warp runtime with incremental checkpointing + * + * Verifies that incremental checkpointing produces bit-identical LP state to a + * reference serial execution. The model (application_incremental.c) calls + * WriteMemory() explicitly before every write to model-allocated memory, + * exactly as a compiler instrumentation pass would inject automatically. + * The CRC checksum computed at LP_FINI is compared against the reference output + * from the serial run. + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include + +#include "application.h" + +struct simulation_configuration conf = { + .lps = N_LPS, + .n_threads = 2, + .termination_time = 0.0, + .gvt_period = 100000, + .log_level = LOG_SILENT, + .stats_file = NULL, + .ckpt_interval = 0, + .incremental_ckpt = true, + .full_ckpt_period = 10, + .core_binding = false, + .synchronization = TIME_WARP, + .dispatcher = ProcessEvent, + .committed = CanEnd, +}; + +static int correctness(void *config) +{ + RootsimInit(config); + return RootsimRun(); +} + +int main(void) +{ + crc_table_init(); + test("Correctness test (parallel, incremental checkpointing)", correctness, &conf); +} diff --git a/test/integration/phold_incremental.c b/test/integration/phold_incremental.c new file mode 100644 index 00000000..7834677b --- /dev/null +++ b/test/integration/phold_incremental.c @@ -0,0 +1,126 @@ +/** + * @file test/integration/phold_incremental.c + * + * @brief A PHOLD benchmark with incremental checkpointing enabled + * + * This test runs the PHOLD workload with incremental checkpointing to validate + * correctness and performance of the incremental checkpointing path under a + * realistic Time Warp simulation workload. + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include + +#include + +#include +#include +#include +#include + +#ifndef NUM_LPS +#define NUM_LPS 8192 +#endif + +#ifndef NUM_THREADS +#define NUM_THREADS 0 +#endif + +#define EVENT 1 + +struct phold_state { + __uint128_t seed; +}; + +struct phold_message { + long int dummy_data; +}; + +static simtime_t p_remote = 0.25; +static simtime_t mean = 1.0; +static simtime_t lookahead = 0.0; +static int start_events = 1; + +static double Random(struct phold_state *state) +{ + const __uint128_t multiplier = (((__uint128_t)0x0fc94e3bf4e9ab32ULL) << 64) + 0x866458cd56f5e605ULL; + state->seed *= multiplier; + const uint64_t ret = state->seed >> 64u; + return (double)ret / (double)UINT64_MAX; +} + +static double Expent(struct phold_state *state) +{ + return -mean * log(1 - Random(state)); +} + +void ProcessEvent(lp_id_t me, simtime_t now, unsigned event_type, const void *event_content, unsigned event_size, + void *st) +{ + (void)event_size; + (void)now; + struct phold_state *state = st; + + switch(event_type) { + case LP_INIT: + state = rs_malloc(sizeof(*state)); + SetState(state); + state->seed = me + 1; + for(int i = 0; i < start_events; i++) { + lp_id_t dest = (lp_id_t)(Random(state) * NUM_LPS); + simtime_t ts = now + lookahead + Expent(state); + ScheduleNewEvent(dest, ts, EVENT, NULL, 0); + } + break; + + case EVENT: + { + struct phold_message msg = { + .dummy_data = (long int)(event_content ? *(const long int *)event_content : 0)}; + lp_id_t dest; + if(Random(state) < p_remote) + dest = (lp_id_t)(Random(state) * NUM_LPS); + else + dest = me; + simtime_t ts = now + lookahead + Expent(state); + ScheduleNewEvent(dest, ts, EVENT, &msg, sizeof(msg)); + break; + } + + case LP_FINI: + rs_free(state); + break; + + default: + fprintf(stderr, "Unknown event type\n"); + abort(); + } +} + +bool CanEnd(_unused lp_id_t me, _unused const void *snapshot) +{ + return false; +} + +struct simulation_configuration conf = { + .lps = NUM_LPS, + .n_threads = NUM_THREADS, + .termination_time = 1000, + .gvt_period = 1000, + .log_level = LOG_INFO, + .stats_file = "phold_incremental", + .ckpt_interval = 0, + .incremental_ckpt = true, + .full_ckpt_period = 10, + .core_binding = true, + .synchronization = TIME_WARP, + .dispatcher = ProcessEvent, + .committed = CanEnd, +}; + +int main(void) +{ + RootsimInit(&conf); + return RootsimRun(); +} diff --git a/test/mm/buddy_hard.c b/test/mm/buddy_hard.c index 3a1308b5..b75b7183 100644 --- a/test/mm/buddy_hard.c +++ b/test/mm/buddy_hard.c @@ -46,7 +46,7 @@ static void allocation_init(struct alc *alc) abort(); } alc->c = c; - // __write_mem(alc->ptr, alc->c * sizeof(unsigned)); + // WriteMemory(alc->ptr, alc->c * sizeof(unsigned)); while(c--) { const unsigned v = test_random_u(); @@ -95,7 +95,7 @@ static void allocation_partial_write(struct alc *alc, const unsigned p) const unsigned e = test_random_range(c + 1); const unsigned l = test_random_range(e + 1); - // __write_mem(alc[i].ptr + l, (e - l) * sizeof(unsigned)); + // WriteMemory(alc[i].ptr + l, (e - l) * sizeof(unsigned)); for(unsigned j = l; j < e; ++j) { const unsigned v = test_random_u(); diff --git a/test/mm/incremental.c b/test/mm/incremental.c new file mode 100644 index 00000000..a7793eca --- /dev/null +++ b/test/mm/incremental.c @@ -0,0 +1,355 @@ +/** + * @file test/mm/incremental.c + * + * @brief Test: Incremental checkpointing at buddy-system and WriteMemory level + * + * Tests cover two groups: + * Group A: WriteMemory dirty-bitmap marking — verifies that the compiler-injected + * instrumentation hook correctly marks dirty blocks in the bitmap. + * Group B: Buddy-level incremental checkpoint take/restore — verifies the full + * checkpoint chain: full → incremental → incremental → restore. + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +/* ------------------------------------------------------------------------- + * Group A: WriteMemory dirty-bitmap marking tests + * ---------------------------------------------------------------------- */ + +/** + * Test that WriteMemory is a no-op when incremental_ckpt is disabled. + */ +static int test_write_mem_disabled(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + + /* Incremental disabled: WriteMemory must be a no-op. */ + global_config.incremental_ckpt = false; + + unsigned char *buf = rs_malloc(128); + test_assert(buf != NULL); + + /* Record state of dirty bitmaps before. */ + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, buf); + test_assert(buddy != NULL); + + /* Clear dirty bits (they may have been set by malloc in incremental mode). */ + buddy_dirty_reset(buddy); + + /* Call WriteMemory: should NOT set any dirty bits. */ + WriteMemory(buf, 128); + + int errs = 0; + for(size_t i = 0; i < sizeof(buddy->dirty); i++) + errs += buddy->dirty[i] != 0; + + rs_free(buf); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return errs > 0; +} + +/** + * Test that WriteMemory correctly marks dirty bits when enabled. + * Writes to a known address and verifies the corresponding bitmap bits are set. + */ +static int test_write_mem_marks_dirty(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + /* Allocate a 64-byte block (one bitmap block). */ + unsigned char *buf = rs_malloc(1U << B_BLOCK_EXP); + test_assert(buf != NULL); + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, buf); + test_assert(buddy != NULL); + + /* Reset dirty bitmaps. */ + buddy_dirty_reset(buddy); + + /* Mark as dirty via WriteMemory. */ + WriteMemory(buf, 1U << B_BLOCK_EXP); + + /* At least one dirty bit in the base_mem region must be set. */ + int errs = 1; + const uint32_t tree_blocks = (1U << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); + for(uint32_t i = tree_blocks; i < tree_blocks + (1U << (B_TOTAL_EXP - B_BLOCK_EXP)); i++) { + if(bitmap_check(buddy->dirty, i)) { + errs = 0; + break; + } + } + + rs_free(buf); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return errs; +} + +/** + * Test that WriteMemory is a no-op for pointers outside the buddy range. + */ +static int test_write_mem_out_of_range(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + unsigned char *buf = rs_malloc(64); + test_assert(buf != NULL); + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, buf); + test_assert(buddy != NULL); + buddy_dirty_reset(buddy); + + /* Pointer before the first buddy: should be ignored. */ + WriteMemory((char *)array_get_at(lp->mm_state.buddies, 0) - 1, 64); + + int errs = 0; + for(size_t i = 0; i < sizeof(buddy->dirty); i++) + errs += buddy->dirty[i] != 0; + + rs_free(buf); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return errs > 0; +} + +/** + * Test that buddy_malloc sets dirty bits in the tree region when incremental is enabled. + */ +static int test_alloc_marks_tree_dirty(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + /* Must allocate first so the buddy system is created. */ + void *buf = rs_malloc(64); + test_assert(buf != NULL); + + /* Get the buddy that owns buf and reset dirty bits. */ + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, buf); + test_assert(buddy != NULL); + buddy_dirty_reset(buddy); + + /* Free and re-alloc: buddy_malloc and buddy_free must dirty tree bits. */ + rs_free(buf); + buf = rs_malloc(64); + test_assert(buf != NULL); + buddy = buddy_find_by_address(&lp->mm_state, buf); + + int found = 0; + const uint32_t tree_blocks = (1U << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); + for(uint32_t i = 0; i < tree_blocks; i++) { + if(bitmap_check(buddy->dirty, i)) { + found = 1; + break; + } + } + + rs_free(buf); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return !found; +} + +/* ------------------------------------------------------------------------- + * Group B: Buddy-level incremental checkpoint take/restore + * ---------------------------------------------------------------------- */ + +#define INCR_TEST_PATTERN_A 0xAAAAAAAAAAAAAAAAULL +#define INCR_TEST_PATTERN_B 0xBBBBBBBBBBBBBBBBULL +#define INCR_TEST_PATTERN_C 0xCCCCCCCCCCCCCCCCULL + +/** + * Test incremental take/restore for a single buddy, single level. + * Sequence: alloc → full checkpoint → modify → incremental checkpoint → + * corrupt → restore incremental → verify. + */ +static int test_incremental_single(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + /* Allocate two 64-byte blocks and fill with pattern A. */ + uint64_t *a = rs_malloc(64); + uint64_t *b = rs_malloc(64); + test_assert(a && b); + memset(a, 0xAA, 64); + memset(b, 0xAA, 64); + + /* Force full checkpoint at index 0. */ + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + /* Modify b with pattern B, mark dirty via WriteMemory. */ + WriteMemory(b, 64); + memset(b, 0xBB, 64); + + /* Take incremental checkpoint at index 1. */ + model_allocator_checkpoint_take(&lp->mm_state, 1); + + /* Corrupt both blocks. */ + memset(a, 0xFF, 64); + memset(b, 0xFF, 64); + + /* Restore to incremental checkpoint (index 1). + * b should be restored to pattern B; a was not dirty so stays FF + * (it will be restored from full when chain walk reaches it). */ + model_allocator_checkpoint_restore(&lp->mm_state, 1); + + /* After restore to checkpoint 1: b = BB (was dirty in incr), a = AA (from full or not modified) */ + int errs = 0; + for(int i = 0; i < 8; i++) { + errs += b[i] != INCR_TEST_PATTERN_B; + errs += a[i] != INCR_TEST_PATTERN_A; + } + + rs_free(a); + rs_free(b); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return errs > 0; +} + +/** + * Test a chain: full checkpoint → incr 1 → incr 2 → restore to full. + * Verifies that both incremental logs are properly skipped during fossil + * collection and the chain walk reaches the full checkpoint. + */ +static int test_incremental_chain(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + uint64_t *a = rs_malloc(64); + uint64_t *b = rs_malloc(64); + test_assert(a && b); + + /* Initial state: pattern A. */ + memset(a, 0xAA, 64); + memset(b, 0xAA, 64); + + /* Full checkpoint at index 0. */ + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + /* Modify a → incremental checkpoint at index 1. */ + WriteMemory(a, 64); + memset(a, 0xBB, 64); + model_allocator_checkpoint_take(&lp->mm_state, 1); + + /* Modify b → incremental checkpoint at index 2. */ + WriteMemory(b, 64); + memset(b, 0xCC, 64); + model_allocator_checkpoint_take(&lp->mm_state, 2); + + /* Corrupt everything. */ + memset(a, 0xFF, 64); + memset(b, 0xFF, 64); + + /* Restore to checkpoint 0 (full): both blocks should be AA. */ + model_allocator_checkpoint_restore(&lp->mm_state, 0); + + int errs = 0; + for(int i = 0; i < 8; i++) { + errs += a[i] != INCR_TEST_PATTERN_A; + errs += b[i] != INCR_TEST_PATTERN_A; + } + + rs_free(a); + rs_free(b); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + return errs > 0; +} + +/** + * Test that full checkpointing still works correctly when incremental mode + * is enabled (full checkpoint resets dirty bitmaps). + */ +static int test_full_resets_dirty(void *_) +{ + (void)_; + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = true; + + void *buf = rs_malloc(64); + test_assert(buf != NULL); + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, buf); + + /* Some dirty bits should be set from malloc. */ + int any_dirty_before = 0; + for(size_t i = 0; i < sizeof(buddy->dirty); i++) + any_dirty_before |= buddy->dirty[i]; + + /* Full checkpoint must reset dirty bitmap. */ + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + int any_dirty_after = 0; + for(size_t i = 0; i < sizeof(buddy->dirty); i++) + any_dirty_after |= buddy->dirty[i]; + + rs_free(buf); + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + + /* Must have had dirty bits before, none after the full checkpoint. */ + return !any_dirty_before || any_dirty_after; +} + +/* ------------------------------------------------------------------------- + * Test entry point + * ---------------------------------------------------------------------- */ + +int incremental_checkpoint_test(_unused void *_) +{ + int errs = 0; + + /* Group A: WriteMemory */ + errs += test_write_mem_disabled(NULL); + errs += test_write_mem_marks_dirty(NULL); + errs += test_write_mem_out_of_range(NULL); + errs += test_alloc_marks_tree_dirty(NULL); + + /* Group B: incremental checkpoint chain */ + errs += test_incremental_single(NULL); + errs += test_incremental_chain(NULL); + errs += test_full_resets_dirty(NULL); + + return errs > 0; +} diff --git a/test/mm/main.c b/test/mm/main.c index ac1e8f6b..91e5d91e 100644 --- a/test/mm/main.c +++ b/test/mm/main.c @@ -15,6 +15,8 @@ extern int model_allocator_test(void *); extern int model_allocator_test_hard(void *); extern int parallel_malloc_test(void *); +extern int incremental_checkpoint_test(void *); +extern int model_allocator_full_test(void *); int main(void) { @@ -23,4 +25,6 @@ int main(void) test("Testing buddy system", model_allocator_test, NULL); test("Testing buddy system (hard test)", model_allocator_test_hard, NULL); test("Testing parallel memory operations", parallel_malloc_test, NULL); + test("Testing incremental checkpointing", incremental_checkpoint_test, NULL); + test("Testing model allocator (full + incremental)", model_allocator_full_test, NULL); } diff --git a/test/mm/model_allocator.c b/test/mm/model_allocator.c new file mode 100644 index 00000000..787a6c05 --- /dev/null +++ b/test/mm/model_allocator.c @@ -0,0 +1,885 @@ +/** + * @file test/mm/model_allocator.c + * + * @brief Test: model allocator: rs_malloc/calloc/free/realloc + checkpointing + * + * Covers all functionalities of the model allocator in both full and + * incremental checkpointing modes. The tests are organized in groups: + * + * Group A: lp_init field initialisation + * Group B: rs_malloc / rs_free / buddy_find_by_address / full_ckpt_size + * Group C: rs_calloc (zero-fill + dirty mark) + * Group D: rs_realloc (NULL-ptr, zero-size, in-place, copy path, dirty mark) + * Group E: full checkpointing: ckpt_size / incr_ckpt_size fields, restore + * Group F: incremental checkpointing: take / restore mid-chain / incr_ckpt_size + * Group G: model_allocator_fossil_lp_collect: ref_idx rebase, chain anchor + * + * SPDX-FileCopyrightText: 2008-2025 HPCS Group + * SPDX-License-Identifier: GPL-3.0-only + */ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#define BUDDY_CAPACITY (1U << B_TOTAL_EXP) +#define BLOCK_SIZE (1U << B_BLOCK_EXP) + +static struct lp_ctx *setup_lp(bool incremental) +{ + struct lp_ctx *lp = test_lp_mock_get(); + current_lp = lp; + model_allocator_lp_init(&lp->mm_state); + global_config.incremental_ckpt = incremental; + global_config.full_ckpt_period = 0; + return lp; +} + +static void teardown_lp(struct lp_ctx *lp) +{ + model_allocator_lp_fini(&lp->mm_state); + global_config.incremental_ckpt = false; + global_config.full_ckpt_period = 0; +} + +/* ====================================== + * Group A: lp_init field initialisation + * ====================================== */ + +/** + * After model_allocator_lp_init, all mm_state fields must be in a well-defined + * initial state: empty arrays, correct full_ckpt_size baseline, and zero/NULL + * for the incremental fields. + */ +static int test_lp_init_fields(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + struct mm_state *s = &lp->mm_state; + + test_assert(array_count(s->buddies) == 0); + test_assert(array_count(s->logs) == 0); + + /* + * full_ckpt_size baseline = header + sentinel pointer: + * offsetof(mm_checkpoint, chkps) + sizeof(buddy_state *) + */ + const uint_fast32_t expected_base = offsetof(struct mm_checkpoint, chkps) + sizeof(struct buddy_state *); + test_assert(s->full_ckpt_size == expected_base); + + test_assert(s->force_full == false); + test_assert(s->ckpt_since_last_full == 0); + test_assert(s->last_dirty_buddy == NULL); + + teardown_lp(lp); + return 0; +} + +/* ====================================================================== + * Group B: rs_malloc / rs_free / buddy_find_by_address / full_ckpt_size + * ====================================================================== */ + +/** + * rs_malloc(0) must return NULL without touching mm_state. + */ +static int test_malloc_zero(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + uint_fast32_t size_before = lp->mm_state.full_ckpt_size; + void *p = rs_malloc(0); + test_assert(p == NULL); + test_assert(lp->mm_state.full_ckpt_size == size_before); + test_assert(array_count(lp->mm_state.buddies) == 0); + + teardown_lp(lp); + return 0; +} + +/** + * A small allocation must: + * - return a non-NULL pointer + * - create exactly one buddy system + * - increase full_ckpt_size by BLOCK_SIZE + buddy-header overhead + * After free, full_ckpt_size must drop by BLOCK_SIZE. + */ +static int test_malloc_single_block(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + const uint_fast32_t base = lp->mm_state.full_ckpt_size; + void *p = rs_malloc(1); + test_assert(p != NULL); + test_assert(array_count(lp->mm_state.buddies) == 1); + + const uint_fast32_t buddy_hdr = offsetof(struct buddy_checkpoint, base_mem); + test_assert(lp->mm_state.full_ckpt_size == base + BLOCK_SIZE + buddy_hdr); + + rs_free(p); + test_assert(lp->mm_state.full_ckpt_size == base + buddy_hdr); + + teardown_lp(lp); + return 0; +} + +/** rs_free(NULL) must be a safe no-op. */ +static int test_free_null(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + uint_fast32_t size_with = lp->mm_state.full_ckpt_size; + + rs_free(NULL); + test_assert(lp->mm_state.full_ckpt_size == size_with); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * buddy_find_by_address must return the buddy that owns a given pointer, + * and the pointer must lie in that buddy's base_mem range. + */ +static int test_find_by_address(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *p = rs_malloc(BLOCK_SIZE * 4); + test_assert(p != NULL); + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, p); + test_assert(buddy != NULL); + test_assert((unsigned char *)p >= buddy->base_mem); + test_assert((unsigned char *)p < buddy->base_mem + BUDDY_CAPACITY); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * When the first buddy is full a second must be allocated and sorted into + * the array. + * buddy_find_by_address must correctly resolve the pointer in the second buddy. + */ +static int test_malloc_second_buddy(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *fill = rs_malloc(BUDDY_CAPACITY); + test_assert(fill != NULL); + test_assert(array_count(lp->mm_state.buddies) == 1); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + test_assert(array_count(lp->mm_state.buddies) == 2); + + test_assert(array_get_at(lp->mm_state.buddies, 0) < array_get_at(lp->mm_state.buddies, 1)); + + struct buddy_state *b = buddy_find_by_address(&lp->mm_state, p); + test_assert(b != NULL); + test_assert((unsigned char *)p >= b->base_mem); + test_assert((unsigned char *)p < b->base_mem + BUDDY_CAPACITY); + + rs_free(fill); + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * full_ckpt_size must be consistent: the full checkpoint's ckpt_size and + * incr_ckpt_size must both equal full_ckpt_size after a full checkpoint. + */ +static int test_full_ckpt_size_consistency(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *ptrs[8]; + const size_t sizes[8] = {64, 128, 64, 256, 64, 128, 64, 128}; + for(int i = 0; i < 8; i++) + ptrs[i] = rs_malloc(sizes[i]); + + rs_free(ptrs[2]); + ptrs[2] = NULL; + rs_free(ptrs[5]); + ptrs[5] = NULL; + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + test_assert(array_count(lp->mm_state.logs) == 1); + struct mm_log entry = array_get_at(lp->mm_state.logs, 0); + test_assert(!is_log_incremental(entry)); + + struct mm_checkpoint *ckpt = log_get_ckpt(entry); + test_assert(ckpt->ckpt_size == lp->mm_state.full_ckpt_size); + test_assert(ckpt->incr_ckpt_size == lp->mm_state.full_ckpt_size); + + for(int i = 0; i < 8; i++) + if(ptrs[i]) + rs_free(ptrs[i]); + teardown_lp(lp); + return 0; +} + +/* =================== + * Group C: rs_calloc + * =================== */ + +/** rs_calloc must return a zero-filled region. */ +static int test_calloc_zeroed(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + const size_t n = 16, sz = 8; + unsigned char *p = rs_calloc(n, sz); + test_assert(p != NULL); + + int errs = 0; + for(size_t i = 0; i < n * sz; i++) + errs += p[i] != 0; + + rs_free(p); + teardown_lp(lp); + return errs > 0; +} + +/** + * In incremental mode, rs_calloc must mark the data region dirty so the + * zero-fill is captured in the next incremental checkpoint. + */ +static int test_calloc_marks_dirty(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + unsigned char *p = rs_calloc(1, BLOCK_SIZE); + test_assert(p != NULL); + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, p); + test_assert(buddy != NULL); + + const uint32_t tree_blocks = (1U << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); + int found = 0; + for(uint32_t i = tree_blocks; i < tree_blocks + (1U << (B_TOTAL_EXP - B_BLOCK_EXP)); i++) { + if(bitmap_check(buddy->dirty, i)) { + found = 1; + break; + } + } + + rs_free(p); + teardown_lp(lp); + return !found; +} + +/* ===================== + * Group D: rs_realloc + * ===================== */ + +/** rs_realloc(NULL, size) must behave like rs_malloc(size). */ +static int test_realloc_null_ptr(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *p = rs_realloc(NULL, BLOCK_SIZE * 2); + test_assert(p != NULL); + test_assert(array_count(lp->mm_state.buddies) == 1); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** rs_realloc(NULL, 0) and rs_realloc(ptr, 0) must return NULL + EINVAL. */ +static int test_realloc_zero_size(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + errno = 0; + void *r = rs_realloc(NULL, 0); + test_assert(r == NULL); + test_assert(errno == EINVAL); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + errno = 0; + r = rs_realloc(p, 0); + test_assert(r == NULL); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * rs_realloc to the same rounded block size must be in-place: + * the pointer must be unchanged, and data must be preserved. + */ +static int test_realloc_same_size_inplace(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + memset(p, 0xAB, BLOCK_SIZE); + + void *q = rs_realloc(p, BLOCK_SIZE); + test_assert(q == p); + + int errs = 0; + for(size_t i = 0; i < BLOCK_SIZE; i++) + errs += ((unsigned char *)q)[i] != 0xAB; + + rs_free(q); + teardown_lp(lp); + return errs > 0; +} + +/** + * rs_realloc to a larger size (different rounded block size) triggers the + * alloc-copy-free path. In incremental mode, the destination must be dirtied. + */ +static int test_realloc_copy_path(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + unsigned char *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + memset(p, 0xCD, BLOCK_SIZE); + WriteMemory(p, BLOCK_SIZE); + + const size_t new_size = BLOCK_SIZE * 4; + unsigned char *q = rs_realloc(p, new_size); + test_assert(q != NULL); + + int errs = 0; + for(size_t i = 0; i < BLOCK_SIZE; i++) + errs += q[i] != 0xCD; + + struct buddy_state *buddy = buddy_find_by_address(&lp->mm_state, q); + test_assert(buddy != NULL); + const uint32_t tree_blocks = (1U << (B_TOTAL_EXP - 2 * B_BLOCK_EXP + 1)); + int dirty_found = 0; + for(uint32_t i = tree_blocks; i < tree_blocks + (1U << (B_TOTAL_EXP - B_BLOCK_EXP)); i++) { + if(bitmap_check(buddy->dirty, i)) { + dirty_found = 1; + break; + } + } + errs += !dirty_found; + + rs_free(q); + teardown_lp(lp); + return errs > 0; +} + +/* ============================= + * Group E: Full checkpointing + * ============================= */ + +/** + * A full checkpoint + restore must reproduce the exact memory contents + * present at checkpoint time. + */ +static int test_full_checkpoint_restore(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + uint64_t *buf = rs_malloc(sizeof(uint64_t) * 8); + test_assert(buf != NULL); + for(uint64_t i = 0; i < 8; i++) + buf[i] = i * 0x1111111111111111ULL; + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + test_assert(array_count(lp->mm_state.logs) == 1); + test_assert(!is_log_incremental(array_get_at(lp->mm_state.logs, 0))); + + struct mm_checkpoint *ckpt = log_get_ckpt(array_get_at(lp->mm_state.logs, 0)); + test_assert(ckpt->ckpt_size == lp->mm_state.full_ckpt_size); + test_assert(ckpt->incr_ckpt_size == lp->mm_state.full_ckpt_size); + + memset(buf, 0xFF, sizeof(uint64_t) * 8); + model_allocator_checkpoint_restore(&lp->mm_state, 0); + + int errs = 0; + for(uint64_t i = 0; i < 8; i++) + errs += buf[i] != i * 0x1111111111111111ULL; + + rs_free(buf); + teardown_lp(lp); + return errs > 0; +} + +/** + * Multiple full checkpoints: restore to an intermediate one must reproduce + * that exact state, and later logs must be freed. + */ +static int test_full_multiple_checkpoints(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + uint64_t *buf = rs_malloc(sizeof(uint64_t) * 4); + test_assert(buf != NULL); + + memset(buf, 0xAA, sizeof(uint64_t) * 4); + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 10); + + memset(buf, 0xBB, sizeof(uint64_t) * 4); + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 20); + + memset(buf, 0xCC, sizeof(uint64_t) * 4); + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 30); + + test_assert(array_count(lp->mm_state.logs) == 3); + + // Restore to ref 20 (buf = BBBB) + array_count_t restored = model_allocator_checkpoint_restore(&lp->mm_state, 20); + test_assert(restored == 20); + + int errs = 0; + for(int i = 0; i < 4; i++) + errs += buf[i] != 0xBBBBBBBBBBBBBBBBULL; + + // Only checkpoints 0..1 must remain + test_assert(array_count(lp->mm_state.logs) == 2); + + // full_ckpt_size after restore must match the restored checkpoint + test_assert(lp->mm_state.full_ckpt_size == log_get_ckpt(array_get_at(lp->mm_state.logs, 1))->ckpt_size); + + rs_free(buf); + teardown_lp(lp); + return errs > 0; +} + +/** + * force_full must be cleared and ckpt_since_last_full reset to 0 after + * a full checkpoint is taken. + */ +static int test_force_full_flag_reset(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + test_assert(lp->mm_state.force_full == true); + + model_allocator_checkpoint_take(&lp->mm_state, 0); + test_assert(lp->mm_state.force_full == false); + test_assert(lp->mm_state.ckpt_since_last_full == 0); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/* ==================================== + * Group F: Incremental checkpointing + * ==================================== */ + +/** + * An incremental checkpoint must have incr_ckpt_size < ckpt_size + * (only dirty blocks are saved, not the whole state). + */ +static int test_incremental_smaller_than_full(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + uint64_t *buf = rs_malloc(sizeof(uint64_t) * 8); + test_assert(buf != NULL); + memset(buf, 0xAA, sizeof(uint64_t) * 8); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + // Dirty only one word + WriteMemory(&buf[0], sizeof(uint64_t)); + buf[0] = 0xDEADBEEFCAFEBABEULL; + model_allocator_checkpoint_take(&lp->mm_state, 1); + + test_assert(array_count(lp->mm_state.logs) == 2); + struct mm_log incr_log = array_get_at(lp->mm_state.logs, 1); + test_assert(is_log_incremental(incr_log)); + + struct mm_checkpoint *incr_ckpt = log_get_ckpt(incr_log); + test_assert(incr_ckpt->ckpt_size == lp->mm_state.full_ckpt_size); + test_assert(incr_ckpt->incr_ckpt_size < incr_ckpt->ckpt_size); + + rs_free(buf); + teardown_lp(lp); + return 0; +} + +/** + * With full_ckpt_period = 3, the 3rd incremental must be promoted to a full + * checkpoint, resetting ckpt_since_last_full to 0. + */ +static int test_full_ckpt_period(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + global_config.full_ckpt_period = 3; + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + test_assert(lp->mm_state.ckpt_since_last_full == 0); + + for(int i = 1; i <= 3; i++) { + WriteMemory(p, BLOCK_SIZE); + model_allocator_checkpoint_take(&lp->mm_state, (array_count_t)i); + } + + test_assert(lp->mm_state.ckpt_since_last_full == 0); + test_assert(!is_log_incremental(array_get_at(lp->mm_state.logs, array_count(lp->mm_state.logs) - 1))); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * Restore to an intermediate incremental in a chain (full→incr1→incr2): + * restoring to incr1 must produce the state at incr1. + */ +static int test_incremental_restore_mid_chain(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + uint64_t *a = rs_malloc(sizeof(uint64_t) * 8); + uint64_t *b = rs_malloc(sizeof(uint64_t) * 8); + test_assert(a && b); + + memset(a, 0xAA, sizeof(uint64_t) * 8); + memset(b, 0xAA, sizeof(uint64_t) * 8); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + WriteMemory(a, sizeof(uint64_t) * 8); + memset(a, 0xBB, sizeof(uint64_t) * 8); + model_allocator_checkpoint_take(&lp->mm_state, 1); + + WriteMemory(b, sizeof(uint64_t) * 8); + memset(b, 0xCC, sizeof(uint64_t) * 8); + model_allocator_checkpoint_take(&lp->mm_state, 2); + + memset(a, 0xFF, sizeof(uint64_t) * 8); + memset(b, 0xFF, sizeof(uint64_t) * 8); + + // Restore to incr1: a = BB, b = AA + array_count_t r = model_allocator_checkpoint_restore(&lp->mm_state, 1); + test_assert(r == 1); + + int errs = 0; + for(int i = 0; i < 8; i++) { + errs += a[i] != 0xBBBBBBBBBBBBBBBBULL; + errs += b[i] != 0xAAAAAAAAAAAAAAAAULL; + } + + test_assert(lp->mm_state.full_ckpt_size == log_get_ckpt(array_get_at(lp->mm_state.logs, 1))->ckpt_size); + + // Dirty bitmaps must be clear after restore + for(array_count_t i = 0; i < array_count(lp->mm_state.buddies); i++) { + struct buddy_state *buddy = array_get_at(lp->mm_state.buddies, i); + for(size_t j = 0; j < sizeof(buddy->dirty); j++) + errs += buddy->dirty[j] != 0; + } + + rs_free(a); + rs_free(b); + teardown_lp(lp); + return errs > 0; +} + +/** + * Restore to the full baseline from the tail of an incremental chain + * must reproduce the state at the full checkpoint. + */ +static int test_incremental_restore_to_full(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + + uint64_t *buf = rs_malloc(sizeof(uint64_t) * 8); + test_assert(buf != NULL); + memset(buf, 0xAA, sizeof(uint64_t) * 8); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + WriteMemory(buf, sizeof(uint64_t) * 8); + memset(buf, 0xBB, sizeof(uint64_t) * 8); + model_allocator_checkpoint_take(&lp->mm_state, 1); + + WriteMemory(buf, sizeof(uint64_t) * 8); + memset(buf, 0xCC, sizeof(uint64_t) * 8); + model_allocator_checkpoint_take(&lp->mm_state, 2); + + memset(buf, 0xFF, sizeof(uint64_t) * 8); + model_allocator_checkpoint_restore(&lp->mm_state, 0); + + int errs = 0; + for(int i = 0; i < 8; i++) + errs += buf[i] != 0xAAAAAAAAAAAAAAAAULL; + + rs_free(buf); + teardown_lp(lp); + return errs > 0; +} + +/** + * After restore, ckpt_since_last_full and last_dirty_buddy must both be + * reset regardless of how they were set before the restore. + */ +static int test_restore_resets_incremental_state(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + global_config.full_ckpt_period = 100; + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 0); + + for(int i = 1; i <= 3; i++) { + WriteMemory(p, BLOCK_SIZE); + model_allocator_checkpoint_take(&lp->mm_state, (array_count_t)i); + } + test_assert(lp->mm_state.ckpt_since_last_full > 0); + + // Warm the last_dirty_buddy pointer + WriteMemory(p, BLOCK_SIZE); + test_assert(lp->mm_state.last_dirty_buddy != NULL); + + model_allocator_checkpoint_restore(&lp->mm_state, 2); + test_assert(lp->mm_state.ckpt_since_last_full == 0); + test_assert(lp->mm_state.last_dirty_buddy == NULL); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/* ============================================ + * Group G: model_allocator_fossil_lp_collect + * ============================================ */ + +/** + * fossil_lp_collect on a sequence of full checkpoints must retain all + * checkpoints at or after the target, rebase their ref_idx by the freed + * checkpoint's ref_idx, and return that ref_idx. + */ +static int test_fossil_collect_full_only(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(false); + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + + for(int i = 1; i <= 3; i++) { + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, (array_count_t)(i * 10)); + } + test_assert(array_count(lp->mm_state.logs) == 3); + + // Collect up to ref_idx 15: checkpoint at 10 qualifies + array_count_t freed_ref = model_allocator_fossil_lp_collect(&lp->mm_state, 15); + test_assert(freed_ref == 10); + + /* + * fossil_lp_collect rebases all retained logs by -ref_i (= -10) but + * does NOT remove the anchor log itself. All three logs survive; + * their ref_idx values are shifted by -10. + */ + test_assert(array_count(lp->mm_state.logs) == 3); + test_assert(array_get_at(lp->mm_state.logs, 0).ref_idx == 0); // was 10 + test_assert(array_get_at(lp->mm_state.logs, 1).ref_idx == 10); // was 20 + test_assert(array_get_at(lp->mm_state.logs, 2).ref_idx == 20); // was 30 + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * fossil_lp_collect with an incremental chain must walk back to the full + * checkpoint anchor: the full checkpoint must be retained even if the + * target ref_idx falls within the incremental range. + */ +static int test_fossil_collect_incremental_anchor(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + global_config.full_ckpt_period = 100; + + void *p = rs_malloc(BLOCK_SIZE); + test_assert(p != NULL); + + // Full at 10, incremental at 20 and 30 + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 10); + + WriteMemory(p, BLOCK_SIZE); + model_allocator_checkpoint_take(&lp->mm_state, 20); + WriteMemory(p, BLOCK_SIZE); + model_allocator_checkpoint_take(&lp->mm_state, 30); + + test_assert(array_count(lp->mm_state.logs) == 3); + + /* + * Target 25: the last checkpoint at or before 25 is the incremental + * at 20. Must walk back to the full at 10, which must + * be retained as the chain anchor. All three logs survive. + */ + array_count_t freed_ref = model_allocator_fossil_lp_collect(&lp->mm_state, 25); + test_assert(freed_ref == 10); + test_assert(array_count(lp->mm_state.logs) == 3); + + rs_free(p); + teardown_lp(lp); + return 0; +} + +/** + * After fossil collection the retained chain must still allow a correct + * restore. + */ +static int test_fossil_then_restore(void *_) +{ + (void)_; + struct lp_ctx *lp = setup_lp(true); + global_config.full_ckpt_period = 100; + + uint64_t *buf = rs_malloc(sizeof(uint64_t) * 4); + test_assert(buf != NULL); + + // Full at 10 (AA) + memset(buf, 0xAA, sizeof(uint64_t) * 4); + model_allocator_checkpoint_next_force_full(&lp->mm_state); + model_allocator_checkpoint_take(&lp->mm_state, 10); + + // Incremental at 20 (BB) + WriteMemory(buf, sizeof(uint64_t) * 4); + memset(buf, 0xBB, sizeof(uint64_t) * 4); + model_allocator_checkpoint_take(&lp->mm_state, 20); + + // Full at 30 (CC) + model_allocator_checkpoint_next_force_full(&lp->mm_state); + WriteMemory(buf, sizeof(uint64_t) * 4); + memset(buf, 0xCC, sizeof(uint64_t) * 4); + model_allocator_checkpoint_take(&lp->mm_state, 30); + + /* + * GVT advances past 25: finds the incremental at 20, + * walks back to the full at 10 (anchor), and returns ref 10. + * fossil_lp_collect rebases all logs by -10 but keeps the anchor. + * All three checkpoints survive (full@0, incr@10, full@20 after rebase). + */ + array_count_t freed_ref = model_allocator_fossil_lp_collect(&lp->mm_state, 25); + test_assert(freed_ref == 10); + test_assert(array_count(lp->mm_state.logs) == 3); + + // The last log (full@30, rebased to 20) must not be incremental + test_assert(!is_log_incremental(array_get_at(lp->mm_state.logs, array_count(lp->mm_state.logs) - 1))); + + // Overwrite and restore: buf must become CC. Rebased ref = 30 - freed_ref + memset(buf, 0xFF, sizeof(uint64_t) * 4); + model_allocator_checkpoint_restore(&lp->mm_state, 30 - (array_count_t)freed_ref); + + int errs = 0; + for(int i = 0; i < 4; i++) + errs += buf[i] != 0xCCCCCCCCCCCCCCCCULL; + + rs_free(buf); + teardown_lp(lp); + return errs > 0; +} + +/* ================= + * Test entry point + * ================= */ + +int model_allocator_full_test(_unused void *_) +{ + int errs = 0; + + // Group A + errs += test_lp_init_fields(NULL); + + // Group B + errs += test_malloc_zero(NULL); + errs += test_malloc_single_block(NULL); + errs += test_free_null(NULL); + errs += test_find_by_address(NULL); + errs += test_malloc_second_buddy(NULL); + errs += test_full_ckpt_size_consistency(NULL); + + // Group C + errs += test_calloc_zeroed(NULL); + errs += test_calloc_marks_dirty(NULL); + + // Group D + errs += test_realloc_null_ptr(NULL); + errs += test_realloc_zero_size(NULL); + errs += test_realloc_same_size_inplace(NULL); + errs += test_realloc_copy_path(NULL); + + // Group E + errs += test_full_checkpoint_restore(NULL); + errs += test_full_multiple_checkpoints(NULL); + errs += test_force_full_flag_reset(NULL); + + // Group F + errs += test_incremental_smaller_than_full(NULL); + errs += test_full_ckpt_period(NULL); + errs += test_incremental_restore_mid_chain(NULL); + errs += test_incremental_restore_to_full(NULL); + errs += test_restore_resets_incremental_state(NULL); + + // Group G + errs += test_fossil_collect_full_only(NULL); + errs += test_fossil_collect_incremental_anchor(NULL); + errs += test_fossil_then_restore(NULL); + + return errs > 0; +}