Contando a velocidade de download em seu aplicativo

Antecedentes


Eu tenho um projeto pequeno e acolhedor para animais de estimação, que permite baixar arquivos da Internet. Os arquivos são agrupados e o usuário não mostra cada arquivo, mas sim alguns agrupamentos. E todo o processo de download (e a exibição desse processo) dependia muito dos dados. Os dados foram obtidos em tempo real, ou seja, o usuário começa a baixar e não há informações sobre o quanto você precisa baixar na realidade.


A implementação ingênua de pelo menos algum tipo de informação é simplificada - o progresso do download é exibido como a proporção entre o número de downloads e o número total. Não há muitas informações para o usuário - apenas uma faixa rastejante, mas isso é melhor que nada, e é notavelmente melhor que o mecanismo de carregamento atualmente popular sem indicar progresso.


E então um usuário aparece com um problema lógico - em um grupo grande, não está claro por que o progresso está apenas se arrastando - eu preciso baixar muitos arquivos ou uma velocidade baixa? Como mencionei acima - o número de arquivos não é conhecido antecipadamente. Por isso, decidi adicionar um contador de velocidade.


Análise


É uma boa prática ver quem já resolveu um problema semelhante para não reinventar a roda. Um software diferente fecha essas tarefas diferentes, mas a tela parece praticamente a mesma:


uTorrentDownloadmaster
uTorrentDownloadmaster

O ponto chave que identifiquei por mim mesmo é que a primeira exibição de velocidade é necessária no momento atual. Não qual velocidade era média, nem qual velocidade como um todo era média desde o momento em que começou, ou seja, qual é esse valor no momento atual. De fato, isso é importante quando eu chegar ao código - explicarei separadamente.


Portanto, precisamos de um dígito simples como 10 MB/s ou algo assim. Como calculamos isso?


Teoria e Prática


A implementação de download existente usava o HttpWebRequest e eu decidi não refazer o download em si - não toque no mecanismo de trabalho.


Portanto, a implementação inicial sem qualquer cálculo:


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

No nível dessa API, você só pode responder a um download completo de um arquivo; em pequenos grupos (ou mesmo em um único arquivo), não é possível calcular a velocidade. Seguimos o código- fonte CopyToAsync , copie e cole a lógica simples a partir daí:


  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); } 

Agora podemos responder a todos os buffers que nos foram dados pela rede.


Então, primeiro, o que fazemos em vez do CopyToAsync in a box:


  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(); } } 

A única coisa realmente adicionada é o NetworkSpeed.AddInfo . E a única coisa que transmitimos é o número de bytes baixados.


O código em si para o download é semelhante a este:


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

Opção para 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); 

Opção para HttpClient
  var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync(); 

Bem, metade do problema está resolvido - sabemos o quanto baixamos. Nós nos voltamos para a velocidade.


De acordo com a wikipedia :


Taxa de transferência de dados - a quantidade de dados transmitidos por unidade de tempo.

Primeira abordagem ingênua


Nós temos um volume. O tempo pode ser gasto literalmente desde a inicialização e obtenha a diferença com o DateTime.Now . Tirar e compartilhar?
Para utilitários de console como curl, isso é possível e faz sentido.
Mas se o seu aplicativo for um pouco mais complicado, literalmente o botão de pausa complicará drasticamente sua vida.


Um pouco sobre a pausa
Talvez eu seja muito ingênua, ou talvez a pergunta não seja tão simples - mas a pausa me faz pensar constantemente. Uma pausa durante o download pode se comportar de pelo menos três maneiras:


  • interromper o upload do arquivo, comece novamente depois
  • apenas não faça o download do arquivo, espere que o servidor continue após
  • baixar arquivos já iniciados, não baixar novos, baixar novos depois

Como os dois primeiros levam à perda de informações já baixadas, eu uso o terceiro.
Um pouco mais alto, notei que a velocidade é necessária precisamente em um ponto no tempo. Portanto, uma pausa complica este assunto:


  • você não pode calcular corretamente qual era a velocidade média, apenas tomando o volume por um tempo
  • Uma pausa pode ter motivos externos que alteram a velocidade e o canal (reconectando-se à rede do provedor, alternando para VPN, encerrando o uTorrent que levou todo o canal), o que levará a uma alteração na velocidade real
    De fato, uma pausa divide todos os indicadores em antes e depois dela. Isso não afeta particularmente o código abaixo, apenas um minuto de informações divertidas para se pensar.

