/** * Regulador de qualidade: numa janela, mede o p95 do intervalo entre quadros e o do trabalho de JS de cada quadro, * e sobe/desce degraus de `NIVEIS`. Sem estado no módulo. */ import { NIVEIS, REGULADOR } from "./valores"; export type Regulador = { nivel: number; /** Primeiro nível cujo `pixelRatio` cabe no do aparelho; o regulador nunca sobe acima dele. */ topo: number; /** Intervalos entre quadros da janela atual, em ms. */ intervalos: number[]; /** Tempo de JS de cada quadro da janela atual (lógica + chamada de render), em ms. */ trabalhos: number[]; /** Soma dos intervalos da janela atual, em ms. */ somaDaJanela: number; /** Tempo seguido de janelas com folga, em ms. */ folga: number; /** Soma dos intervalos aceitos desde o início, em ms. */ tempo: number; /** Valor de `tempo` na última subida; null se nunca subiu. */ tempoDaSubida: number | null; /** Desceu logo depois de subir: não sobe mais. */ subidaTravada: boolean; /** Descer não adiantou duas vezes seguidas: o limite é da tela, não da nossa carga. Não muda mais de nível. */ travado: boolean; /** p95 do intervalo na janela que acabou de descer; null se a janela anterior não desceu. */ p95DaDescida: number | null; descidasSemGanho: number; }; const ULTIMO = NIVEIS.length - 1; export function criarRegulador(pixelRatio: number): Regulador { // abaixo do menor `pixelRatio` (zoom de afastar), vale o primeiro degrau com o menor const tela = Math.max(pixelRatio, NIVEIS[ULTIMO].pixelRatio); const topo = NIVEIS.findIndex((n) => n.pixelRatio <= tela); return { nivel: topo, topo, intervalos: [], trabalhos: [], somaDaJanela: 0, folga: 0, tempo: 0, tempoDaSubida: null, subidaTravada: false, travado: false, p95DaDescida: null, descidasSemGanho: 0, }; } export function nivelAtual(r: Regulador): (typeof NIVEIS)[number] { return NIVEIS[r.nivel]; } /** Ordena a própria janela: ela é esvaziada logo depois. */ function p95(quadros: number[]): number { quadros.sort((a, b) => a - b); return quadros[Math.ceil(0.95 * quadros.length) - 1]; } function descer(r: Regulador, intervalo: number): void { r.folga = 0; if (r.nivel >= ULTIMO) return; r.nivel += 1; r.p95DaDescida = intervalo; const subiuHaPouco = r.tempoDaSubida !== null && r.tempo - r.tempoDaSubida < REGULADOR.trava; if (subiuHaPouco) r.subidaTravada = true; } function acumularFolga(r: Regulador, duracao: number): void { r.folga += duracao; if (r.folga < REGULADOR.subida) return; if (!r.subidaTravada && r.nivel > r.topo) { r.nivel -= 1; r.tempoDaSubida = r.tempo; } r.folga = 0; } /** * Na janela seguinte a uma descida: se o intervalo não melhorou `REGULADOR.ganho`, conta uma descida sem ganho; na * segunda seguida, volta os degraus e trava. Devolve true se travou. */ function conferirGanho(r: Regulador, intervalo: number): boolean { const antes = r.p95DaDescida; r.p95DaDescida = null; if (antes === null) return false; if (intervalo <= antes * (1 - REGULADOR.ganho)) { r.descidasSemGanho = 0; return false; } r.descidasSemGanho += 1; if (r.descidasSemGanho < REGULADOR.descidasSemGanho) return false; r.nivel = Math.max(r.topo, r.nivel - r.descidasSemGanho); r.travado = true; return true; } function fecharJanela(r: Regulador): void { const intervalo = p95(r.intervalos); const trabalho = p95(r.trabalhos); const duracao = r.somaDaJanela; r.intervalos.length = 0; r.trabalhos.length = 0; r.somaDaJanela = 0; if (conferirGanho(r, intervalo)) return; if (intervalo > REGULADOR.limite) descer(r, intervalo); else if (trabalho < REGULADOR.folgaDeTrabalho) acumularFolga(r, duracao); else r.folga = 0; } export function amostrar( r: Regulador, intervaloMs: number, trabalhoMs: number, ): number { if (r.travado || intervaloMs > REGULADOR.ignorar) return r.nivel; r.intervalos.push(intervaloMs); r.trabalhos.push(trabalhoMs); r.somaDaJanela += intervaloMs; r.tempo += intervaloMs; if (r.somaDaJanela >= REGULADOR.janela) fecharJanela(r); return r.nivel; }