◂ Escritos
#001· SysForge· Rust · Linux · /proc· 6 min de leitura

Como calcular uso de CPU no Linux com Rust

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:

console /proc/stat
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:

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):

ΔTotal = total₂ − total₁
ΔIdle  = idle₂ − idle₁
CPU% = 100 · ( 1 − ΔIdle / ΔTotal )

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).

rust cpu/sample.rs
/// 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:

rust cpu/collector.rs
#[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)
    }
}
Duas decisões pequenas que importam

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.

Painel de CPU do SysForge atualizando em tempo real no terminal
O painel de sistema atualizando a cada tick com o valor calculado.

08 O que aprendi

Um número que eu achei que seria uma linha de código virou a base de meia dúzia de coletores. Geralmente é assim.