add feature to create an affinity to pick adjecent pieces aligned to 4MiB extents. It's an attempt to improve disk I/O, by writing larger contiguous ranges of bytes. It's off by default.

This commit is contained in:
arvidn 2019-08-04 16:21:02 -07:00 committed by Arvid Norberg
parent ffd4b39b09
commit 07ab3b9739
9 changed files with 367 additions and 4 deletions

View File

@ -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.

View File

@ -69,6 +69,7 @@ namespace libtorrent {
using prio_index_t = aux::strong_typedef<int, struct prio_index_tag_t>;
using picker_options_t = flags::bitfield_flag<std::uint16_t, struct picker_options_tag>;
using download_queue_t = aux::strong_typedef<std::uint8_t, struct dl_queue_tag>;
using piece_extent_t = aux::strong_typedef<int, struct piece_extent_tag>;
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<piece_index_t> 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<block_info> 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<piece_index_t, int> 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<piece_extent_t> 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;

View File

@ -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
};

View File

@ -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<lt::session> ses[2]) {
TEST_EQUAL(is_seed(*ses[0]), true);
}
);
}

View File

@ -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();

View File

@ -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))

View File

@ -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<int>(p) / extent_size};
}
index_range<piece_index_t> 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<int>(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];

View File

@ -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<int_setting_entry_t, settings_pack::num_int_settings> const int_settings

View File

@ -112,12 +112,13 @@ std::shared_ptr<piece_picker> 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<piece_picker> p = std::make_shared<piece_picker>(blocks_per_piece, blocks_per_piece, num_pieces);
std::shared_ptr<piece_picker> p = std::make_shared<piece_picker>(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<piece_picker> 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<piece_block> full_piece(int const p, int const blocks)
{
std::vector<piece_block> 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<piece_picker> const& p, std::vector<piece_block> 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<piece_block> 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<piece_block> 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<piece_block> 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<piece_block> 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<piece_block> 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<piece_block> 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