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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Предыстория


Есть у меня маленький и уютный 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/


Интересные статьи

Интересные статьи

Всем привет! Не так давно на работе в рамках тестирования нового бизнес-процесса мне понадобилась возможность авторизации под разными пользователями. Переход в соответствующий р...
Почти каждый растущий проект рано или поздно начинает смотреть в сторону многомодульной архитектуры. Разработчики не хотят ждать пока пересобирается полностью весь проект, когда была изменена т...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
Привет, хабр! В свое время Product Owner попросил подумать нас о создании эффективного процесса по внедрению анимации в наше приложение на android/ios. В то время мы делали задачу по предзапол...
Вступительное слово Я выступил с этим докладом на английском языке на конференции GopherCon Russia 2019 в Москве и на русском — на митапе в Нижнем Новгороде. Речь в нём идёт о bitmap-индексе...