import Foundation /// Wie schnell ein Download gerade läuft — und wie lange er noch braucht. /// /// **Ein Wertetyp mit Uhr von außen.** Die Rückmeldungen der Übertragung /// kommen mehrmals je Sekunde; was daraus eine Zahl macht, ist reine Rechnung /// und gehört nicht in den Koordinator. Die Zeitpunkte kommen als Argument /// herein, damit sich eine Übertragung ohne echte Uhr nachstellen lässt — eine /// Rate, die man nur im Betrieb beurteilen kann, ist keine prüfbare Zahl. /// /// **Warum geglättet.** Zwischen zwei Rückmeldungen schwankt die rohe /// Differenz um ein Vielfaches: ein Puffer läuft voll, der Funk hat ein Loch, /// das System hält die Hintergrundsitzung kurz an. Ungeglättet stünde dort /// eine Zahl, die zehnmal je Sekunde springt — und eine springende Zahl ist /// keine Auskunft, sondern Unruhe. public struct DownloadRate: Sendable, Equatable { /// Wie stark ein neuer Messwert gegenüber dem bisherigen zählt. /// /// Ein Drittel: schnell genug, dass ein echter Einbruch binnen weniger /// Sekunden ankommt, träge genug, dass einzelne Ausreißer untergehen. private static let weight = 1.0 / 3.0 /// Kürzere Abstände werden nicht gemessen. /// /// Über fünfzig Millisekunden gerechnet entscheidet der Zufall, wann das /// System den Puffer leert — nicht die Leitung. static let minimumInterval: Duration = .milliseconds(500) /// Ab dieser Lücke beginnt die Messung von vorn. /// /// So lange ohne eine einzige Rückmeldung heißt nicht „langsam", sondern /// „stand still": die App war angehalten, das Gerät gesperrt, das Netz /// weg. Diese Pause in die Rate einzurechnen ergäbe eine Zahl nahe null /// und eine Restzeit von Stunden. static let maximumInterval: Duration = .seconds(10) /// Ein Messpunkt: wie viel war wann da. /// /// Als Typ und nicht als Tupel — ein Tupel kann `Equatable` nicht erben, /// und ohne das wäre der ganze Wertetyp nicht vergleichbar. private struct Anchor: Equatable { var bytes: Int64 var at: ContinuousClock.Instant } /// Der letzte Messpunkt, gegen den gerechnet wird. private var anchor: Anchor? private var smoothed: Double? public init() {} /// Bytes je Sekunde — `nil`, solange nichts gemessen wurde. /// /// **Nicht null.** Null wäre eine Auskunft („es geht nichts durch"), und /// die stimmt in der ersten halben Sekunde nie. public var bytesPerSecond: Double? { smoothed } /// Nimmt den Stand der Übertragung entgegen. public mutating func record(bytes: Int64, at now: ContinuousClock.Instant) { guard let anchor else { self.anchor = Anchor(bytes: bytes, at: now) return } let elapsed = now - anchor.at guard elapsed >= Self.minimumInterval else { return } defer { self.anchor = Anchor(bytes: bytes, at: now) } // Rückwärts heißt: eine neue Aufgabe fängt von vorn an. Dann ist jede // bisherige Messung ungültig — und eine negative Rate wäre Unsinn. guard bytes >= anchor.bytes, elapsed <= Self.maximumInterval else { smoothed = nil return } let seconds = elapsed.inSeconds guard seconds > 0 else { return } let gemessen = Double(bytes - anchor.bytes) / seconds smoothed = smoothed.map { $0 + (gemessen - $0) * Self.weight } ?? gemessen } /// Wie lange es bei dieser Rate noch dauert. /// /// `nil`, solange keine Rate feststeht oder die Gesamtgröße unbekannt ist. /// Eine Restzeit ohne Rate wäre geraten, und geraten sieht man ihr nicht /// an. public func remaining(received: Int64, total: Int64) -> Duration? { guard let smoothed, smoothed > 0, total > received else { return nil } return .seconds(Double(total - received) / smoothed) } } extension Duration { /// Die Dauer als Sekunden. /// /// `components` liefert ganze Sekunden und Attosekunden getrennt; für eine /// Division braucht es beides in einer Zahl. var inSeconds: Double { Double(components.seconds) + Double(components.attoseconds) / 1e18 } }