Lots of improvements to get a useable performance

This commit is contained in:
Ben Davis
2017-08-29 11:29:55 +01:00
parent 7e3ce5c48d
commit dba46bf8a7
25 changed files with 469 additions and 84 deletions
+23 -2
View File
@@ -21,6 +21,11 @@
B514DD8F1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */; };
B514DD911F40D15C00C932F8 /* StringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD901F40D15C00C932F8 /* StringUtilities.swift */; };
B514DD931F40D27500C932F8 /* TorrentInfoRowData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */; };
B514DD951F40DB3100C932F8 /* TorrentFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */; };
B514DD9A1F40DCCA00C932F8 /* BigTorrentTest.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */; };
B514DD9C1F40DD1B00C932F8 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD9B1F40DD1B00C932F8 /* URL.swift */; };
B514DDA01F40DFC000C932F8 /* Data.bin in Resources */ = {isa = PBXBuildFile; fileRef = B514DD9F1F40DFC000C932F8 /* Data.bin */; };
B514DDA41F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */; };
B51638931F0EE9B6009E563E /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638921F0EE9B6009E563E /* TCPConnection.swift */; };
B51638951F0EEAA4009E563E /* TCPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */; };
B51638971F0EEC2B009E563E /* GCDAsyncSocketStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */; };
@@ -173,6 +178,12 @@
B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedTrackerTests.swift; sourceTree = "<group>"; };
B514DD901F40D15C00C932F8 /* StringUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilities.swift; sourceTree = "<group>"; };
B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentInfoRowData.swift; sourceTree = "<group>"; };
B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentFileManagerTests.swift; sourceTree = "<group>"; };
B514DD961F40DC8300C932F8 /* LancasterPics.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = LancasterPics.torrent; sourceTree = "<group>"; };
B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = BigTorrentTest.torrent; sourceTree = "<group>"; };
B514DD9B1F40DD1B00C932F8 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
B514DD9F1F40DFC000C932F8 /* Data.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = Data.bin; path = "../../../../../../Library/Developer/CoreSimulator/Devices/BB5761CA-9F0E-4CAF-97A8-22BA0F3CA49D/data/Documents/Data.bin"; sourceTree = "<group>"; };
B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedNetworkSpeedTracker.swift; sourceTree = "<group>"; };
B51638921F0EE9B6009E563E /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = "<group>"; };
B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnectionTests.swift; sourceTree = "<group>"; };
B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncSocketStub.swift; sourceTree = "<group>"; };
@@ -393,7 +404,9 @@
children = (
B54D0C2B1CA5787E004343BD /* Data+sha1.swift */,
B514DD8A1F40C40500C932F8 /* NetworkSpeedTracker.swift */,
B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */,
B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */,
B514DD9B1F40DD1B00C932F8 /* URL.swift */,
);
path = Utilities;
sourceTree = "<group>";
@@ -507,6 +520,9 @@
isa = PBXGroup;
children = (
B5C5F9661F250CC4007623B2 /* TorrentFileManager.swift */,
B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */,
B514DD9F1F40DFC000C932F8 /* Data.bin */,
B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */,
B5AF7AA51F252A66003FD66F /* MultiFileHandle.swift */,
B5AF7AA81F252A70003FD66F /* MultFileHandleTests.swift */,
B5AF7AAC1F253055003FD66F /* FileHandleFake.swift */,
@@ -799,7 +815,9 @@
files = (
B5E9B0E51F02FAC600EF58E3 /* TestText.torrent in Resources */,
B514DD891F40B58200C932F8 /* text.txt in Resources */,
B514DD9A1F40DCCA00C932F8 /* BigTorrentTest.torrent in Resources */,
B514DD831F40A2B800C932F8 /* TrackerManagerTests.torrent in Resources */,
B514DDA01F40DFC000C932F8 /* Data.bin in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -953,10 +971,12 @@
B5C5F9671F250CC4007623B2 /* TorrentFileManager.swift in Sources */,
B5F81E491F0436D600B25C70 /* TorrentPeerInfo.swift in Sources */,
B527F6311F1BF30E001F06AF /* TorrentClient.swift in Sources */,
B514DD9C1F40DD1B00C932F8 /* URL.swift in Sources */,
B5BFD9D01F3FB13E00CE0186 /* TorrentTracker.swift in Sources */,
B55317DC1F02FC4D00909ADF /* TorrentHTTPTracker.swift in Sources */,
B52EE3401F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift in Sources */,
B5F27C621F3F93AC0040589C /* TorrentProgressManager.swift in Sources */,
B514DDA41F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift in Sources */,
B514DD8B1F40C40500C932F8 /* NetworkSpeedTracker.swift in Sources */,
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */,
B558F4831F0A647D00438BB4 /* InternetProtocol.swift in Sources */,
@@ -1002,6 +1022,7 @@
B535534C1F125DB500A6DCBE /* TorrentPeerMessageBufferTests.swift in Sources */,
B527F6271F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift in Sources */,
B5BFD9CE1F3FAFA500CE0186 /* TorrentTrackerManagerTests.swift in Sources */,
B514DD951F40DB3100C932F8 /* TorrentFileManagerTests.swift in Sources */,
B56A8A071C83539300426AC8 /* TestHelpers.swift in Sources */,
B53553401F112BDB00A6DCBE /* BitFieldTests.swift in Sources */,
B514DD881F40B2EC00C932F8 /* TorrentClientManagerStubs.swift in Sources */,
@@ -1324,7 +1345,7 @@
DEVELOPMENT_TEAM = HT9AS2MWK9;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = BitTorrentExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "uk.co.-bendavisapps.BitTorrentExample";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1348,7 +1369,7 @@
DEVELOPMENT_TEAM = HT9AS2MWK9;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = BitTorrentExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "uk.co.-bendavisapps.BitTorrentExample";
PRODUCT_NAME = "$(TARGET_NAME)";
Binary file not shown.
Binary file not shown.
@@ -82,4 +82,10 @@ class MultFileHandleTests: XCTestCase {
XCTAssert(fileHandle.synchronizeFileCalled)
}
}
func test_canReadLastBytes() {
sut.seek(toFileOffset: 8)
let data = sut.readData(ofLength: 2)
XCTAssertEqualData(data, Data(bytes: [9,10]))
}
}
@@ -98,6 +98,7 @@ class MultiFileHandle: FileHandleProtocol {
}
private func incrementCurrentFile() {
guard (fileIndex + 1) < files.count else { return }
fileIndex += 1
currentFile.handle.seek(toFileOffset: 0)
}
@@ -17,7 +17,7 @@ public class TorrentFileManager {
let metaInfo: TorrentMetaInfo
let rootDirectory: String
fileprivate let fileHandle: MultiFileHandle
fileprivate let fileHandle: FileHandleProtocol
convenience init(metaInfo: TorrentMetaInfo, rootDirectory: String) {
let fileHandles = TorrentFileManager.createFileHandles(for: metaInfo, in: rootDirectory)
@@ -54,6 +54,22 @@ public class TorrentFileManager {
fileHandle.seek(toFileOffset: UInt64(byteIndex))
return fileHandle.readData(ofLength: length)
}
// TODO: Multi-threaded check
func reCheckProgress() -> BitField {
var result = BitField(size: metaInfo.info.pieces.count)
for (pieceIndex, _) in result {
autoreleasepool {
let correctSha1 = metaInfo.info.pieces[pieceIndex]
let piece = getPiece(at: pieceIndex)
let sha1 = piece.sha1()
if sha1 == correctSha1 {
result.set(at: pieceIndex)
}
}
}
return result
}
}
// MARK: - Prepare directory
@@ -100,3 +116,30 @@ extension TorrentFileManager {
close(fileDescriptor) // Now we have a file of the correct size we close it
}
}
// Save/Load progress
extension TorrentFileManager {
static func saveProgressBitfield(_ bitfield: BitField, infoHash: Data) {
let fileName = String(asciiData: infoHash.base64EncodedData())!
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
.userDomainMask,
true)[0] as String
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
let fileURL = documentsUrl.appendingPathComponent("torrent_progress.bin", isDirectory: false)
try? bitfield.toData().write(to: fileURL)
}
static func loadSavedProgressBitfield(infoHash: Data) -> BitField? {
let fileName = String(asciiData: infoHash.base64EncodedData())!
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
.userDomainMask,
true)[0] as String
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
let fileURL = documentsUrl.appendingPathComponent("torrent_progress.bin", isDirectory: false)
if let data = try? Data(contentsOf: fileURL) {
return BitField(data: data)
}
return nil
}
}
@@ -0,0 +1,72 @@
//
// TorrentFileManagerTests.swift
// BitTorrentTests
//
// Created by Ben Davis on 13/08/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import XCTest
@testable import BitTorrent
class TorrentFileManagerTests: XCTestCase {
let metaInfo: TorrentMetaInfo = {
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "BigTorrentTest",
ofType: "torrent")
let data = try! Data(contentsOf: URL(fileURLWithPath: path!))
return TorrentMetaInfo(data: data)!
}()
let piece1: Data = {
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "Data",
ofType: "bin")
return try! Data(contentsOf: URL(fileURLWithPath: path!))
}()
var fileHandle: FileHandleFake!
var sut: TorrentFileManager!
override func setUp() {
super.setUp()
fileHandle = FileHandleFake(data: Data(repeating: 0, count: metaInfo.info.length))
sut = TorrentFileManager(metaInfo: metaInfo, rootDirectory: "/", fileHandles: [fileHandle])
}
func test_canSetPiece() {
// Given
let pieceLength = metaInfo.info.pieceLength
// When
sut.setPiece(at: 1, data: piece1)
// Then
XCTAssertEqualData(fileHandle.data[pieceLength..<pieceLength*2], piece1)
}
func test_canGetPiece() {
// Given
sut.setPiece(at: 1, data: piece1)
// When
let result = sut.getPiece(at: 1)
// Then
XCTAssertEqualData(result, piece1)
}
// Really slow test (takes ~3.5 seconds)
// TODO: Need to create a smaller torrent with multiple pieces to verify against
// func test_canGetProgressFromFile() {
//
// var expected = BitField(size: metaInfo.info.pieces.count)
// expected.set(at: 1)
//
// sut.setPiece(at: 1, data: piece1)
//
// let result = sut.reCheckProgress()
//
// XCTAssertEqual(result, expected)
// }
}
@@ -16,6 +16,8 @@ protocol TorrentPeerManagerDelegate: class {
func torrentPeerManager(_ sender: TorrentPeerManager, failedToGetPieceAtIndex index: Int)
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager)
func torrentPeerManagerCurrentBitfieldForHandshake(_ sender: TorrentPeerManager) -> BitField
func torrentPeerManager(_ sender: TorrentPeerManager,
@@ -32,7 +34,8 @@ class TorrentPeerManager {
}
}
var maximumNumberOfConnectedPeers = 20
var maximumNumberOfConnectedPeers = 20 // variable for testing
var minimumNumberOfConnectedPeers = 5 // variable for testing
let maximumNumberOfPiecesPerPeer = 2
weak var delegate: TorrentPeerManagerDelegate?
@@ -49,12 +52,15 @@ class TorrentPeerManager {
return peers.filter({ $0.connected && $0.currentProgress.complete }).count
}
private(set) var downloadSpeedTracker = NetworkSpeedTracker ()
private(set) var downloadSpeedTracker: NetworkSpeedTrackable!
init(clientId: Data, infoHash: Data, bitFieldSize: Int) {
self.clientId = clientId
self.infoHash = infoHash
self.bitFieldSize = bitFieldSize
self.downloadSpeedTracker = CombinedNetworkSpeedTracker { [unowned self] in
return self.peers.map { $0.downloadSpeedTracker }
}
}
// Exposed for testing
@@ -80,8 +86,12 @@ class TorrentPeerManager {
let bitField = delegate.torrentPeerManagerCurrentBitfieldForHandshake(self)
let peersToConnectTo = peers[0 ..< numberToConnectTo]
connectToPeers(peersToConnectTo, bitField: bitField)
if numberOfConnectedPeers < minimumNumberOfConnectedPeers {
delegate.torrentPeerManagerNeedsMorePeers(self)
}
}
// Using sequence to allow array slices
private func connectToPeers<T: Sequence>(_ peers: T, bitField: BitField) where T.Element == TorrentPeer {
for peer in peers {
peer.delegate = self
@@ -118,7 +128,6 @@ extension TorrentPeerManager: TorrentPeerDelegate {
}
func peer(_ sender: TorrentPeer, gotPieceAtIndex index: Int, piece: Data) {
downloadSpeedTracker.increase(by: piece.count)
delegate?.torrentPeerManager(self, downloadedPieceAtIndex: index, piece: piece)
requestNextPiece(from: sender)
// TODO: send have to peers
@@ -133,8 +142,8 @@ extension TorrentPeerManager: TorrentPeerDelegate {
private func requestNextPiece(from peer: TorrentPeer) {
if let pieceRequest = delegate?.torrentPeerManager(self, nextPieceFromAvailable: peer.currentProgress) {
peer.downloadPiece(atIndex: pieceRequest.pieceIndex, size: pieceRequest.size)
} else {
print("No available pieces for peer \(peer.peerInfo.ip):\(peer.peerInfo.port) to download")
} else if enableLogging {
print("No available pieces for peer \(peer.peerInfo.ip):\(peer.peerInfo.port) to download")
}
}
}
@@ -11,6 +11,11 @@ import XCTest
class TorrentPeerFake: TorrentPeer {
var testDownloadSpeedTracker = NetworkSpeedTracker()
override var downloadSpeedTracker: NetworkSpeedTracker {
return testDownloadSpeedTracker
}
var connectCalled = false
var connectHandshakeData: (clientId: Data, bitField: BitField)?
override func connect(withHandshakeData handshakeData: (clientId: Data, bitField: BitField)) throws {
@@ -34,6 +39,14 @@ class TorrentPeerFake: TorrentPeer {
class TorrentPeerManagerDelegateStub: TorrentPeerManagerDelegate {
var torrentPeerManagerNeedsMorePeersCalled = false
var torrentPeerManagerNeedsMorePeersParameter: TorrentPeerManager?
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager) {
torrentPeerManagerNeedsMorePeersCalled = true
torrentPeerManagerNeedsMorePeersParameter = sender
}
var downloadedPieceAtIndexCalled = false
var downloadedPieceAtIndexParameters: (sender: TorrentPeerManager, pieceIndex: Int, piece: Data)?
func torrentPeerManager(_ sender: TorrentPeerManager,
@@ -283,7 +296,7 @@ class TorrentPeerManagerTests: XCTestCase {
XCTAssertFalse(peer.downloadPieceCalled)
}
func test_downloadSpeedRecordedOnGettingPiece() {
func test_downloadSpeedSumsPeerDownloadSpeeds() {
// Given
let peerInfo = TorrentPeerInfo(ip: "127.0.0.1", port: 123, peerId: nil)
@@ -293,9 +306,23 @@ class TorrentPeerManagerTests: XCTestCase {
let pieceSize = 100
// When
sut.peer(peer, gotPieceAtIndex: 0, piece: Data(repeating: 0, count: pieceSize))
peer.testDownloadSpeedTracker.increase(by: pieceSize)
// Then
XCTAssertEqual(sut.downloadSpeedTracker.totalNumberOfBytes, pieceSize)
}
func test_morePeersRequestedWhenNumberDropsBelowMin() {
// Given
sut.minimumNumberOfConnectedPeers = 2
let peerInfo = TorrentPeerInfo(ip: "127.0.0.1", port: 123, peerId: nil)
// When
sut.addPeers(withInfo: [peerInfo])
// Then
XCTAssert(delegate.torrentPeerManagerNeedsMorePeersCalled)
XCTAssert(delegate.torrentPeerManagerNeedsMorePeersParameter === sut)
}
}
+10 -5
View File
@@ -109,6 +109,14 @@ class TorrentPeer {
}
downloadPieceRequests.removeAll()
}
private func onConnectionDropped() {
keepAliveTimer?.invalidate()
keepAliveTimer = nil
killAllDownloads()
connected = false
delegate?.peerLost(self)
}
}
// Keep alive
@@ -142,8 +150,7 @@ extension TorrentPeer {
}
@objc private func didntReceiveKeepAlive() {
keepAliveTimer = nil
delegate?.peerLost(self)
onConnectionDropped()
}
}
@@ -161,9 +168,7 @@ extension TorrentPeer: TorrentPeerCommunicatorDelegate {
}
func peerLost(_ sender: TorrentPeerCommunicator) {
killAllDownloads()
delegate?.peerLost(self)
connected = false
onConnectionDropped()
}
func peerSentHandshake(_ sender: TorrentPeerCommunicator, sentHandshakeWithPeerId peerId: Data, onDHT: Bool) {
+1
View File
@@ -329,6 +329,7 @@ class TorrentPeerTests: XCTestCase {
let e = expectation(description: "Keep alive sent")
DispatchQueue.main.async {
XCTAssert(self.delegate.peerLostCalled)
XCTAssertFalse(self.sut.connected)
e.fulfill()
}
waitForExpectations(timeout: 0.1)
@@ -22,7 +22,13 @@ class TorrentProgressManager {
convenience init(metaInfo: TorrentMetaInfo, rootDirectory: String) {
let downloadDirectory = rootDirectory + "/" + metaInfo.sensibleDownloadDirectoryName()
let fileManager = TorrentFileManager(metaInfo: metaInfo, rootDirectory: downloadDirectory)
let progress = TorrentProgress(size: metaInfo.info.pieces.count)
let progress: TorrentProgress
if let bitField = TorrentFileManager.loadSavedProgressBitfield(infoHash: metaInfo.infoHash) {
progress = TorrentProgress(bitField: bitField)
} else {
progress = TorrentProgress(size: metaInfo.info.pieces.count)
}
self.init(fileManager: fileManager, progress: progress)
}
@@ -31,6 +37,11 @@ class TorrentProgressManager {
self.progress = progress
}
public func forceReCheck() {
let bitField = fileManager.reCheckProgress()
progress = TorrentProgress(bitField: bitField)
}
func getNextPieceToDownload(from availablePieces: BitField) -> TorrentPieceRequest? {
for (i, isSet) in availablePieces where isSet {
if !progress.hasPiece(i) && !progress.isCurrentlyDownloading(piece: i) {
@@ -44,6 +55,7 @@ class TorrentProgressManager {
func setDownloadedPiece(_ piece: Data, pieceIndex: Int) {
progress.finishedDownloading(piece: pieceIndex)
fileManager.setPiece(at: pieceIndex, data: piece)
TorrentFileManager.saveProgressBitfield(progress.bitField, infoHash: metaInfo.infoHash)
}
func setLostPiece(at index: Int) {
@@ -21,6 +21,11 @@ class TorrentProgressManagerTests: XCTestCase {
return TorrentMetaInfo(data: data)!
}()
let finalData: Data = {
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "text", ofType: "txt")
return try! Data(contentsOf: URL(fileURLWithPath: path!))
}()
let completeBitField: BitField = {
var result = BitField(size: 1)
result.set(at: 0)
@@ -42,6 +47,18 @@ class TorrentProgressManagerTests: XCTestCase {
sut = TorrentProgressManager(fileManager: fileManager, progress: progress)
}
func test_canForceReCheck() {
// Given
fileHandle.data = finalData
// When
sut.forceReCheck()
// Then
XCTAssert(sut.progress.complete)
}
func test_exampleMetaInfoOnlyHas1Piece() {
XCTAssertEqual(metaInfo.info.pieces.count, 1)
}
+11 -4
View File
@@ -35,7 +35,7 @@ public class TorrentClient {
public var progress: TorrentProgress { return progressManager.progress }
public var numberOfConnectedPeers: Int { return peerManager.numberOfConnectedPeers }
public var numberOfConnectedSeeds: Int { return peerManager.numberOfConnectedPeers }
public var downloadSpeedTracker: NetworkSpeedTracker { return peerManager.downloadSpeedTracker }
public var downloadSpeedTracker: NetworkSpeedTrackable { return peerManager.downloadSpeedTracker }
let progressManager: TorrentProgressManager
let peerManager: TorrentPeerManager
@@ -55,7 +55,6 @@ public class TorrentClient {
trackerManager.delegate = self
peerManager.delegate = self
peerManager.enableLogging = true
}
// For testing
@@ -69,6 +68,10 @@ public class TorrentClient {
self.trackerManager = trackerManager
}
public func forceReCheck() {
progressManager.forceReCheck()
}
public func start() {
trackerManager.start()
status = .started
@@ -85,9 +88,9 @@ extension TorrentClient: TorrentTrackerManagerDelegate {
return TorrentTrackerManagerAnnonuceInfo(
numberOfBytesRemaining: progress.remaining * metaInfo.info.pieceLength,
numberOfBytesUploaded: progress.uploaded * metaInfo.info.pieceLength,
numberOfBytesUploaded: 0,
numberOfBytesDownloaded: progress.downloaded * metaInfo.info.pieceLength,
numberOfPeersToFetch: 20)
numberOfPeersToFetch: 50)
}
}
@@ -105,6 +108,10 @@ extension TorrentClient: TorrentPeerManagerDelegate {
progressManager.setLostPiece(at: index)
}
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager) {
trackerManager.forceRestart()
}
func torrentPeerManagerCurrentBitfieldForHandshake(_ sender: TorrentPeerManager) -> BitField {
return progressManager.progress.bitField
}
@@ -13,7 +13,6 @@ public struct TorrentProgress {
public private(set) var bitField: BitField
private var piecesBeingDownloaded: [Int] = []
public private(set) var uploaded: Int = 0
public private(set) var downloaded: Int = 0
public var remaining: Int {
@@ -32,6 +31,13 @@ public struct TorrentProgress {
self.bitField = BitField(size: size)
}
init(bitField: BitField) {
self.bitField = bitField
for (_, isSet) in bitField where isSet {
downloaded += 1
}
}
func isCurrentlyDownloading(piece: Int) -> Bool {
return piecesBeingDownloaded.contains(piece)
}
@@ -14,7 +14,6 @@ class TorrentProgressTests: XCTestCase {
func test_torrentProgressIsInitialisedAsNoneDownloaded() {
let result = TorrentProgress(size: 10)
XCTAssertEqual(result.downloaded, 0)
XCTAssertEqual(result.uploaded, 0)
}
func test_canMarkPieceAsDownloading() {
@@ -30,6 +30,15 @@ class TorrentTrackerManager {
let clientId: String
let port: Int
var announceTimeInterval: TimeInterval = 600
private lazy var announceTimer: Timer = {
return Timer.scheduledTimer(timeInterval: self.announceTimeInterval,
target: self,
selector: #selector(announce),
userInfo: nil,
repeats: true)
}()
init(metaInfo: TorrentMetaInfo, clientId: Data, port: Int) {
self.metaInfo = metaInfo
self.clientId = String(data: clientId, encoding: .utf8)!
@@ -51,16 +60,17 @@ class TorrentTrackerManager {
private static func createTrackers(from metaInfo: TorrentMetaInfo) -> [TorrentTracker] {
let announceList = metaInfo.announceList?.first ?? [metaInfo.announce]
let announceList = metaInfo.announceList ?? [[metaInfo.announce]]
let flatAnnounceList = announceList.flatMap { return $0 }
var lastPortNumberUsed: UInt16 = 3475
var result: [TorrentTracker] = []
for url in announceList {
for url in flatAnnounceList {
if url.scheme == "http" {
if url.scheme == "http" || url.scheme == "https" {
let tracker = TorrentHTTPTracker(announceURL: url)
let tracker = TorrentHTTPTracker(announceURL: url.bySettingScheme(to: "https"))
result.append(tracker)
} else if url.scheme == "udp" {
@@ -76,10 +86,14 @@ class TorrentTrackerManager {
}
func start() {
announce()
forceRestart()
}
private func announce() {
func forceRestart() {
announceTimer.fire()
}
@objc private func announce() {
guard let delegate = delegate else { return }
@@ -23,6 +23,7 @@ class TorrentTrackerStub: TorrentTracker {
numberOfBytesDownloaded: Int,
numberOfPeersToFetch: Int)?
var onAnnounceClient: (()->Void)?
func announceClient(with peerId: String,
port: Int,
event: TorrentTrackerEvent,
@@ -40,6 +41,7 @@ class TorrentTrackerStub: TorrentTracker {
numberOfBytesUploaded,
numberOfBytesDownloaded,
numberOfPeersToFetch)
onAnnounceClient?()
}
}
@@ -73,6 +75,12 @@ class TorrentTrackerManagerTests: XCTestCase {
let clientIdData = "-BD0000-bxa]N#IRKqv`".data(using: .ascii)!
let listeningPort = 123
let announceInfo = TorrentTrackerManagerAnnonuceInfo(numberOfBytesRemaining: 1,
numberOfBytesUploaded: 2,
numberOfBytesDownloaded: 3,
numberOfPeersToFetch: 4)
func test_createsTrackers() {
let sut = TorrentTrackerManager(metaInfo: metaInfo, clientId: clientIdData, port: listeningPort)
@@ -89,7 +97,8 @@ class TorrentTrackerManagerTests: XCTestCase {
return
}
XCTAssertEqual(httpTracker.announceURL, metaInfo.announceList![0][0])
let httpsScheme = metaInfo.announceList![0][0].bySettingScheme(to: "https")
XCTAssertEqual(httpTracker.announceURL, httpsScheme)
XCTAssertEqual(udpTracker.announceURL, metaInfo.announceList![0][1])
XCTAssert(httpTracker.delegate === sut)
@@ -101,18 +110,14 @@ class TorrentTrackerManagerTests: XCTestCase {
// Given
let tracker = TorrentTrackerStub()
let delegate = TorrentTrackerManagerDelegateStub()
let announceInfo = TorrentTrackerManagerAnnonuceInfo(numberOfBytesRemaining: 1,
numberOfBytesUploaded: 2,
numberOfBytesDownloaded: 3,
numberOfPeersToFetch: 4)
let sut = TorrentTrackerManager(metaInfo: metaInfo,
clientId: clientIdData,
port: listeningPort,
trackers: [tracker])
sut.delegate = delegate
delegate.torrentTrackerManagerAnnonuceInfoResult = announceInfo
sut.delegate = delegate
// When
sut.start()
@@ -133,4 +138,56 @@ class TorrentTrackerManagerTests: XCTestCase {
XCTAssertEqual(announceClientParameters.numberOfBytesDownloaded, announceInfo.numberOfBytesDownloaded)
XCTAssertEqual(announceClientParameters.numberOfPeersToFetch, announceInfo.numberOfPeersToFetch)
}
func test_announceRepeats() {
// Given
let tracker = TorrentTrackerStub()
let delegate = TorrentTrackerManagerDelegateStub()
let sut = TorrentTrackerManager(metaInfo: metaInfo,
clientId: clientIdData,
port: listeningPort,
trackers: [tracker])
sut.delegate = delegate
sut.announceTimeInterval = 0
// When
sut.start()
// Then
let expectation = self.expectation(description: "Announce is repeatedly called")
tracker.onAnnounceClient = {
tracker.onAnnounceClient = nil
expectation.fulfill()
}
waitForExpectations(timeout: 0.1)
}
func test_canForceReAnnounce_resetsAnnounceTimer() {
// Given
let tracker = TorrentTrackerStub()
let delegate = TorrentTrackerManagerDelegateStub()
let sut = TorrentTrackerManager(metaInfo: metaInfo,
clientId: clientIdData,
port: listeningPort,
trackers: [tracker])
sut.delegate = delegate
sut.announceTimeInterval = 600
sut.start()
// Then
let expectation = self.expectation(description: "Announce is repeatedly called")
tracker.onAnnounceClient = {
tracker.onAnnounceClient = nil
expectation.fulfill()
}
// When
sut.forceRestart()
waitForExpectations(timeout: 0.1)
}
}
@@ -0,0 +1,44 @@
//
// CombinedNetworkSpeedTracker.swift
// BitTorrent
//
// Created by Ben Davis on 13/08/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
class CombinedNetworkSpeedTracker: NetworkSpeedTrackable {
let trackers: () -> [NetworkSpeedTracker]
init(trackers: @escaping () -> [NetworkSpeedTracker]) {
self.trackers = trackers
}
// MARK: - NetworkSpeedTrackable
var totalNumberOfBytes: Int {
var result = 0
for tracker in trackers() {
result += tracker.totalNumberOfBytes
}
return result
}
func numberOfBytesDownloaded(since date: Date) -> Int {
var result = 0
for tracker in trackers() {
result += tracker.numberOfBytesDownloaded(since: date)
}
return result
}
func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
var result = 0
for tracker in trackers() {
result += tracker.numberOfBytesDownloaded(over: timeInterval)
}
return result
}
}
+36 -27
View File
@@ -8,9 +8,37 @@
import Foundation
extension Collection {
func firstWhere(_ predicate: (Element) -> Bool) -> Element? {
return first(where: predicate)
public protocol NetworkSpeedTrackable {
var totalNumberOfBytes: Int { get }
func numberOfBytesDownloaded(since date: Date) -> Int
func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int
}
public struct NetworkSpeedTracker: NetworkSpeedTrackable {
public var totalNumberOfBytes: Int = 0
// TODO: limit number of dataPoints stored
private var dataPoints: [NetworkSpeedDataPoint] = [NetworkSpeedDataPoint(0)]
mutating func increase(by bytes: Int) {
totalNumberOfBytes += bytes
addDataPoint(NetworkSpeedDataPoint(totalNumberOfBytes))
}
private mutating func addDataPoint(_ dataPoint: NetworkSpeedDataPoint) {
dataPoints = [dataPoint] + dataPoints
}
public func numberOfBytesDownloaded(since date: Date) -> Int {
guard let previouslyDataPoint = dataPoints.firstWhere({ $0.dateRecorded < date }) else {
return totalNumberOfBytes
}
return totalNumberOfBytes - previouslyDataPoint.numberOfBytes
}
public func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
return numberOfBytesDownloaded(since: Date(timeIntervalSinceNow: -timeInterval))
}
}
@@ -27,7 +55,7 @@ public struct NetworkSpeedDataPoint: Equatable, Comparable {
public static func ==(_ lhs: NetworkSpeedDataPoint, _ rhs: NetworkSpeedDataPoint) -> Bool {
return (
lhs.numberOfBytes == rhs.numberOfBytes &&
lhs.dateRecorded == rhs.dateRecorded
lhs.dateRecorded == rhs.dateRecorded
)
}
@@ -35,28 +63,9 @@ public struct NetworkSpeedDataPoint: Equatable, Comparable {
return lhs.dateRecorded.compare(rhs.dateRecorded) == .orderedAscending
}
}
public struct NetworkSpeedTracker {
var totalNumberOfBytes: Int = 0
private var dataPoints: [NetworkSpeedDataPoint] = [NetworkSpeedDataPoint(0)]
mutating func increase(by bytes: Int) {
totalNumberOfBytes += bytes
addDataPoint(NetworkSpeedDataPoint(totalNumberOfBytes))
}
private mutating func addDataPoint(_ dataPoint: NetworkSpeedDataPoint) {
dataPoints = [dataPoint] + dataPoints
}
public func numberOfBytesDownloaded(since date: Date) -> Int {
guard let previouslyDataPoint = dataPoints.firstWhere({ $0.dateRecorded < date }) else {
return totalNumberOfBytes
}
return totalNumberOfBytes - previouslyDataPoint.numberOfBytes
}
public func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
return numberOfBytesDownloaded(since: Date(timeIntervalSinceNow: -timeInterval))
// Not sure why I need this 🤷
extension Collection {
func firstWhere(_ predicate: (Element) -> Bool) -> Element? {
return first(where: predicate)
}
}
+17
View File
@@ -0,0 +1,17 @@
//
// URL.swift
// BitTorrent
//
// Created by Ben Davis on 13/08/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
extension URL {
public func bySettingScheme(to scheme: String) -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
components.scheme = scheme
return components.url!
}
}
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIFileSharingEnabled</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
+4 -8
View File
@@ -10,16 +10,14 @@ import Foundation
extension TimeInterval {
var milliseconds: Int {
return Int((self.truncatingRemainder(dividingBy: 1)) * 1000)
}
var seconds: Int {
return Int(self.remainder(dividingBy: 60))
let remaining = self - Double(hours*60*60) - Double(minutes*60)
return Int(remaining / 60)
}
var minutes: Int {
return Int((self/60).remainder(dividingBy: 60))
let remaining = self - Double(hours*60*60)
return Int(remaining / 60)
}
var hours: Int {
@@ -31,8 +29,6 @@ extension TimeInterval {
return "\(self.hours)h \(self.minutes)m \(self.seconds)s"
} else if self.minutes != 0 {
return "\(self.minutes)m \(self.seconds)s"
} else if self.milliseconds != 0 {
return "\(self.seconds)s \(self.milliseconds)ms"
} else {
return "\(self.seconds)s"
}
+4 -3
View File
@@ -74,15 +74,16 @@ enum TorrentInfoRowData: Int {
case .eta:
let speed = client.downloadSpeedTracker.numberOfBytesDownloaded(over: speedSampleSize)
let speedPerSecond = Double(speed) / speedSampleSize
guard speed > 0 else { return "" }
let remaining = client.progress.remaining * client.metaInfo.info.pieceLength
guard speed > 0 else { return "n/a" }
guard remaining > 0 else { return "n/a" }
return TimeInterval(remaining / speed).stringTime
return (Double(remaining) / speedPerSecond).stringTime
case .uploaded:
return bytesToString(client.progress.uploaded * client.metaInfo.info.pieceLength)
return "????"
}
}
}
+31 -12
View File
@@ -11,15 +11,15 @@ import BitTorrent
class TorrentViewController: UIViewController {
static let refreshRate: TimeInterval = 1
let refreshRate: TimeInterval = 1
let tableView = UITableView()
let torrentClient: TorrentClient
lazy var refreshTimer: Timer = {
return Timer.scheduledTimer(timeInterval: TorrentViewController.refreshRate,
return Timer.scheduledTimer(timeInterval: self.refreshRate,
target: self,
selector: #selector(TorrentViewController.timerFired),
selector: #selector(timerFired),
userInfo: nil,
repeats: true)
}()
@@ -44,7 +44,14 @@ class TorrentViewController: UIViewController {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
if tableView.contentInset == .zero {
tableView.contentInset = view.safeAreaInsets
if #available(iOS 11, *) {
tableView.contentInset = view.safeAreaInsets
} else {
tableView.contentInset = UIEdgeInsetsMake(topLayoutGuide.length,
0,
bottomLayoutGuide.length,
0)
}
tableView.contentOffset = .zero
}
}
@@ -69,7 +76,7 @@ extension TorrentViewController: UITableViewDataSource {
if section == 0 {
return TorrentInfoRowData.numberOfRows
} else {
return 1
return 2
}
}
@@ -77,11 +84,19 @@ extension TorrentViewController: UITableViewDataSource {
if indexPath.section == 0 {
return cellForTorrentInfoSection(at: indexPath, tableView: tableView)
} else {
let startCell = UITableViewCell(style: .default, reuseIdentifier: nil)
startCell.textLabel?.text = "Start"
startCell.textLabel?.textAlignment = .center
startCell.textLabel?.textColor = .blue
return startCell
if indexPath.row == 0 {
let startCell = UITableViewCell(style: .default, reuseIdentifier: nil)
startCell.textLabel?.text = "Force re-check"
startCell.textLabel?.textAlignment = .center
startCell.textLabel?.textColor = .blue
return startCell
} else {
let reCheckCell = UITableViewCell(style: .default, reuseIdentifier: nil)
startCell.textLabel?.text = "Start"
startCell.textLabel?.textAlignment = .center
startCell.textLabel?.textColor = .blue
return startCell
}
}
}
@@ -121,10 +136,14 @@ extension TorrentViewController: UITableViewDelegate {
guard let cell = tableView.cellForRow(at: indexPath) else { return }
cell.setSelected(false, animated: false)
if indexPath.section == 1 {
guard indexPath.section == 1 else { return }
if indexPath.row == 0 {
torrentClient.forceReCheck()
} else {
torrentClient.start()
tableView.reloadData()
}
tableView.reloadData()
}
}