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:
@@ -103,6 +103,10 @@ enum PipeCmd {
|
||||
/// Variables `KEY=VALUE` para sustitución `${KEY}` en el spec.
|
||||
#[arg(long = "var", value_parser = parse_kv)]
|
||||
vars: Vec<(String, String)>,
|
||||
/// Tras lanzar, suscribir al primer flow socket y volcar bytes
|
||||
/// a stdout hasta EOF. Implica `--tap`.
|
||||
#[arg(long)]
|
||||
tail: bool,
|
||||
},
|
||||
/// Guardar un pipeline bajo un nombre (persiste con el snapshot).
|
||||
Save {
|
||||
@@ -130,6 +134,8 @@ enum PipeCmd {
|
||||
tap: bool,
|
||||
#[arg(long = "var", value_parser = parse_kv)]
|
||||
vars: Vec<(String, String)>,
|
||||
#[arg(long)]
|
||||
tail: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -243,9 +249,14 @@ async fn main() -> Result<()> {
|
||||
.cpu_usec
|
||||
.map(|u| format!("{:.3} s", u as f64 / 1_000_000.0))
|
||||
.unwrap_or_else(|| "—".into());
|
||||
let cpu_pct = info
|
||||
.cpu_percent
|
||||
.map(|p| format!("{p:.1} %"))
|
||||
.unwrap_or_else(|| "— (esperando 2do sample)".into());
|
||||
println!("rss: {rss}");
|
||||
println!("rss_peak: {peak}");
|
||||
println!("cpu: {cpu}");
|
||||
println!("cpu_pct: {cpu_pct}");
|
||||
println!("source: {}", info.source);
|
||||
println!("uptime: {} ms", info.uptime_ms);
|
||||
}
|
||||
@@ -287,18 +298,28 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Pipeline(PipeCmd::Run { spec, tap, vars }) => {
|
||||
Cmd::Pipeline(PipeCmd::Run { spec, tap, vars, tail }) => {
|
||||
let p = load_pipeline_spec(&spec).with_context(|| format!("load {}", spec.display()))?;
|
||||
// --tail implica --tap (no hay flow socket sin tap).
|
||||
let effective_tap = tap || tail;
|
||||
let resp = round_trip(
|
||||
&mut stream,
|
||||
Request::PipelineRun {
|
||||
spec: p,
|
||||
tap,
|
||||
tap: effective_tap,
|
||||
vars: vars.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
print_pipeline_started(resp)?;
|
||||
let socket = print_pipeline_started_returning_socket(resp)?;
|
||||
if tail {
|
||||
if let Some(sock) = socket {
|
||||
eprintln!("--- tailing {} ---", sock.display());
|
||||
tail_socket(&sock).await?;
|
||||
} else {
|
||||
eprintln!("--tail: no hay flow socket disponible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Pipeline(PipeCmd::Save { name, spec }) => {
|
||||
@@ -351,17 +372,26 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Pipeline(PipeCmd::RunSaved { name, tap, vars }) => {
|
||||
Cmd::Pipeline(PipeCmd::RunSaved { name, tap, vars, tail }) => {
|
||||
let effective_tap = tap || tail;
|
||||
let resp = round_trip(
|
||||
&mut stream,
|
||||
Request::PipelineRunSaved {
|
||||
name,
|
||||
tap,
|
||||
tap: effective_tap,
|
||||
vars: vars.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
print_pipeline_started(resp)?;
|
||||
let socket = print_pipeline_started_returning_socket(resp)?;
|
||||
if tail {
|
||||
if let Some(sock) = socket {
|
||||
eprintln!("--- tailing {} ---", sock.display());
|
||||
tail_socket(&sock).await?;
|
||||
} else {
|
||||
eprintln!("--tail: no hay flow socket disponible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Commands { workspace } => {
|
||||
@@ -511,29 +541,55 @@ fn print_unexpected(r: &Response) {
|
||||
eprintln!("unexpected response: {r:?}");
|
||||
}
|
||||
|
||||
fn print_pipeline_started(resp: Response) -> Result<()> {
|
||||
/// Imprime el resultado del launch del pipeline y retorna el path del
|
||||
/// primer flow socket (si hay), útil para `--tail`.
|
||||
fn print_pipeline_started_returning_socket(resp: Response) -> Result<Option<PathBuf>> {
|
||||
match resp {
|
||||
Response::PipelineStarted { pipeline, command_pids, edges } => {
|
||||
println!("pipeline {pipeline}");
|
||||
for (label, pid) in command_pids {
|
||||
println!(" {:<20} pid={pid}", label);
|
||||
}
|
||||
let mut first_socket: Option<PathBuf> = None;
|
||||
if !edges.is_empty() {
|
||||
println!("edges:");
|
||||
for e in edges {
|
||||
for e in &edges {
|
||||
println!(
|
||||
" {}.{} → {}.{} ty={:?} mime={:?} conf={:.2}",
|
||||
e.from_label, e.from_output, e.to_label, e.to_input,
|
||||
e.ty, e.mime, e.confidence,
|
||||
);
|
||||
if first_socket.is_none() {
|
||||
first_socket = e.flow_socket.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(first_socket)
|
||||
}
|
||||
Response::Error { message } => Err(anyhow!(message)),
|
||||
other => {
|
||||
print_unexpected(&other);
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn tail_socket(socket: &std::path::Path) -> Result<()> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
// Pequeña ventana de retry — el daemon retiene el flow channel
|
||||
// antes de retornar, así que en la práctica ya está bindeado.
|
||||
let mut s = UnixStream::connect(socket)
|
||||
.await
|
||||
.with_context(|| format!("connect {}", socket.display()))?;
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
let n = s.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().write_all(&buf[..n]);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -344,6 +344,7 @@ async fn dispatch(
|
||||
rss_bytes: s.rss_bytes,
|
||||
rss_peak_bytes: s.rss_peak_bytes,
|
||||
cpu_usec: s.cpu_usec,
|
||||
cpu_percent: s.cpu_percent,
|
||||
source: s.source,
|
||||
uptime_ms: s.uptime_ms,
|
||||
},
|
||||
|
||||
@@ -389,19 +389,24 @@ impl Render for Shell {
|
||||
.map(|h| h.iter().map(|s| s.rss_bytes.unwrap_or(0)).collect())
|
||||
.unwrap_or_default();
|
||||
let spark = sparkline(&rss_series, STATS_HISTORY_LEN);
|
||||
let (rss_now, peak) = history
|
||||
.and_then(|h| h.back())
|
||||
.map(|s| (s.rss_bytes.unwrap_or(0), s.rss_peak_bytes.unwrap_or(0)))
|
||||
.unwrap_or((0, 0));
|
||||
let latest = history.and_then(|h| h.back());
|
||||
let (rss_now, peak, cpu_pct) = latest
|
||||
.map(|s| (
|
||||
s.rss_bytes.unwrap_or(0),
|
||||
s.rss_peak_bytes.unwrap_or(0),
|
||||
s.cpu_percent.unwrap_or(0.0),
|
||||
))
|
||||
.unwrap_or((0, 0, 0.0));
|
||||
let rss_mb = rss_now as f64 / 1024.0 / 1024.0;
|
||||
let peak_mb = peak as f64 / 1024.0 / 1024.0;
|
||||
format!(
|
||||
"{:<14} {:<14} {} {:>6.1}M peak {:>6.1}M",
|
||||
"{:<14} {:<14} {} {:>6.1}M peak {:>6.1}M {:>5.1}%cpu",
|
||||
&w.id.to_string()[20..],
|
||||
w.label,
|
||||
spark,
|
||||
rss_mb,
|
||||
peak_mb,
|
||||
cpu_pct,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user