Nonius, início

Como um ponto viaja

O Nonius envia uma série como colunas de inteiros que o navegador lê onde elas chegam: sem JSON, sem decodificador. Cinco passos, de um ponto até um feed ao vivo, mostrados com os bytes de um tile de verdade.

Passo 1

O ponto: três inteiros

Todo ponto é um tempo, um valor e um peso, cada um um inteiro de 64 bits. O valor é escalado: com expoente 2, 10800000000 quer dizer 108.000.000,00, exatamente.

ti64

1788220800000000000

nanossegundos desde 1970: 2026-09-01 00:00:00 UTC

vi64

10800000000

× 10−2, o expoente da série: 108.000.000,00

wu64

3

um peso: um volume, uma contagem de amostras; 1 quando não há

Por que não floats

Perto de 2026, o menor passo entre dois tempos que um número consegue guardar:

float de 32 bits137 s

float de 64 bits256 ns

inteiro de 64 bits, Nonius1 ns

Os floats só aparecem no último momento, depois de subtraído o início da janela visível, quando os números que sobram são pequenos.

Passo 2

NSB1: colunas que começam num múltiplo de 64

Os pontos viajam em colunas: todos os tempos juntos, depois todos os valores, depois os pesos. Um contêiner é um cabeçalho de 64 bytes, uma entrada de 48 bytes por coluna dizendo onde ela começa, as próprias colunas e um bloco de JSON dizendo o que é o tile. Aqui está um tile bruto inteiro, o tipo que carrega os próprios pontos (passo 4), com dois pontos e nenhum vizinho: todos os seus 477 bytes.

  • cabeçalho, 0–63
  • diretório de colunas, 64–207
  • preenchimento com zeros
  • t em 256, v em 320, w em 384
  • metadados, 448–476

O cabeçalho, 64 bytes

0
número mágico, ASCII NONIUSB1
8
versão, 1
10
flags: tempos em ordem, tempos únicos
12
número de colunas
16
número de linhas
24
onde começam as colunas
32
onde começam os metadados
40
o tamanho dos metadados em bytes
48
hash FNV-1a-64 dos bytes das colunas, preenchimento incluído

Posições em bytes. Todo inteiro do contêiner é little-endian; os campos entre estes são zero.

A entrada de uma coluna, 48 bytes

0
nome, 16 bytes de ASCII completados com zeros
16
tipo: 1 inteiro de 64 bits com sinal, 2 sem sinal, 3 float
17
expoente: 9 para t, o da série para os valores, 0 para os pesos
24
onde começam seus bytes
32
seu tamanho: 8 bytes por linha, sem preenchimento

Assim o expoente de um valor viaja com o tile: neste, a entrada de v diz 2, e 10800000000 é 108.000.000,00.

A coluna t, no byte 256

0100: 00 00 f7 db 57 09 d1 18 dc 05 f7 db 57 09 d1 18

Oito bytes por linha, o menos significativo primeiro: 0x18d10957dbf70000 é 1788220800000000000, e a linha seguinte é 1.500 a mais, 1,5 µs depois.

Lido no lugar

const t = new BigInt64Array(bytes.buffer, 256, 2);
// 1788220800000000000n, 1788220800000001500n

Toda coluna começa num múltiplo de 64, então quem lê enxerga a coluna como um array onde ela está, sem interpretá-la: o layout dos buffers do Arrow. O pacote do navegador copia um tile uma vez, para a memória do gráfico, e o lê ali. Um hash das colunas deixa quem lê recusar um tile danificado.

Passo 3

A pirâmide: resumos em cada zoom

Um dia pode ter milhões de pontos, e uma tela tem cerca de duas mil colunas de pixels. Por isso o servidor guarda a série resumida em níveis, os buckets de cada nível quatro vezes mais largos que os do nível de baixo, todos alinhados a 1970.

Um bucket são oito números

  • first_t
  • first_v
  • last_t
  • last_v
  • min
  • max
  • count
  • weight

O primeiro e o último ponto, o menor e o maior valor, quantos são e o seu peso. Desenhada como um traço do menor ao maior, cada coluna ligada à seguinte, a linha é a mesma, pixel a pixel, que a linha desenhada com todos os pontos.

A visualização escolhe o nível

Com resumos a partir de 1 ms, um dia em 1.920 pixels pede colunas de 45 s. O nível mais grosso que ainda dá 16 buckets ou mais por coluna é o nível 5, 1,024 s: 43 dos seus buckets formam uma coluna, 1.964 colunas para o dia. Aproxime o bastante e nenhum nível é fino o suficiente: a visualização lê os pontos brutos.

Passo 4

Tiles por HTTP, como os de um mapa

O cliente pede só os tiles de que a sua visualização precisa. Um tile é uma sequência fixa de colunas de um nível, num endereço cuja resposta nunca muda, então o navegador e todo cache no caminho o guardam por um ano.

GET /v1/series/day/columns/0/16/27940950000

day
a série
0
o nível
16
buckets juntados numa coluna
27940950000
qual sequência de colunas: aqui, quatro colunas por tile

Cada tile cobre um trecho fixo: um tile de colunas, uma sequência de colunas; um tile bruto, um trecho de tempo: o tile bruto i cobre [i · R, (i + 1) · R) nanossegundos desde 1970. Os metadados da série, em /v1/series/{id}, dão essas larguras (R é o seu raw_tile_ns). Um tile também traz o vizinho mais próximo de cada lado, uma coluna ou um ponto logo fora do seu trecho, quando existe, para que a linha possa ser desenhada entrando e saindo da visualização. Eles contam entre as suas linhas, e quem lê os distingue pelo tempo.

O que volta

200 OK
Content-Type: application/vnd.nonius.nsb1
ETag: "e490c413ec950166"
Cache-Control: public, max-age=31536000, immutable

Um contêiner NSB1 de oito colunas, uma por campo do bucket, e três linhas: as duas colunas do tile que têm pontos e a seguinte, que a linha precisa para sair da visualização.

A última linha diz o que ele é

{"tile":"columns","level":0,"k":16,"i":"27940950000"}

O cliente confere com o endereço que pediu. O ETag é o hash do cabeçalho, então pedir de novo um tile que não mudou custa um 304. Bem de perto, os tiles brutos em /raw/{i} trazem os próprios pontos: três colunas, t, v e w, como as do passo 2.

Passo 5

O feed ao vivo: quadros numerados

Pontos novos chegam num WebSocket em quadros, cada um numerado e com as mesmas três colunas, 24 bytes por ponto. Redes perdem quadros; os números mostram onde.

  1. NLS1início: a partir do 0
  2. NLF1 · 0aplicado
  3. NLF1 · 1perdido
  4. NLF1 · 2uma lacuna: reconectar a partir do 0
  5. NLF1 · 1, 2aplicados, em ordem

Um quadro

O número mágico, o número de linhas, o número do quadro, e então as colunas.

Nada é adivinhado

Um quadro fora de vez nunca é aplicado. O cliente reconecta a partir do último quadro que aplicou, e tudo antes dele continua certo. Quando o feed está parado, heartbeats dizem o próximo número, então até a perda do último quadro é notada.

O todo

As especificações são escritas para que se possa construir um cliente só a partir delas, e um foi construído: suas colunas batem com as do núcleo. Estão em inglês.