Был простой код скачивания:
internal static async Task
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
Для хранения информации (и прокидывания наверх), завел простую структуру:
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
private static 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
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
private static 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
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. Частота его обновления специально синхронизирована с таймером - иначе цифра меняется слишком часто и пользователь не понимает, какова скорость.
Комментариев нет:
Отправить комментарий