From 35fe175f2ae2f045c293f708b3a29b1769064372 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 28 Nov 2021 19:12:54 -0600 Subject: [PATCH] refactor: add file-piece-map (#2246) * refactor: add file-piece-map * refactor: use tr_file_priorities * refactor: use tr_files_wanted --- Transmission.xcodeproj/project.pbxproj | 8 + libtransmission/CMakeLists.txt | 2 + libtransmission/completion.cc | 2 +- libtransmission/completion.h | 2 +- libtransmission/file-piece-map.cc | 180 ++++++++++ libtransmission/file-piece-map.h | 92 +++++ libtransmission/inout.cc | 7 +- libtransmission/peer-mgr.cc | 8 +- libtransmission/resume.cc | 34 +- libtransmission/rpcimpl.cc | 49 ++- libtransmission/torrent-ctor.cc | 27 +- libtransmission/torrent.cc | 322 +++--------------- libtransmission/torrent.h | 101 +++--- libtransmission/transmission.h | 13 - tests/libtransmission/CMakeLists.txt | 1 + tests/libtransmission/completion-test.cc | 4 +- tests/libtransmission/file-piece-map-test.cc | 335 +++++++++++++++++++ 17 files changed, 785 insertions(+), 402 deletions(-) create mode 100644 libtransmission/file-piece-map.cc create mode 100644 libtransmission/file-piece-map.h create mode 100644 tests/libtransmission/file-piece-map-test.cc diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index b9361b59e..5f428ad20 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 0A6169A70FE5C9A200C66CE6 /* bitfield.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0A6169A50FE5C9A200C66CE6 /* bitfield.cc */; }; 0A6169A80FE5C9A200C66CE6 /* bitfield.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A6169A60FE5C9A200C66CE6 /* bitfield.h */; }; + 1BB44E07B1B52E28291B4E32 /* file-piece-map.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1BB44E07B1B52E28291B4E30 /* file-piece-map.cc */; }; + 1BB44E07B1B52E28291B4E33 /* file-piece-map.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BB44E07B1B52E28291B4E31 /* file-piece-map.h */; }; 35F373030C2DA89000DAA8F2 /* FilePriorityCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 35F373010C2DA88F00DAA8F2 /* FilePriorityCell.mm */; }; 3C7A11970D0B2EE300B5701F /* getgateway.c in Sources */ = {isa = PBXBuildFile; fileRef = 3C7A11910D0B2EE300B5701F /* getgateway.c */; }; 3C7A11980D0B2EE300B5701F /* getgateway.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C7A11920D0B2EE300B5701F /* getgateway.h */; }; @@ -492,6 +494,8 @@ /* Begin PBXFileReference section */ 0A6169A50FE5C9A200C66CE6 /* bitfield.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = bitfield.cc; sourceTree = ""; }; 0A6169A60FE5C9A200C66CE6 /* bitfield.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bitfield.h; sourceTree = ""; }; + 1BB44E07B1B52E28291B4E30 /* file-piece-map.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = file-piece-map.cc; sourceTree = ""; }; + 1BB44E07B1B52E28291B4E31 /* file-piece-map.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = file-piece-map.h; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 29B97316FDCFA39411CA2CEA /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; @@ -1396,6 +1400,8 @@ 4D8017E910BBC073008A4AF2 /* torrent-magnet.h */, 0A6169A50FE5C9A200C66CE6 /* bitfield.cc */, 0A6169A60FE5C9A200C66CE6 /* bitfield.h */, + 1BB44E07B1B52E28291B4E30 /* file-piece-map.cc */, + 1BB44E07B1B52E28291B4E31 /* file-piece-map.h */, C1425B321EE9C5EA001DB85F /* tr-assert.cc */, C1425B331EE9C5EA001DB85F /* tr-assert.h */, A22CFCA60FC24ED80009BD3E /* tr-dht.cc */, @@ -1883,6 +1889,7 @@ A21FBBAB0EDA78C300BC3C51 /* bandwidth.h in Headers */, A22CFCA90FC24ED80009BD3E /* tr-dht.h in Headers */, 0A6169A80FE5C9A200C66CE6 /* bitfield.h in Headers */, + 1BB44E07B1B52E28291B4E33 /* file-piece-map.h in Headers */, A25964A7106D73A800453B31 /* announcer.h in Headers */, ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */, 4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */, @@ -2479,6 +2486,7 @@ C1033E081A3279B800EF44D8 /* crypto-utils-ccrypto.cc in Sources */, A22CFCA80FC24ED80009BD3E /* tr-dht.cc in Sources */, 0A6169A70FE5C9A200C66CE6 /* bitfield.cc in Sources */, + 1BB44E07B1B52E28291B4E32 /* file-piece-map.cc in Sources */, A25964A6106D73A800453B31 /* announcer.cc in Sources */, 4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */, 4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */, diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 5efdc8468..c57d45fa6 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -25,6 +25,7 @@ set(PROJECT_FILES crypto.cc error.cc fdlimit.cc + file-piece-map.cc file-posix.cc file-win32.cc file.cc @@ -156,6 +157,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS crypto-utils.h crypto.h fdlimit.h + file-piece-map.h handshake.h history.h inout.h diff --git a/libtransmission/completion.cc b/libtransmission/completion.cc index e98837ee7..92b5f46c6 100644 --- a/libtransmission/completion.cc +++ b/libtransmission/completion.cc @@ -59,7 +59,7 @@ uint64_t tr_completion::computeSizeWhenDone() const auto size = size_t{ 0 }; for (tr_piece_index_t piece = 0; piece < block_info_->n_pieces; ++piece) { - if (!tor_->pieceIsDnd(piece)) + if (tor_->pieceIsWanted(piece)) { size += block_info_->pieceSize(piece); } diff --git a/libtransmission/completion.h b/libtransmission/completion.h index ebdbd3827..f88342cfc 100644 --- a/libtransmission/completion.h +++ b/libtransmission/completion.h @@ -29,7 +29,7 @@ struct tr_completion { struct torrent_view { - virtual bool pieceIsDnd(tr_piece_index_t piece) const = 0; + virtual bool pieceIsWanted(tr_piece_index_t piece) const = 0; virtual ~torrent_view() = default; }; diff --git a/libtransmission/file-piece-map.cc b/libtransmission/file-piece-map.cc new file mode 100644 index 000000000..60e07312b --- /dev/null +++ b/libtransmission/file-piece-map.cc @@ -0,0 +1,180 @@ +/* + * This file Copyright (C) Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#include +#include +#include + +#include "transmission.h" + +#include "block-info.h" +#include "file-piece-map.h" + +void tr_file_piece_map::reset(tr_block_info const& block_info, uint64_t const* file_sizes, size_t n_files) +{ + files_.resize(n_files); + files_.shrink_to_fit(); + + uint64_t offset = 0; + for (tr_file_index_t i = 0; i < n_files; ++i) + { + auto const file_size = file_sizes[i]; + auto const begin_piece = block_info.pieceOf(offset); + tr_piece_index_t end_piece = 0; + if (file_size != 0) + { + auto const last_byte = offset + file_size - 1; + auto const final_piece = block_info.pieceOf(last_byte); + end_piece = final_piece + 1; + } + else + { + end_piece = begin_piece + 1; + } + files_[i] = piece_span_t{ begin_piece, end_piece }; + offset += file_size; + } +} + +void tr_file_piece_map::reset(tr_info const& info) +{ + tr_file_index_t const n = info.fileCount; + auto file_sizes = std::vector(n); + std::transform(info.files, info.files + n, std::begin(file_sizes), [](tr_file const& file) { return file.length; }); + reset({ info.totalSize, info.pieceSize }, std::data(file_sizes), std::size(file_sizes)); +} + +tr_file_piece_map::piece_span_t tr_file_piece_map::pieceSpan(tr_file_index_t file) const +{ + return files_[file]; +} + +tr_file_piece_map::file_span_t tr_file_piece_map::fileSpan(tr_piece_index_t piece) const +{ + struct Compare + { + int compare(tr_piece_index_t piece, piece_span_t span) const // <=> + { + if (piece < span.begin) + { + return -1; + } + + if (piece >= span.end) + { + return 1; + } + + return 0; + } + + bool operator()(tr_piece_index_t piece, piece_span_t span) const // < + { + return compare(piece, span) < 0; + } + + int compare(piece_span_t span, tr_piece_index_t piece) const // <=> + { + return -compare(piece, span); + } + + bool operator()(piece_span_t span, tr_piece_index_t piece) const // < + { + return compare(span, piece) < 0; + } + }; + + auto const begin = std::begin(files_); + auto const pair = std::equal_range(begin, std::end(files_), piece, Compare{}); + return { tr_piece_index_t(std::distance(begin, pair.first)), tr_piece_index_t(std::distance(begin, pair.second)) }; +} + +/*** +**** +***/ + +tr_file_priorities::tr_file_priorities(tr_file_piece_map const* fpm) +{ + reset(fpm); +} + +void tr_file_priorities::reset(tr_file_piece_map const* fpm) +{ + fpm_ = fpm; + + auto const n = std::size(*fpm_); + priorities_.resize(n); + priorities_.shrink_to_fit(); + std::fill_n(std::begin(priorities_), n, TR_PRI_NORMAL); +} + +void tr_file_priorities::set(tr_file_index_t file, tr_priority_t priority) +{ + priorities_[file] = priority; +} + +void tr_file_priorities::set(tr_file_index_t const* files, size_t n, tr_priority_t priority) +{ + for (size_t i = 0; i < n; ++i) + { + set(files[i], priority); + } +} + +tr_priority_t tr_file_priorities::filePriority(tr_file_index_t file) const +{ + return priorities_[file]; +} + +tr_priority_t tr_file_priorities::piecePriority(tr_piece_index_t piece) const +{ + auto const [begin_idx, end_idx] = fpm_->fileSpan(piece); + auto const begin = std::begin(priorities_) + begin_idx; + auto const end = std::begin(priorities_) + end_idx; + auto const it = std::max_element(begin, end); + if (it == end) + { + return TR_PRI_NORMAL; + } + return *it; +} + +/*** +**** +***/ + +void tr_files_wanted::reset(tr_file_piece_map const* fpm) +{ + fpm_ = fpm; + wanted_ = tr_bitfield{ std::size(*fpm) }; + wanted_.setHasAll(); // by default we want all files +} + +void tr_files_wanted::set(tr_file_index_t file, bool wanted) +{ + wanted_.set(file, wanted); +} + +void tr_files_wanted::set(tr_file_index_t const* files, size_t n, bool wanted) +{ + for (size_t i = 0; i < n; ++i) + { + set(files[i], wanted); + } +} + +bool tr_files_wanted::fileWanted(tr_file_index_t file) const +{ + return wanted_.test(file); +} + +bool tr_files_wanted::pieceWanted(tr_piece_index_t piece) const +{ + auto const [begin, end] = fpm_->fileSpan(piece); + return wanted_.count(begin, end) != 0; +} diff --git a/libtransmission/file-piece-map.h b/libtransmission/file-piece-map.h new file mode 100644 index 000000000..4a4668958 --- /dev/null +++ b/libtransmission/file-piece-map.h @@ -0,0 +1,92 @@ +/* + * This file Copyright (C) Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#pragma once + +#ifndef __TRANSMISSION__ +#error only libtransmission should #include this header. +#endif + +#include + +#include "transmission.h" + +#include "bitfield.h" + +struct tr_block_info; + +class tr_file_piece_map +{ +public: + template + struct index_span_t + { + T begin; + T end; + }; + using file_span_t = index_span_t; + using piece_span_t = index_span_t; + + tr_file_piece_map(tr_info const& info) + { + reset(info); + } + tr_file_piece_map(tr_block_info const& block_info, uint64_t const* file_sizes, size_t n_files) + { + reset(block_info, file_sizes, n_files); + } + void reset(tr_block_info const& block_info, uint64_t const* file_sizes, size_t n_files); + void reset(tr_info const& info); + + [[nodiscard]] piece_span_t pieceSpan(tr_file_index_t file) const; + [[nodiscard]] file_span_t fileSpan(tr_piece_index_t piece) const; + [[nodiscard]] size_t size() const + { + return std::size(files_); + } + +private: + std::vector files_; +}; + +class tr_file_priorities +{ +public: + explicit tr_file_priorities(tr_file_piece_map const* fpm); + void reset(tr_file_piece_map const*); + void set(tr_file_index_t file, tr_priority_t priority); + void set(tr_file_index_t const* files, size_t n, tr_priority_t priority); + + [[nodiscard]] tr_priority_t filePriority(tr_file_index_t file) const; + [[nodiscard]] tr_priority_t piecePriority(tr_piece_index_t piece) const; + +private: + tr_file_piece_map const* fpm_; + std::vector priorities_; +}; + +class tr_files_wanted +{ +public: + explicit tr_files_wanted(tr_file_piece_map const* fpm) + : wanted_(std::size(*fpm)) + { + reset(fpm); + } + void reset(tr_file_piece_map const* fpm); + + void set(tr_file_index_t file, bool wanted); + void set(tr_file_index_t const* files, size_t n, bool wanted); + + [[nodiscard]] bool fileWanted(tr_file_index_t file) const; + [[nodiscard]] bool pieceWanted(tr_piece_index_t piece) const; + +private: + tr_file_piece_map const* fpm_; + tr_bitfield wanted_; +}; diff --git a/libtransmission/inout.cc b/libtransmission/inout.cc index be88094fa..08d5cd4e1 100644 --- a/libtransmission/inout.cc +++ b/libtransmission/inout.cc @@ -53,7 +53,7 @@ static int readOrWriteBytes( bool const doWrite = ioMode >= TR_IO_WRITE; TR_ASSERT(fileIndex < tr_torrentFileCount(tor)); - auto const file = tr_torrentFile(tor, fileIndex); + auto const& file = tor->info.files[fileIndex]; TR_ASSERT(file.length == 0 || fileOffset < file.length); TR_ASSERT(fileOffset + buflen <= file.length); @@ -91,8 +91,9 @@ static int readOrWriteBytes( { /* open (and maybe create) the file */ auto const filename = tr_strvPath(base, subpath); - tr_preallocation_mode const prealloc = (!file.wanted || !doWrite) ? TR_PREALLOCATE_NONE : - tor->session->preallocationMode; + tr_preallocation_mode const prealloc = (!doWrite || !tor->fileIsWanted(fileIndex)) ? + TR_PREALLOCATE_NONE : + tor->session->preallocationMode; fd = tr_fdFileCheckout(session, tor->uniqueId, fileIndex, filename.c_str(), doWrite, prealloc, file.length); if (fd == TR_BAD_SYS_FILE) diff --git a/libtransmission/peer-mgr.cc b/libtransmission/peer-mgr.cc index a3fb85005..60613024c 100644 --- a/libtransmission/peer-mgr.cc +++ b/libtransmission/peer-mgr.cc @@ -577,7 +577,7 @@ std::vector tr_peerMgrGetNextRequests(tr_torrent* torrent, tr_p bool clientCanRequestPiece(tr_piece_index_t piece) const override { - return !torrent_->pieceIsDnd(piece) && peer_->have.test(piece); + return torrent_->pieceIsWanted(piece) && peer_->have.test(piece); } bool isEndgame() const override @@ -1694,7 +1694,7 @@ uint64_t tr_peerMgrGetDesiredAvailable(tr_torrent const* tor) for (size_t i = 0; i < n_pieces; ++i) { - if (!tor->pieceIsDnd(i) && have.at(i)) + if (tor->pieceIsWanted(i) && have.at(i)) { desired_available += tor->countMissingBytesInPiece(i); } @@ -2022,7 +2022,7 @@ static void rechokeDownloads(tr_swarm* s) for (int i = 0; i < n; ++i) { - piece_is_interesting[i] = !tor->pieceIsDnd(i) && !tor->hasPiece(i); + piece_is_interesting[i] = tor->pieceIsWanted(i) && !tor->hasPiece(i); } /* decide WHICH peers to be interested in (based on their cancel-to-block ratio) */ @@ -2703,7 +2703,7 @@ static void bandwidthPulse(evutil_socket_t /*fd*/, short /*what*/, void* vmgr) if (tor->swarm->needsCompletenessCheck) { tor->swarm->needsCompletenessCheck = false; - tr_torrentRecheckCompleteness(tor); + tor->recheckCompleteness(); } /* stop torrents that are ready to stop, but couldn't be stopped diff --git a/libtransmission/resume.cc b/libtransmission/resume.cc index 766835417..886caf415 100644 --- a/libtransmission/resume.cc +++ b/libtransmission/resume.cc @@ -156,38 +156,27 @@ static uint64_t loadDND(tr_variant* dict, tr_torrent* tor) if (tr_variantDictFindList(dict, TR_KEY_dnd, &list) && tr_variantListSize(list) == n) { - tr_file_index_t* dl = tr_new(tr_file_index_t, n); - tr_file_index_t* dnd = tr_new(tr_file_index_t, n); - tr_file_index_t dlCount = 0; - tr_file_index_t dndCount = 0; + auto wanted = std::vector{}; + auto unwanted = std::vector{}; + wanted.reserve(n); + unwanted.reserve(n); for (tr_file_index_t i = 0; i < n; ++i) { auto tmp = false; if (tr_variantGetBool(tr_variantListChild(list, i), &tmp) && tmp) { - dnd[dndCount++] = i; + unwanted.push_back(i); } else { - dl[dlCount++] = i; + wanted.push_back(i); } } - if (dndCount != 0) - { - tr_torrentInitFileDLs(tor, dnd, dndCount, false); - tr_logAddTorDbg(tor, "Resume file found %d files listed as dnd", dndCount); - } + tor->initFilesWanted(std::data(unwanted), std::size(unwanted), false); + tor->initFilesWanted(std::data(wanted), std::size(wanted), true); - if (dlCount != 0) - { - tr_torrentInitFileDLs(tor, dl, dlCount, true); - tr_logAddTorDbg(tor, "Resume file found %d files marked for download", dlCount); - } - - tr_free(dnd); - tr_free(dl); ret = TR_FR_DND; } else @@ -232,7 +221,7 @@ static uint64_t loadFilePriorities(tr_variant* dict, tr_torrent* tor) auto priority = int64_t{}; if (tr_variantGetInt(tr_variantListChild(list, i), &priority)) { - tr_torrentInitFilePriority(tor, i, priority); + tor->setFilePriority(i, priority); } } @@ -570,7 +559,6 @@ static uint64_t loadProgress(tr_variant* dict, tr_torrent* tor) for (tr_file_index_t fi = 0; fi < n_files; ++fi) { tr_variant* const b = tr_variantListChild(l, fi); - tr_file* const f = &inf->files[fi]; auto time_checked = time_t{}; if (tr_variantIsInt(b)) @@ -585,8 +573,8 @@ static uint64_t loadProgress(tr_variant* dict, tr_torrent* tor) tr_variantGetInt(tr_variantListChild(b, 0), &offset); time_checked = tr_time(); - size_t const pieces = f->priv.lastPiece + 1 - f->priv.firstPiece; - for (size_t i = 0; i < pieces; ++i) + auto const [begin, end] = tor->piecesInFile(fi); + for (size_t i = 0, n = end - begin; i < n; ++i) { int64_t piece_time = 0; tr_variantGetInt(tr_variantListChild(b, i + 1), &piece_time); diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 94a54b7dd..45599adb3 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -13,6 +13,7 @@ #include /* strtol */ #include /* strcmp */ #include +#include #include #include @@ -983,11 +984,11 @@ static char const* setLabels(tr_torrent* tor, tr_variant* list) static char const* setFilePriorities(tr_torrent* tor, int priority, tr_variant* list) { - int fileCount = 0; - size_t const n = tr_variantListSize(list); char const* errmsg = nullptr; - tr_file_index_t* files = tr_new0(tr_file_index_t, tor->info.fileCount); + auto files = std::vector{}; + files.reserve(tr_torrentFileCount(tor)); + size_t const n = tr_variantListSize(list); if (n != 0) { for (size_t i = 0; i < n; ++i) @@ -997,7 +998,7 @@ static char const* setFilePriorities(tr_torrent* tor, int priority, tr_variant* { if (0 <= tmp && tmp < tor->info.fileCount) { - files[fileCount++] = tmp; + files.push_back(tmp); } else { @@ -1006,41 +1007,39 @@ static char const* setFilePriorities(tr_torrent* tor, int priority, tr_variant* } } } - else /* if empty set, apply to all */ + else // if empty set, apply to all { for (tr_file_index_t t = 0; t < tor->info.fileCount; ++t) { - files[fileCount++] = t; + files.push_back(t); } } - if (fileCount != 0) - { - tr_torrentSetFilePriorities(tor, files, fileCount, priority); - } + tor->setFilePriorities(std::data(files), std::size(files), priority); - tr_free(files); return errmsg; } -static char const* setFileDLs(tr_torrent* tor, bool do_download, tr_variant* list) +static char const* setFileDLs(tr_torrent* tor, bool wanted, tr_variant* list) { char const* errmsg = nullptr; - int fileCount = 0; - size_t const n = tr_variantListSize(list); - tr_file_index_t* files = tr_new0(tr_file_index_t, tor->info.fileCount); + auto const n_files = tr_torrentFileCount(tor); + size_t const n_items = tr_variantListSize(list); - if (n != 0) /* if argument list, process them */ + auto files = std::vector{}; + files.reserve(n_files); + + if (n_items != 0) // if argument list, process them { - for (size_t i = 0; i < n; ++i) + for (size_t i = 0; i < n_items; ++i) { auto tmp = int64_t{}; if (tr_variantGetInt(tr_variantListChild(list, i), &tmp)) { if (0 <= tmp && tmp < tor->info.fileCount) { - files[fileCount++] = tmp; + files.push_back(tmp); } else { @@ -1049,20 +1048,14 @@ static char const* setFileDLs(tr_torrent* tor, bool do_download, tr_variant* lis } } } - else /* if empty set, apply to all */ + else // if empty set, apply to all { - for (tr_file_index_t t = 0; t < tor->info.fileCount; ++t) - { - files[fileCount++] = t; - } + files.resize(n_files); + std::iota(std::begin(files), std::end(files), 0); } - if (fileCount != 0) - { - tr_torrentSetFileDLs(tor, files, fileCount, do_download); - } + tor->setFilesWanted(std::data(files), std::size(files), wanted); - tr_free(files); return errmsg; } diff --git a/libtransmission/torrent-ctor.cc b/libtransmission/torrent-ctor.cc index 25f1e1180..147462b49 100644 --- a/libtransmission/torrent-ctor.cc +++ b/libtransmission/torrent-ctor.cc @@ -46,8 +46,8 @@ struct tr_ctor std::string incomplete_dir; - std::vector want; - std::vector not_want; + std::vector wanted; + std::vector unwanted; std::vector low; std::vector normal; std::vector high; @@ -188,32 +188,21 @@ void tr_ctorSetFilePriorities(tr_ctor* ctor, tr_file_index_t const* files, tr_fi void tr_ctorInitTorrentPriorities(tr_ctor const* ctor, tr_torrent* tor) { - for (auto file_index : ctor->low) - { - tr_torrentInitFilePriority(tor, file_index, TR_PRI_LOW); - } - - for (auto file_index : ctor->normal) - { - tr_torrentInitFilePriority(tor, file_index, TR_PRI_NORMAL); - } - - for (auto file_index : ctor->high) - { - tr_torrentInitFilePriority(tor, file_index, TR_PRI_HIGH); - } + tor->setFilePriorities(std::data(ctor->low), std::size(ctor->low), TR_PRI_LOW); + tor->setFilePriorities(std::data(ctor->normal), std::size(ctor->normal), TR_PRI_NORMAL); + tor->setFilePriorities(std::data(ctor->high), std::size(ctor->high), TR_PRI_HIGH); } void tr_ctorSetFilesWanted(tr_ctor* ctor, tr_file_index_t const* files, tr_file_index_t fileCount, bool wanted) { - auto& indices = wanted ? ctor->want : ctor->not_want; + auto& indices = wanted ? ctor->wanted : ctor->unwanted; indices.assign(files, files + fileCount); } void tr_ctorInitTorrentWanted(tr_ctor const* ctor, tr_torrent* tor) { - tr_torrentInitFileDLs(tor, std::data(ctor->not_want), std::size(ctor->not_want), false); - tr_torrentInitFileDLs(tor, std::data(ctor->want), std::size(ctor->want), true); + tor->initFilesWanted(std::data(ctor->unwanted), std::size(ctor->unwanted), false); + tor->initFilesWanted(std::data(ctor->wanted), std::size(ctor->wanted), true); } /*** diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index b04f8d498..a051e96a9 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -579,61 +579,6 @@ static void onTrackerResponse(tr_torrent* tor, tr_tracker_event const* event, vo **** ***/ -static constexpr void initFilePieces(tr_torrent* tor, tr_file_index_t fileIndex) -{ - TR_ASSERT(tor != nullptr); - TR_ASSERT(fileIndex < tor->info.fileCount); - - tr_file& file = tor->info.files[fileIndex]; - uint64_t first_byte = file.priv.offset; - uint64_t last_byte = first_byte + (file.length != 0 ? file.length - 1 : 0); - - file.priv.firstPiece = tor->pieceOf(first_byte); - file.priv.lastPiece = tor->pieceOf(last_byte); -} - -static constexpr bool pieceHasFile(tr_piece_index_t piece, tr_file const* file) -{ - return file->priv.firstPiece <= piece && piece <= file->priv.lastPiece; -} - -static tr_priority_t calculatePiecePriority(tr_info const& info, tr_piece_index_t piece, tr_file_index_t file_hint) -{ - // safeguard against a bad arg - file_hint = std::min(file_hint, info.fileCount - 1); - - // find the first file with data in this piece - tr_file_index_t first = file_hint; - while (first > 0 && pieceHasFile(piece, &info.files[first - 1])) - { - --first; - } - - // the priority is the max of all the file priorities in the piece - tr_priority_t priority = TR_PRI_LOW; - for (tr_file_index_t i = first; i < info.fileCount; ++i) - { - tr_file const* file = &info.files[i]; - - if (!pieceHasFile(piece, file)) - { - break; - } - - priority = std::max(priority, file->priv.priority); - - /* When dealing with multimedia files, getting the first and - last pieces can sometimes allow you to preview it a bit - before it's fully downloaded... */ - if ((file->priv.priority >= TR_PRI_NORMAL) && (file->priv.firstPiece == piece || file->priv.lastPiece == piece)) - { - priority = TR_PRI_HIGH; - } - } - - return priority; -} - static void tr_torrentInitFilePieces(tr_torrent* tor) { uint64_t offset = 0; @@ -644,44 +589,9 @@ static void tr_torrentInitFilePieces(tr_torrent* tor) { inf->files[f].priv.offset = offset; offset += inf->files[f].length; - initFilePieces(tor, f); } } -static void tr_torrentInitPiecePriorities(tr_torrent* tor) -{ - tor->piece_priorities_.clear(); - - // throw away file prorities once we're done downloading, - // they just waste time & space - if (tr_torrentIsSeed(tor)) - { - return; - } - - /* build the array of first-file hints to give calculatePiecePriority */ - tr_info* inf = &tor->info; - tr_file_index_t* firstFiles = tr_new(tr_file_index_t, inf->pieceCount); - tr_file_index_t f = 0; - - for (tr_piece_index_t p = 0; p < inf->pieceCount; ++p) - { - while (inf->files[f].priv.lastPiece < p) - { - ++f; - } - - firstFiles[p] = f; - } - - for (tr_piece_index_t p = 0; p < inf->pieceCount; ++p) - { - tor->setPiecePriority(p, calculatePiecePriority(*inf, p, firstFiles[p])); - } - - tr_free(firstFiles); -} - static void torrentStart(tr_torrent* tor, bool bypass_queue); static void tr_torrentFireMetadataCompleted(tr_torrent* tor); @@ -752,6 +662,10 @@ static void torrentInit(tr_torrent* tor, tr_ctor const* ctor) static int nextUniqueId = 1; + tor->fpm_.reset(tor->info); + tor->file_priorities_.reset(&tor->fpm_); + tor->files_wanted_.reset(&tor->fpm_); + tor->session = session; tor->uniqueId = nextUniqueId++; tor->queuePosition = tr_sessionCountTorrents(session); @@ -814,7 +728,6 @@ static void torrentInit(tr_torrent* tor, tr_ctor const* ctor) tr_ctorInitTorrentPriorities(ctor, tor); tr_ctorInitTorrentWanted(ctor, tor); - tr_torrentInitPiecePriorities(tor); refreshCurrentDir(tor); @@ -1315,8 +1228,8 @@ tr_file_view tr_torrentFile(tr_torrent const* torrent, tr_file_index_t i) auto const& file = torrent->info.files[i]; auto const* const name = file.name; - auto const priority = file.priv.priority; - auto const wanted = !file.priv.dnd; + auto const priority = torrent->file_priorities_.filePriority(i); + auto const wanted = torrent->files_wanted_.fileWanted(i); auto const length = file.length; if (torrent->completeness == TR_SEED || length == 0) @@ -1461,7 +1374,7 @@ static void torrentStartImpl(void* vtor) TR_ASSERT(tr_isTorrent(tor)); - tr_torrentRecheckCompleteness(tor); + tor->recheckCompleteness(); torrentSetQueued(tor, false); time_t const now = tr_time(); @@ -1603,7 +1516,7 @@ static void onVerifyDoneThreadFunc(void* vdata) { if (!data->aborted) { - tr_torrentRecheckCompleteness(tor); + tor->recheckCompleteness(); } if (data->callback_func != nullptr) @@ -1973,65 +1886,65 @@ static void torrentCallScript(tr_torrent const* tor, char const* script) tr_free(torrent_dir); } -void tr_torrentRecheckCompleteness(tr_torrent* tor) +void tr_torrent::recheckCompleteness() { - auto const lock = tor->unique_lock(); + auto const lock = unique_lock(); - auto const completeness = tor->completion.status(); + auto const new_completeness = completion.status(); - if (completeness != tor->completeness) + if (new_completeness != completeness) { - bool const recentChange = tor->downloadedCur != 0; - bool const wasLeeching = !tr_torrentIsSeed(tor); - bool const wasRunning = tor->isRunning; + bool const recentChange = downloadedCur != 0; + bool const wasLeeching = !tr_torrentIsSeed(this); + bool const wasRunning = isRunning; if (recentChange) { tr_logAddTorInfo( - tor, + this, _("State changed from \"%1$s\" to \"%2$s\""), - getCompletionString(tor->completeness), + getCompletionString(this->completeness), getCompletionString(completeness)); } - tor->completeness = completeness; - tr_fdTorrentClose(tor->session, tor->uniqueId); + this->completeness = new_completeness; + tr_fdTorrentClose(this->session, this->uniqueId); - if (tr_torrentIsSeed(tor)) + if (tr_torrentIsSeed(this)) { if (recentChange) { - tr_announcerTorrentCompleted(tor); - tor->doneDate = tor->anyDate = tr_time(); + tr_announcerTorrentCompleted(this); + this->doneDate = this->anyDate = tr_time(); } if (wasLeeching && wasRunning) { /* clear interested flag on all peers */ - tr_peerMgrClearInterest(tor); + tr_peerMgrClearInterest(this); } - if (tor->currentDir == tor->incompleteDir) + if (this->currentDir == this->incompleteDir) { - tor->setLocation(tor->downloadDir, true, nullptr, nullptr); + this->setLocation(this->downloadDir, true, nullptr, nullptr); } } - fireCompletenessChange(tor, completeness, wasRunning); + fireCompletenessChange(this, completeness, wasRunning); - if (tr_torrentIsSeed(tor) && wasLeeching && wasRunning) + if (tr_torrentIsSeed(this) && wasLeeching && wasRunning) { /* if completeness was TR_LEECH, the seed limit check will have been skipped in bandwidthPulse */ - tr_torrentCheckSeedLimit(tor); + tr_torrentCheckSeedLimit(this); } - tr_torrentSetDirty(tor); + this->setDirty(); - if (tr_torrentIsSeed(tor)) + if (tr_torrentIsSeed(this)) { - tr_torrentSave(tor); - callScriptIfEnabled(tor, TR_SCRIPT_ON_TORRENT_DONE); + tr_torrentSave(this); + callScriptIfEnabled(this, TR_SCRIPT_ON_TORRENT_DONE); } } } @@ -2058,151 +1971,15 @@ void tr_torrentSetMetadataCallback(tr_torrent* tor, tr_torrent_metadata_func fun tor->metadata_func_user_data = user_data; } -/** -*** File priorities -**/ - -void tr_torrentInitFilePriority(tr_torrent* tor, tr_file_index_t fileIndex, tr_priority_t priority) -{ - TR_ASSERT(tr_isTorrent(tor)); - TR_ASSERT(fileIndex < tor->info.fileCount); - TR_ASSERT(tr_isPriority(priority)); - - auto& info = tor->info; - tr_file* file = &info.files[fileIndex]; - - file->priv.priority = priority; - - for (tr_piece_index_t i = file->priv.firstPiece; i <= file->priv.lastPiece; ++i) - { - tor->setPiecePriority(i, calculatePiecePriority(info, i, fileIndex)); - } -} - -void tr_torrentSetFilePriorities( - tr_torrent* tor, - tr_file_index_t const* files, - tr_file_index_t fileCount, - tr_priority_t priority) -{ - TR_ASSERT(tr_isTorrent(tor)); - auto const lock = tor->unique_lock(); - - for (tr_file_index_t i = 0; i < fileCount; ++i) - { - if (files[i] < tor->info.fileCount) - { - tr_torrentInitFilePriority(tor, files[i], priority); - } - } - - tr_torrentSetDirty(tor); -} - -tr_priority_t* tr_torrentGetFilePriorities(tr_torrent const* tor) -{ - TR_ASSERT(tr_isTorrent(tor)); - - tr_priority_t* p = tr_new0(tr_priority_t, tor->info.fileCount); - - for (tr_file_index_t i = 0; i < tor->info.fileCount; ++i) - { - p[i] = tor->info.files[i].priv.priority; - } - - return p; -} - /** *** File DND **/ -static void setFileDND(tr_torrent* tor, tr_file_index_t fileIndex, bool doDownload) -{ - bool const dnd = !doDownload; - tr_file* file = &tor->info.files[fileIndex]; - - file->priv.dnd = dnd; - auto const firstPiece = file->priv.firstPiece; - auto const lastPiece = file->priv.lastPiece; - - /* can't set the first piece to DND unless - every file using that piece is DND */ - auto firstPieceDND = dnd; - - if (fileIndex > 0) - { - for (tr_file_index_t i = fileIndex - 1; firstPieceDND; --i) - { - if (tor->info.files[i].priv.lastPiece != firstPiece) - { - break; - } - - firstPieceDND = tor->info.files[i].priv.dnd; - - if (i == 0) - { - break; - } - } - } - - /* can't set the last piece to DND unless - every file using that piece is DND */ - auto lastPieceDND = dnd; - - for (tr_file_index_t i = fileIndex + 1; lastPieceDND && i < tor->info.fileCount; ++i) - { - if (tor->info.files[i].priv.firstPiece != lastPiece) - { - break; - } - - lastPieceDND = tor->info.files[i].priv.dnd; - } - - // update dnd_pieces_ - - if (firstPiece == lastPiece) - { - tor->dnd_pieces_.set(firstPiece, firstPieceDND && lastPieceDND); - } - else - { - tor->dnd_pieces_.set(firstPiece, firstPieceDND); - tor->dnd_pieces_.set(lastPiece, lastPieceDND); - for (tr_piece_index_t pp = firstPiece + 1; pp < lastPiece; ++pp) - { - tor->dnd_pieces_.set(pp, dnd); - } - } -} - -void tr_torrentInitFileDLs(tr_torrent* tor, tr_file_index_t const* files, tr_file_index_t fileCount, bool doDownload) +void tr_torrentSetFileDLs(tr_torrent* tor, tr_file_index_t const* files, tr_file_index_t n_files, bool wanted) { TR_ASSERT(tr_isTorrent(tor)); - auto const lock = tor->unique_lock(); - for (tr_file_index_t i = 0; i < fileCount; ++i) - { - if (files[i] < tor->info.fileCount) - { - setFileDND(tor, files[i], doDownload); - } - } - - tor->completion.invalidateSizeWhenDone(); -} - -void tr_torrentSetFileDLs(tr_torrent* tor, tr_file_index_t const* files, tr_file_index_t fileCount, bool doDownload) -{ - TR_ASSERT(tr_isTorrent(tor)); - auto const lock = tor->unique_lock(); - - tr_torrentInitFileDLs(tor, files, fileCount, doDownload); - tr_torrentSetDirty(tor); - tr_torrentRecheckCompleteness(tor); + tor->setFilesWanted(files, n_files, wanted); } /*** @@ -2528,16 +2305,18 @@ uint64_t tr_torrentGetBytesLeftToAllocate(tr_torrent const* tor) uint64_t bytesLeft = 0; - for (tr_file_index_t i = 0; i < tor->info.fileCount; ++i) + for (tr_file_index_t i = 0, n = tr_torrentFileCount(tor); i < n; ++i) { - if (!tor->info.files[i].priv.dnd) + auto const file = tr_torrentFile(tor, i); + + if (file.wanted) { - tr_sys_path_info info; - uint64_t const length = tor->info.files[i].length; + uint64_t const length = file.length; char* path = tr_torrentFindFile(tor, i); bytesLeft += length; + tr_sys_path_info info; if (path != nullptr && tr_sys_path_get_info(path, 0, &info, nullptr) && info.type == TR_SYS_PATH_IS_FILE && info.size <= length) { @@ -3008,15 +2787,13 @@ static void tr_torrentPieceCompleted(tr_torrent* tor, tr_piece_index_t pieceInde { tr_peerMgrPieceCompleted(tor, pieceIndex); - /* if this piece completes any file, invoke the fileCompleted func for it */ - for (tr_file_index_t i = 0; i < tor->info.fileCount; ++i) + // if this piece completes any file, invoke the fileCompleted func for it + auto const [begin, end] = tor->fpm_.fileSpan(pieceIndex); + for (tr_file_index_t file = begin; file < end; ++file) { - tr_file const* file = &tor->info.files[i]; - - if ((file->priv.firstPiece <= pieceIndex) && (pieceIndex <= file->priv.lastPiece) && - tor->completion.hasBlocks(tr_torGetFileBlockSpan(tor, i))) + if (tor->completion.hasBlocks(tr_torGetFileBlockSpan(tor, file))) { - tr_torrentFileCompleted(tor, i); + tr_torrentFileCompleted(tor, file); } } } @@ -3567,3 +3344,12 @@ void tr_torrent::swapMetainfo(tr_metainfo_parsed& parsed) std::swap(this->piece_checksums_, parsed.pieces); std::swap(this->infoDictLength, parsed.info_dict_length); } + +void tr_torrentSetFilePriorities( + tr_torrent* tor, + tr_file_index_t const* files, + tr_file_index_t fileCount, + tr_priority_t priority) +{ + tor->setFilePriorities(files, fileCount, priority); +} diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index a1a7b4229..1b87d5998 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -26,6 +25,7 @@ #include "block-info.h" #include "completion.h" #include "file.h" +#include "file-piece-map.h" #include "quark.h" #include "session.h" #include "tr-assert.h" @@ -56,15 +56,10 @@ void tr_ctorInitTorrentWanted(tr_ctor const* ctor, tr_torrent* tor); *** **/ -/* just like tr_torrentSetFileDLs but doesn't trigger a fastresume save */ -void tr_torrentInitFileDLs(tr_torrent* tor, tr_file_index_t const* files, tr_file_index_t fileCount, bool do_download); - using tr_labels_t = std::unordered_set; void tr_torrentSetLabels(tr_torrent* tor, tr_labels_t&& labels); -void tr_torrentRecheckCompleteness(tr_torrent*); - void tr_torrentChangeMyPort(tr_torrent* session); tr_sha1_digest_t tr_torrentInfoHash(tr_torrent const* torrent); @@ -90,8 +85,6 @@ void tr_torrentGetBlockLocation( tr_block_span_t tr_torGetFileBlockSpan(tr_torrent const* tor, tr_file_index_t const file); -void tr_torrentInitFilePriority(tr_torrent* tor, tr_file_index_t fileIndex, tr_priority_t priority); - void tr_torrentCheckSeedLimit(tr_torrent* tor); /** save a torrent's .resume file if it's changed since the last time it was saved */ @@ -238,41 +231,54 @@ public: completion.setHasPiece(piece, has); } - bool pieceIsDnd(tr_piece_index_t piece) const final + /// FILE <-> PIECE + + auto piecesInFile(tr_file_index_t file) const { - return dnd_pieces_.test(piece); + return fpm_.pieceSpan(file); } + /// WANTED + + bool pieceIsWanted(tr_piece_index_t piece) const final + { + return files_wanted_.pieceWanted(piece); + } + + bool fileIsWanted(tr_file_index_t file) const + { + return files_wanted_.fileWanted(file); + } + + void initFilesWanted(tr_file_index_t const* files, size_t n_files, bool wanted) + { + setFilesWanted(files, n_files, wanted, /*is_bootstrapping*/ true); + } + + void setFilesWanted(tr_file_index_t const* files, size_t n_files, bool wanted) + { + setFilesWanted(files, n_files, wanted, /*is_bootstrapping*/ false); + } + + void recheckCompleteness(); // TODO(ckerr): should be private + /// PRIORITIES - void setPiecePriority(tr_piece_index_t piece, tr_priority_t priority) - { - // since 'TR_PRI_NORMAL' is by far the most common, save some - // space by treating anything not in the map as normal - if (priority == TR_PRI_NORMAL) - { - piece_priorities_.erase(piece); - - if (std::empty(piece_priorities_)) - { - // ensure we release piece_priorities_' internal memory - piece_priorities_ = decltype(piece_priorities_){}; - } - } - else - { - piece_priorities_[piece] = priority; - } - } - tr_priority_t piecePriority(tr_piece_index_t piece) const { - auto const it = piece_priorities_.find(piece); - if (it == std::end(piece_priorities_)) - { - return TR_PRI_NORMAL; - } - return it->second; + return file_priorities_.piecePriority(piece); + } + + void setFilePriorities(tr_file_index_t const* files, tr_file_index_t fileCount, tr_priority_t priority) + { + file_priorities_.set(files, fileCount, priority); + setDirty(); + } + + void setFilePriority(tr_file_index_t file, tr_priority_t priority) + { + file_priorities_.set(file, priority); + setDirty(); } /// CHECKSUMS @@ -310,8 +316,7 @@ public: // if a file has changed, mark its pieces as unchecked if (mtime == 0 || mtime != mtimes[i]) { - auto const begin = info.files[i].priv.firstPiece; - auto const end = info.files[i].priv.lastPiece + 1; + auto const [begin, end] = piecesInFile(i); checked_pieces_.unsetSpan(begin, end); } } @@ -360,8 +365,6 @@ public: std::optional verify_progress; - std::unordered_map piece_priorities_; - tr_stat_errtype error = TR_STAT_OK; char errorString[128] = {}; tr_quark error_announce_url = TR_KEY_NONE; @@ -491,7 +494,25 @@ public: static auto constexpr MagicNumber = int{ 95549 }; + tr_file_piece_map fpm_ = tr_file_piece_map{ info }; + tr_file_priorities file_priorities_{ &fpm_ }; + tr_files_wanted files_wanted_{ &fpm_ }; + private: + void setFilesWanted(tr_file_index_t const* files, size_t n_files, bool wanted, bool is_bootstrapping) + { + auto const lock = unique_lock(); + + files_wanted_.set(files, n_files, wanted); + completion.invalidateSizeWhenDone(); + + if (!is_bootstrapping) + { + setDirty(); + recheckCompleteness(); + } + } + mutable std::vector piece_checksums_; }; diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index feb372d7a..0d4b8923a 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -1188,15 +1188,6 @@ void tr_torrentSetFilePriorities( tr_file_index_t fileCount, tr_priority_t priority); -/** - * @brief Get this torrent's file priorities. - * - * @return A malloc()ed array of tor->info.fileCount items, - * each holding a TR_PRI_NORMAL, TR_PRI_HIGH, or TR_PRI_LOW. - * It's the caller's responsibility to free() this. - */ -tr_priority_t* tr_torrentGetFilePriorities(tr_torrent const* torrent); - /** @brief Set a batch of files to be downloaded or not. */ void tr_torrentSetFileDLs(tr_torrent* torrent, tr_file_index_t const* files, tr_file_index_t fileCount, bool do_download); @@ -1603,10 +1594,6 @@ struct tr_file_priv { uint64_t offset; // file begins at the torrent's nth byte time_t mtime; - tr_piece_index_t firstPiece; // We need pieces [firstPiece... - tr_piece_index_t lastPiece; // ...lastPiece] to dl this file - int8_t priority; // TR_PRI_HIGH, _NORMAL, or _LOW - bool dnd; // "do not download" flag bool is_renamed; // true if we're using a different path from the one in the metainfo; ie, if the user has renamed it */ }; /** @brief a part of tr_info that represents a single file of the torrent's content */ diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index 9de7eb45d..91a7360ae 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(libtransmission-test crypto-test.cc error-test.cc file-test.cc + file-piece-map-test.cc getopt-test.cc history-test.cc json-test.cc diff --git a/tests/libtransmission/completion-test.cc b/tests/libtransmission/completion-test.cc index d7e6be849..c869d7aee 100644 --- a/tests/libtransmission/completion-test.cc +++ b/tests/libtransmission/completion-test.cc @@ -27,9 +27,9 @@ struct TestTorrent : public tr_completion::torrent_view { std::set dnd_pieces; - [[nodiscard]] bool pieceIsDnd(tr_piece_index_t piece) const final + [[nodiscard]] bool pieceIsWanted(tr_piece_index_t piece) const final { - return dnd_pieces.count(piece) != 0; + return dnd_pieces.count(piece) == 0; } }; diff --git a/tests/libtransmission/file-piece-map-test.cc b/tests/libtransmission/file-piece-map-test.cc new file mode 100644 index 000000000..e5874f333 --- /dev/null +++ b/tests/libtransmission/file-piece-map-test.cc @@ -0,0 +1,335 @@ +/* + * This file Copyright (C) Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#include +#include +#include + +#include "transmission.h" + +#include "block-info.h" +#include "file-piece-map.h" + +#include "gtest/gtest.h" + +class FilePieceMapTest : public ::testing::Test +{ +protected: + static constexpr size_t TotalSize{ 1001 }; + static constexpr size_t PieceSize{ 100 }; + tr_block_info const block_info_{ TotalSize, PieceSize }; + + static constexpr std::array FileSizes{ + 500, // [offset 0] begins and ends on a piece boundary + 0, // [offset 500] zero-sized files + 0, 0, 0, + 50, // [offset 500] begins on a piece boundary + 100, // [offset 550] neither begins nor ends on a piece boundary, spans >1 piece + 10, // [offset 650] small files all contained in a single piece + 9, 8, 7, 6, + 311, // [offset 690] ends end-of-torrent + 0, // [offset 1001] zero-sized files at the end-of-torrent + 0, 0, 0, + // sum is 1001 == TotalSize + }; + + void SetUp() override + { + EXPECT_EQ(11, block_info_.n_pieces); + EXPECT_EQ(PieceSize, block_info_.piece_size); + EXPECT_EQ(TotalSize, block_info_.total_size); + EXPECT_EQ(TotalSize, std::accumulate(std::begin(FileSizes), std::end(FileSizes), uint64_t{ 0 })); + } +}; + +TEST_F(FilePieceMapTest, pieceSpan) +{ + // Note to reviewers: it's easy to see a nonexistent fencepost error here. + // Remember everything is zero-indexed, so the 11 valid pieces are [0..10] + // and that last piece #10 has one byte in it. Piece #11 is the 'end' iterator position. + auto constexpr ExpectedPieceSpans = std::array{ { + { 0, 5 }, + { 5, 6 }, + { 5, 6 }, + { 5, 6 }, + { 5, 6 }, + { 5, 6 }, + { 5, 7 }, + { 6, 7 }, + { 6, 7 }, + { 6, 7 }, + { 6, 7 }, + { 6, 7 }, + { 6, 11 }, + { 10, 11 }, + { 10, 11 }, + { 10, 11 }, + { 10, 11 }, + } }; + EXPECT_EQ(std::size(FileSizes), std::size(ExpectedPieceSpans)); + + auto const fpm = tr_file_piece_map{ block_info_, std::data(FileSizes), std::size(FileSizes) }; + tr_file_index_t const n = std::size(fpm); + EXPECT_EQ(std::size(FileSizes), n); + uint64_t offset = 0; + for (tr_file_index_t file = 0; file < n; ++file) + { + EXPECT_EQ(ExpectedPieceSpans[file].begin, fpm.pieceSpan(file).begin); + EXPECT_EQ(ExpectedPieceSpans[file].end, fpm.pieceSpan(file).end); + offset += FileSizes[file]; + } + EXPECT_EQ(TotalSize, offset); + EXPECT_EQ(block_info_.n_pieces, fpm.pieceSpan(std::size(FileSizes) - 1).end); +} + +TEST_F(FilePieceMapTest, priorities) +{ + auto const fpm = tr_file_piece_map{ block_info_, std::data(FileSizes), std::size(FileSizes) }; + auto file_priorities = tr_file_priorities(&fpm); + tr_file_index_t const n_files = std::size(FileSizes); + + // make a helper to compare file & piece priorities + auto expected_file_priorities = std::vector(n_files, TR_PRI_NORMAL); + auto expected_piece_priorities = std::vector(block_info_.n_pieces, TR_PRI_NORMAL); + auto const compare_to_expected = [&, this]() + { + for (tr_file_index_t i = 0; i < n_files; ++i) + { + EXPECT_EQ(int(expected_file_priorities[i]), int(file_priorities.filePriority(i))); + } + for (tr_piece_index_t i = 0; i < block_info_.n_pieces; ++i) + { + EXPECT_EQ(int(expected_piece_priorities[i]), int(file_priorities.piecePriority(i))); + } + }; + + // check default priority is normal + compare_to_expected(); + + // set the first file as high priority. + // since this begins and ends on a piece boundary, + // this shouldn't affect any other files' pieces + auto pri = TR_PRI_HIGH; + file_priorities.set(0, pri); + expected_file_priorities[0] = pri; + for (size_t i = 0; i < 5; ++i) + { + expected_piece_priorities[i] = pri; + } + compare_to_expected(); + + // This file shares a piece with another file. + // If _either_ is set to high, the piece's priority should be high. + // file #5: byte [500..550) piece [5, 6) + // file #6: byte [550..650) piece [5, 7) + // + // first test setting file #5... + pri = TR_PRI_HIGH; + file_priorities.set(5, pri); + expected_file_priorities[5] = pri; + expected_piece_priorities[5] = pri; + compare_to_expected(); + // ...and that shared piece should still be the same when both are high... + file_priorities.set(6, pri); + expected_file_priorities[6] = pri; + expected_piece_priorities[5] = pri; + expected_piece_priorities[6] = pri; + compare_to_expected(); + // ...and that shared piece should still be the same when only 6 is high... + pri = TR_PRI_NORMAL; + file_priorities.set(5, pri); + expected_file_priorities[5] = pri; + compare_to_expected(); + + // setup for the next test: set all files to low priority + pri = TR_PRI_LOW; + for (tr_file_index_t i = 0; i < n_files; ++i) + { + file_priorities.set(i, pri); + } + std::fill(std::begin(expected_file_priorities), std::end(expected_file_priorities), pri); + std::fill(std::begin(expected_piece_priorities), std::end(expected_piece_priorities), pri); + compare_to_expected(); + + // Raise the priority of a small 1-piece file. + // Since it's the highest priority in the piece, piecePriority() should return its value. + // file #8: byte [650, 659) piece [6, 7) + pri = TR_PRI_NORMAL; + file_priorities.set(8, pri); + expected_file_priorities[8] = pri; + expected_piece_priorities[6] = pri; + compare_to_expected(); + // Raise the priority of another small 1-piece file in the same piece. + // Since _it_ now has the highest priority in the piece, piecePriority should return _its_ value. + // file #9: byte [659, 667) piece [6, 7) + pri = TR_PRI_HIGH; + file_priorities.set(9, pri); + expected_file_priorities[9] = pri; + expected_piece_priorities[6] = pri; + compare_to_expected(); + + // Prep for the next test: set all files to normal priority + pri = TR_PRI_NORMAL; + for (tr_file_index_t i = 0; i < n_files; ++i) + { + file_priorities.set(i, pri); + } + std::fill(std::begin(expected_file_priorities), std::end(expected_file_priorities), pri); + std::fill(std::begin(expected_piece_priorities), std::end(expected_piece_priorities), pri); + compare_to_expected(); + + // *Sigh* OK what happens to piece priorities if you set the priority + // of a zero-byte file. Arguably nothing should happen since you can't + // download an empty file. But that would complicate the code for a + // pretty stupid use case, and treating 0-sized files the same as any + // other does no real harm. Let's KISS. + // + // Check that even zero-sized files can change a piece's priority + // file #1: byte [500, 500) piece [5, 6) + pri = TR_PRI_HIGH; + file_priorities.set(1, pri); + expected_file_priorities[1] = pri; + expected_piece_priorities[5] = pri; + compare_to_expected(); + // Check that zero-sized files at the end of a torrent change the last piece's priority. + // file #16 byte [1001, 1001) piece [10, 11) + file_priorities.set(16, pri); + expected_file_priorities[16] = pri; + expected_piece_priorities[10] = pri; + compare_to_expected(); + + // test the batch API + auto file_indices = std::vector(n_files); + std::iota(std::begin(file_indices), std::end(file_indices), 0); + pri = TR_PRI_HIGH; + file_priorities.set(std::data(file_indices), std::size(file_indices), pri); + std::fill(std::begin(expected_file_priorities), std::end(expected_file_priorities), pri); + std::fill(std::begin(expected_piece_priorities), std::end(expected_piece_priorities), pri); + compare_to_expected(); + pri = TR_PRI_LOW; + file_priorities.set(std::data(file_indices), std::size(file_indices), pri); + std::fill(std::begin(expected_file_priorities), std::end(expected_file_priorities), pri); + std::fill(std::begin(expected_piece_priorities), std::end(expected_piece_priorities), pri); + compare_to_expected(); +} + +TEST_F(FilePieceMapTest, wanted) +{ + auto const fpm = tr_file_piece_map{ block_info_, std::data(FileSizes), std::size(FileSizes) }; + auto files_wanted = tr_files_wanted(&fpm); + tr_file_index_t const n_files = std::size(FileSizes); + + // make a helper to compare file & piece priorities + auto expected_files_wanted = tr_bitfield(n_files); + auto expected_pieces_wanted = tr_bitfield(block_info_.n_pieces); + auto const compare_to_expected = [&, this]() + { + for (tr_file_index_t i = 0; i < n_files; ++i) + { + EXPECT_EQ(int(expected_files_wanted.test(i)), int(files_wanted.fileWanted(i))); + } + for (tr_piece_index_t i = 0; i < block_info_.n_pieces; ++i) + { + EXPECT_EQ(int(expected_pieces_wanted.test(i)), int(files_wanted.pieceWanted(i))); + } + }; + + // check everything is wanted by default + expected_files_wanted.setHasAll(); + expected_pieces_wanted.setHasAll(); + compare_to_expected(); + + // set the first file as not wanted. + // since this begins and ends on a piece boundary, + // this shouldn't affect any other files' pieces + bool wanted = false; + files_wanted.set(0, wanted); + expected_files_wanted.set(0, wanted); + expected_pieces_wanted.setSpan(0, 5, wanted); + compare_to_expected(); + + // now test when a piece has >1 file. + // if *any* file in that piece is wanted, then we want the piece too. + // file #1: byte [100..100) piece [5, 6) (zero-byte file) + // file #2: byte [100..100) piece [5, 6) (zero-byte file) + // file #3: byte [100..100) piece [5, 6) (zero-byte file) + // file #4: byte [100..100) piece [5, 6) (zero-byte file) + // file #5: byte [500..550) piece [5, 6) + // file #6: byte [550..650) piece [5, 7) + // + // first test setting file #5... + files_wanted.set(5, false); + expected_files_wanted.unset(5); + compare_to_expected(); + // marking all the files in the piece as unwanted + // should cause the piece to become unwanted + files_wanted.set(1, false); + files_wanted.set(2, false); + files_wanted.set(3, false); + files_wanted.set(4, false); + files_wanted.set(5, false); + files_wanted.set(6, false); + expected_files_wanted.setSpan(1, 7, false); + expected_pieces_wanted.unset(5); + compare_to_expected(); + // but as soon as any of them is turned back to wanted, + // the piece should pop back. + files_wanted.set(6, true); + expected_files_wanted.set(6, true); + expected_pieces_wanted.set(5); + compare_to_expected(); + files_wanted.set(5, true); + files_wanted.set(6, false); + expected_files_wanted.set(5); + expected_files_wanted.unset(6); + compare_to_expected(); + files_wanted.set(4, true); + files_wanted.set(5, false); + expected_files_wanted.set(4); + expected_files_wanted.unset(5); + compare_to_expected(); + + // Prep for the next test: set all files to unwanted priority + for (tr_file_index_t i = 0; i < n_files; ++i) + { + files_wanted.set(i, false); + } + expected_files_wanted.setHasNone(); + expected_pieces_wanted.setHasNone(); + compare_to_expected(); + + // *Sigh* OK what happens to files_wanted if you say the only + // file you want is a zero-byte file? Arguably nothing should happen + // since you can't download a zero-byte file. But that would complicate + // the coe for a stupid use case, so let's KISS. + // + // Check that even zero-sized files can change a file's 'wanted' state + // file #1: byte [500, 500) piece [5, 6) + files_wanted.set(1, true); + expected_files_wanted.set(1); + expected_pieces_wanted.set(5); + compare_to_expected(); + // Check that zero-sized files at the end of a torrent change the last piece's state. + // file #16 byte [1001, 1001) piece [10, 11) + files_wanted.set(16, true); + expected_files_wanted.set(16); + expected_pieces_wanted.set(10); + compare_to_expected(); + + // test the batch API + auto file_indices = std::vector(n_files); + std::iota(std::begin(file_indices), std::end(file_indices), 0); + files_wanted.set(std::data(file_indices), std::size(file_indices), true); + expected_files_wanted.setHasAll(); + expected_pieces_wanted.setHasAll(); + compare_to_expected(); + files_wanted.set(std::data(file_indices), std::size(file_indices), false); + expected_files_wanted.setHasNone(); + expected_pieces_wanted.setHasNone(); + compare_to_expected(); +}