Kitsune2: Consultas 251x Mais Rápidas, o Caminho de Atualização 33x, e Gossip Começou 15x Mais Cedo
Dentro de uma manhã de experimentos na camada de rede do Holochain: como os nós encontram pares, buscam dados faltantes, retomam o gossip, e o que 6 pequenos patches mudaram ao longo do caminho.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
Tenho acordado por volta das 4:30 a.m. ultimamente, bem antes do nascer do sol, e aproveitando isso. Em 19 de setembro usei aquela manhã para iniciar um laboratório sobre Kitsune2, a camada de rede peer-to-peer abaixo do Holochain, e fui procurar onde ele estava gastando tempo.Até as 12:20 p.m., o laboratório produziu 6 patches. Consultas de endpoint: 251× mais rápidas em 10,000 identidades retidas. O caminho de atualização peer que os chama: 33× mais rápido. Início de gossip após uma busca concluída: 3,002 ms reduzidos para 202 ms. Tráfego de requisição em pico: 96.9% menos mensagens. 3 das mudanças repararam trabalho que estava esperando desnecessariamente, ou esperando por algo que nunca aconteceria.Tenho codificado há mais de 25 anos e construindo com IA desde GPT-3. Eu o uso para isolar algo que me importa ou que ainda não entendo, trabalhar nisso, e testar o que achamos que aprendemos. Eu amo essa parte: transformar uma pergunta em um experimento e ver o que se mantém. Esta manhã, com agentes de codificação de IA, apliquei essa abordagem à maquinaria que permite que os nós do Holochain mantenham uns aos outros atualizados.Aplicações Holochain compartilham dados entre computadores participantes, ou nodes. Cada participante mantém seu próprio histórico assinado e ajuda a validar e armazenar uma porção dos dados compartilhados da aplicação. Essa loja compartilhada é a distributed hash table, ou DHT. Cópias vivem com vários pares, então os dados podem permanecer disponíveis quando seu autor vai offline. Holochain's DHT guide explica como essas responsabilidades são distribuídas.Kitsune2 lida com a comunicação peer-to-peer embaixo disso. Imagine um nó se atualizando: ele aprende onde os pares podem ser alcançados, compara o que possui com o que eles possuem, e pede as peças que lhe faltam. Gossip é a troca através da qual os pares descobrem essas diferenças; fetch recupera as operações faltantes, as unidades de dados que Kitsune2 transfere. O nó receptor entrega-as ao armazenamento de operações do seu host. Holochain lida com sua validação e integração.Diagram source
graph TD
A[Aprenda identidades e pontos finais dos pares] --> B[Compare dados através de gossip]
B --> C[Identifique operações faltantes]
C --> D[Recupere operações dos pares]
D --> E[Entregue as operações recebidas ao armazenamento do host]
E --> F[Sinalize que as buscas pendentes foram concluídas]
F e1@--> B
e1@{ animation: fast }
Copy sourceOs patches ficam ao longo dessa sequência. Alguns reduzem o trabalho que um nó faz; outros permitem que ele continue quando o trabalho já terminou. As figuras acima medem esses passos particulares em nós de teste locais.
Ajustamos o ponto de partida no commit upstream 06555ee, fonte de desenvolvimento 0.6.0-dev.1. Cada comparação usa essa base mais a sequência de patches registrada. As etiquetas abaixo conectam as descobertas aos registros do nosso experimento; todos os patches 6 agora estão submetidos para revisão upstream.
Etiqueta
O que mudou
Resultado medido
K3a
Chame o helper de drenagem existente quando uma busca for concluída
Início de gossip após uma busca concluída: 3,002 → 202 ms
K3b
Bloqueio do receptor limitado ao dequeue
A concorrência efetiva era 1. Os workers configurados agora sobrepõem-se genuinamente
K3c
Propriedade de admissão marcada por geração
Cancelou a corrente de chamadores 0 pendentes de chaves. Base pendente 8
K3d-P1
Limpeza de resposta baseada em conjunto
1.9–6.1× processamento de resposta mais rápido em grandes cargas pendentes
K3d-P2
Agrupamento opt-in de mesmo par (padrão 1)
Picos: 87.5–96.9% menos mensagens de solicitação
K1
Inverta URL para índice do agente
Em 10,000 identidades retidas: atualizações 21–33×, pesquisas 118–251× mais rápidas, para 3.10× memória de armazenamento
O primeiro reparo reconecta a conclusão da busca à tarefa de gossip aguardando por ela. A mudança chama um auxiliar existente que informa aos ouvintes que o mapa pendente foi esvaziado. Testamos tanto uma busca concluída quanto uma busca deliberadamente mantida aberta.
Gossip initiation after a fetch completes, with the control that must not move (ms)Chart data
milliseconds
Upstream 06555ee
With the drain notification restored
Fetch completed
3001
202
Fetch still pending (control)
3002
3001
Após a conclusão, o gossip pode prosseguir. Enquanto a busca permanece pendente, ambas as versões ainda atingem o temporizador de fallback. O teste isolado acima mediu 3,001 → 202 ms; o replay combinado posterior mediu 3,002 → 202 ms. Ambos usaram um fallback de 3,000 ms e um atraso inicial de 200 ms. A diferença aproximada de 15× registra qual rota a tarefa tomou: esperar pelo fallback ou acordar na conclusão.
Cada alteração vive em um crate, e 5 dos 6 em um único arquivo#
Um crate Rust é um pacote de código. Todos os patches 6 vivem no crate principal, que contém os registros pares e a maquinaria de busca. Eles mudam como o gossip obtém seus dados e sinais de conclusão sem editar o algoritmo de gossip ou o transporte de rede subjacente. Agrupamento adiciona um campo de configuração opcional do núcleo.
Etiqueta
Arquivo
Linhas
K1
core/src/factories/core_known_peers.rs
+33 / −10
K3a
core/src/factories/core_fetch.rs
+1
K3b
core/src/factories/core_fetch.rs
+11 / −3
K3c
core/src/factories/core_fetch.rs
+71 / −53
K3d-P1
core/src/factories/core_fetch.rs
+6 / −1
K3d-P2
core/src/factories/core_fetch.rs, 1 arquivo de teste
+89 / −17
5 patches empilhados em uma ordem registrada em core_fetch.rs, que solicita operações ausentes e processa respostas. O patch restante fica em core_known_peers.rs, o registro retido de qual identidade está associada a qual endpoint de rede. Em seguida, aplicamos todos os 6 a uma única árvore de origem e os executamos juntos.
Os dados haviam chegado, mas o gossip ainda estava esperando#
CoreFetch mantém um mapa das operações que ele pediu e de quem. Quando a resposta de um peer chega e as operações são armazenadas com sucesso, o código remove esses identificadores do mapa. Outras partes do arquivo, o caminho de send-error e o caminho de peer-cleanup, chamam um helper chamado notify_listeners_if_queue_drained logo depois de removerem as entradas. O caminho de sucesso nunca o chamou.Um nó pode expandir a porção do DHT que armazena, chamada de seu storage arc. Para fazer isso, ele precisa se atualizar sobre os dados dentro do intervalo maior. No caminho de arc-growth que testamos, o gossip espera que fetches pendentes terminem antes de iniciar com um peer, com um timer como fallback. Os dados chegaram e o mapa pendente esvaziou, mas a tarefa que esperava por esse sinal continuou dormindo até que o timer expirasse.Diagram source
sequenceDiagram
participant G as Gossip arc-growth loop
participant F as CoreFetch
participant P as Peer
G->>F: Register a drain waiter
P->>F: Final response, operations stored
F->>F: retain() empties the pending map
Note over F: helper exists, never called
F--xG: Notification that never arrives
G->>G: Initiate after the roughly 3-second fallback
Copy sourceVerificar após a conclusão oculta a diferença: um waiter recém‑registrado vê o mapa vazio e retorna imediatamente. Registramos o nosso antes da conclusão. Quando os dados foram armazenados, aquele waiter ainda não tinha notificação no código original; com o patch, ele recebeu uma. Isso corrige a transição entre receber dados e iniciar a próxima troca de gossip.Também verificamos respostas parciais, erros de armazenamento, vários ouvintes, e ouvintes que se registram como solicitações concluídas ou quando novo trabalho chega. Uma resposta parcial deve deixar o waiter pendente; apenas completar a última busca pendente deve acordá‑lo.
5 trabalhadores foram configurados, e 1 estava realmente enviando#
O lado de saída de CoreFetch executa um número configurado de tarefas de trabalhador puxando de um canal compartilhado. O loop foi escrito assim:rustCopy source
while let Some(..) = outgoing_request_rx.lock().await.recv().await { // send the request}
O bloqueio permite que o trabalhador 1 pegue a próxima solicitação da fila compartilhada. Mas Rust mantém esse guardião de bloqueio temporário vivo durante todo o corpo deste while let, incluindo a espera pelo envio de rede. Enquanto o trabalhador 1 enviava, o outro 4 nem conseguia coletar sua próxima solicitação. Todas as solicitações ainda podiam chegar eventualmente, então um teste de entrega poderia passar enquanto a concorrência configurada não era usada.Movemos a recepção para um bloco curto para que o bloqueio seja liberado antes que o envio comece. Os trabalhadores ainda se revezam coletando solicitações, depois as enviam simultaneamente. Um envio lento não mantém mais o bloqueio da fila enquanto outros trabalhadores têm solicitações prontas.
Outgoing sends in flight at the same moment, by configured worker countChart data
concurrent sends observed
Upstream 06555ee
Dequeue-scoped guard
1 worker
1
1
2 workers
1
2
5 workers
1
5
A medição mantém cada envio aberto com um relógio pausado e conta quantos entraram antes de qualquer ser liberado, então confirma que nada entra além do limite configurado.
Cancelar um chamador deixou o nó esperando por operações não enviadas de 8#
Uma solicitação de busca passa por registros de 2: o mapa pendente diz o que o nó está esperando, e a fila de saída mantém trabalho para os trabalhadores de envio. request_ops, o método que solicita operações, adicionou todo um lote ao mapa pendente antes de esperar espaço nessa fila.Se o chamador foi cancelado enquanto esperava por capacidade, algumas operações já haviam sido marcadas como pendentes mesmo que nenhum trabalhador as tivesse recebido. Preenchemos a fila, cancelamos um chamador bloqueado, e deixamos todas as solicitações 16,385 admitidas terminarem. 8 operações permaneceram pendentes. Elas nunca entraram na fila de saída, então não havia resposta em seu caminho para limpá-las.Isso se conecta diretamente ao primeiro reparo: uma notificação de conclusão não pode disparar enquanto o mapa ainda contém trabalho não concluído. Aqui, a própria contabilidade do nó a mantinha esperando.O patch reserva a capacidade da fila primeiro, então registra e enfileira a operação sem outro await entre esses passos. Cada entrada também carrega uma tag de geração, então a limpeza de um envio falhado mais antigo não pode apagar uma solicitação mais nova para a mesma operação. Trabalho já admitido sobrevive ao cancelamento do chamador; trabalho ainda esperando por admissão não deixa entrada pendente atrás.Isso repara a propriedade das solicitações. Limites de memória total permanecem uma questão de design separada, já que um chamador ainda pode fornecer um grande lote.
Menos mensagens de requisição, com um limite sobre o tamanho máximo de cada uma#
Um nó de recuperação pode precisar de muitas operações do mesmo par. O formato de requisição do Kitsune2 já suporta uma lista de identificadores, mas o código de saída envia 1 por mensagem. Antes de combinar essas requisições, verificamos como respostas maiores seriam tratadas.Para cada operação pendente, a limpeza de resposta escaneou a lista de identificadores retornados para ver se poderia remover essa entrada. Com 8,192 entradas pendentes e 1,024 identificadores retornados, isso repete uma grande quantidade de buscas. Construir um HashSet dos identificadores retornados torna cada verificação de membro barata; o processamento de resposta mediu 6.1× mais rápido nesse caso. Com apenas 16 identificadores retornados, armazenar os dados dominou e a mudança trouxe pouco.Depois combinamos requisições prontas para o mesmo par. Em um estouro de 64 pequenas operações, tamanhos de lote de 8 e 32 reduziram mensagens de requisição em 87.5% e 96.9%, respectivamente.
Burst behaviour relative to the default batch size of 1 (percent of control)Chart data
percent of control
Request messages
Elapsed time
Batch 1 (control)
100
105.4
Batch 8
12.5
72.6
Batch 32
3.1
67.7
O estouro terminou cerca de 27–32% mais cedo. As mesmas operações ainda precisavam ser transferidas e armazenadas; a economia estava em quantas requisições as transportavam. O controle batch-1 mediu 105.4% do tempo decorrido de base, mostrando aproximadamente 5% desvio de tempo mesmo com batching inativo.Um worker combina apenas requisições que já estão prontas, então uma requisição isolada nunca espera por um lote preencher. A divisão de lote usa tanto um limite de identificador configurado quanto um 4,096-byte encoded request threshold. Contar itens sozinho não restringe seu tamanho total; o teste com 200 identificadores os divide em várias mensagens limitadas. Um único identificador ainda é enviado se sozinho exceder esse limite, então isso não é um limite universal de tamanho de cabo. Respostas carregam os dados reais e precisariam de um limite de tamanho separado no nó que responde.O candidato mantém o tamanho do lote 1 como padrão e aceita as configurações existentes sem alterações. Deixamos a opção de lotes maiores porque o teste separado para uma única solicitação produziu resultados inconsistentes de latência final, cobertos abaixo.
Tirar o escaneamento do caminho de atualização valeu 33×, e 251× no próprio lookup#
CoreKnownPeers lembra identidades e os pontos finais de rede associados a elas. Várias identidades podem compartilhar um ponto final, e uma identidade bloqueada ainda precisa ser identificável depois de ter sido removida da loja de pares ativos. 10,000 identidades retidas portanto significa um histórico de identidades nesta loja, não 10,000 computadores conectados ao mesmo tempo.Em uma atualização da loja de pares, o controle de acesso pergunta quais identidades pertencem ao ponto final. O lookup, get_by_url, escaneou todo o mapa retido sob um bloqueio a cada vez. À medida que esse histórico crescia, atualizar as informações atuais do par se tornava mais caro.Adicionamos um índice do ponto final URL para as identidades associadas a ele: HashMap<Url, HashSet<AgentId>>`. Os registros originais permanecem autoritativos. Quando uma identidade muda de ponto final, o índice a move para o conjunto correspondente; o lookup pode então ir diretamente para esse conjunto. Ambas as versões passaram nas verificações de resultado 19,422 cobrindo atualizações, anúncios obsoletos, remoções e pontos finais compartilhados.
Microseconds per 16-advertisement update call (log scale)Chart data
microseconds per update call
retained identities
Upstream linear scan
Derived URL index
100
6.857
4.911
1,000
18.103
4.774
10,000
154.849
4.72
Cada chamada temporizada acima insere 16 anúncios e executa os ouvintes de controle de acesso. Em 10,000 identidades retidas, caiu de cerca de 155 µs para 4.72 µs, um ganho pareado de 33×. A versão indexada permanece quase plana nesses tamanhos porque esses ouvintes não escaneiam mais o histórico crescente.Medido no API em vez de através do chamador, o lookup em si passa de um escaneamento para um hash. Um miss em 10,000 identidades retidas caiu de 11,423 ns para 45 ns, um ganho pareado de 251×, e um hit único de 9,309 ns para 79 ns, 118×. Uma consulta que retorna muitas identidades ainda escala com o que retorna, então o caso shared-URL ganha 4.4× em vez de 2 ordens de magnitude. O 251× é o resultado isolado do lookup. O 33× mede a chamada de atualização maior que o usa; esses ganhos não podem ser adicionados ou multiplicados.
Requested store memory at 10,000 retained identities, by endpoint shapeChart data
megabytes
Upstream
With the index
Mixed endpoints
1.2
3.71
All shared
1.2
1.74
All URL-less
1.2
1.2
3.10× em uma loja mista, um extra 2.51 MB em 10,000 identidades. Quando cada identidade compartilha um endpoint, o mapa derivado é pequeno e o custo cai para 1.45×. Quando nenhuma identidade tem um URL não há nada a derivar e o custo é zero. Esses números são bytes solicitados pelo alocador para a loja retida, não a memória total do nó.
Com todos os patches 6 juntos, o nó poderia terminar e continuar#
O replay combinado usou os componentes reais de gossip, fetch, peer-access e memory-store do Kitsune2, comunicando-se através de seu transporte de teste em memória. Executamos uma sequência de 5 passos nas árvores de origem e corrigidas:
Atualizar informações do par. Uma identidade muda de ponto final. Informações obsoletas que chegam depois deixam o registro mais recente intacto, e uma identidade bloqueada permanece resolúvel para controle de acesso. Ambas as versões passam; o índice preserva decisões existentes.
Iniciar gossip após a conclusão de um fetch. O nó corrigido inicia após 202 ms. O original segue o caminho de fallback em 3,002 ms.
Receber dados fora de ordem. Operações solicitadas por 3 chegam como respostas 4, incluindo um duplicado. Ambas as versões as armazenam de forma consistente e associam contagens de bytes à operação correta.
Concluir o fetch. As operações armazenadas podem ser recuperadas. O nó corrigido também desperta o ouvinte que registrou enquanto essas operações estavam pendentes.
Continuar após um par falho. Uma falha de envio controlada limpa o estado de solicitação desse par. Outro par fornece a operação, o mapa pendente esvazia, e um fetch subsequente tem sucesso. No original, as verificações de notificação de conclusão falham.
Testes de interação separados combinaram cancelamento com envios concorrentes e agrupamento combinado com notificação de conclusão. Os reparos se mantiveram juntos: trabalho admitido concluído, admissões canceladas não deixaram entradas órfãs, e ouvintes despertaram após a resposta final.Isso nos leva até os dados recebidos no armazenamento do host e o próximo passo de rede pode prosseguir. A validação e integração do Holochain ainda seguem fora deste laboratório. Ainda não medimos quanto essas mudanças reduzem o tempo total de atualização de um nó real.
O índice custa memória e torna as mudanças de endpoint mais caras. No teste de pequeno armazenamento original, atualizar um conjunto fixo de identidades para novos URLs rodou em 0.946× velocidade base com identidades 100 retidas. Em 10,000, um write de mudança de endpoint isolado custou 3.68× tanto. Manter o índice extra tem um preço; as economias de lookup devem reembolsá-lo.Como esse trade-off é real, procuramos a carga de trabalho onde o índice perde. 36 configurações foram pré-determinadas antes da medição, 432 execuções pareadas foram realizadas, e as transcrições de resultados foram hasheadas de forma idêntica entre variantes.
Paired speedup across the 36-config workload envelope, by mix (log scale)Chart data
paired speedup
retained identities
Steady, no rebind
100% rebind
Shared endpoints
URL-less heavy
100
1.61
1.47
1.42
1.09
1,000
7.2
6.26
4.12
2.13
10,000
55.56
36.42
5.55
12.16
Reference line, parity: 1
Cada carga de trabalho nesta matriz mais ampla teve uma mediana acima da paridade, incluindo a mistura onde cada atualização aceita mudou um endpoint: 1.47× em 100 identidades e 36.42× em 10,000. Essas atualizações também acionam lookups de controle de acesso, e suas economias superaram os writes adicionais. Essa carga de trabalho mais ampla difere do teste de conjunto fixo anterior; seu resultado positivo deixa a regressão de pequeno armazenamento registrada.O caso de 100 identidades, principalmente URL-menos permanece inconclusivo: sua mediana 1.09× incluiu um resultado pareado abaixo da paridade. Ainda precisamos de traces de nós reais mostrando contagens de identidade retida, mudanças de endpoint e frequência de lookup antes de recomendar o índice para uma carga de trabalho específica.Envios concorrentes expuseram uma interação de limpeza. Quando os workers puderam sobrepor, a limpeza de um envio falho poderia rodar enquanto outro pedido para o mesmo peer estava em voo. Isso fez a limpeza existente em todo o peer ser mais ampla do que o trabalho que havia falhado. O patch de propriedade reduziu a remoção apenas às entradas ainda pertencentes à geração de pedido atual. Testar os patches juntos importou aqui.A latência de requisição única do batching permanece inconclusiva. Medimos p95, o tempo dentro do qual 95% de requisições terminam. Para uma requisição enviada sozinha, a última execução do worker mostrou 1.01–1.08× baseline p95; a rerun independente mostrou 1.44–1.58×. Esses dispositivos levam dezenas de microssegundos, e seus controles também desviaram. A melhoria em rajadas foi reproduzida, mas o resultado de solicitação única não se estabilizou, então lotes maiores permanecem opt-in.
Comparações repetidas e envios travados permanecem abertos#
Também examinamos 2 outras partes da mesma sequência de catch‑up. Nenhum produziu um patch aceito.Comparando o que os pares já possuem (K4). Gossip usa resumos compactos de dados armazenados para encontrar diferenças. No maior teste, produzir e comparar um snapshot inalterado materializou 262,144 hashes; repetir isso 10 vezes produziu saída idêntica cada vez. Reusar um resumo anterior parece atraente, mas a loja hospedeira pode mudar sem um sinal confiável para invalidar esse resultado salvo. Um resumo em cache poderia então ocultar uma diferença entre pares. As alternativas que avaliamos também ficaram aquém da economia de tempo requerida 20% na loja em memória. Medições de uma loja Holochain real são a próxima peça faltante.Recuperando quando um envio de gossip trava (K2). Ler a fonte levantou uma questão sobre um envio segurando um lock que a tarefa de timeout também precisa. Também precisávamos estabelecer o que acontece com o stream de transporte Iroh subjacente se esse envio for cancelado parcialmente. O provedor de agente bloqueou o trabalho no driver de reprodução, então o teste de runtime nunca foi concluído. Isso permanece uma preocupação não testada, sem patch ou resultado de confiabilidade.
Esses experimentos foram executados no código de desenvolvimento 0.6.0-dev.1, em 1 Apple M4 Max compartilhado com 16 núcleos lógicos. Usamos nós de teste locais e transportes em memória, sem fixação de núcleo ou um segundo host físico. Os resultados cobrem componentes nomeados e suas interações. Throughput de rede, convergência de toda a rede, memória total do nó e latência de aplicação Holochain permanecem não medidos.O método segue meu trabalho sobre benchmark-driven development. Registramos cada pergunta e limiar de aceitação, mantivemos a fonte e os fixtures fixos dentro de cada comparação, e mantivemos comandos, amostras e falhas em recibos apenas-append.Para os 3 reparos, os mesmos testes falharam no código original nas asserções esperadas e passaram com o patch. Para as otimizações, ambas as versões tiveram que preservar o comportamento, então as medições tiveram que atender ao limiar declarado. Execuções de timing alternaram ordem e mantiveram a compilação fora da janela medida. Um agente supervisor reexecutou os comandos de aceitação no código congelado antes de aceitar cada resultado; a discordância sobre latência de solicitação única permaneceu no relatório final.O pacote completo contém 204 diretórios de recibo. Os testes combinados foram aceitos às 12:17 p.m., e os resultados finais foram registrados às 12:20 p.m. Uma verificação final de fechamento foi mantida às 2:38 p.m. Todos os 6 patches estão agora abertos para revisão upstream: completion notification, worker concurrency, admission ownership, response cleanup, request batching, e endpoint indexing.
Por mim. O próprio laboratório: meu método benchmark-driven, apontado para Kitsune2 para descobrir onde ele estava gastando tempo.Por GPT-6 Astra. Os reparos de notificação de conclusão e propriedade de admissão: o diagnóstico, o patch e os testes que falham no código original. Também refinou este artigo a partir dos registros do laboratório.Por GLM-5.3. O reparo de concorrência de trabalhadores, limpeza de respostas e agrupamento, a matriz de carga de configuração 36 para o índice, o perfil de resumo DHT por trás da pergunta aberta K4 e a reprodução combinada de todos os patches 6.Por IA. O índice de endpoint e suas medições, um agente supervisor que reexecutou cada aceitação no código congelado antes que o resultado fosse contado, e o primeiro rascunho deste artigo.Críticas que permaneceram.
De mim: o rascunho explicou os mecanismos internos antes de dizer o que um nó Holochain estava fazendo, e as perguntas abertas 2 chegaram sem conexão com nada. A sequência de recuperação agora introduz cada patch, e cada pergunta aberta fica no passo que lhe interessa.
Da IA: perguntei se a manhã merecia a palavra "breakthroughs". Argumentou que um índice derivado e uma chamada de helper restaurada são correções corretas, não novas técnicas. O artigo é arquivado como uma descoberta.
De GPT-6 Astra: várias reivindicações de rascunho ultrapassaram seus recibos. Ele as manteve no que foi medido: 10.000 identidades retidas são uma história, não 10.000 computadores conectados; o custo de memória do índice permanece no registro; a latência de solicitação única e o caso URL-menos permanecem inconclusivos.