You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Performance/Área sagrada: o relógio do countdown nunca desliga — 10 Hz de re-render de RoomView/Race/RaceTrack durante a corrida inteira (useRoom.ts:411-418) #111
O relógio do countdown é criado uma vez e nunca é desligado quando o countdown acaba —
ele continua batendo a 10 Hz durante a corrida inteira, re-renderizando a árvore da sala
sem que nada mude na tela.
src/lib/useRoom.ts:411-418:
// Tick a clock only while a countdown is pending.conststartMs=room?.start_at ? Date.parse(room.start_at) : 0;useEffect(()=>{if(room?.status!=="racing"||!startMs)return;if(Date.now()>=startMs)return;// guarda só de MONTAGEMconstt=setInterval(()=>setNow(Date.now()),100);return()=>clearInterval(t);},[room?.status,startMs]);// ← nenhuma das duas muda no start
A guarda de :415 só roda quando o efeito roda. As deps são room?.status e startMs:
durante toda a corrida o status permanece "racing" e startMs é o mesmo número, então o
efeito não re-executa e o clearInterval só chega quando o status vira "finished"
(ou no unmount). Entre o fim do countdown e o fim da corrida, o intervalo bate ~10×/s.
O trabalho é 100% desperdício.now (:54) tem um único consumidor — countdownN
(:466-469) — que devolve null assim que now >= startMs. Cada um desses ~10 renders/s
produz uma árvore idêntica à anterior.
E ele alcança a área sagrada (HANDBOOK §2).RoomView não é memo e re-renderiza a cada setNow; dentro dele, durante a corrida, Race (RoomView.tsx:120) e RaceTrack também não são memo (Race.tsx:10, RaceTrack.tsx:17 — só TypingCore, CodeDisplay
e CodeEditor ganharam memo na #105). Ou seja: RoomView + Race + RaceTrack (até 30
linhas de jogador, room.ts:282) reconciliam 10×/s na mesma main thread da textarea.
Não é a #59 nem a fatia 2 dela. A #59 ataca custo proporcional ao nº de jogadores
(mensagens de progresso alheio) e a fatia 2 é width/left → scaleX/translateX em RaceTrack. Este aqui é constante, 10 Hz, independente de quantos jogadores existem —
acontece até numa sala de 1 pessoa. As duas correções são ortogonais.
Quem é atingido: todo mundo que estava presente no countdown, ou seja, todos os
competidores de verdade. Quem entra depois do start cai na guarda de :415 e escapa —
espectador (#64) não paga, competidor paga.
Acceptance criteria
Passado o instante de start_at, nenhum setNow continua agendado enquanto status === "racing". Verificável instrumentando o setter (contador de chamadas).
O countdown visual permanece idêntico: Countdown n conta 3→2→1 com a mesma cadência,
inclusive para quem entra durante o countdown (o caminho de :415 que hoje funciona
não pode regredir).
Sala que termina e volta ao lobby (reset) e depois começa nova corrida volta a contar
o countdown normalmente — o relógio tem de religar, não morrer de vez.
Sem tocar em PROGRESS_THROTTLE_MS, broadcastProgress, maybeFinish ou RACE_DECISION_TICK_MS.
Duas saídas simples, ambas cabendo em poucas linhas de useRoom.ts:
Auto-encerrar o intervalo: no próprio tick, se Date.now() >= startMs, dar um último setNow (para o countdownN virar null) e clearInterval ali mesmo; ou
Trocar por setTimeout encadeado / um timeout único para o instante exato do start,
já que o alvo é conhecido (startMs) e o número de ticks é finito.
Prefira a variante que mantenha a guarda de montagem de :415 intacta (é ela que poupa o
espectador). Cuidado com o caso startMs no passado por relógio dessincronizado: o efeito
não pode virar um loop de setNow.
Classificação: §7.3 normal — só src/lib/useRoom.ts; não toca api/rooms/** nem src/lib/supabase.ts.
Contexto
O relógio do countdown é criado uma vez e nunca é desligado quando o countdown acaba —
ele continua batendo a 10 Hz durante a corrida inteira, re-renderizando a árvore da sala
sem que nada mude na tela.
src/lib/useRoom.ts:411-418:A guarda de
:415só roda quando o efeito roda. As deps sãoroom?.statusestartMs:durante toda a corrida o status permanece
"racing"estartMsé o mesmo número, então oefeito não re-executa e o
clearIntervalsó chega quando o status vira"finished"(ou no unmount). Entre o fim do countdown e o fim da corrida, o intervalo bate ~10×/s.
O trabalho é 100% desperdício.
now(:54) tem um único consumidor —countdownN(
:466-469) — que devolvenullassim quenow >= startMs. Cada um desses ~10 renders/sproduz uma árvore idêntica à anterior.
E ele alcança a área sagrada (HANDBOOK §2).
RoomViewnão é memo e re-renderiza a cadasetNow; dentro dele, durante a corrida,Race(RoomView.tsx:120) eRaceTracktambém não são memo (
Race.tsx:10,RaceTrack.tsx:17— sóTypingCore,CodeDisplaye
CodeEditorganharammemona #105). Ou seja:RoomView+Race+RaceTrack(até 30linhas de jogador,
room.ts:282) reconciliam 10×/s na mesma main thread datextarea.Não é a #59 nem a fatia 2 dela. A #59 ataca custo proporcional ao nº de jogadores
(mensagens de progresso alheio) e a fatia 2 é
width/left→scaleX/translateXemRaceTrack. Este aqui é constante, 10 Hz, independente de quantos jogadores existem —acontece até numa sala de 1 pessoa. As duas correções são ortogonais.
Quem é atingido: todo mundo que estava presente no countdown, ou seja, todos os
competidores de verdade. Quem entra depois do start cai na guarda de
:415e escapa —espectador (#64) não paga, competidor paga.
Acceptance criteria
start_at, nenhumsetNowcontinua agendado enquantostatus === "racing". Verificável instrumentando o setter (contador de chamadas).Countdown nconta 3→2→1 com a mesma cadência,inclusive para quem entra durante o countdown (o caminho de
:415que hoje funcionanão pode regredir).
RoomViewnuma janela de10 s de corrida ociosa (ninguém digitando). Hoje ≈ 100; alvo ≈ 0 vindos do relógio.
reset) e depois começa nova corrida volta a contaro countdown normalmente — o relógio tem de religar, não morrer de vez.
PROGRESS_THROTTLE_MS,broadcastProgress,maybeFinishouRACE_DECISION_TICK_MS.pnpm typecheck && pnpm build,vitest,validate-metrics,validate-persistence.Dica de abordagem
Duas saídas simples, ambas cabendo em poucas linhas de
useRoom.ts:Date.now() >= startMs, dar um últimosetNow(para ocountdownNvirarnull) eclearIntervalali mesmo; ousetTimeoutencadeado / um timeout único para o instante exato do start,já que o alvo é conhecido (
startMs) e o número de ticks é finito.Prefira a variante que mantenha a guarda de montagem de
:415intacta (é ela que poupa oespectador). Cuidado com o caso
startMsno passado por relógio dessincronizado: o efeitonão pode virar um loop de
setNow.Classificação: §7.3 normal — só
src/lib/useRoom.ts; não tocaapi/rooms/**nemsrc/lib/supabase.ts.