Подсчет скорости скачивания в вашем приложении

Предыстория

Есть у меня маленький и уютный pet-project, который позволяет качать файлы из интернета. Файлы при этом группируются и пользователю отображается не каждый файл, а некоторая группировка. И весь процесс скачивания (и отображение этого процесса) сильно зависел от данных. Данные при этом получались на лету, т.е. пользователь запускает на скачивание и нет никакой информации, сколько придётся качать в реальности.

Наивная реализация хоть какого-то информирования сделана простой — отображается прогресс скачивания как отношение количества скачанных к общему количеству. Информации для пользователя не много — просто ползущая полоска, но это уже лучше чем ничего, и это заметно лучше чем популярный нынче механизм загрузки без индикации прогресса.

И тут появляется пользователь с логичной проблемой — на большой группировке непонятно, почему прогресс еле ползёт — много файлов надо скачать или низкая скорость? Как я упоминал выше — количество файлов заранее неизвестно. Поэтому, я принял решение добавить счетчик скорости.

Анализ

Хорошая практика — посмотреть тех, кто уже решал аналогичную задачу, чтобы не изобретать велосипед. Разный софт закрывает этим разные задачи, но выглядит отображение довольно одинаково:

uTorrent DownloadMaster
uTorrent DownloadMaster

Ключевой момент, который я выделил для себя — в первую отображение скорости нужно на текущий момент времени. Не какая скорость была средней, не какая скорость в целом средняя с момента начала, а именно какова эта цифра на текущий момент. На самом деле это важно, когда дойду до кода — поясню отдельно.

Итак, нам нужна простая цифра вида 10 MB/s или что-то подобное. Как же нам её посчитать?

Теория и практика

Существующая реализация скачивания использовала HttpWebRequest и я решил не переделывать само скачивание — не стоит трогать работающий механизм.

Итак, начальная реализация без какого-либо подсчета:

            var request = WebRequest.Create(uri);             var response = await request.GetResponseAsync();             using (var ms = new MemoryStream())             {                 await response.GetResponseStream().CopyToAsync(ms);                 return ms.ToArray();             }

На уровне такого API реагировать можно только на полное скачивание файла, для небольших групп (или даже для одного файла) скорость фактически не посчитать. Идём за исходниками CopyToAsync, копипастим оттуда простую логику:

            byte[] buffer = new byte[bufferSize];             int bytesRead;             while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)             {                 await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);             }

Теперь мы можем реагировать на каждый буфер, отданный нам по сети.

Итак, во первых, что мы делаем вместо коробочного CopyToAsync:

        public static async Task<byte[]> GetBytesAsync(this Stream from)         {             using (var memory = new MemoryStream())             {                 byte[] buffer = new byte[81920];                 int bytesRead;                 while ((bytesRead = await from.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)                 {                     await memory.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);                     NetworkSpeed.AddInfo(bytesRead);                 }                 return memory.ToArray();             }         }

Единственное, что реально добавлено — NetworkSpeed.AddInfo. И единственное, что мы передаем — количество скачанных байт.

Сам код для скачивания выглядит в итоге так:

            var request = WebRequest.Create(uri);             var response = await request.GetResponseAsync();             var array = await response.GetResponseStream().GetBytesAsync();

Вариант для WebClient

            var client = new WebClient();             var lastRecorded = 0L;             client.DownloadProgressChanged += (sender, eventArgs) =>             {                 NetworkSpeed.AddInfo(eventArgs.BytesReceived - lastRecorded);                 lastRecorded = eventArgs.BytesReceived;             };             var array = await client.DownloadDataTaskAsync(uri);

Вариант для HttpClient

            var httpClient = new HttpClient();             var content = await httpClient.GetStreamAsync(uri);             var array = await content.GetBytesAsync();

Хорошо, половина задачи решена — мы знаем, сколько мы скачали. Переходим к скорости.

Согласно википедии :

Скорость передачи данных — объём данных, передаваемых за единицу времени.

Первый наивный подход

У нас есть объём. Время можно взять буквально запуска и получать разницу с DateTime.Now. Берем и делим?
Для консольных утилит типа curl такое возможно и имеет смысл.
Но если ваше приложение чуть сложнее, то буквально кнопка "пауза" резко усложнит вам жизнь.

Немного про паузу
Может я очень наивен, а может вопрос действительно не так прост — но пауза меня заставляет задумываться постоянно. Пауза при скачивании может вести себя минимум тремя способами:

  • прерывать закачку файлов, начинать заново после
  • просто не качать файл дальше, надеяться что сервер даст продолжить после
  • докачивать уже начатые файлы, не качать новые, качать новые после

Так как первые два приводят к потере уже скачанной информации, я у себя использую третий.
Чуть выше я обращал внимание, что скорость нужна именно на момент времени. Так вот, пауза это дело усложняет:

  • нельзя нормально посчитать, какой была средняя скорость, просто взяв объем на время
  • пауза может иметь внешние причины, которые поменяют скорость и канал (переподключение к сети провайдера, переключение на VPN, завершение uTorrent-a занявшего весь канал), что приведёт к изменению реальной скорости
    Фактически, пауза разделяет любые показатели на до и после неё. Это не влияет особо на код ниже, просто минутка забавной информации на подумать.

Второй наивный подход

Добавим таймер. Таймер каждый период времени будет брать всю свежую информацию о скачанном объеме и пересчитывать показатель скорости. А если таймер поставить в секунду, то вся полученная за эту секунду информация о скачанном объеме и будет равна скорости за эту секунду:

