]> git.hungrycats.org Git - bees/commitdiff
scan_one_extent: eliminate nuisance dedupes, drop caches after reading data
authorZygo Blaxell <bees@furryterror.org>
Sat, 23 Nov 2024 16:14:37 +0000 (11:14 -0500)
committerZygo Blaxell <bees@furryterror.org>
Sun, 1 Dec 2024 04:30:33 +0000 (23:30 -0500)
A laundry list of problems fixed:

 * Track which physical blocks have been read recently without making
 any changes, and don't read them again.

 * Separate dedupe, split, and hole-punching operations into distinct
 planning and execution phases.

 * Keep the longest dedupe from overlapping dedupe matches, and flatten
 them into non-overlapping operations.

 * Don't scan extents that have blocks already in the hash table.
 We can't (yet) touch such an extent without making unreachable space.
 Let them go.

 * Give better information in the scan summary visualization:  show dedupe
 range start and end points (<ddd>), matching blocks (=), copy blocks
 (+), zero blocks (0), inserted blocks (.), unresolved match blocks
 (M), should-have-been-inserted-but-for-some-reason-wasn't blocks (i),
 and there's-a-bug-we-didn't-do-this-one blocks (#).

 * Drop cached data from extents that have been inserted into the hash
 table without modification.

 * Rewrite the hole punching for uncompressed extents, which apparently
 hasn't worked properly since the beginning.

Nuisance dedupe elimination:

 * Don't do more than 100 dedupe, copy, or hole-punch operations per
 extent ref.

 * Don't split an extent or punch a hole unless dedupe would save at
 least half of the extent ref's size.

 * Write a "skip:" summary showing the planned work when nuisance
 dedupe elimination decides to skip an extent.

Signed-off-by: Zygo Blaxell <bees@furryterror.org>
src/bees-context.cc
src/bees-resolve.cc
src/bees.h

index 47c30021e695a6abf3887be7b08e649261a90353..2a8522321a29c45a97956b1922ae2ef2466ffe3d 100644 (file)
@@ -264,6 +264,7 @@ BeesContext::rewrite_file_range(const BeesFileRange &bfr)
        // BEESLOG("BeesResolver br(..., " << bfr << ")");
        BEESTRACE("BeesContext::rewrite_file_range calling BeesResolver " << bfr);
        BeesResolver br(m_ctx, BeesAddress(bfr.fd(), bfr.begin()));
+       BEESTRACE("BeesContext::rewrite_file_range calling replace_src " << dup_bbd);
        // BEESLOG("\treplace_src " << dup_bbd);
        br.replace_src(dup_bbd);
        BEESCOUNT(scan_rewrite);
@@ -291,13 +292,35 @@ BeesContext::rewrite_file_range(const BeesFileRange &bfr)
        }
 }
 
-BeesFileRange
+struct BeesSeenRange {
+       uint64_t bytenr;
+       off_t offset;
+       off_t length;
+};
+
+static
+bool
+operator<(const BeesSeenRange &bsr1, const BeesSeenRange &bsr2)
+{
+       return tie(bsr1.bytenr, bsr1.offset, bsr1.length) < tie(bsr2.bytenr, bsr2.offset, bsr2.length);
+}
+
+static
+__attribute__((unused))
+ostream&
+operator<<(ostream &os, const BeesSeenRange &tup)
+{
+       return os << "BeesSeenRange { " << to_hex(tup.bytenr) << ", " << to_hex(tup.offset) << "+" << pretty(tup.length) << " }";
+}
+
+void
 BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
 {
        BEESNOTE("Scanning " << pretty(e.size()) << " "
                << to_hex(e.begin()) << ".." << to_hex(e.end())
                << " " << name_fd(bfr.fd()) );
        BEESTRACE("scan extent " << e);
+       BEESTRACE("scan bfr " << bfr);
        BEESCOUNT(scan_extent);
 
        // We keep moving this method around
@@ -319,7 +342,7 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
        if (e.flags() & Extent::HOLE) {
                // Nothing here, dispose of this early
                BEESCOUNT(scan_hole);
-               return bfr;
+               return;
        }
 
        if (e.flags() & Extent::PREALLOC) {
@@ -338,38 +361,57 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                if (m_ctx->dedup(brp)) {
                        BEESCOUNT(dedup_prealloc_hit);
                        BEESCOUNTADD(dedup_prealloc_bytes, e.size());
-                       return bfr;
+                       return;
                } else {
                        BEESCOUNT(dedup_prealloc_miss);
                }
        }
 
+       // If we already read this extent and inserted it into the hash table, no need to read it again
+       static mutex s_seen_mutex;
+       unique_lock<mutex> lock_seen(s_seen_mutex);
+       const BeesSeenRange tup = {
+               .bytenr = e.bytenr(),
+               .offset = e.offset(),
+               .length = e.size(),
+       };
+       static set<BeesSeenRange> s_seen;
+       if (s_seen.size() > BEES_MAX_EXTENT_REF_COUNT) {
+               s_seen.clear();
+               BEESCOUNT(scan_seen_clear);
+       }
+       const auto seen_rv = s_seen.find(tup) != s_seen.end();
+       if (!seen_rv) {
+               BEESCOUNT(scan_seen_miss);
+       } else {
+               // BEESLOGDEBUG("Skip " << tup << " " << e);
+               BEESCOUNT(scan_seen_hit);
+               return;
+       }
+       lock_seen.unlock();
+
        // OK we need to read extent now
        bees_readahead(bfr.fd(), bfr.begin(), bfr.size());
 
        map<off_t, pair<BeesHash, BeesAddress>> insert_map;
-       set<off_t> noinsert_set;
-
-       // Hole handling
-       bool extent_compressed = e.flags() & FIEMAP_EXTENT_ENCODED;
-       bool extent_contains_zero = false;
-       bool extent_contains_nonzero = false;
-
-       // Need to replace extent
-       bool rewrite_extent = false;
+       set<off_t> dedupe_set;
+       set<off_t> zero_set;
 
        // Pretty graphs
        off_t block_count = ((e.size() + BLOCK_MASK_SUMS) & ~BLOCK_MASK_SUMS) / BLOCK_SIZE_SUMS;
        BEESTRACE(e << " block_count " << block_count);
        string bar(block_count, '#');
 
-       for (off_t next_p = e.begin(); next_p < e.end(); ) {
+       // List of dedupes found
+       list<BeesRangePair> dedupe_list;
+       list<BeesFileRange> copy_list;
+       list<pair<BeesHash, BeesAddress>> front_hash_list;
+       list<uint64_t> invalidate_addr_list;
 
-               // Guarantee forward progress
-               off_t p = next_p;
-               next_p += BLOCK_SIZE_SUMS;
+       off_t next_p = e.begin();
+       for (off_t p = e.begin(); p < e.end(); p += BLOCK_SIZE_SUMS) {
 
-               off_t bar_p = (p - e.begin()) / BLOCK_SIZE_SUMS;
+               const off_t bar_p = (p - e.begin()) / BLOCK_SIZE_SUMS;
                BeesAddress addr(e, p);
 
                // This extent should consist entirely of non-magic blocks
@@ -384,41 +426,29 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
 
                // Calculate the hash first because it lets us shortcut on is_data_zero
                BEESNOTE("scan hash " << bbd);
-               BeesHash hash = bbd.hash();
+               const BeesHash hash = bbd.hash();
+
+               // Weed out zero blocks
+               BEESNOTE("is_data_zero " << bbd);
+               const bool data_is_zero = bbd.is_data_zero();
+               if (data_is_zero) {
+                       bar.at(bar_p) = '0';
+                       zero_set.insert(p);
+                       BEESCOUNT(scan_zero);
+                       continue;
+               }
 
                // Schedule this block for insertion if we decide to keep this extent.
                BEESCOUNT(scan_hash_preinsert);
                BEESTRACE("Pushing hash " << hash << " addr " << addr << " bbd " << bbd);
                insert_map.insert(make_pair(p, make_pair(hash, addr)));
-               bar.at(bar_p) = 'R';
+               bar.at(bar_p) = 'i';
 
-               // Weed out zero blocks
-               BEESNOTE("is_data_zero " << bbd);
-               bool extent_is_zero = bbd.is_data_zero();
-               if (extent_is_zero) {
-                       bar.at(bar_p) = '0';
-                       if (extent_compressed) {
-                               if (!extent_contains_zero) {
-                                       // BEESLOG("compressed zero bbd " << bbd << "\n\tin extent " << e);
-                               }
-                               extent_contains_zero = true;
-                               // Do not attempt to lookup hash of zero block
-                               continue;
-                       } else {
-                               BEESLOGINFO("zero bbd " << bbd << "\n\tin extent " << e);
-                               BEESCOUNT(scan_zero_uncompressed);
-                               rewrite_extent = true;
-                               break;
-                       }
-               } else {
-                       if (extent_contains_zero && !extent_contains_nonzero) {
-                               // BEESLOG("compressed nonzero bbd " << bbd << "\n\tin extent " << e);
-                       }
-                       extent_contains_nonzero = true;
-               }
+               // Ensure we fill in the entire insert_map without skipping any non-zero blocks
+               if (p < next_p) continue;
 
                BEESNOTE("lookup hash " << bbd);
-               auto found = hash_table->find_cell(hash);
+               const auto found = hash_table->find_cell(hash);
                BEESCOUNT(scan_lookup);
 
                set<BeesResolver> resolved_addrs;
@@ -429,7 +459,7 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                // are at least two distinct addresses to look at.
                found_addrs.insert(addr);
 
-               for (auto i : found) {
+               for (const auto &i : found) {
                        BEESTRACE("found (hash, address): " << i);
                        BEESCOUNT(scan_found);
 
@@ -438,15 +468,21 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
 
                        BeesAddress found_addr(i.e_addr);
 
-#if 0
                        // If address already in hash table, move on to next extent.
-                       // We've already seen this block and may have made additional references to it.
-                       // The current extent is effectively "pinned" and can't be modified any more.
+                       // Only extents that are scanned but not modified are inserted, so if there's
+                       // a matching hash:address pair in the hash table:
+                       // 1.  We have already scanned this extent.
+                       // 2.  We may have already created references to this extent.
+                       // 3.  We won't scan this extent again.
+                       // The current extent is effectively "pinned" and can't be modified
+                       // without rescanning all the existing references.
                        if (found_addr.get_physical_or_zero() == addr.get_physical_or_zero()) {
+                               // No log message because this happens to many thousands of blocks
+                               // when bees is interrupted.
+                               // BEESLOGDEBUG("Found matching hash " << hash << " at same address " << addr << ", skipping " << bfr);
                                BEESCOUNT(scan_already);
-                               return bfr;
+                               return;
                        }
-#endif
 
                        // Block must have matching EOF alignment
                        if (found_addr.is_unaligned_eof() != addr.is_unaligned_eof()) {
@@ -467,7 +503,7 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                                // Extents may become non-toxic so give them a chance to expire.
                                // hash_table->push_front_hash_addr(hash, found_addr);
                                BEESCOUNT(scan_toxic_hash);
-                               return bfr;
+                               return;
                        }
 
                        // Distinct address, go resolve it
@@ -488,8 +524,8 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                                        abandon_extent = true;
                                } else if (!resolved.count()) {
                                        BEESCOUNT(scan_resolve_zero);
-                                       // Didn't find anything, address is dead
-                                       BEESTRACE("matched hash " << hash << " addr " << addr << " count zero");
+                                       // Didn't find a block at the table address, address is dead
+                                       BEESLOGDEBUG("Erasing stale addr " << addr << " hash " << hash);
                                        hash_table->erase_hash_addr(hash, found_addr);
                                } else {
                                        resolved_addrs.insert(resolved);
@@ -498,7 +534,7 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                        });
 
                        if (abandon_extent) {
-                               return bfr;
+                               return;
                        }
                }
 
@@ -510,7 +546,7 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
                        BEESCOUNT(matched_2_or_more);
                }
 
-               // No need to do all this unless there are two or more distinct matches
+               // No need to do all this unless there are one or more distinct matches
                if (!resolved_addrs.empty()) {
                        bar.at(bar_p) = 'M';
                        BEESCOUNT(matched_1_or_more);
@@ -519,149 +555,307 @@ BeesContext::scan_one_extent(const BeesFileRange &bfr, const Extent &e)
 
                        BeesFileRange replaced_bfr;
 
-                       BeesAddress last_replaced_addr;
                        for (auto it = resolved_addrs.begin(); it != resolved_addrs.end(); ++it) {
-                               // FIXME:  Need to terminate this loop on replace_dst exception condition
-                               // catch_all([&]() {
-                                       auto it_copy = *it;
-                                       BEESNOTE("finding one match (out of " << it_copy.count() << ") at " << it_copy.addr() << " for " << bbd);
-                                       BEESTRACE("finding one match (out of " << it_copy.count() << ") at " << it_copy.addr() << " for " << bbd);
-                                       replaced_bfr = it_copy.replace_dst(bbd);
-                                       BEESTRACE("next_p " << to_hex(next_p) << " -> replaced_bfr " << replaced_bfr);
-
-                                       // If we didn't find this hash where the hash table said it would be,
-                                       // correct the hash table.
-                                       if (it_copy.found_hash()) {
-                                               BEESCOUNT(scan_hash_hit);
-                                       } else {
-                                               // BEESLOGDEBUG("erase src hash " << hash << " addr " << it_copy.addr());
-                                               BEESCOUNT(scan_hash_miss);
-                                               hash_table->erase_hash_addr(hash, it_copy.addr());
-                                       }
+                               auto it_copy = *it;
+                               BEESNOTE("finding one match (out of " << it_copy.count() << ") at " << it_copy.addr() << " for " << bbd);
+                               BEESTRACE("finding one match (out of " << it_copy.count() << ") at " << it_copy.addr() << " for " << bbd);
+                               // If we replaced extents containing the incoming addr,
+                               // push the addr we kept to the front of the hash LRU.
+                               auto replaced_brp = it_copy.replace_dst(bbd);
+                               replaced_bfr = replaced_brp.second;
+                               BEESTRACE("next_p " << to_hex(next_p) << " -> replaced_bfr " << replaced_bfr);
+
+                               // If we did find a block, but not this hash, correct the hash table
+                               if (it_copy.found_hash()) {
+                                       BEESCOUNT(scan_hash_hit);
+                               } else {
+                                       BEESLOGDEBUG("Erasing stale hash " << hash << " addr " << it_copy.addr());
+                                       hash_table->erase_hash_addr(hash, it_copy.addr());
+                                       BEESCOUNT(scan_hash_miss);
+                               }
 
-                                       if (it_copy.found_dup()) {
-                                               BEESCOUNT(scan_dup_hit);
+                               if (it_copy.found_dup()) {
+                                       THROW_CHECK0(runtime_error, replaced_bfr);
+                                       BEESCOUNT(scan_dup_hit);
 
-                                               // FIXME:  we will thrash if we let multiple references to identical blocks
-                                               // exist in the hash table.  Erase all but the last one.
-                                               if (last_replaced_addr) {
-                                                       BEESLOGINFO("Erasing redundant hash " << hash << " addr " << last_replaced_addr);
-                                                       hash_table->erase_hash_addr(hash, last_replaced_addr);
-                                                       BEESCOUNT(scan_erase_redundant);
-                                               }
-                                               last_replaced_addr = it_copy.addr();
-
-                                               // Invalidate resolve cache so we can count refs correctly
-                                               m_ctx->invalidate_addr(it_copy.addr());
-                                               m_ctx->invalidate_addr(bbd.addr());
-
-                                               // Remove deduped blocks from insert map
-                                               THROW_CHECK0(runtime_error, replaced_bfr);
-                                               for (off_t ip = replaced_bfr.begin(); ip < replaced_bfr.end(); ip += BLOCK_SIZE_SUMS) {
-                                                       BEESCOUNT(scan_dup_block);
-                                                       noinsert_set.insert(ip);
-                                                       if (ip >= e.begin() && ip < e.end()) {
-                                                               off_t bar_p = (ip - e.begin()) / BLOCK_SIZE_SUMS;
-                                                               bar.at(bar_p) = 'd';
-                                                       }
-                                               }
+                                       // Save this match.  If a better match is found later,
+                                       // it will be replaced.  (FIXME:  not really...)
+                                       dedupe_list.push_back(replaced_brp);
 
-                                               // next_p may be past EOF so check p only
-                                               THROW_CHECK2(runtime_error, p, replaced_bfr, p < replaced_bfr.end());
+                                       // Push matching block to front of LRU
+                                       front_hash_list.push_back(make_pair(hash, it_copy.addr()));
 
-                                               BEESCOUNT(scan_bump);
-                                               next_p = replaced_bfr.end();
-                                       } else {
-                                               BEESCOUNT(scan_dup_miss);
-                                       }
-                               // });
-                       }
-                       if (last_replaced_addr) {
-                               // If we replaced extents containing the incoming addr,
-                               // push the addr we kept to the front of the hash LRU.
-                               hash_table->push_front_hash_addr(hash, last_replaced_addr);
-                               BEESCOUNT(scan_push_front);
+                                       off_t bar_p = (p - e.begin()) / BLOCK_SIZE_SUMS;
+                                       bar.at(bar_p) = '=';
+
+                                       // Invalidate resolve cache so we can count refs correctly
+                                       invalidate_addr_list.push_back(it_copy.addr());
+                                       invalidate_addr_list.push_back(bbd.addr());
+
+                                       // next_p may be past EOF so check p only
+                                       THROW_CHECK2(runtime_error, p, replaced_bfr, p < replaced_bfr.end());
+
+                                       // We may find duplicate ranges of various lengths, so make sure
+                                       // we don't pick a smaller one
+                                       next_p = max(next_p, replaced_bfr.end());
+                               } else {
+                                       BEESCOUNT(scan_dup_miss);
+                               }
                        }
                } else {
                        BEESCOUNT(matched_0);
                }
        }
 
