diff --git a/cmdline/elem.c b/cmdline/elem.c index ebf3e569..3ec737f7 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -1389,6 +1389,20 @@ static int extent_parity_inside_compare_unlock(const void* void_a, const void* v return 0; } +struct snapraid_extent* fs_par2extent_find_next(struct snapraid_disk* disk, block_off_t parity_pos) +{ + struct extent_parity_inside arg = { parity_pos }; + struct snapraid_extent* extent; + + fs_lock(disk); + + /* non-overlapping extents let this search find the containing extent or the next one */ + extent = tommy_tree_search_greater_equal_compare(&disk->fs_parity, extent_parity_inside_compare_unlock, &arg); + + fs_unlock(disk); + return extent; +} + /** * Search the extent at the specified parity position. * The search is optimized for sequential accesses. diff --git a/cmdline/elem.h b/cmdline/elem.h index e767b024..b9a638ae 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -1243,6 +1243,13 @@ void fs_deallocate(struct snapraid_disk* disk, block_off_t pos); */ struct snapraid_block* fs_file2block_get(struct snapraid_file* file, block_off_t file_pos); +/** + * Find the first extent ending after the specified parity position. + * Return 0 if no such extent exists. The caller must ensure that the extent + * trees remain unchanged while using the returned extent or its tree node. + */ +struct snapraid_extent* fs_par2extent_find_next(struct snapraid_disk* disk, block_off_t parity_pos); + /** * Get the file position from the parity position. * Return 0 if no file is using it. diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 0072b318..602f80e3 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -2270,7 +2270,7 @@ static void test_unsynced_sweep(void) state.block_size = 4096; tommy_list_init(&state.disklist); - if (fs_count_unsynced(&state, 100) != 0) { + if (fs_count_unsynced(&state, 0, 100) != 0) { log_fatal(EINTERNAL, "test_unsynced_sweep: no disks failed\n"); exit(EXIT_FAILURE); } @@ -2281,7 +2281,7 @@ static void test_unsynced_sweep(void) tommy_list_insert_tail(&state.disklist, &disk2->node, disk2); /* initially both disks are empty */ - if (fs_count_unsynced(&state, 100) != 0) { + if (fs_count_unsynced(&state, 10, 100) != 0) { log_fatal(EINTERNAL, "test_unsynced_sweep: empty disks failed\n"); exit(EXIT_FAILURE); } @@ -2301,7 +2301,7 @@ static void test_unsynced_sweep(void) 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) { + if (fs_count_unsynced(&state, 15, 20) != 5) { log_fatal(EINTERNAL, "test_unsynced_sweep: mixed block run failed\n"); exit(EXIT_FAILURE); } @@ -2313,7 +2313,7 @@ static void test_unsynced_sweep(void) 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) { + if (fs_count_unsynced(&state, 15, 23) != 5) { log_fatal(EINTERNAL, "test_unsynced_sweep: deleted without other file failed\n"); exit(EXIT_FAILURE); } @@ -2330,7 +2330,7 @@ static void test_unsynced_sweep(void) /* 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) { + if (fs_count_unsynced(&state, 15, 23) != 7) { log_fatal(EINTERNAL, "test_unsynced_sweep: deleted with file failed\n"); exit(EXIT_FAILURE); } @@ -2349,14 +2349,19 @@ static void test_unsynced_sweep(void) * 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. + * Check every window between limits in gaps, within extents and runs, + * and at their boundaries, including empty windows and nonzero starts. */ 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); + block_off_t j; + for (j = 0; j <= i; ++j) { + uint64_t expected = bounds[i].unsynced - bounds[j].unsynced; + uint64_t count = fs_count_unsynced(&state, bounds[j].blockmax, bounds[i].blockmax); + if (count != expected) { + log_fatal(EINTERNAL, "test_unsynced_sweep: window [%" PRIu64 ", %" PRIu64 ") expected %" PRIu64 ", got %" PRIu64 "\n", + bounds[j].blockmax, bounds[i].blockmax, expected, count); + exit(EXIT_FAILURE); + } } } diff --git a/cmdline/state.c b/cmdline/state.c index 81a41914..ce9afc4d 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -2242,6 +2242,10 @@ static inline void disk_advance( block_off_t run_rem = 1; const unsigned char* ptr = (const unsigned char*)block; + /* stop inspecting block flags at the end of the requested window */ + if (extent_rem > blockmax - pos) + extent_rem = blockmax - pos; + *has_file_ptr = has_file; *has_invalid_ptr = has_invalid; @@ -2264,14 +2268,16 @@ static inline void disk_advance( * 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, + * Counts only positions in [blockmin, blockmax). + * The sweep takes O(D * log(E) + B + R * D) time, where E is the number of + * extents per disk, B is the number of blocks inspected within the window, * 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. + * remain unchanged during the count, as no locks are held during the sweep. */ -uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax) +uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmin, block_off_t blockmax) { tommy_tree_node* cursors[RAID_DATA_MAX]; block_off_t next_events[RAID_DATA_MAX]; @@ -2280,7 +2286,7 @@ uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax) unsigned disk_count = 0; tommy_node* node; uint64_t total_unsynced = 0; - block_off_t pos = 0; + block_off_t pos = blockmin; size_t stride = block_sizeof(); unsigned d; @@ -2294,9 +2300,10 @@ uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax) for (node = state->disklist; node != 0; node = node->next) { struct snapraid_disk* disk = node->data; - cursors[disk_count] = tommy_tree_head(&disk->fs_parity); + struct snapraid_extent* ext = fs_par2extent_find_next(disk, blockmin); + cursors[disk_count] = ext ? &ext->parity_node : 0; /* force an initial event so flags are set before their first use */ - next_events[disk_count] = 0; + next_events[disk_count] = blockmin; ++disk_count; } @@ -2508,6 +2515,8 @@ struct state_read_context { static void state_read_content(struct snapraid_state* state, const char* path, STREAM* f, struct state_read_context* context) { block_off_t blockmax; + block_off_t inv_min; + block_off_t inv_max; uint64_t count_file; uint64_t count_hardlink; uint64_t count_symlink; @@ -2523,7 +2532,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S int has_hash; int has_prevhash; int has_info; - int has_invalid_parity; char buffer[PATH_MAX]; ssize_t ret; tommy_array disk_mapping; @@ -2532,6 +2540,8 @@ static void state_read_content(struct snapraid_state* state, const char* path, S tommy_hashdyn bucket_hash; blockmax = 0; + inv_min = BLOCK_MAX; + inv_max = 0; count_file = 0; count_hardlink = 0; count_symlink = 0; @@ -2547,7 +2557,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S has_hash = 0; has_prevhash = 0; has_info = 0; - has_invalid_parity = 0; mapping_max = 0; memset(disk_file_count, 0, sizeof(disk_file_count)); tommy_array_init(&disk_mapping); @@ -2819,8 +2828,12 @@ static void state_read_content(struct snapraid_state* state, const char* path, S } /* all file block states except BLK require a parity update */ - if (v_state != BLOCK_STATE_BLK) - has_invalid_parity = 1; + if (v_state != BLOCK_STATE_BLK) { + if (v_pos < inv_min) + inv_min = v_pos; + if (v_pos + v_count > inv_max) + inv_max = v_pos + v_count; + } v_file_pos = v_idx; ret = state_read_block_run(f, file, v_file_pos, v_count, v_state, discard_hash); @@ -2989,14 +3002,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S } } } - - /* - * A synchronized content has no invalid block state, so its unsynced - * count is certainly zero. Scan the extent maps only when needed. - */ - if (has_invalid_parity) { - count_unsynced = fs_count_unsynced(state, blockmax); - } } else if (c == 'h') { /* hole */ block_off_t v_pos; @@ -3049,7 +3054,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S switch (c) { case 'o' : /* if it's a run of deleted blocks */ - has_invalid_parity = 1; + if (v_pos < inv_min) + inv_min = v_pos; + if (v_pos + v_count > inv_max) + inv_max = v_pos + v_count; /* ensure the converted byte size fits in signed data_off_t without overflow */ if (v_count > INT64_MAX / (uint64_t)state->block_size) { @@ -4023,6 +4031,16 @@ static void state_read_content(struct snapraid_state* state, const char* path, S /* LCOV_EXCL_STOP */ } + /* + * Every invalid file or deleted run expands this window while parsing. + * Outside it all parity is valid, so no position can be unsynced. + * Count only after all records are loaded, independently of the info record. + * A synchronized content leaves the window empty and needs no sweep. + */ + if (inv_min < inv_max) { + count_unsynced = fs_count_unsynced(state, inv_min, inv_max); + } + context->blockmax = blockmax; context->count_map = mapping_max; context->count_file = count_file; diff --git a/cmdline/state.h b/cmdline/state.h index 441e1415..68ee7515 100644 --- a/cmdline/state.h +++ b/cmdline/state.h @@ -242,10 +242,10 @@ 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, + * Counts positions in [blockmin, 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); +uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmin, block_off_t blockmax); /** * Write the new state.