feat(shipote): CPU% + pipeline live-tail + replay por bytes (fase J)
- CPU% derivado server-side entre samples (WorkspaceState.last_cpu_sample). 100% = 1 core saturado. Primer sample devuelve None (sin baseline). - shipote pipeline run --tail: tras lanzar, suscribe al primer flow_socket y vuelca bytes hasta EOF. Auto-implica --tap. - DiscernPolicy.replay_bytes: cap adicional por bytes para el replay buffer del FlowChannel. evict_for_incoming considera el chunk entrante para que post-push el buffer NUNCA exceda los caps. - shipote-shell: stats history extiende sparkline con %CPU. 80 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 21, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,9 @@ pub struct WorkspaceState {
|
||||
pub root_card: Card,
|
||||
pub commands: HashMap<Ulid, CommandState>,
|
||||
pub started: Instant,
|
||||
/// Última muestra de `(wall_instant, cpu_usec)` usada para calcular
|
||||
/// `cpu_percent` en la próxima medición. None hasta el primer measure.
|
||||
pub last_cpu_sample: Option<(Instant, u64)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -354,9 +357,12 @@ impl WorkspaceManager {
|
||||
/// comandos vivos. Lee `/proc/<pid>/` directamente; si el spec declara
|
||||
/// `soma.cgroup.path`, también intenta el cgroup (más preciso, incluye
|
||||
/// descendants).
|
||||
///
|
||||
/// `cpu_percent` se calcula entre samples consecutivos. Necesita ≥2
|
||||
/// llamadas para tener un valor (la primera siempre retorna `None`).
|
||||
pub async fn workspace_stats(&self, id: WorkspaceId) -> Option<stats::WorkspaceStats> {
|
||||
let g = self.inner.lock().await;
|
||||
let ws = g.workspaces.get(&id)?;
|
||||
let mut g = self.inner.lock().await;
|
||||
let ws = g.workspaces.get_mut(&id)?;
|
||||
let alive: Vec<i32> = ws
|
||||
.commands
|
||||
.values()
|
||||
@@ -367,8 +373,6 @@ impl WorkspaceManager {
|
||||
let cgroup_path = if ws.spec.soma.cgroup.path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// resolve_cgroup_path está en ente_incarnate, pero acá basta
|
||||
// con el path absoluto bajo /sys/fs/cgroup. Resolución gruesa.
|
||||
Some(std::path::PathBuf::from(format!(
|
||||
"/sys/fs/cgroup{}",
|
||||
ws.spec.soma.cgroup.path
|
||||
@@ -376,6 +380,20 @@ impl WorkspaceManager {
|
||||
};
|
||||
let mut s = stats::measure(&alive, cgroup_path.as_deref(), ws.started);
|
||||
s.commands_total = total;
|
||||
|
||||
// CPU%: diff entre el sample actual y el previo, dividido por
|
||||
// wall time. 100% = 1 core saturado. >100% = varios cores.
|
||||
let now = Instant::now();
|
||||
if let Some(cpu_now) = s.cpu_usec {
|
||||
if let Some((prev_t, prev_cpu)) = ws.last_cpu_sample {
|
||||
let dt_us = now.duration_since(prev_t).as_micros() as u64;
|
||||
let d_cpu = cpu_now.saturating_sub(prev_cpu);
|
||||
if dt_us > 0 {
|
||||
s.cpu_percent = Some(100.0 * d_cpu as f32 / dt_us as f32);
|
||||
}
|
||||
}
|
||||
ws.last_cpu_sample = Some((now, cpu_now));
|
||||
}
|
||||
Some(s)
|
||||
}
|
||||
|
||||
@@ -403,6 +421,7 @@ impl WorkspaceManager {
|
||||
root_card: card,
|
||||
commands: HashMap::new(),
|
||||
started: Instant::now(),
|
||||
last_cpu_sample: None,
|
||||
};
|
||||
self.inner.lock().await.workspaces.insert(id, state);
|
||||
info!(%id, ?ttl, "workspace created");
|
||||
|
||||
Reference in New Issue
Block a user