Страницы

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

вторник, 10 декабря 2019 г.

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

#c_sharp #download #benchmark


Был простой код скачивания:

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


Ответы

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

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

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