Реализация класса NetworkSpeed целиком

    public class NetworkSpeed     {         public static double TotalSpeed { get { return totalSpeed; } }          private static double totalSpeed = 0;          private const uint TimerInterval = 1000;          private static Timer speedTimer = new Timer(state =>         {             var now = 0L;             while (ReceivedStorage.TryDequeue(out var added))                 now += added;             totalSpeed = now;         }, null, 0, TimerInterval);          private static readonly ConcurrentQueue<long> ReceivedStorage = new ConcurrentQueue<long>();          public static void Clear()         {             while (ReceivedStorage.TryDequeue(out _))             {             }              totalSpeed = 0;         }          public static void AddInfo(long received)         {             ReceivedStorage.Enqueue(received);         }     }

По сравнению с первым вариантом, такая реализация начинает реагировать на паузу — скорость снижается до 0 в ближайшую секунду после того, как перестают приходить данные снаружи.
Но, есть и минусы. Мы работаем с буфером в 80кб, а значит загрузка начатая в одной секунде, отобразится только в следующей. И при большом потоке параллельных загрузок такие погрешности в измерениях будут отображать что угодно — у меня разброс был до 30% от реальных цифр. Я бы может и не заметил, но превышение 100мбит выглядело слишком уж подозрительно.

Третий подход

Второй вариант уже достаточно близок к правде, плюс у него ошибка наблюдалась больше на старте закачки, а не на всём жизненном цикле.
Поэтому, простое решение — брать в качестве показателя не цифру за секунду, а среднюю за три последние секунды. Три здесь — магическая константа, подобранная на глаз. С одной стороны хотелось приятное отображение роста и спада скорости, с другой — чтобы скорость была поближе к правде.

Реализация чуть усложняется, но в целом ничего такого:

Реализация класса NetworkSpeed целиком

    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.TryDequeue(out var added))                 now += added;             LastSpeeds.Enqueue(now);             totalSpeed = LastSpeeds.Average();             OnUpdated(totalSpeed);         }, null, 0, TimerInterval);          private static readonly LimitedConcurrentQueue<double> LastSpeeds = new LimitedConcurrentQueue<double>(Seconds);          private static readonly ConcurrentQueue<long> ReceivedStorage = new ConcurrentQueue<long>();          public static void Clear()         {             while (ReceivedStorage.TryDequeue(out _))             {             }             while (LastSpeeds.TryDequeue(out _))             {             }              totalSpeed = 0;         }          public static void AddInfo(long received)         {             ReceivedStorage.Enqueue(received);         }          public static event Action<double> Updated;          private class LimitedConcurrentQueue<T> : ConcurrentQueue<T>         {             public uint Limit { get; }              public new void Enqueue(T item)             {                 while (Count >= Limit)                     TryDequeue(out _);                 base.Enqueue(item);             }              public LimitedConcurrentQueue(uint limit)             {                 Limit = limit;             }         }          private static void OnUpdated(double obj)         {             Updated?.Invoke(obj);         }     }

Пара моментов:

  • на момент реализации не нашел готовой очереди с ограничением на количество элементов и взял её в интернете, в коде выше это LimitedConcurrentQueue.
  • вместо реализации INotifyPropertyChanged почему то Action, использование фактически одинаковое, причин не помню. Логика простая — показатель меняется, надо пользователей об этом уведомить. Реализация может быть любой, хоть IObservable, кому как удобнее.

И немного читабельности

API отдает скорость в байтах, для читаемости пригодится простой (взятый в интернете)

конвертер

    public static string HumanizeByteSize(this long byteCount)     {       string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB       if (byteCount == 0)         return "0" + suf[0];       long bytes = Math.Abs(byteCount);       int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));       double num = Math.Round(bytes / Math.Pow(1024, place), 1);       return Math.Sign(byteCount) * num + suf[place];     }      public static string HumanizeByteSize(this double byteCount)     {       if (double.IsNaN(byteCount) || double.IsInfinity(byteCount) || byteCount == 0)         return string.Empty;        return HumanizeByteSize((long)byteCount);     }

Напомню, что скорость в байтах, т.е. на 100мбитный канал должно выдать не более 12.5МБ.

Как это в итоге выглядит:

Скачивание образа ubuntu

Current speed 904,5KB/s
Current speed 1,8MB/s
Current speed 2,9MB/s
Current speed 3,2MB/s
Current speed 2,9MB/s
Current speed 2,8MB/s
Current speed 3MB/s
Current speed 3,1MB/s
Current speed 3,2MB/s
Current speed 3,3MB/s
Current speed 3,5MB/s
Current speed 3,6MB/s
Current speed 3,6MB/s
Current speed 3,6MB/s

Ну и несколько образов сразу

Current speed 1,2MB/s
Current speed 3,8MB/s
Current speed 7,3MB/s
Current speed 10MB/s
Current speed 10,3MB/s
Current speed 10MB/s
Current speed 9,7MB/s
Current speed 9,8MB/s
Current speed 10,1MB/s
Current speed 9,8MB/s
Current speed 9,1MB/s
Current speed 8,6MB/s
Current speed 8,4MB/s

Заключение

Интересно было разобраться с вроде банальной задачей по подсчету скорости. И даже несмотря на то, что код работает и отдает какие то цифры, хочется послушать критики — что упустил, как можно было сделать лучше, может какие есть готовые решения.

Хочется сказать спасибо Stack Overflow на русском и конкретно VladD-exrabbit — в хорошем вопросе хоть и есть половина ответа, но любые подсказки и любая помощь всегда двигают тебя вперёд.

Хочу напомнить, что это pet-project — поэтому класс статический и один на всех, поэтому точность не особо. Я вижу много мелочей, которые можно было бы сделать лучше, но… всегда есть чем заняться ещё, так что пока скорость я считаю вот так и считаю что это не самый плохой вариант.


ссылка на оригинал статьи https://habr.com/ru/post/465669/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *