diff --git a/include/libtorrent/alert_types.hpp b/include/libtorrent/alert_types.hpp index c1e9770a2..68122f938 100644 --- a/include/libtorrent/alert_types.hpp +++ b/include/libtorrent/alert_types.hpp @@ -2702,6 +2702,7 @@ TORRENT_VERSION_NAMESPACE_2 static constexpr picker_flags_t backup1 = 13_bit; static constexpr picker_flags_t backup2 = 14_bit; static constexpr picker_flags_t end_game = 15_bit; + static constexpr picker_flags_t extent_affinity = 16_bit; // this is a bitmask of which features were enabled for this particular // pick. The bits are defined in the picker_flags_t enum. diff --git a/include/libtorrent/piece_picker.hpp b/include/libtorrent/piece_picker.hpp index 36f5fae4a..5bbf8634f 100644 --- a/include/libtorrent/piece_picker.hpp +++ b/include/libtorrent/piece_picker.hpp @@ -69,6 +69,7 @@ namespace libtorrent { using prio_index_t = aux::strong_typedef; using picker_options_t = flags::bitfield_flag; using download_queue_t = aux::strong_typedef; + using piece_extent_t = aux::strong_typedef; struct piece_count { @@ -141,6 +142,11 @@ namespace libtorrent { // range of pieces. static constexpr picker_options_t align_expanded_pieces = 6_bit; + // this will create an affinity to pick pieces in extents of 4 MiB, in an + // attempt to improve disk I/O by picking ranges of pieces (if pieces are + // small) + static constexpr picker_options_t piece_extent_affinity = 7_bit; + struct downloading_piece { downloading_piece() @@ -469,6 +475,11 @@ namespace libtorrent { private: + piece_extent_t extent_for(piece_index_t) const; + index_range extent_for(piece_extent_t) const; + + void record_downloading_piece(piece_index_t const p); + int num_pad_blocks() const { return m_num_pad_blocks; } span mutable_blocks_for_piece(downloading_piece const& dp); @@ -741,6 +752,13 @@ namespace libtorrent { // tracks the number of blocks in a specific piece that are pad blocks std::unordered_map m_pads_in_piece; + // when the adjecent_piece affinity is enabled, this contains the most + // recent "extents" of adjecent pieces that have been requested from + // this is mutable because it's updated by functions to pick pieces, which + // are const. That's an efficient place to update it, since it's being + // traversed already. + mutable std::vector m_recent_extents; + // the number of bits set in the m_pad_blocks bitfield, i.e. // the number of blocks marked as pads int m_num_pad_blocks = 0; diff --git a/include/libtorrent/settings_pack.hpp b/include/libtorrent/settings_pack.hpp index 7ab89a1e5..c11d26525 100644 --- a/include/libtorrent/settings_pack.hpp +++ b/include/libtorrent/settings_pack.hpp @@ -734,6 +734,12 @@ namespace libtorrent { // preferred in the routing table. dht_prefer_verified_node_ids, + // when this is true, create an affinity for downloading 4 MiB extents + // of adjecent pieces. This is an attempt to achieve better disk I/O + // throughput by downloading larger extents of bytes, for torrents with + // small piece sizes + piece_extent_affinity, + max_bool_setting_internal }; diff --git a/simulation/test_transfer.cpp b/simulation/test_transfer.cpp index 080891415..a0110c13f 100644 --- a/simulation/test_transfer.cpp +++ b/simulation/test_transfer.cpp @@ -370,3 +370,21 @@ TORRENT_TEST(disable_disk_cache) ); } +TORRENT_TEST(piece_extent_affinity) +{ + using namespace lt; + run_test( + [](lt::session& ses0, lt::session& ses1) + { + settings_pack p; + p.set_bool(settings_pack::piece_extent_affinity, true); + ses0.apply_settings(p); + ses1.apply_settings(p); + }, + [](lt::session&, lt::alert const*) {}, + [](std::shared_ptr ses[2]) { + TEST_EQUAL(is_seed(*ses[0]), true); + } + ); +} + diff --git a/src/alert.cpp b/src/alert.cpp index c044a12be..ea1190364 100644 --- a/src/alert.cpp +++ b/src/alert.cpp @@ -2258,6 +2258,7 @@ namespace { constexpr picker_flags_t picker_log_alert::backup1; constexpr picker_flags_t picker_log_alert::backup2; constexpr picker_flags_t picker_log_alert::end_game; + constexpr picker_flags_t picker_log_alert::extent_affinity; std::string picker_log_alert::message() const { @@ -2279,6 +2280,7 @@ namespace { "backup1 ", "backup2 ", "end_game " + "extent_affinity " }; std::string ret = peer_alert::message(); diff --git a/src/peer_connection.cpp b/src/peer_connection.cpp index 8544b1721..1104850b4 100644 --- a/src/peer_connection.cpp +++ b/src/peer_connection.cpp @@ -928,7 +928,12 @@ namespace libtorrent { // request blocks from the same piece ret |= piece_picker::reverse; } - + else + { + if (m_settings.get_bool(settings_pack::piece_extent_affinity) + && t->num_time_critical_pieces() == 0) + ret |= piece_picker::piece_extent_affinity; + } } if (m_settings.get_bool(settings_pack::prioritize_partial_pieces)) diff --git a/src/piece_picker.cpp b/src/piece_picker.cpp index aff0087ae..bc6f177da 100644 --- a/src/piece_picker.cpp +++ b/src/piece_picker.cpp @@ -46,6 +46,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/performance_counters.hpp" // for counters #include "libtorrent/alert_types.hpp" // for picker_log_alert #include "libtorrent/download_priority.hpp" +#include "libtorrent/disk_interface.hpp" // for default_block_size #if TORRENT_USE_ASSERTS #include "libtorrent/peer_connection.hpp" @@ -117,6 +118,7 @@ namespace libtorrent { constexpr picker_options_t piece_picker::sequential; constexpr picker_options_t piece_picker::time_critical_mode; constexpr picker_options_t piece_picker::align_expanded_pieces; + constexpr picker_options_t piece_picker::piece_extent_affinity; constexpr download_queue_t piece_picker::piece_pos::piece_downloading; constexpr download_queue_t piece_picker::piece_pos::piece_full; @@ -127,6 +129,9 @@ namespace libtorrent { constexpr download_queue_t piece_picker::piece_pos::piece_downloading_reverse; constexpr download_queue_t piece_picker::piece_pos::piece_full_reverse; + // the max number of blocks to create an affinity for + constexpr int max_piece_affinity_extent = 4 * 1024 * 1024 / default_block_size; + piece_picker::piece_picker(int const blocks_per_piece , int const blocks_in_last_piece, int const total_num_pieces) : m_priority_boundaries(1, m_pieces.end_index()) @@ -2129,6 +2134,41 @@ namespace { } else { + // TODO: Is it a good idea that this affinity takes precedence over + // piece priority? + if (options & piece_extent_affinity) + { + int to_erase = -1; + int idx = -1; + for (piece_extent_t const e : m_recent_extents) + { + ++idx; + bool have_all = true; + for (piece_index_t const p : extent_for(e)) + { + if (!m_piece_map[p].have()) have_all = false; + if (!is_piece_free(p, pieces)) continue; + + ret |= picker_log_alert::extent_affinity; + + num_blocks = add_blocks(p, pieces + , interesting_blocks, backup_blocks + , backup_blocks2, num_blocks + , prefer_contiguous_blocks, peer, suggested_pieces + , options); + if (num_blocks <= 0) + { + // if we have all pieces belonging to this extent, remove it + if (to_erase != -1) m_recent_extents.erase(m_recent_extents.begin() + to_erase); + return ret; + } + } + // if we have all pieces belonging to this extent, remove it + if (have_all) to_erase = idx; + } + if (to_erase != -1) m_recent_extents.erase(m_recent_extents.begin() + to_erase); + } + for (piece_index_t i : m_pieces) { pc.inc_stats_counter(counters::piece_picker_rare_loops); @@ -2987,6 +3027,68 @@ get_out: return info[block.block_index].state == block_info::state_finished; } + piece_extent_t piece_picker::extent_for(piece_index_t const p) const + { + int const extent_size = max_piece_affinity_extent / m_blocks_per_piece; + return piece_extent_t{static_cast(p) / extent_size}; + } + + index_range piece_picker::extent_for(piece_extent_t const e) const + { + int const extent_size = max_piece_affinity_extent / m_blocks_per_piece; + int const begin = static_cast(e) * extent_size; + int const end = std::min(begin + extent_size, num_pieces()); + return { piece_index_t{begin}, piece_index_t{end}}; + } + + void piece_picker::record_downloading_piece(piece_index_t const p) + { + // if a single piece is large enough, don't bother with the affinity of + // adjecent pieces. + if (m_blocks_per_piece >= max_piece_affinity_extent) return; + + piece_extent_t const this_extent = extent_for(p); + + // if the extent is already in the list, nothing to do + if (std::find(m_recent_extents.begin() + , m_recent_extents.end(), this_extent) != m_recent_extents.end()) + return; + + download_priority_t const this_prio = piece_priority(p); + + // figure out if it's worth recording this downloading piece + // if we already have all blocks in this extent, there's no point in + // adding it + bool have_all = true; + + for (auto const piece : extent_for(this_extent)) + { + if (piece == p) continue; + + if (!m_piece_map[piece].have()) have_all = false; + + // if at least one piece in this extent has a different priority than + // the one we just started downloading, don't create an affinity for + // adjecent pieces. This probably means the pieces belong to different + // files, or that some other mechanism determining the priority should + // take precedence. + if (piece_priority(piece) != this_prio) return; + } + + // if we already have all the *other* pieces in this extent, there's no + // need to inflate their priorities + if (have_all) return; + + // TODO: should 5 be configurable? + if (m_recent_extents.size() < 5) + m_recent_extents.push_back(this_extent); + + // limit the number of extent affinities active at any given time to limit + // the cost of checking them. Also, don't replace them, commit to + // finishing them before starting another extent. This is analoguous to + // limiting the number of partial pieces. + } + // options may be 0 or piece_picker::reverse // returns false if the block could not be marked as downloading bool piece_picker::mark_as_downloading(piece_block const block @@ -3020,6 +3122,17 @@ get_out: if (prio >= 0 && !m_dirty) update(prio, p.index); + // if the piece extent affinity is enabled, (maybe) record downloading a + // block from this piece to make other peers prefer adjecent pieces + // if reverse is set, don't encourage other peers to pick nearby + // pieces, as that's assumed to be low priority. + // if time critical mode is enabled, we're likely to either download + // adjacent pieces anyway, but more importantly, we don't want to + // create artificially higher priority for adjecent pieces if they + // aren't important or urgent + if (options & piece_extent_affinity) + record_downloading_piece(block.piece_index); + auto const dp = add_download_piece(block.piece_index); auto const binfo = mutable_blocks_for_piece(*dp); block_info& info = binfo[block.block_index]; diff --git a/src/settings_pack.cpp b/src/settings_pack.cpp index b69e232ea..db64a1ece 100644 --- a/src/settings_pack.cpp +++ b/src/settings_pack.cpp @@ -208,6 +208,7 @@ constexpr int CLOSE_FILE_INTERVAL = 0; SET(proxy_tracker_connections, true, nullptr), SET(enable_ip_notifier, true, &session_impl::update_ip_notifier), SET(dht_prefer_verified_node_ids, true, &session_impl::update_dht_settings), + SET(piece_extent_affinity, false, nullptr), }}); aux::array const int_settings diff --git a/test/test_piece_picker.cpp b/test/test_piece_picker.cpp index 799880c4b..a8aa06c80 100644 --- a/test/test_piece_picker.cpp +++ b/test/test_piece_picker.cpp @@ -112,12 +112,13 @@ std::shared_ptr setup_picker( char const* availability , char const* have_str , char const* priority - , char const* partial) + , char const* partial + , int num_blocks_per_piece = blocks_per_piece) { const int num_pieces = int(strlen(availability)); TORRENT_ASSERT(int(strlen(have_str)) == num_pieces); - std::shared_ptr p = std::make_shared(blocks_per_piece, blocks_per_piece, num_pieces); + std::shared_ptr p = std::make_shared(num_blocks_per_piece, num_blocks_per_piece, num_pieces); for (piece_index_t i(0); i < piece_index_t(num_pieces); ++i) { @@ -193,7 +194,7 @@ std::shared_ptr setup_picker( { if (!have[i]) continue; p->we_have(i); - for (int j = 0; j < blocks_per_piece; ++j) + for (int j = 0; j < num_blocks_per_piece; ++j) TEST_CHECK(p->is_finished(piece_block(i, j))); } @@ -2146,5 +2147,203 @@ TORRENT_TEST(pad_blocks_some_wanted) TEST_EQUAL(p->want().pad_blocks, 2); } +namespace { + +std::vector full_piece(int const p, int const blocks) +{ + std::vector ret; + for (int i = 0;i < blocks; ++i) + ret.push_back(piece_block(piece_index_t{p}, i)); + return ret; +} +void mark_downloading(std::shared_ptr const& p, std::vector const blocks + , torrent_peer* const peer, picker_options_t const opts) +{ + for (auto const& b : blocks) + p->mark_as_downloading(b, peer, opts); +} +} + +TORRENT_TEST(piece_extent_affinity) +{ + int const blocks = 64; + // these are 2 extents. the first 4 pieces and the last 4 pieces + auto const have_none = " "; + auto const have_all = "********"; + + auto p = setup_picker("33133233", have_none, "", "", blocks); + + std::vector picked = pick_pieces(p, have_all, blocks, 0, &tmp0 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(2, blocks)); + mark_downloading(p, full_piece(2, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // without the piece_extent_affinity, we would pick piece 5, because of + // availability + picked = pick_pieces(p, have_all, blocks, 0, &tmp1); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(5, blocks)); + mark_downloading(p, full_piece(5, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // with piece_extent_affinity, we would pick piece 0, because it's the same + // extent as the piece we just picked + picked = pick_pieces(p, have_all, blocks, 0, &tmp2, options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(0, blocks)); + mark_downloading(p, full_piece(0, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // then we should pick piece 1 + picked = pick_pieces(p, have_all, blocks, 0, &tmp3, options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(1, blocks)); + mark_downloading(p, full_piece(1, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // then we should pick piece 3. The last piece of the extent + picked = pick_pieces(p, have_all, blocks, 0, &tmp4, options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(3, blocks)); + mark_downloading(p, full_piece(3, blocks), &tmp0, options | piece_picker::piece_extent_affinity); +} + +TORRENT_TEST(piece_extent_affinity_priority) +{ + int const blocks = 64; + auto const have_none = " "; + auto const have_all = "********"; + + auto p = setup_picker("33333233", have_none, "43444444", "", blocks); + // we pick piece 2. Since piece 1 has a different priority this should not + // create an affinity for the extent + mark_downloading(p, full_piece(2, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // so next piece to be picked will *not* be the extent, but piece 5, which + // has the lowest availability + + std::vector picked = pick_pieces(p, have_all, blocks, 0, &tmp1 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(5, blocks)); +} + +TORRENT_TEST(piece_extent_affinity_large_pieces) +{ + int const blocks = 256; + auto const have_none = " "; + auto const have_all = "********"; + + auto p = setup_picker("33333233", have_none, "", "", blocks); + // we pick piece 2. Since the pieces are so large (4 MiB), there is no + // affinity for piece extents. + mark_downloading(p, full_piece(2, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + + // so next piece to be picked will *not* be the extent, but piece 5, which + // has the next lowest availability + std::vector picked = pick_pieces(p, have_all, blocks, 0, &tmp1 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(5, blocks)); +} + +TORRENT_TEST(piece_extent_affinity_active_limit) +{ + // an extent is two pieces wide, 6 extents total. + // make ure we limit the number of extents to 5 + int const blocks = 128; + auto const have_none = " "; + + auto p = setup_picker("333333333333", have_none, "444444444455", "", blocks); + // open up the first 5 extents + mark_downloading(p, full_piece(0, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(2, blocks), &tmp1, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(4, blocks), &tmp2, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(6, blocks), &tmp3, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(8, blocks), &tmp4, options | piece_picker::piece_extent_affinity); + + // this should not open up another extent. We should still have a bias + // towards pieces 1, 3, 5, 7 and 9. + mark_downloading(p, full_piece(10, blocks), &tmp5, options | piece_picker::piece_extent_affinity); + + // a peer that only has piece 0, 1, 10, 11, will always pick 1, never 11, + // even though 10 and 11 have higher priority + + std::vector picked = pick_pieces(p, "** **", blocks, 0, &tmp1 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(1, blocks)); +} + +TORRENT_TEST(piece_extent_affinity_clear_done) +{ + // an extent is two pieces wide, 7 extents total. + // make sure we remove an active extent when we have all the pieces, and + // allow a new extent to be added + int const blocks = 128; + auto const have_none = " "; + + auto p = setup_picker("33333333333333", have_none, "44444444444455", "", blocks); + // open up the first 5 extents + mark_downloading(p, full_piece(0, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(2, blocks), &tmp1, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(4, blocks), &tmp2, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(6, blocks), &tmp3, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(8, blocks), &tmp4, options | piece_picker::piece_extent_affinity); + + // now all 5 extents are in use, if we finish a whole extent, it should be + // removed from the list + p->we_have(piece_index_t{0}); + p->we_have(piece_index_t{1}); + + // we need to invoke the piece picker once to detect and reap this full + // extent + pick_pieces(p, "**************", blocks, 0, &tmp1, options | piece_picker::piece_extent_affinity); + + // this *should* open up another extent. We should still have a bias + // towards pieces 1, 3, 5, 7 and 9. + mark_downloading(p, full_piece(10, blocks), &tmp5, options | piece_picker::piece_extent_affinity); + + // a peer that only has piece 10, 11, 12, 13 will always pick 11, since it's + // part of an extent that was just opened, never 12 or 13 even though they + // have higher priority + std::vector picked = pick_pieces(p, " ****", blocks, 0, &tmp1 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(11, blocks)); +} + +TORRENT_TEST(piece_extent_affinity_no_duplicates) +{ + // an extent is 8 pieces wide, 3 extents total. + // make sure that downloading pieces from the same extent don't create + // multiple entries in the recent-extent list, but they all use a single + // entry + int const blocks = 32; + auto const have_none = " "; + + auto p = setup_picker("333333333333333333333333", have_none + , "444444444444444444444455", "", blocks); + // download 5 pieces from the first extent + mark_downloading(p, full_piece(0, blocks), &tmp0, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(2, blocks), &tmp1, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(4, blocks), &tmp2, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(6, blocks), &tmp3, options | piece_picker::piece_extent_affinity); + mark_downloading(p, full_piece(1, blocks), &tmp4, options | piece_picker::piece_extent_affinity); + + // since all these belong to the same extent (0), there should be a single + // entry in the recent extent list. Make sure that it's possible to open up a + // second extent, to show that all 5 entries weren't used up by 5 duplicates + // of 0. + // opens up extent 1 + mark_downloading(p, full_piece(8, blocks), &tmp5, options | piece_picker::piece_extent_affinity); + + // now, from a peer that doesn't have anything from the first extent, still + // pick from the second extent even though the last two pieces have higher + // priority. + std::vector picked = pick_pieces(p, " ****************", blocks, 0, &tmp1 + , options | piece_picker::piece_extent_affinity); + TEST_CHECK(verify_pick(p, picked)); + TEST_CHECK(picked == full_piece(9, blocks)); +} + //TODO: 2 test picking with partial pieces and other peers present so that both // backup_pieces and backup_pieces2 are used