perf(race): corta re-render de CodeDisplay/CodeEditor por progresso alheio (#59, fatia 1) - #105
Conversation
…lheio (#59) Durante a corrida, cada mensagem de progresso de qualquer adversario invalidava a arvore inteira da sala ate a textarea: setProgress -> useMemo(players) -> compatRoom -> <Race> -> TypingCore -> CodeDisplay + CodeEditor. Com 6 jogadores a 120ms sao ~50 re-renders/s competindo com o input do jogador — o custo de digitar crescia com o numero de gente na sala (area sagrada, HANDBOOK §2). Fatia 1 de 2 (plano do Conselho): cortar a propagacao, sem tocar em PROGRESS_THROTTLE_MS, broadcastProgress, sanitizeResults ou no caminho do finish. 1. useRoom: `abandon` dependia de `progress`, ou seja ganhava identidade nova a cada mensagem de adversario e invalidava qualquer memo rio abaixo. Agora a ultima mensagem propria vive em `myLastMsgRef` (preenchido onde ela ja e construida, dentro de broadcastProgress) e `abandon` le do ref — deps `[broadcastProgress]`, identidade estavel. O ref e zerado nos mesmos pontos em que `setProgress({})` zera o estado, para nao carregar valor velho entre rodadas. 2. React.memo em TypingCore (corta o trafego de adversario na raiz), CodeDisplay e CodeEditor (cortam tambem o tick de 200ms do relogio, que re-renderizava os dois 5x/s ate numa corrida solo). RaceTrack e FloatingChat continuam sem memo de proposito: `players` muda legitimamente a cada progresso e a barra TEM de andar — memo ali nao mediria nada. Fica para a fatia 2 (pista fora do layout). Refs #59
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🩺 Quórum adversarial (HANDBOOK §7.2) — 3 APROVA / 0 VETO ✅ mergeadaClassifiquei como quórum por julgamento, não por lista: o diff é pequeno (+33/−8) e fora das
Conferi eu mesmo os dois pontos que sustentam tudo, em vez de aceitar o voto: (a) só existem
Ressalvas registradas (não bloqueantes, para a fatia 2):
|
Contexto
Durante a corrida, cada mensagem de progresso de qualquer adversário invalidava a árvore
inteira da sala até a
textarea:useRoom.setProgress→useMemo(players)→RoomView.compatRoom→<Race>→TypingCore→CodeDisplay+CodeEditor. Com 6 jogadores a 120ms são ~50 re-renders/s competindo com oinput — o custo de digitar crescia com o número de gente na sala (área sagrada, HANDBOOK §2).
Esta é a fatia 1 de 2 do plano do 🏛️ Parecer do Conselho na #59 — segui o plano na íntegra,
inclusive a correção de premissa do item 1 (ver abaixo), que a medição confirmou ser decisiva.
O que mudou e por quê
src/lib/useRoom.ts—abandonvirou estável. Ele dependia deprogress, o estado quemuda a cada mensagem de adversário: ganhava identidade nova a cada broadcast e descia até
CodeEditor(onAbandon), invalidando qualquermemorio abaixo. Agora a última mensagemprópria vive num
myLastMsgRef, preenchido onde ela já é construída (dentro debroadcastProgress), eabandonlê do ref → deps[broadcastProgress].O ref é zerado nos mesmos dois pontos em que o efeito de reset faz
setProgress({}), paranão carregar valor velho entre rodadas — a semântica anterior (
progress[meId]) é preservadaexatamente, porque aquele mapa só era escrito com essa mesma mensagem.
React.memoemTypingCore— corta o tráfego de adversário na raiz.<Race>continuare-renderizando a cada mensagem (a pista TEM de andar), mas as props do núcleo
(
code/language/startedAt/finishedAt/onProgress/onAbandon) são estáveis na corrida.React.memoemCodeDisplayeCodeEditor— cortam também o segundo emissor que aissue não mapeava: o
setInterval(200ms)do relógio do próprioTypingCore, que re-renderizavaos dois 5×/s até numa corrida solo.
CodeDisplayreconstrói um<span>por caractere; é oitem caro da conta.
RaceTrackeFloatingChatficaram de fora de propósito (o Conselho pediu que não fossemvendidos como vitória):
playersmuda legitimamente a cada progresso, entãomemoali nãomediria nada. É fatia 2.
Medição (AC 2)
Harness de dev temporário (removido antes do commit, §11 da
cr-fleet-ops) que reproduz acadeia de identidade da produção — cópia literal do
useMemodeplayerse docompatRoom—com 4 adversários e contadores de render por componente. Como o Browser pane headless estrangula
setIntervalpara ~1 Hz em aba de fundo, a métrica é determinística por mensagem (flushSync,1 commit por mensagem) em vez de por janela de tempo — imune ao throttling. Números brutos
(StrictMode do dev conta 2 renders por render lógico; o fator é o mesmo nos três cenários):
200 mensagens de progresso alheio:
origin/mainmemo(comabandoninstável)memo+abandonestável)O cenário B é a confirmação empírica do achado do Conselho:
memosozinho não mordeCodeEditor/TypingCoreenquantoabandonfor instável. Repetido com N=200 e N=400 em regimepermanente:
CodeDisplay/CodeEditor/TypingCore= 0 nos dois. Queda de 100% (meta era ≥90%).Janela ociosa de 12s, sem tráfego de adversário (só o tick de 200ms + heartbeat):
TypingCore24 ·CodeDisplay0 ·CodeEditor0 (antes acompanhavam oTypingCore1:1).Validação (gate §6, resultado real)
pnpm install --frozen-lockfile✅ ·pnpm typecheck✅ ·pnpm build✅ (rota/harness/perf59ausente do output — o harness temporário saiu mesmo)
node .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs→ 45/45 ✅node scripts/validate-persistence.mjs→ 72/72 ✅pnpm test(vitest) → 109/109 ✅pnpm lint= N/A (não há config ESLint no repo; a CI também não roda lint)Riscos
abandon(state → ref). Se algum caminho novoescrevesse
progress[meId]sem passar porbroadcastProgress, o ref divergiria; hoje nãoexiste esse caminho (o único write do meu id é a linha otimista do próprio
broadcastProgress).O reset entre rodadas foi coberto explicitamente.
PROGRESS_THROTTLE_MS,broadcastProgress,sanitizeResultse ocaminho do
finishestão intactos. O coalescing desetProgress(item opcional da issue) foideliberadamente deixado fora — o Conselho apontou que um flush que engolisse a última
mensagem de quem terminou prenderia a sala em
racing.memoemTypingCoretambém vale para o modo Practice (props idem estáveis).Não verificado (honestidade, §11)
Corrida real com 2+ clientes e Realtime ao vivo é inalcançável neste ambiente: a medição é
local com broadcast stubado, sobre uma réplica literal da cadeia de identidade da produção,
não sobre o
useRoomconectado. Confirmação visual em produção continua sendo do dono.AC não atendidos (por isso
Refs, nãoCloses)RaceTracknemFloatingChat":insatisfazível nesta fatia e, na verdade, incorreto como alvo — cada tecla atualiza o meu
progresso otimista (
useRoom.ts:508, fora do throttle de rede), então a minha barra tem deandar. Re-render da pista por tecla é comportamento correto.
layout (
width/left→scaleX/translateXemRaceTrack.tsx:114-129) + coalescing desetProgresscom flush garantido da mensagem final. E uma P3 separada paraMotionConfig reducedMotion="user"(gap pré-existente que o parecer registrou; não entrou aqui).Refs #59