Segunda abordagem ingênua


Adicione um cronômetro. O temporizador a cada período recolhe todas as informações mais recentes sobre o volume baixado e recalcula o indicador de velocidade. E se você definir o cronômetro por segundo, todas as informações recebidas para este segundo sobre o volume baixado serão iguais à velocidade desse segundo:


Toda a implementação da classe 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); } } 

Comparado com a primeira opção, essa implementação começa a responder a uma pausa - a velocidade cai para 0 no próximo segundo após a chegada dos dados externos.
Mas também existem desvantagens. Estamos trabalhando com um buffer de 80kb, o que significa que o download iniciado em um segundo será exibido apenas no próximo. E com um grande fluxo de downloads paralelos, esses erros de medição exibirão qualquer coisa - eu tive um spread de até 30% dos números reais. Eu poderia não ter notado, mas exceder 100 Mbit parecia muito suspeito .


Terceira abordagem


A segunda opção já está próxima o suficiente da verdade, e seu erro foi observado mais no início do download, e não ao longo do ciclo de vida.
Portanto, uma solução simples é tomar como indicador, não o valor por segundo, mas a média nos últimos três segundos. Três aqui é uma constante mágica combinada a olho. Por um lado, queria uma exibição agradável do crescimento e declínio da velocidade, por outro - para que a velocidade estivesse mais próxima da verdade.


A implementação é um pouco complicada, mas, em geral, nada como isto:


Toda a implementação da classe 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); } } 

Alguns pontos:


  • no momento da implementação, não encontrei a fila finalizada com um limite no número de elementos e a levei à Internet, no código acima é LimitedConcurrentQueue .
  • em vez de implementar INotifyPropertyChanged por algum motivo, Action , o uso é praticamente o mesmo, não me lembro dos motivos. A lógica é simples - o indicador está mudando, os usuários precisam ser notificados sobre isso. A implementação pode ser qualquer, mesmo IObservable , para quem é mais conveniente.

E um pouco de legibilidade


A API fornece a velocidade em bytes, para facilitar a leitura, uma simples (usada na Internet) é útil


conversor
  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); } 

Deixe-me lembrá-lo de que a velocidade em bytes, ou seja, por canal de 100 bits não deve emitir mais de 12,5 MB.


Como finalmente se parece:


Faça o download da imagem do ubuntu
Velocidade atual 904.5KB / s
Velocidade atual 1.8MB / s
Velocidade atual 2.9MB / s
Velocidade atual 3.2MB / s
Velocidade atual 2.9MB / s
Velocidade atual 2.8MB / s
Velocidade atual 3MB / s
Velocidade atual 3.1MB / s
Velocidade atual 3.2MB / s
Velocidade atual 3.3MB / s
Velocidade atual 3,5MB / s
Velocidade atual 3.6MB / s
Velocidade atual 3.6MB / s
Velocidade atual 3.6MB / s
...

Bem, várias imagens de uma só vez
Velocidade atual 1,2MB / s
Velocidade atual 3.8MB / s
Velocidade atual 7.3MB / s
Velocidade atual 10MB / s
Velocidade atual 10.3MB / s
Velocidade atual 10MB / s
Velocidade atual 9.7MB / s
Velocidade atual 9.8MB / s
Velocidade atual 10.1MB / s
Velocidade atual 9.8MB / s
Velocidade atual 9.1MB / s
Velocidade atual 8.6MB / s
Velocidade atual 8.4MB / s
...

Conclusão


Foi interessante lidar com uma tarefa aparentemente banal de contar velocidade. E mesmo que o código funcione e dê alguns números, quero ouvir os críticos - o que perdi, como poderia fazer melhor, talvez existam algumas soluções prontas.


Quero agradecer ao Stack Overflow em russo e especificamente ao VladD-exrabbit - embora exista metade da resposta em uma boa pergunta, qualquer dica e ajuda sempre o levam adiante.


Quero lembrá-lo de que este é um projeto de estimação - é por isso que a classe é estática e uma, de modo que a precisão não é realmente. Vejo muitas pequenas coisas que poderiam ser feitas melhor, mas ... sempre há mais algo a fazer, então, por enquanto, acho que é a velocidade e acho que essa não é uma má opção.

Source: https://habr.com/ru/post/pt465669/


All Articles