-       // If the extent was compressed and all zeros, nuke entire thing
-       if (!rewrite_extent && (extent_contains_zero && !extent_contains_nonzero)) {
-               rewrite_extent = true;
-               BEESCOUNT(scan_zero_compressed);
+       bool force_insert = false;
+
+       // We don't want to punch holes into compressed extents, unless:
+       // 1.  There was dedupe of non-zero blocks, so we always have to copy the rest of the extent
+       // 2.  The entire extent is zero and the whole thing can be replaced with a single hole
+       const bool extent_compressed = e.flags() & FIEMAP_EXTENT_ENCODED;
+       if (extent_compressed && dedupe_list.empty() && !insert_map.empty()) {
+               // BEESLOGDEBUG("Compressed extent with non-zero data and no dedupe, skipping");
+               BEESCOUNT(scan_compressed_no_dedup);
+               force_insert = true;
        }
 
-       // If we deduped any blocks then we must rewrite the remainder of the extent
-       if (!noinsert_set.empty()) {
-               rewrite_extent = true;
+       // FIXME:  dedupe_list contains a lot of overlapping matches.  Get rid of all but one.
+       list<BeesRangePair> dedupe_list_out;
+       dedupe_list.sort([](const BeesRangePair &a, const BeesRangePair &b) {
+               return b.second.size() < a.second.size();
+       });
+       // Shorten each dedupe brp by removing any overlap with earlier (longer) extents in list
+       for (auto i : dedupe_list) {
+               bool insert_i = true;
+               BEESTRACE("i = " << i << " insert_i " << insert_i);
+               for (const auto &j : dedupe_list_out) {
+                       BEESTRACE("j = " << j);
+                       // No overlap, try next one
+                       if (j.second.end() <= i.second.begin() || j.second.begin() >= i.second.end()) {
+                               continue;
+                       }
+                       // j fully overlaps or is the same as i, drop i
+                       if (j.second.begin() <= i.second.begin() && j.second.end() >= i.second.end()) {
+                               insert_i = false;
+                               break;
+                       }
+                       // i begins outside j, i ends inside j, remove the end of i
+                       if (i.second.end() > j.second.begin() && i.second.begin() <= j.second.begin()) {
+                               const auto delta = i.second.end() - j.second.begin();
+                               if (delta == i.second.size()) {
+                                       insert_i = false;
+                                       break;
+                               }
+                               i.shrink_end(delta);
+                               continue;
+                       }
+                       // i begins inside j, ends outside j, remove the begin of i
+                       if (i.second.begin() < j.second.end() && i.second.end() >= j.second.end()) {
+                               const auto delta = j.second.end() - i.second.begin();
+                               if (delta == i.second.size()) {
+                                       insert_i = false;
+                                       break;
+                               }
+                               i.shrink_begin(delta);
+                               continue;
+                       }
+                       // i fully overlaps j, split i into two parts, push the other part onto dedupe_list
+                       if (j.second.begin() > i.second.begin() && j.second.end() < i.second.end()) {
+                               auto other_i = i;
+                               const auto end_left_delta = i.second.end() - j.second.begin();
+                               const auto begin_right_delta = i.second.begin() - j.second.end();
+                               i.shrink_end(end_left_delta);
+                               other_i.shrink_begin(begin_right_delta);
+                               dedupe_list.push_back(other_i);
+                               continue;
+                       }
+                       // None of the sbove.  Oops!
+                       THROW_CHECK0(runtime_error, false);
+               }
+               if (insert_i) {
+                       dedupe_list_out.push_back(i);
+               }
+       }
+       dedupe_list = dedupe_list_out;
+       dedupe_list_out.clear();
+
+       // Count total dedupes
+       uint64_t bytes_deduped = 0;
+       for (const auto &i : dedupe_list) {
+               // Remove deduped blocks from insert map and zero map
+               for (off_t ip = i.second.begin(); ip < i.second.end(); ip += BLOCK_SIZE_SUMS) {
+                       BEESCOUNT(scan_dup_block);
+                       dedupe_set.insert(ip);
+                       zero_set.erase(ip);
+               }
+               bytes_deduped += i.second.size();
        }
 
-       // If we need to replace part of the extent, rewrite all instances of it
-       if (rewrite_extent) {
-               bool blocks_rewritten = false;
+       // Copy all blocks of the extent that were not deduped or zero, but don't copy an entire extent
+       uint64_t bytes_zeroed = 0;
+       if (!force_insert) {
                BEESTRACE("Rewriting extent " << e);
                off_t last_p = e.begin();
                off_t p = last_p;
-               off_t next_p;
+               off_t next_p = last_p;
                BEESTRACE("next_p " << to_hex(next_p) << " p " << to_hex(p) << " last_p " << to_hex(last_p));
                for (next_p = e.begin(); next_p < e.end(); ) {
                        p = next_p;
-                       next_p += BLOCK_SIZE_SUMS;
+                       next_p = min(next_p + BLOCK_SIZE_SUMS, e.end());
 
-                       // BEESLOG("noinsert_set.count(" << to_hex(p) << ") " << noinsert_set.count(p));
-                       if (noinsert_set.count(p)) {
+                       // Can't be both dedupe and zero
+                       THROW_CHECK2(runtime_error, zero_set.count(p), dedupe_set.count(p), zero_set.count(p) + dedupe_set.count(p) < 2);
+                       if (zero_set.count(p)) {
+                               bytes_zeroed += next_p - p;
+                       }
+                       // BEESLOG("dedupe_set.count(" << to_hex(p) << ") " << dedupe_set.count(p));
+                       if (dedupe_set.count(p)) {
                                if (p - last_p > 0) {
-                                       rewrite_file_range(BeesFileRange(bfr.fd(), last_p, p));
-                                       blocks_rewritten = true;
+                                       THROW_CHECK2(runtime_error, p, e.end(), p <= e.end());
+                                       copy_list.push_back(BeesFileRange(bfr.fd(), last_p, p));
                                }
                                last_p = next_p;
-                       } else {
-                               off_t bar_p = (p - e.begin()) / BLOCK_SIZE_SUMS;
-                               bar.at(bar_p) = '+';
                        }
                }
                BEESTRACE("last");
-               if (next_p - last_p > 0) {
-                       rewrite_file_range(BeesFileRange(bfr.fd(), last_p, next_p));
-                       blocks_rewritten = true;
+               if (next_p > last_p) {
+                       THROW_CHECK2(runtime_error, next_p, e.end(), next_p <= e.end());
+                       copy_list.push_back(BeesFileRange(bfr.fd(), last_p, next_p));
                }
-               if (blocks_rewritten) {
-                       // Nothing left to insert, all blocks clobbered
-                       insert_map.clear();
+       }
+
+       // Don't copy an entire extent
+       if (!bytes_zeroed && copy_list.size() == 1 && copy_list.begin()->size() == e.size()) {
+               copy_list.clear();
+       }
+
+       // Count total copies
+       uint64_t bytes_copied = 0;
+       for (const auto &i : copy_list) {
+               bytes_copied += i.size();
+       }
+
+       BEESTRACE("bar: " << bar);
+
+       // Don't do nuisance dedupes part 1:  free more blocks than we create
+       THROW_CHECK3(runtime_error, bytes_copied, bytes_zeroed, bytes_deduped, bytes_copied >= bytes_zeroed);
+       const auto cost_copy = bytes_copied - bytes_zeroed;
+       const auto gain_dedupe = bytes_deduped + bytes_zeroed;
+       if (cost_copy > gain_dedupe) {
+               BEESLOGDEBUG("Too many bytes copied (" << pretty(bytes_copied) << ") for bytes deduped (" << pretty(bytes_deduped) << ") and holes punched (" << pretty(bytes_zeroed) << "), skipping extent");
+               BEESCOUNT(scan_skip_bytes);
+               force_insert = true;
+       }
+
+       // Don't do nuisance dedupes part 2:  nobody needs more than 100 dedupe/copy ops in one extent
+       if (dedupe_list.size() + copy_list.size() > 100) {
+               BEESLOGDEBUG("Too many dedupe (" << dedupe_list.size() << ") and copy (" << copy_list.size() << ") operations, skipping extent");
+               BEESCOUNT(scan_skip_ops);
+               force_insert = true;
+       }
+
+       // Track whether we rewrote anything
+       bool extent_modified = false;
+
+       // If we didn't delete the dedupe list, do the dedupes now
+       for (const auto &i : dedupe_list) {
+               BEESNOTE("dedup " << i);
+               if (force_insert || m_ctx->dedup(i)) {
+                       BEESCOUNT(replacedst_dedup_hit);
+                       THROW_CHECK0(runtime_error, i.second);
+                       for (off_t ip = i.second.begin(); ip < i.second.end(); ip += BLOCK_SIZE_SUMS) {
+                               if (ip >= e.begin() && ip < e.end()) {
+                                       off_t bar_p = (ip - e.begin()) / BLOCK_SIZE_SUMS;
+                                       if (bar.at(bar_p) != '=') {
+                                               if (ip == i.second.begin()) {
+                                                       bar.at(bar_p) = '<';
+                                               } else if (ip + BLOCK_SIZE_SUMS >= i.second.end()) {
+                                                       bar.at(bar_p) = '>';
+                                               } else {
+                                                       bar.at(bar_p) = 'd';
+                                               }
+                                       }
+                               }
+                       }
+                       extent_modified = !force_insert;
                } else {
-                       // BEESLOG("No blocks rewritten");
-                       BEESCOUNT(scan_no_rewrite);
+                       BEESLOGINFO("dedup failed: " << i);
+                       BEESCOUNT(replacedst_dedup_miss);
+                       // User data changed while we were looking up the extent, or we have a bug.
+                       // We can't fix this, but we can immediately stop wasting effort.
+                       return;
                }
        }
 
-       // We did not rewrite the extent and it contained data, so insert it.
-       for (auto i : insert_map) {
-               off_t bar_p = (i.first - e.begin()) / BLOCK_SIZE_SUMS;
-               BEESTRACE("e " << e << "bar_p = " << bar_p << " i.first-e.begin() " << i.first - e.begin() << " i.second " << i.second.first << ", " << i.second.second);
-               if (noinsert_set.count(i.first)) {
-                       // FIXME:  we removed one reference to this copy.  Avoid thrashing?
-                       hash_table->erase_hash_addr(i.second.first, i.second.second);
-                       // Block was clobbered, do not insert
-                       // Will look like 'Ddddd' because we skip deduped blocks
-                       bar.at(bar_p) = 'D';
-                       BEESCOUNT(inserted_clobbered);
-               } else {
+       // Then the copy/rewrites
+       for (const auto &i : copy_list) {
+               if (!force_insert) {
+                       rewrite_file_range(i);
+                       extent_modified = true;
+               }
+               for (auto p = i.begin(); p < i.end(); p += BLOCK_SIZE_SUMS) {
+                       off_t bar_p = (p - e.begin()) / BLOCK_SIZE_SUMS;
+                       // Leave zeros as-is because they aren't really copies
+                       if (bar.at(bar_p) != '0') {
+                               bar.at(bar_p) = '+';
+                       }
+               }
+       }
+
+       if (!force_insert) {
+               // Push matched hashes to front
+               for (const auto &i : front_hash_list) {
+                       hash_table->push_front_hash_addr(i.first, i.second);
+                       BEESCOUNT(scan_push_front);
+               }
+               // Invalidate cached resolves
+               for (const auto &i : invalidate_addr_list) {
+                       m_ctx->invalidate_addr(i);
+               }
+       }
+
+       // Don't insert hashes pointing to an extent we just deleted
+       if (!extent_modified) {
+               // We did not rewrite the extent and it contained data, so insert it.
+               // BEESLOGDEBUG("Inserting " << insert_map.size() << " hashes from " << bfr);
+               for (const auto &i : insert_map) {
                        hash_table->push_random_hash_addr(i.second.first, i.second.second);
-                       bar.at(bar_p) = '.';
-                       BEESCOUNT(inserted_block);
+                       off_t bar_p = (i.first - e.begin()) / BLOCK_SIZE_SUMS;
+                       if (bar.at(bar_p) == 'i') {
+                               bar.at(bar_p) = '.';
+                       }
+                       BEESCOUNT(scan_hash_insert);
                }
        }
 
        // Visualize
        if (bar != string(block_count, '.')) {
-               BEESLOGINFO("scan: " << pretty(e.size()) << " " << to_hex(e.begin()) << " [" << bar << "] " << to_hex(e.end()) << ' ' << name_fd(bfr.fd()));
+               BEESLOGINFO(
+                       (force_insert ? "skip" : "scan") << ": "
+                       << pretty(e.size()) << " "
+                       << dedupe_list.size() << "d" << copy_list.size() << "c"
+                       << ((bytes_zeroed + BLOCK_SIZE_SUMS - 1) / BLOCK_SIZE_SUMS) << "p {"
+                       << to_hex(e.bytenr()) << "+" << to_hex(e.offset()) << "} "
+                       << to_hex(e.begin()) << " [" << bar << "] " << to_hex(e.end())
+                       << ' ' << name_fd(bfr.fd())
+               );
+       }
+
+       // Put this extent into the recently seen list if we didn't rewrite it,
+       // and remove it if we did.
+       lock_seen.lock();
+       if (extent_modified) {
+               s_seen.erase(tup);
+               BEESCOUNT(scan_seen_erase);
+       } else {
+               // BEESLOGDEBUG("Seen " << tup << " " << e);
+               s_seen.insert(tup);
+               BEESCOUNT(scan_seen_insert);
        }
+       lock_seen.unlock();
 
-       // Costs 10% on benchmarks
-       // bees_unreadahead(bfr.fd(), bfr.begin(), bfr.size());
-       return bfr;
+       // Still hurts benchmarks...or does it?
+       bees_unreadahead(bfr.fd(), bfr.begin(), bfr.size());
 }
 
 shared_ptr<Exclusion>
@@ -725,6 +919,7 @@ BeesContext::scan_forward(const BeesFileRange &bfr_in)
                                }
                                Timer one_extent_timer;
                                scan_one_extent(bfr, e);
+                               // BEESLOGDEBUG("Scanned " << e << " " << bfr);
                                BEESCOUNTADD(scanf_extent_ms, one_extent_timer.age() * 1000);
                                BEESCOUNT(scanf_extent);
                        });
index 0d4d2dcd042864d725354a2572cfb90c6b401a29..46446fdd14b7ffa8dfb5af3f3ef291a4698ee973 100644 (file)
@@ -384,7 +384,7 @@ BeesResolver::for_each_extent_ref(BeesBlockData bbd, function<bool(const BeesFil
        return stop_now;
 }
 
-BeesFileRange
+BeesRangePair
 BeesResolver::replace_dst(const BeesFileRange &dst_bfr_in)
 {
        BEESTRACE("replace_dst dst_bfr " << dst_bfr_in);
@@ -400,6 +400,7 @@ BeesResolver::replace_dst(const BeesFileRange &dst_bfr_in)
        BEESTRACE("overlap_bfr " << overlap_bfr);
 
        BeesBlockData bbd(dst_bfr);
+       BeesRangePair rv = { BeesFileRange(), BeesFileRange() };
 
        for_each_extent_ref(bbd, [&](const BeesFileRange &src_bfr_in) -> bool {
                // Open src
@@ -436,21 +437,12 @@ BeesResolver::replace_dst(const BeesFileRange &dst_bfr_in)
                        BEESCOUNT(replacedst_grown);
                }
 
-               // Dedup
-               BEESNOTE("dedup " << brp);
-               if (m_ctx->dedup(brp)) {
-                       BEESCOUNT(replacedst_dedup_hit);
-                       m_found_dup = true;
-                       overlap_bfr = brp.second;
-                       // FIXME:  find best range first, then dedupe that
-                       return true; // i.e. break
-               } else {
-                       BEESCOUNT(replacedst_dedup_miss);
-                       return false; // i.e. continue
-               }
+               rv = brp;
+               m_found_dup = true;
+               return true;
        });
        // BEESLOG("overlap_bfr after " << overlap_bfr);
-       return overlap_bfr.copy_closed();
+       return rv;
 }
 
 BeesFileRange
index 2db4341c0df047d0a5c08befcaaea35e48a5779f..94381880adf1881051179658b869040fd351d093 100644 (file)
@@ -749,7 +749,7 @@ class BeesContext : public enable_shared_from_this<BeesContext> {
 
        BeesResolveAddrResult resolve_addr_uncached(BeesAddress addr);
 
-       BeesFileRange scan_one_extent(const BeesFileRange &bfr, const Extent &e);
+       void scan_one_extent(const BeesFileRange &bfr, const Extent &e);
        void rewrite_file_range(const BeesFileRange &bfr);
 
 public:
@@ -842,7 +842,7 @@ public:
        BeesFileRange find_one_match(BeesHash hash);
 
        void replace_src(const BeesFileRange &src_bfr);
-       BeesFileRange replace_dst(const BeesFileRange &dst_bfr);
+       BeesRangePair replace_dst(const BeesFileRange &dst_bfr);
 
        bool found_addr() const { return m_found_addr; }
        bool found_data() const { return m_found_data; }