Skip to content

Commit 8b95c16

Browse files
authored
Merge pull request #162 from httpsphl/perf/idle-cpu
perf(app): mascots stand still while another app has the focus
2 parents e7eb35f + 000f10f commit 8b95c16

4 files changed

Lines changed: 35 additions & 2 deletions

File tree

‎app/src/paused.css‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@
88
animation-play-state: paused !important;
99
}
1010
/* biome-ignore-end lint/complexity/noImportantStyles: a hidden window must pause every animation, inline ones included */
11+
12+
/* The owner works in another app (shell/visibility.ts): the mascots and the
13+
working trace, drawn by the CPU on every frame, stand still. Spinners,
14+
live dots and what arrives keep moving: they are cheap and say something
15+
happens. */
16+
:root[data-blurred] [data-mood] *,
17+
:root[data-blurred] .state-trace {
18+
animation-play-state: paused;
19+
}

‎app/src/shell/visibility.test.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ describe("window visibility", () => {
2929
expect(document.documentElement.hasAttribute("data-hidden")).toBe(false);
3030
});
3131

32+
test("the mascots stand still while another app has the focus", () => {
33+
const focus = vi.spyOn(document, "hasFocus").mockReturnValue(false);
34+
window.dispatchEvent(new Event("blur"));
35+
expect(document.documentElement.hasAttribute("data-blurred")).toBe(true);
36+
focus.mockReturnValue(true);
37+
window.dispatchEvent(new Event("focus"));
38+
expect(document.documentElement.hasAttribute("data-blurred")).toBe(false);
39+
focus.mockRestore();
40+
});
41+
3242
test("the status is not polled while the window is hidden, and is read when it comes back", async () => {
3343
vi.useFakeTimers();
3444
const fake = new FakeBotloft();

‎app/src/shell/visibility.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,25 @@ export function onVisibility(listener: () => void): () => void {
3636
return () => listeners.delete(listener);
3737
}
3838

39+
/**
40+
* Whether the window has the focus: while the owner works in another app,
41+
* the mascots stand still (paused.css), so a Botloft left open in the
42+
* background costs next to nothing.
43+
*/
44+
function focusChanged(): void {
45+
document.documentElement.toggleAttribute("data-blurred", !document.hasFocus());
46+
}
47+
3948
/** Starts following the window; once, before the app renders. */
4049
export function watchVisibility(): void {
4150
document.addEventListener("visibilitychange", changed);
42-
window.addEventListener("focus", () => markHidden(false));
51+
window.addEventListener("focus", () => {
52+
markHidden(false);
53+
focusChanged();
54+
});
55+
window.addEventListener("blur", focusChanged);
4356
changed();
57+
focusChanged();
4458
}
4559

4660
export function useWindowVisible(): boolean {

‎docs/spec.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ app/src/
808808

809809
Componentes dependem só de `BotloftApi` e `Host`, nunca do cliente concreto nem do Tauri.
810810

811-
O store recarrega crews, bots e `system.status` a cada (re)conexão e depois segue as notificações. `system.status` não tem notificação e é relido a cada 15 s, menos com a janela fora de vista (minimizada, escondida perto do relógio ou aberta escondida no login do Windows): aí para, e é relido quando ela volta. Fora de vista, as animações também param onde estão (`paused.css`) e continuam quando a janela volta. O Windows pode não avisar a página quando o próprio app esconde a janela, então o app marca isso ele mesmo (`shell/visibility.ts`), e a janela voltar à frente desmarca.
811+
O store recarrega crews, bots e `system.status` a cada (re)conexão e depois segue as notificações. `system.status` não tem notificação e é relido a cada 15 s, menos com a janela fora de vista (minimizada, escondida perto do relógio ou aberta escondida no login do Windows): aí para, e é relido quando ela volta. Fora de vista, as animações também param onde estão (`paused.css`) e continuam quando a janela volta. O Windows pode não avisar a página quando o próprio app esconde a janela, então o app marca isso ele mesmo (`shell/visibility.ts`), e a janela voltar à frente desmarca. Com a janela à vista mas sem o foco (o dono trabalha em outro app), os mascotes e o traço de "trabalhando" param onde estão, porque o WebView os redesenha na CPU a cada quadro: um Botloft aberto atrás de outras janelas custa quase nada. Spinners, pontos ao vivo e o que chega continuam. Medido no WebView2 com bots parados: cerca de 1 núcleo com os mascotes animando, 0,01 com eles parados. Enquanto alguma animação roda, o WebView produz um quadro por atualização da tela (180 Hz no monitor do dono), então limitar os quadros de cada animação (`steps`) não muda o custo; parar é o que muda.
812812

813813
O chat e as mensagens de uma equipe guardam todas as páginas já carregadas. As linhas fora de vista não são diagramadas nem pintadas até chegarem perto da tela (`content-visibility: auto`, em `offscreen.css`), e guardam o tamanho que tiveram para a barra de rolagem não pular. As 8 mais novas ficam de fora: é onde os cartões chegam com o brilho de atenção, que a contenção cortaria.
814814

0 commit comments

Comments
 (0)