Parecia a parte mais simples do SysForge ↗. Mostrar um número: quanto da CPU está sendo usada agora. Levei três tentativas erradas até entender uma coisa desconfortável, esse número não existe pronto em lugar nenhum.
01 O problema
O SysForge é um dashboard de terminal (TUI) em Rust que junta sistema, Docker,
Git e Kubernetes num painel só. O primeiro painel era o de sistema, e ele
precisava de algo óbvio: uma barra mostrando o uso da CPU em tempo real,
atualizando a cada segundo, igual ao htop ou ao btop.
Minha suposição inicial: deve existir um arquivo, ou uma syscall, que me
devolve CPU: 37%. Foi por aí que comecei, e foi por aí que errei.
02 Minha primeira ideia
Achei que em algum canto de /proc haveria o valor pronto. Fui direto
no /proc/stat, olhei a primeira linha e imaginei que aqueles números
já eram, de algum jeito, a porcentagem atual:
cpu 4705 150 2340 136239 1234 0 567 0 0 0
user nice sys idle iowait irq softirq ...
Cada coluna é um tempo acumulado em jiffies (a unidade de tempo do kernel,
USER_HZ, quase sempre 100 por segundo): tempo em código de usuário,
em processos com nice, em kernel, idle (ocioso), esperando I/O, e por aí vai.
Meu plano ingênuo: somar tudo, tirar a proporção do idle, pronto.
Rodei uma vez: deu 4% numa máquina que claramente estava fazendo mais que isso. Rodei de novo: 4% de novo. E de novo. Sempre o mesmo valor, independente do que a máquina estivesse fazendo. Algo estava errado no meu modelo mental.
03 O que descobri
Esses números não são o uso agora. São contadores acumulados
desde que a máquina ligou. Cada campo conta quantos jiffies a CPU passou naquele
estado desde o boot, o idle sozinho já eram centenas de milhares.
Por isso o meu idle / total dava sempre quase a mesma coisa: eu não
estava medindo o instante, estava medindo a média da vida inteira
da máquina. Um número que praticamente não se move de um segundo pro outro.
Descobri também dois detalhes que importam pro cálculo:
- O tempo ocioso “de verdade” é
idle + iowait, esperar o disco também conta como CPU parada. - O total é a soma de todos os campos daquela linha, não só de alguns.
04 O erro
Um snapshot sozinho não diz nada sobre o presente. É como olhar o hodômetro do carro uma vez e tentar descobrir a velocidade atual: o número está lá, mas velocidade é distância dividida por tempo. Com uma leitura só, você tem a distância total da vida do carro, não a velocidade de agora.
Uso de CPU é a mesma coisa: é uma derivada. Você precisa de duas leituras e do que mudou entre elas.
05 A solução
A porcentagem de uso é a variação entre duas amostras tiradas com um intervalo (no SysForge, um tick):
Em palavras: no intervalo entre as duas leituras, que fração do tempo a CPU não ficou ociosa. Se entre um segundo e outro o total subiu 1000 jiffies e o idle subiu 700, a CPU trabalhou em 30% do tempo. Esse número se move, é o que a barra precisa.
06 Implementação
No SysForge eu separo isso em structs com uma responsabilidade cada, um padrão
que acabei reusando depois em rede, disco e processos. Aqui bastam duas:
CpuSample (os dados crus de uma leitura) e CpuCollector
(que guarda a amostra anterior e calcula o delta).
/// Uma leitura crua da primeira linha de /proc/stat.
struct CpuSample {
idle: u64, // idle + iowait, em jiffies
total: u64, // soma de todos os campos
}
impl CpuSample {
fn read() -> std::io::Result<Self> {
let stat = std::fs::read_to_string("/proc/stat")?;
// a primeira linha é a CPU agregada: "cpu 4705 150 ..."
let line = stat.lines().next().unwrap_or_default();
let values: Vec<u64> = line
.split_whitespace()
.skip(1) // pula o rótulo "cpu"
.filter_map(|v| v.parse().ok())
.collect();
// idle (col 3) + iowait (col 4), com fallback se faltar campo
let idle = values.get(3).copied().unwrap_or(0)
+ values.get(4).copied().unwrap_or(0);
let total: u64 = values.iter().sum();
Ok(CpuSample { idle, total })
}
}
O coletor guarda a leitura anterior e, a cada chamada, devolve a porcentagem calculada com o delta:
#[derive(Default)]
struct CpuCollector {
previous: Option<CpuSample>,
}
impl CpuCollector {
/// Retorna Some(uso%) quando já há duas amostras; None na primeira vez.
fn sample(&mut self) -> std::io::Result<Option<f64>> {
let current = CpuSample::read()?;
let usage = self.previous.as_ref().map(|prev| {
let delta_idle = current.idle.saturating_sub(prev.idle) as f64;
let delta_total = current.total.saturating_sub(prev.total) as f64;
if delta_total == 0.0 {
0.0 // nada mudou entre as amostras
} else {
100.0 * (1.0 - delta_idle / delta_total)
}
});
self.previous = Some(current); // guarda pra próxima rodada
Ok(usage)
}
}
saturating_sub: os contadores só crescem, mas isso me protege de
qualquer leitura estranha virar um número negativo gigante ao dar wrap no
u64. E o None na primeira chamada: sem amostra anterior
não há delta, então a UI mostra — até a segunda leitura chegar,
em vez de dividir por zero.
07 Resultado
Com o CpuCollector guardando a amostra anterior, cada tick vira uma
porcentagem real, e a barra do SysForge finalmente respira junto com a máquina.
08 O que aprendi
- Muita métrica de sistema é um contador acumulado, não um valor instantâneo. “Uso agora” quase sempre é uma derivada: (valor₂ − valor₁) dividido pelo intervalo.
- Ler
/procna mão, em vez de puxar uma crate comosysinfo, me obrigou a entender o que cada número significa, e esse entendimento se pagou: o mesmo “duas amostras + delta” reapareceu no throughput de rede e nas taxas de I/O de disco. - Separar dado cru → estado que calcula o delta → dado pronto pra tela deixou o resto do SysForge muito mais fácil de crescer.
Um número que eu achei que seria uma linha de código virou a base de meia dúzia de coletores. Geralmente é assim.