From 0d2f466b11b405c285627ae6e72fdcb59e465ec2 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Sat, 3 Oct 2026 20:31:41 +0200 Subject: [PATCH] Count unsynced blocks using an extent event sweep instead of per-block lookups --- cmdline/selftest.c | 127 +++++++++++++++++++++++++++++++++++++ cmdline/state.c | 154 ++++++++++++++++++++++++++++++++++++++------- cmdline/state.h | 7 +++ 3 files changed, 264 insertions(+), 24 deletions(-) diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 0aae3f3b..0072b318 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -2245,6 +2245,130 @@ static void test_extent_run(void) file_free(file); } +static void test_unsynced_sweep(void) +{ + struct snapraid_state state; + struct snapraid_disk* disk1; + struct snapraid_disk* disk2; + struct snapraid_file* file1; + struct snapraid_file* file2; + struct snapraid_file* file3; + struct snapraid_file* file4; + struct snapraid_file* del; + block_off_t i; + static const struct { + block_off_t blockmax; + uint64_t unsynced; + } bounds[] = { + { 0, 0 }, { 10, 0 }, { 15, 0 }, { 16, 1 }, { 19, 4 }, + { 20, 5 }, { 21, 5 }, { 22, 6 }, { 23, 7 }, { 29, 7 }, + { 30, 7 }, { 31, 8 }, { 32, 9 }, { 33, 10 }, { 34, 11 }, + { 100, 11 } + }; + + memset(&state, 0, sizeof(state)); + state.block_size = 4096; + tommy_list_init(&state.disklist); + + if (fs_count_unsynced(&state, 100) != 0) { + log_fatal(EINTERNAL, "test_unsynced_sweep: no disks failed\n"); + exit(EXIT_FAILURE); + } + + disk1 = disk_alloc("d1", "", 0, "", 0); + disk2 = disk_alloc("d2", "", 0, "", 0); + tommy_list_insert_tail(&state.disklist, &disk1->node, disk1); + tommy_list_insert_tail(&state.disklist, &disk2->node, disk2); + + /* initially both disks are empty */ + if (fs_count_unsynced(&state, 100) != 0) { + log_fatal(EINTERNAL, "test_unsynced_sweep: empty disks failed\n"); + exit(EXIT_FAILURE); + } + + /* file1 on disk1: 10 blocks at parity 10..19 (first 5 BLK, next 5 CHG) */ + file1 = file_alloc(4096, "f1", 10 * 4096, 0, 0, 0); + for (i = 0; i < 5; ++i) + block_state_set(file_block(file1, i), BLOCK_STATE_BLK); + for (i = 5; i < 10; ++i) + block_state_set(file_block(file1, i), BLOCK_STATE_CHG); + fs_allocate(disk1, 10, file1, 0, 10); + + /* file2 on disk2: 20 blocks at parity 0..19 (all BLK) */ + file2 = file_alloc(4096, "f2", 20 * 4096, 0, 0, 0); + for (i = 0; i < 20; ++i) + block_state_set(file_block(file2, i), BLOCK_STATE_BLK); + fs_allocate(disk2, 0, file2, 0, 20); + + /* parity 0..14 has only BLK or EMPTY; parity 15..19 has CHG on disk1 and BLK on disk2 -> 5 unsynced */ + if (fs_count_unsynced(&state, 20) != 5) { + log_fatal(EINTERNAL, "test_unsynced_sweep: mixed block run failed\n"); + exit(EXIT_FAILURE); + } + + /* deleted run on disk1: 3 blocks at parity 20..22 */ + del = file_alloc(4096, "", 3 * 4096, 0, 0, 0); + for (i = 0; i < 3; ++i) + block_state_set(file_block(del, i), BLOCK_STATE_DELETED); + fs_allocate(disk1, 20, del, 0, 3); + + /* disk2 is empty at 20..22 -> DELETED alone does not count as unsynced */ + if (fs_count_unsynced(&state, 23) != 5) { + log_fatal(EINTERNAL, "test_unsynced_sweep: deleted without other file failed\n"); + exit(EXIT_FAILURE); + } + + /* file3 on disk2: first 2 blocks at parity 21..22 (BLK) */ + file3 = file_alloc(4096, "f3", 6 * 4096, 0, 0, 0); + for (i = 0; i < 2; ++i) + block_state_set(file_block(file3, i), BLOCK_STATE_BLK); + block_state_set(file_block(file3, 2), BLOCK_STATE_REP); + block_state_set(file_block(file3, 3), BLOCK_STATE_REP); + block_state_set(file_block(file3, 4), BLOCK_STATE_BLK); + block_state_set(file_block(file3, 5), BLOCK_STATE_REBUILD); + fs_allocate(disk2, 21, file3, 0, 2); + + /* parity 20: DELETED on d1, empty on d2 -> not unsynced; + * parity 21..22: DELETED on d1, BLK on d2 -> unsynced (2 blocks). Total 5 + 2 = 7 */ + if (fs_count_unsynced(&state, 23) != 7) { + log_fatal(EINTERNAL, "test_unsynced_sweep: deleted with file failed\n"); + exit(EXIT_FAILURE); + } + + /* the second extent of file3 starts at a nonzero file offset after a gap */ + fs_allocate(disk2, 30, file3, 2, 4); + + /* file4 shares extent boundaries with file3 and changes flags at 32 and 33 */ + file4 = file_alloc(4096, "f4", 4 * 4096, 0, 0, 0); + for (i = 0; i < 4; ++i) + block_state_set(file_block(file4, i), BLOCK_STATE_BLK); + block_state_set(file_block(file4, 2), BLOCK_STATE_CHG); + fs_allocate(disk1, 30, file4, 0, 4); + + /** + * Positions 30..33 are all unsynced: REP, REP, CHG, REBUILD respectively. + * At 32 and 33 the invalid block changes disks at the same event boundary; + * count each position once using the refreshed flags from both disks. + * Check limits in gaps, within extents and runs, and at their boundaries. + */ + for (i = 0; i < sizeof(bounds) / sizeof(bounds[0]); ++i) { + uint64_t count = fs_count_unsynced(&state, bounds[i].blockmax); + if (count != bounds[i].unsynced) { + log_fatal(EINTERNAL, "test_unsynced_sweep: limit %" PRIu64 " expected %" PRIu64 ", got %" PRIu64 "\n", + bounds[i].blockmax, bounds[i].unsynced, count); + exit(EXIT_FAILURE); + } + } + + file_free(file1); + file_free(file2); + file_free(file3); + file_free(file4); + file_free(del); + disk_free(disk1); + disk_free(disk2); +} + static void test_raid(void) { /* vandermonde raid parity generation with 32 data disks */ @@ -2965,6 +3089,9 @@ void test(int argc, char* argv[]) /* extent-aware file-to-parity run lookup */ test_extent_run(); + /* extent sweep for unsynced parity blocks */ + test_unsynced_sweep(); + /* wildcard and path pattern matching */ test_wnmatch(); diff --git a/cmdline/state.c b/cmdline/state.c index 613ea606..aca4f3ec 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -2197,31 +2197,139 @@ static inline int fs_is_block_deleted(struct snapraid_disk* disk, block_off_t po } /** - * Check if a block parity needs sync - * - * This is the same logic used in "status" to detect an incomplete "sync", - * that ignores invalid block, if they are not used by a file in any disk. - * - * This means that DELETED blocks won't necessarily imply an invalid parity. + * Advance a disk cursor to pos and cache its flags and next event boundary. + * Events occur at extent boundaries and changes in block flags within an extent. */ -static int fs_is_block_unsynced(struct snapraid_state* state, block_off_t pos) +static inline void disk_advance( + tommy_tree_node** cursor_ptr, + block_off_t pos, + block_off_t blockmax, + int* has_file_ptr, + int* has_invalid_ptr, + block_off_t* next_event_ptr, + size_t stride +) { - int one_invalid = 0; - int one_valid = 0; - for (tommy_node* i = state->disklist; i != 0; i = i->next) { - struct snapraid_disk* disk = i->data; - struct snapraid_block* block = fs_par2block_find(disk, pos); - - if (block_has_file(block)) - one_valid = 1; - if (block_has_invalid_parity(block)) - one_invalid = 1; - - if (one_invalid && one_valid) - return 1; + /* advance cursor if the current extent ends at or before pos */ + while (*cursor_ptr != 0) { + struct snapraid_extent* ext = (*cursor_ptr)->data; + if (ext->parity_pos + ext->count > pos) + break; + *cursor_ptr = tommy_tree_next(*cursor_ptr); } - return 0; + if (*cursor_ptr == 0) { + /* no more extents on this disk */ + *has_file_ptr = 0; + *has_invalid_ptr = 0; + *next_event_ptr = blockmax; + return; + } + + struct snapraid_extent* ext = (*cursor_ptr)->data; + if (ext->parity_pos > pos) { + /* in a gap before the next extent */ + *has_file_ptr = 0; + *has_invalid_ptr = 0; + *next_event_ptr = ext->parity_pos; + } else { + /* pos is inside this extent */ + block_off_t extent_rem = ext->parity_pos + ext->count - pos; + block_off_t file_pos = ext->file_pos + (pos - ext->parity_pos); + struct snapraid_block* block = file_block(ext->file, file_pos); + int has_file = block_has_file(block); + int has_invalid = block_has_invalid_parity(block); + block_off_t run_rem = 1; + const unsigned char* ptr = (const unsigned char*)block; + + *has_file_ptr = has_file; + *has_invalid_ptr = has_invalid; + + /* find how many consecutive blocks on this disk share the same flags */ + while (run_rem < extent_rem) { + const struct snapraid_block* next_b = (const struct snapraid_block*)(ptr + run_rem * stride); + if (block_has_file(next_b) != has_file || block_has_invalid_parity(next_b) != has_invalid) + break; + ++run_rem; + } + + *next_event_ptr = pos + run_rem; + } +} + +/** + * Count the number of unsynced parity positions using an event sweep across extents. + * + * An unsynced parity position is one where at least one disk has a live file + * block, and at least one disk has an invalid parity block (CHG, REP, REBUILD, + * or DELETED). + * + * The sweep takes O(B + R * D) time, where B is the number of blocks inspected, + * R is the number of event positions processed (including initialization), and + * D is the number of disks. Cached event boundaries avoid rescanning block runs. + * + * The caller must ensure that the disk list, extent trees, and block states + * remain unchanged during the count, as this function does not acquire locks. + */ +uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax) +{ + tommy_tree_node* cursors[RAID_DATA_MAX]; + block_off_t next_events[RAID_DATA_MAX]; + int disk_has_file[RAID_DATA_MAX]; + int disk_has_invalid[RAID_DATA_MAX]; + unsigned disk_count = 0; + tommy_node* node; + uint64_t total_unsynced = 0; + block_off_t pos = 0; + size_t stride = block_sizeof(); + unsigned d; + + /* state_map() validates the disk limit only after content loading */ + if (tommy_list_count(&state->disklist) > RAID_DATA_MAX) { + /* LCOV_EXCL_START */ + log_fatal(ESOFT, "Too many data disks. Maximum allowed is %u.\n", RAID_DATA_MAX); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + for (node = state->disklist; node != 0; node = node->next) { + struct snapraid_disk* disk = node->data; + cursors[disk_count] = tommy_tree_head(&disk->fs_parity); + /* force an initial event so flags are set before their first use */ + next_events[disk_count] = 0; + ++disk_count; + } + + while (pos < blockmax) { + block_off_t next_pos = blockmax; + int one_file = 0; + int one_invalid = 0; + + for (d = 0; d < disk_count; ++d) { + /* refresh expired runs before counting the interval starting at pos */ + if (next_events[d] == pos) { + disk_advance( + &cursors[d], pos, blockmax, + &disk_has_file[d], &disk_has_invalid[d], + &next_events[d], stride + ); + } + + if (disk_has_file[d]) + one_file = 1; + if (disk_has_invalid[d]) + one_invalid = 1; + if (next_events[d] < next_pos) + next_pos = next_events[d]; + } + + if (one_file && one_invalid) + total_unsynced += (next_pos - pos); + + pos = next_pos; + } + + return total_unsynced; } /** @@ -2880,9 +2988,7 @@ static void state_read_content(struct snapraid_state* state, const char* path, S * count is certainly zero. Scan the extent maps only when needed. */ if (has_invalid_parity) { - for (v_pos = 0; v_pos < blockmax; ++v_pos) - if (fs_is_block_unsynced(state, v_pos)) - ++count_unsynced; + count_unsynced = fs_count_unsynced(state, blockmax); } } else if (c == 'h') { /* hole */ diff --git a/cmdline/state.h b/cmdline/state.h index 993466a7..441e1415 100644 --- a/cmdline/state.h +++ b/cmdline/state.h @@ -240,6 +240,13 @@ void state_probe(struct snapraid_state* state); */ void state_read(struct snapraid_state* state); +/** + * Count the number of unsynced parity positions using an event sweep across extents. + * Counts positions in [0, blockmax). The caller must ensure that the disk list, + * extent trees, and block states remain unchanged for the duration of the call. + */ +uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax); + /** * Write the new state. */