Coverage Report

Created: 2026-06-15 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libtorrent/src/choker.cpp
Line
Count
Source
1
/*
2
3
Copyright (c) 2019, Amir Abrams
4
Copyright (c) 2014-2020, Arvid Norberg
5
Copyright (c) 2016, 2018, 2020-2021, Alden Torres
6
Copyright (c) 2016, Steven Siloti
7
Copyright (c) 2019, Monson Shao
8
All rights reserved.
9
10
You may use, distribute and modify this code under the terms of the BSD license,
11
see LICENSE file.
12
*/
13
14
#include "libtorrent/aux_/choker.hpp"
15
#include "libtorrent/aux_/peer_connection.hpp"
16
#include "libtorrent/aux_/session_settings.hpp"
17
#include "libtorrent/aux_/time.hpp"
18
#include "libtorrent/aux_/torrent.hpp"
19
20
#include <functional>
21
22
using namespace std::placeholders;
23
24
namespace libtorrent::aux {
25
26
namespace {
27
28
  int compare_peers(peer_connection const* lhs, peer_connection const* rhs)
29
0
  {
30
0
    int const prio1 = lhs->get_priority(peer_connection::upload_channel);
31
0
    int const prio2 = rhs->get_priority(peer_connection::upload_channel);
32
33
0
    if (prio1 != prio2) return prio1 > prio2 ? 1 : -1;
34
35
    // compare how many bytes they've sent us
36
0
    std::int64_t const c1 = lhs->downloaded_in_last_round();
37
0
    std::int64_t const c2 = rhs->downloaded_in_last_round();
38
39
0
    if (c1 != c2) return c1 > c2 ? 1 : -1;
40
0
    return 0;
41
0
  }
42
43
  // return true if 'lhs' peer should be preferred to be unchoke over 'rhs'
44
  bool unchoke_compare_rr(peer_connection const* lhs
45
    , peer_connection const* rhs, int pieces)
46
0
  {
47
0
    int const cmp = compare_peers(lhs, rhs);
48
0
    if (cmp != 0) return cmp > 0;
49
50
    // when seeding, rotate which peer is unchoked in a round-robin fashion
51
52
    // the amount uploaded since unchoked (not just in the last round)
53
0
    std::int64_t const u1 = lhs->uploaded_since_unchoked();
54
0
    std::int64_t const u2 = rhs->uploaded_since_unchoked();
55
56
    // the way the round-robin unchoker works is that it,
57
    // by default, prioritizes any peer that is already unchoked.
58
    // this maintain the status quo across unchoke rounds. However,
59
    // peers that are unchoked, but have sent more than one quota
60
    // since they were unchoked, they get de-prioritized.
61
62
0
    auto const t1 = lhs->associated_torrent().lock();
63
0
    auto const t2 = rhs->associated_torrent().lock();
64
0
    TORRENT_ASSERT(t1);
65
0
    TORRENT_ASSERT(t2);
66
67
    // if a peer is already unchoked, the number of bytes sent since it was unchoked
68
    // is greater than the send quanta, and it has been unchoked for at least one minute
69
    // then it's done with its upload slot, and we can de-prioritize it
70
0
    bool const c1_quota_complete = !lhs->is_choked()
71
0
      && u1 > std::int64_t(t1->torrent_file().piece_length()) * pieces
72
0
      && aux::time_now() - lhs->time_of_last_unchoke() > minutes(1);
73
0
    bool const c2_quota_complete = !rhs->is_choked()
74
0
      && u2 > std::int64_t(t2->torrent_file().piece_length()) * pieces
75
0
      && aux::time_now() - rhs->time_of_last_unchoke() > minutes(1);
76
77
    // if c2 has completed a quanta, it should be de-prioritized
78
    // and vice versa
79
0
    if (c1_quota_complete != c2_quota_complete)
80
0
      return int(c1_quota_complete) < int(c2_quota_complete);
81
82
    // when seeding, prefer the peer we're uploading the fastest to
83
84
    // force the upload rate to zero for choked peers because
85
    // if the peers just got choked the previous round
86
    // there may have been a residual transfer which was already
87
    // in-flight at the time and we don't want that to cause the peer
88
    // to be ranked at the top of the choked peers
89
0
    std::int64_t const c1 = lhs->is_choked() ? 0 : lhs->uploaded_in_last_round();
90
0
    std::int64_t const c2 = rhs->is_choked() ? 0 : rhs->uploaded_in_last_round();
91
92
0
    if (c1 != c2) return c1 > c2;
93
94
    // if the peers are still identical (say, they're both waiting to be unchoked)
95
    // prioritize the one that has waited the longest to be unchoked
96
    // the round-robin unchoker relies on this logic. Don't change it
97
    // without moving this into that unchoker logic
98
0
    return lhs->time_of_last_unchoke() < rhs->time_of_last_unchoke();
99
0
  }
100
101
  // return true if 'lhs' peer should be preferred to be unchoke over 'rhs'
102
  bool unchoke_compare_fastest_upload(peer_connection const* lhs
103
    , peer_connection const* rhs)
104
0
  {
105
0
    int const cmp = compare_peers(lhs, rhs);
106
0
    if (cmp != 0) return cmp > 0;
107
108
    // when seeding, prefer the peer we're uploading the fastest to
109
0
    std::int64_t const c1 = lhs->uploaded_in_last_round();
110
0
    std::int64_t const c2 = rhs->uploaded_in_last_round();
111
112
0
    if (c1 != c2) return c1 > c2;
113
114
    // prioritize the one that has waited the longest to be unchoked
115
    // the round-robin unchoker relies on this logic. Don't change it
116
    // without moving this into that unchoker logic
117
0
    return lhs->time_of_last_unchoke() < rhs->time_of_last_unchoke();
118
0
  }
119
120
  int anti_leech_score(peer_connection const* peer)
121
0
  {
122
    // the anti-leech seeding algorithm is based on the paper "Improving
123
    // BitTorrent: A Simple Approach" from Chow et. al. and ranks peers based
124
    // on how many pieces they have, preferring to unchoke peers that just
125
    // started and peers that are close to completing. Like this:
126
    //   ^
127
    //   | \                       / |
128
    //   |  \                     /  |
129
    //   |   \                   /   |
130
    // s |    \                 /    |
131
    // c |     \               /     |
132
    // o |      \             /      |
133
    // r |       \           /       |
134
    // e |        \         /        |
135
    //   |         \       /         |
136
    //   |          \     /          |
137
    //   |           \   /           |
138
    //   |            \ /            |
139
    //   |             V             |
140
    //   +---------------------------+
141
    //   0%    num have pieces     100%
142
0
    std::shared_ptr<torrent> const t = peer->associated_torrent().lock();
143
0
    TORRENT_ASSERT(t);
144
145
0
    std::int64_t const total_size = t->torrent_file().total_size();
146
0
    if (total_size == 0) return 0;
147
    // Cap the given_size so that it never causes the score to increase
148
0
    std::int64_t const given_size = std::min(peer->statistics().total_payload_upload()
149
0
      , total_size / 2);
150
0
    std::int64_t const have_size = std::max(given_size
151
0
      , std::int64_t(t->torrent_file().piece_length()) * peer->num_have_pieces());
152
0
    return int(std::abs((have_size - total_size / 2) * 2000 / total_size));
153
0
  }
154
155
  // return true if 'lhs' peer should be preferred to be unchoke over 'rhs'
156
  bool unchoke_compare_anti_leech(peer_connection const* lhs
157
    , peer_connection const* rhs)
158
0
  {
159
0
    int const cmp = compare_peers(lhs, rhs);
160
0
    if (cmp != 0) return cmp > 0;
161
162
0
    int const score1 = anti_leech_score(lhs);
163
0
    int const score2 = anti_leech_score(rhs);
164
0
    if (score1 != score2) return score1 > score2;
165
166
    // prioritize the one that has waited the longest to be unchoked
167
    // the round-robin unchoker relies on this logic. Don't change it
168
    // without moving this into that unchoker logic
169
0
    return lhs->time_of_last_unchoke() < rhs->time_of_last_unchoke();
170
0
  }
171
172
  bool upload_rate_compare(peer_connection const* lhs
173
    , peer_connection const* rhs)
174
0
  {
175
    // take torrent priority into account
176
0
    std::int64_t const c1 = lhs->uploaded_in_last_round()
177
0
      * lhs->get_priority(peer_connection::upload_channel);
178
0
    std::int64_t const c2 = rhs->uploaded_in_last_round()
179
0
      * rhs->get_priority(peer_connection::upload_channel);
180
181
0
    return c1 > c2;
182
0
  }
183
184
  } // anonymous namespace
185
186
  int unchoke_sort(std::vector<peer_connection*>& peers
187
    , time_duration const unchoke_interval
188
    , aux::session_settings const& sett)
189
64
  {
190
64
#if TORRENT_USE_ASSERTS
191
64
    for (auto p : peers)
192
0
    {
193
0
      TORRENT_ASSERT(p->self());
194
0
      TORRENT_ASSERT(p->associated_torrent().lock());
195
0
    }
196
64
#endif
197
198
64
    int upload_slots = sett.get_int(settings_pack::unchoke_slots_limit);
199
64
    if (upload_slots < 0)
200
0
      upload_slots = std::numeric_limits<int>::max();
201
202
    // ==== rate-based ====
203
    //
204
    // The rate based unchoker looks at our upload rate to peers, and find
205
    // a balance between number of upload slots and the rate we achieve. The
206
    // intention is to not spread upload bandwidth too thin, but also to not
207
    // unchoke few enough peers to not be able to saturate the up-link.
208
    // this is done by traversing the peers sorted by our upload rate to
209
    // them in decreasing rates. For each peer we increase the threshold by
210
    // 2 kiB/s. The first peer we get to whom we upload slower than
211
    // the threshold, we stop and that's the number of unchoke slots we have.
212
64
    if (sett.get_int(settings_pack::choking_algorithm)
213
64
      == settings_pack::rate_based_choker)
214
0
    {
215
      // first reset the number of unchoke slots, because we'll calculate
216
      // it purely based on the current state of our peers.
217
0
      upload_slots = 0;
218
219
0
      int rate_threshold = sett.get_int(settings_pack::rate_choker_initial_threshold);
220
221
0
      std::sort(peers.begin(), peers.end()
222
0
        , [](peer_connection const* lhs, peer_connection const* rhs)
223
0
        { return upload_rate_compare(lhs, rhs); });
224
225
0
      for (auto const* p : peers)
226
0
      {
227
0
        int const rate = int(p->uploaded_in_last_round()
228
0
          * 1000 / total_milliseconds(unchoke_interval));
229
230
        // always have at least 1 unchoke slot
231
0
        if (rate < rate_threshold) break;
232
233
0
        ++upload_slots;
234
235
        // TODO: make configurable
236
0
        rate_threshold += 2048;
237
0
      }
238
0
      ++upload_slots;
239
0
    }
240
241
    // sorts the peers that are eligible for unchoke by download rate and
242
    // secondary by total upload. The reason for this is, if all torrents are
243
    // being seeded, the download rate will be 0, and the peers we have sent
244
    // the least to should be unchoked
245
246
    // we use partial sort here, because we only care about the top
247
    // upload_slots peers.
248
249
64
    int const slots = std::min(upload_slots, int(peers.size()));
250
251
64
    if (sett.get_int(settings_pack::seed_choking_algorithm)
252
64
      == settings_pack::round_robin)
253
64
    {
254
64
      int const pieces = sett.get_int(settings_pack::seeding_piece_quota);
255
256
64
      std::nth_element(peers.begin(), peers.begin()
257
64
        + slots, peers.end()
258
64
        , [pieces](peer_connection const* lhs, peer_connection const* rhs)
259
64
        { return unchoke_compare_rr(lhs, rhs, pieces); });
260
64
    }
261
0
    else if (sett.get_int(settings_pack::seed_choking_algorithm)
262
0
      == settings_pack::fastest_upload)
263
0
    {
264
0
      std::nth_element(peers.begin(), peers.begin()
265
0
        + slots, peers.end()
266
0
        , [](peer_connection const* lhs, peer_connection const* rhs)
267
0
        { return unchoke_compare_fastest_upload(lhs, rhs); });
268
0
    }
269
0
    else if (sett.get_int(settings_pack::seed_choking_algorithm)
270
0
      == settings_pack::anti_leech)
271
0
    {
272
0
      std::nth_element(peers.begin(), peers.begin()
273
0
        + slots, peers.end()
274
0
        , [](peer_connection const* lhs, peer_connection const* rhs)
275
0
        { return unchoke_compare_anti_leech(lhs, rhs); });
276
0
    }
277
0
    else
278
0
    {
279
0
      int const pieces = sett.get_int(settings_pack::seeding_piece_quota);
280
0
      std::nth_element(peers.begin(), peers.begin()
281
0
        + slots, peers.end()
282
0
        , [pieces](peer_connection const* lhs, peer_connection const* rhs)
283
0
        { return unchoke_compare_rr(lhs, rhs, pieces); } );
284
285
0
      TORRENT_ASSERT_FAIL();
286
0
    }
287
288
64
    return upload_slots;
289
64
  }
290
291
}