Страницы

Поиск по вопросам

понедельник, 29 октября 2018 г.

Как определить скорость скачивания в многопоточной программе?

Был простой код скачивания:
internal static async Task DownloadFile(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 секунды скорость. Средний показатель в целом стал более-менее стабильным, правда немного завышает показатели на моих данных. Ну и очевидно, что если тредпул будет перегружен, то таймер вовремя не отработает и скорость начнёт "скакать".


Ответ

Публикую решение, которое меня устроило. Точность итоговой цифры 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. Частота его обновления специально синхронизирована с таймером - иначе цифра меняется слишком часто и пользователь не понимает, какова скорость.

Комментариев нет:

Отправить комментарий