#c_sharp #download #benchmark
Был простой код скачивания: internal static async TaskDownloadFile(Uri uri) { byte[] result; WebResponse response; var file = new ImageFile(); var request = WebRequest.Create(uri); try { response = await request.GetResponseAsync(); using (var ms = new MemoryStream()) { response.GetResponseStream().CopyTo(ms); result = ms.ToArray(); } } catch (System.Exception ex) { } if (response.ContentLength == result.LongLength) file.Body = result; return file; } Захотелось добавить показатель скорости скачивания. Гугл подсказал, что можно ориентироваться на скорость чтения потока: result = await CopyTo(response.GetResponseStream(), response.ContentLength, progressChanged); private static async Task CopyTo(Stream from, long totalBytes, Action loadedEvent) { var sw = new Stopwatch(); sw.Start(); var data = new byte[totalBytes]; byte[] buffer = new byte[81920]; int currentIndex = 0; while (true) { int num = await from.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); if (num != 0) { Array.Copy(buffer, 0, data, currentIndex, num); currentIndex += num; loadedEvent?.Invoke(new DownloadProgress(currentIndex, totalBytes, sw.ElapsedMilliseconds)); } else break; } sw.Stop(); return data; } Для хранения информации (и прокидывания наверх), завел простую структуру: public struct DownloadProgress { public readonly long BytesReceived; public readonly long TotalBytesToReceive; public readonly long TimeMs; public double GetSpeed() { var seconds = TimeMs / 1000.0; if (seconds > 0) return BytesReceived / seconds; return 0; } public DownloadProgress(long received, long total, long time) { this.BytesReceived = received; this.TotalBytesToReceive = total; this.TimeMs = time; } } Итого, если качать в один поток, то GetSpeed в любой момент времени (кроме первой секунды где то) показывает реальную скорость. Класс, который качает общую цифру в итоге хранит у себя: this.Speed = 0; var file = await ImageFile.DownloadFile(this.ImageLink, dp => this.Speed = dp.GetSpeed()); this.Speed = 0; Дальше я думал осталось самое легкое - просто на верхнем уровне сложил все скорости и всё: return this.ActivePages != null && this.ActivePages.Any() ? this.ActivePages.Sum(p => p.Speed) : 0; На деле, получилось очень неприятное поведение: Большую часть времени скорость действительно отображается корректная. Скорость часто скачет, причем разброс иногда превосходит ширину канала. При канале в 650кб\сек цифры скачут от 300кб\сек до 5-8мб\сек. Если скорость ниже вполне может быть при окончании одной загрузки и начале следующей, то вот превышение ширины канала очевидно невозможно технически. Возможно я где то округление пропустил, или Stopwatch недостаточно точен для таких расчетов? UPD: таки первое подозрение оправдалось - начальный скачок скорости портил общую статистику. Если метод подсчета скорости сделать вот таким: public double GetSpeed() { var seconds = TimeMs / 1000.0; if (seconds > 0.1) return BytesReceived / seconds; return 0; } То скорость уже в целом намного адекватнее, выше ширины канала изредка скачки ещё бывают, но уже не больше 5%. Можно увеличить игнорируемое время, тогда скачков не будет совсем. Осталась проблема с общим показателем. Если для одного потока цифра была достоверной, то с многопоточным скачиванием цифра часто врёт, средняя от показателя получается ниже реальной (общее время на общий объем). UPD2: минимизировал все расчеты, убрал структуру, вынес логику в статический класс: public class NetworkSpeed { public static double TotalSpeed { get { return totalSpeed; } } private static double totalSpeed = 0; private const uint Seconds = 3; private const uint TimerInterval = 1000; private static Timer speedTimer = new Timer(state => { var now = 0L; while (receivedStorage.Value.Any()) { long added; if (receivedStorage.Value.TryDequeue(out added)) { now += added; } } lastSpeeds.Value.Enqueue(now); totalSpeed = lastSpeeds.Value.Average(); }, null, 0, TimerInterval); private static Lazy > lastSpeeds = new Lazy >(() => new LimitedConcurrentQueue (Seconds)); private static Lazy > receivedStorage = new Lazy >(); public static void Clear() { while (receivedStorage.Value.Count > 0) { long dd; receivedStorage.Value.TryDequeue(out dd); } while (lastSpeeds.Value.Count > 0) { double dd; lastSpeeds.Value.TryDequeue(out dd); } } public static void AddInfo(long received) { receivedStorage.Value.Enqueue(received); } private class LimitedConcurrentQueue : ConcurrentQueue { public uint Limit { get; } public new void Enqueue(T item) { while (Count >= Limit) { T deleted; TryDequeue(out deleted); } base.Enqueue(item); } public LimitedConcurrentQueue(uint limit) { Limit = limit; } } } В итоге, при скачивании достаточно сообщать, сколько байт было скачано в очередной момент: NetworkSpeed.AddInfo(num); И всё, показатель NetworkSpeed.TotalSpeed будет отображать среднюю за последние 3 секунды скорость. Средний показатель в целом стал более-менее стабильным, правда немного завышает показатели на моих данных. Ну и очевидно, что если тредпул будет перегружен, то таймер вовремя не отработает и скорость начнёт "скакать".
Ответы
Ответ 1
Публикую решение, которое меня устроило. Точность итоговой цифры 95-99%. public class NetworkSpeed { public static double TotalSpeed { get { return totalSpeed; } } private static double totalSpeed = 0; private const uint Seconds = 3; private const uint TimerInterval = 1000; private static Timer speedTimer = new Timer(state => { var now = 0L; while (receivedStorage.Value.Any()) { long added; if (receivedStorage.Value.TryDequeue(out added)) { now += added; } } lastSpeeds.Value.Enqueue(now); totalSpeed = lastSpeeds.Value.Average(); }, null, 0, TimerInterval); private static Lazy> lastSpeeds = new Lazy >(() => new LimitedConcurrentQueue (Seconds)); private static Lazy > receivedStorage = new Lazy >(); public static void Clear() { while (receivedStorage.Value.Count > 0) { long dd; receivedStorage.Value.TryDequeue(out dd); } while (lastSpeeds.Value.Count > 0) { double dd; lastSpeeds.Value.TryDequeue(out dd); } } public static void AddInfo(long received) { receivedStorage.Value.Enqueue(received); } private class LimitedConcurrentQueue : ConcurrentQueue { public uint Limit { get; } public new void Enqueue(T item) { while (Count >= Limit) { T deleted; TryDequeue(out deleted); } base.Enqueue(item); } public LimitedConcurrentQueue(uint limit) { Limit = limit; } } } Как этим пользоваться - на верхнем уровне вызываем Clear() когда начинаем или заканчиваем скачивание, чтобы результаты были независимы от других загрузок. В месте, где происходит реальное скачивание - вызываем метод AddInfo, указав сколько байт нам пришло в очередной цикл скачивания. Можно использовать CopyTo из шапки или DownloadProgressChanged у WebClient. Главное передавать именно разницу между предыдущим показателем и текущим. Точность измерений обеспечивается таймером (System.Threading.Timer), а потому, чтобы точность показаний была достоверной, тредпул должен быть свободен для вызова callback. Ну и, понятное дело, результат всех измерений находится в свойстве TotalSpeed. Если хочется, можно добавить событие о его изменении, для своевременного отображения в UI. Частота его обновления специально синхронизирована с таймером - иначе цифра меняется слишком часто и пользователь не понимает, какова скорость.
Комментариев нет:
Отправить комментарий