Tempo de microssegundo em JavaScript

99

Existem funções de temporização em JavaScript com resolução de microssegundos?

Estou ciente de timer.js para Chrome e espero que haja uma solução para outros navegadores amigáveis, como Firefox, Safari, Opera, Epiphany, Konqueror, etc. Não estou interessado em oferecer suporte a nenhum IE, mas respostas incluindo o IE são bem-vindos.

(Dada a baixa precisão do tempo de milissegundos em JS, não estou prendendo a respiração neste aqui!)

Atualização: timer.js anuncia resolução de microssegundos, mas simplesmente multiplica a leitura de milissegundos por 1.000. Verificado por teste e inspeção de código. Decepcionado. : [

mwcz
fonte
2
O que você está tentando fazer em um navegador que requer precisão de microssegundos? Em geral, as garantias de desempenho do comportamento dos navegadores não são tão precisas.
Yuliy
4
Não vai acontecer. Você não pode confiar na precisão de microssegundos, mesmo que ela existisse. O único caso de uso sólido que posso imaginar são clientes nativos no Chrome, mas você não se importa com a API JS. Também adoro tratar o "Epiphany" como um navegador de primeira classe e ignorar o IE.
Raynos
6
'Obter' o tempo em javascript leva algum tempo, assim como retorná-lo - e a latência aumenta se você estiver em uma página da web que está redesenhando ou gerenciando eventos. Eu nem contaria com a precisão de 10 milissegundos mais próxima.
kennebec
1
Tipo, digamos, lançando pop-ups em alta velocidade? Basicamente, o problema é que dar a terceiros muito acesso às máquinas dos usuários apenas pelo fato de uma pessoa visitar um site é um problema sério.
Pointy
1
Não é mais "vulnerável" do que setInterval (popup, 0), que é rápido o suficiente para que o problema seja basicamente equivalente. A precisão de milissegundos também deve ser removida? kennebec: seu comentário faz sentido, obrigado.
mwcz

Respostas:

134

Como aludido na resposta de Mark Rejhon, há uma API disponível em navegadores modernos que expõe dados de tempo de resolução de submilissegundos para o script: o W3C High Resolution Timer , também conhecido como window.performance.now().

now()é melhor do que o tradicional Date.getTime()de duas maneiras importantes:

  1. now()é um duplo com resolução de submilissegundos que representa o número de milissegundos desde o início da navegação da página. Ele retorna o número de microssegundos na fracionária (por exemplo, um valor de 1000,123 é 1 segundo e 123 microssegundos).

  2. now()está aumentando monotonicamente. Isto é importante porque Date.getTime()pode possivelmente saltar para a frente ou mesmo para trás em chamadas subseqüentes. Notavelmente, se a hora do sistema do SO for atualizada (por exemplo, sincronização do relógio atômico), Date.getTime()também será atualizado. now()tem a garantia de estar sempre aumentando monotonicamente, portanto, não é afetado pela hora do sistema do SO - será sempre a hora do relógio de parede (assumindo que seu relógio de parede não seja atômico ...).

now()pode ser usado em quase todos os lugares que new Date.getTime(), + new Datee Date.now()são. A exceção é que Datee os now()tempos não se misturam, pois Dateé baseado em unix-epoch (o número de milissegundos desde 1970), enquanto now()é o número de milissegundos desde que sua navegação na página começou (portanto, será muito menor que Date).

now()é compatível com o Chrome estável, Firefox 15+ e IE10. Existem também vários polyfills disponíveis.

NicJ
fonte
1
os polyfills provavelmente usarão Date.now (), então esta ainda é a melhor opção considerando o IE9 e seus milhões de usuários, por que misturar biblioteca de terceiros então
Vitaliy Terziev
4
Meu relógio de parede é atômico.
programador
4
new Date.getTime()não é uma coisa. new Date().getTime()é.
The Qodesmith
Eu realmente gostei dessa resposta. Eu executei alguns testes e vim com um exemplo que você pode colocar em seu console para ver se ele ainda terá colisões dramáticas ao usar isso. (observe que eu estava tendo 10% de colisões em uma boa máquina, mesmo fazendo algo tão caro quanto um console.logem cada execução) Difícil de decifrar, mas copie todo o código destacado aqui:last=-11; same=0; runs=100; for(let i=0;i<runs;i++) { let now = performance.now(); console.log('.'); if (now === last) { same++; } last = now; } console.log(same, 'were the same');
bladnman
2
Revisitando meu comentário do ano de 2012 . performance.now () agora está meio confuso de novo pelas soluções alternativas Meltdown / Spectre. Alguns navegadores degradaram seriamente o performance.now () devido a razões de segurança. Acho que minha técnica provavelmente recuperou alguma relevância novamente para uma grande quantidade de casos de uso de benchmarking legítimos, sujeitos às limitações de fuzz do temporizador. Dito isso, alguns navegadores agora têm alguns recursos / extensões de perfil de desempenho de desenvolvedor que não existiam em 2012.
Mark Rejhon
20

Agora existe um novo método de medição de microssegundos em javascript: http://gent.ilcore.com/2012/06/better-timer-for-javascript.html

No entanto, no passado, descobri um método bruto de obter precisão de 0,1 milissegundo em JavaScript a partir de um cronômetro de milissegundo. Impossível? Não. Continue lendo:

Estou fazendo alguns experimentos de alta precisão que exigem precisões de cronômetro verificadas automaticamente, e descobri que consegui obter uma precisão de 0,1 milissegundo de maneira confiável com certos navegadores em certos sistemas.

Descobri que, em navegadores modernos com aceleração por GPU em sistemas rápidos (por exemplo, i7 quad core, onde vários núcleos estão ociosos, apenas a janela do navegador) - agora posso confiar que os temporizadores têm precisão de milissegundos. Na verdade, ele se tornou tão preciso em um sistema i7 ocioso que consegui obter com segurança exatamente o mesmo milissegundo, em mais de 1.000 tentativas. Somente quando estou tentando fazer coisas como carregar uma página da web extra, ou outro, a precisão de milissegundos degrada (E eu sou capaz de capturar minha própria precisão degradada fazendo uma verificação de tempo antes e depois, para ver se meu tempo de processamento aumentou repentinamente para 1 ou mais milissegundos - isso me ajuda a invalidar resultados que provavelmente foram afetados de maneira adversa pelas flutuações da CPU).

Tornou-se tão preciso em alguns navegadores acelerados por GPU em sistemas i7 quad-core (quando a janela do navegador é a única janela), que descobri que gostaria de poder acessar um temporizador de precisão de 0,1 ms em JavaScript, já que a precisão é finalmente agora lá em alguns sistemas de navegação de ponta para fazer com que a precisão do cronômetro valha a pena para certos tipos de aplicações de nicho que requerem alta precisão, e onde as aplicações são capazes de verificar se há desvios de precisão.

Obviamente, se você estiver fazendo várias passagens, pode simplesmente executar várias passagens (por exemplo, 10 passagens) e depois dividir por 10 para obter a precisão de 0,1 milissegundo. Esse é um método comum de obter melhor precisão - faça várias passagens e divida o tempo total pelo número de passagens.

NO ENTANTO ... Se eu puder fazer apenas uma única passagem de benchmark de um teste específico devido a uma situação incomumente única, descobri que posso obter 0,1 (e às vezes 0,01 ms) de precisão ao fazer isso:

Inicialização / calibração:

  1. Execute um loop ocupado para esperar até que o cronômetro aumente para o próximo milissegundo (alinhe o cronômetro com o início do próximo intervalo do milissegundo) Este loop ocupado dura menos de um milissegundo.
  2. Execute outro loop ocupado para incrementar um contador enquanto espera o incremento do cronômetro. O contador informa quantos incrementos de contador ocorreram em um milissegundo. Este loop ocupado dura um milissegundo completo.
  3. Repita o procedimento acima, até que os números se tornem ultraestáveis ​​(tempo de carregamento, compilador JIT, etc.). 4. NOTA: A estabilidade do número dá a você a precisão alcançável em um sistema inativo. Você pode calcular a variação, se precisar verificar a precisão. As variações são maiores em alguns navegadores e menores em outros. Maior em sistemas mais rápidos e mais lento em sistemas mais lentos. A consistência também varia. Você pode saber quais navegadores são mais consistentes / precisos do que outros. Sistemas mais lentos e sistemas ocupados levarão a variações maiores entre os passos de inicialização. Isso pode dar a você a oportunidade de exibir uma mensagem de aviso se o navegador não estiver fornecendo precisão suficiente para permitir medições de 0,1 ms ou 0,01 ms. A inclinação do cronômetro pode ser um problema, mas alguns cronômetros de milissegundos inteiros em alguns sistemas aumentam com bastante precisão (bem na hora), o que resultará em valores de calibração muito consistentes nos quais você pode confiar.
  4. Salve o valor final do contador (ou média das últimas passagens de calibração)

Comparação de uma passagem com precisão abaixo de um milissegundo:

  1. Execute um loop ocupado para esperar até que o cronômetro aumente para o próximo milissegundo (alinhe o cronômetro com o início do próximo intervalo do milissegundo). Este loop ocupado dura menos de um milissegundo.
  2. Execute a tarefa que você deseja para comparar com precisão o tempo.
  3. Verifique o cronômetro. Isso fornece milissegundos inteiros.
  4. Execute um loop ocupado final para incrementar um contador enquanto espera o incremento do cronômetro. Este loop ocupado dura menos de um milissegundo.
  5. Divida este valor do contador, pelo valor do contador original da inicialização.
  6. Agora você tem a parte decimal de milissegundos !!!!!!!!

AVISO: Loops ocupados NÃO são recomendados em navegadores da web, mas, felizmente, esses loops ocupados são executados por menos de 1 milissegundo cada, e são executados apenas algumas vezes.

Variáveis ​​como compilação JIT e flutuações de CPU adicionam imprecisões massivas, mas se você executar várias passagens de inicialização, você terá recompilação dinâmica completa e, eventualmente, o contador se estabiliza em algo muito preciso. Certifique-se de que todos os loops ocupados tenham exatamente a mesma função para todos os casos, para que as diferenças nos loops ocupados não levem a diferenças. Certifique-se de que todas as linhas de código sejam executadas várias vezes antes de começar a confiar nos resultados, para permitir que os compiladores JIT já tenham se estabilizado para uma recompilação dinâmica completa (dynarec).

Na verdade, eu testemunhei uma precisão se aproximando de microssegundos em certos sistemas, mas eu não confiaria ainda. Mas a precisão de 0,1 milissegundo parece funcionar de forma bastante confiável, em um sistema quad-core ocioso onde sou a única página do navegador. Eu cheguei a um caso de teste científico em que eu só podia fazer passagens únicas (devido a variáveis ​​únicas ocorrendo) e precisava cronometrar precisamente cada passagem, em vez de calcular a média de várias passagens repetidas, então é por isso que fiz isso.

Fiz vários pré-passes e passes fictícios (também para liquidar o dynarec), para verificar a confiabilidade da precisão de 0,1ms (permaneceu sólida por vários segundos), então mantive minhas mãos fora do teclado / mouse, enquanto o benchmark ocorria, então fiz vários pós-passes para verificar a confiabilidade da precisão de 0,1 ms (permaneceu sólido novamente). Isso também verifica se coisas como mudanças de estado de energia, ou outras coisas, não ocorreram entre o antes e o depois, interferindo nos resultados. Repita o pré-teste e o pós-teste entre cada passagem de benchmark. Com isso, eu estava quase certo de que os resultados intermediários eram precisos. Não há garantia, é claro, mas mostra que uma precisão precisa de <0,1 ms é possível em alguns casos em um navegador da web.

Este método só é útil em muito, muito nicho casos. Mesmo assim, literalmente não será 100% infinitamente garantível, você pode obter uma precisão bastante confiável e até mesmo uma precisão científica quando combinada com várias camadas de verificações internas e externas.

Mark Rejhon
fonte
3
Costumava ser complicado fazer o tempo com precisão superior porque tudo o que tínhamos era Date.now()ou +new Date(). Mas agora temos performance.now(). Embora esteja claro que você encontrou algumas maneiras interessantes de hackear mais recursos, esta resposta é essencialmente obsoleta. Além disso, não recomende nada relacionado a loops ocupados. Só não faça isso. Não precisamos mais disso.
Steven Lu
1
A maioria dos navegadores reduziu a precisão de sua implementação performance.now () para mitigar temporariamente o ataque de temporização de cache. Eu me pergunto se essa resposta ainda tem significado na pesquisa de segurança.
Qi Fan
2
Revisitando meu próprio comentário. Uau, eu postei o acima no ano de 2012 muito antes de performance.now (). Mas agora isso está um pouco confuso de novo pelas soluções alternativas de Meltdown / Spectre. Alguns navegadores degradaram seriamente o performance.now () devido a razões de segurança. Eu acho que a técnica acima provavelmente recuperou alguma relevância novamente para uma grande quantidade de muitos casos de uso de benchmarking legítimos, sujeitos às limitações do timer-fuzz.
Mark Rejhon
3

A resposta é "não", em geral. Se você estiver usando JavaScript em algum ambiente do lado do servidor (ou seja, não em um navegador), todas as apostas estão canceladas e você pode tentar fazer o que quiser.

editar - esta resposta é antiga; os padrões progrediram e novas instalações estão disponíveis como soluções para o problema da hora exata. Mesmo assim, deve ser lembrado que fora do domínio de um verdadeiro sistema operacional em tempo real, o código comum não privilegiado tem controle limitado sobre seu acesso aos recursos de computação. Medir o desempenho não é o mesmo (necessariamente) que prever o desempenho.

Pontudo
fonte
2

Aqui está um exemplo que mostra meu cronômetro de alta resolução para node.js :

 function startTimer() {
   const time = process.hrtime();
   return time;
 }

 function endTimer(time) {
   function roundTo(decimalPlaces, numberToRound) {
     return +(Math.round(numberToRound + `e+${decimalPlaces}`)  + `e-${decimalPlaces}`);
   }
   const diff = process.hrtime(time);
   const NS_PER_SEC = 1e9;
   const result = (diff[0] * NS_PER_SEC + diff[1]); // Result in Nanoseconds
   const elapsed = result * 0.0000010;
   return roundTo(6, elapsed); // Result in milliseconds
 }

Uso:

 const start = startTimer();

 console.log('test');

 console.log(`Time since start: ${endTimer(start)} ms`);

Normalmente, você pode usar:

 console.time('Time since start');

 console.log('test');

 console.timeEnd('Time since start');

Se você estiver cronometrando seções de código que envolvem loop, não poderá obter acesso ao valor de console.timeEnd()para adicionar os resultados do cronômetro. Você pode, mas fica desagradável porque você tem que injetar o valor de sua variável de iteração, como i, e definir uma condição para detectar se o loop está concluído.

Aqui está um exemplo porque pode ser útil:

 const num = 10;

 console.time(`Time til ${num}`);

 for (let i = 0; i < num; i++) {
   console.log('test');
   if ((i+1) === num) { console.timeEnd(`Time til ${num}`); }
   console.log('...additional steps');
 }

Cite: https://nodejs.org/api/process.html#process_process_hrtime_time

agm1984
fonte