feat(shipote): throughput card + rate-limit + snapshot incremental (fase Q)
- shipote-shell Flow channels card extiende con bytes_total + bytes/s por socket. Lookup helper evita borrows en closures. - DiscernPolicy.max_bytes_per_sec: splitter task hace sleep proporcional al tamaño de chunk tras cada broadcast. Token-bucket simple v1. - WorkspaceManager.dirty: AtomicBool. mark_dirty() en mutaciones que afectan al snapshot. save_snapshot skip si clean y path existe. restore_snapshot resetea dirty=false (hidratación no es mutation). 85 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 26, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
|
||||
use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
|
||||
use shipote_protocol::{
|
||||
default_socket_path, read_frame, write_frame, CommandInfo, FlowInfo, QuotaReportInfo, Request,
|
||||
Response, WorkspaceStatsInfo, WorkspaceSummary,
|
||||
default_socket_path, read_frame, write_frame, CommandInfo, FlowInfo, FlowThroughputInfo,
|
||||
QuotaReportInfo, Request, Response, WorkspaceStatsInfo, WorkspaceSummary,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -44,6 +44,8 @@ struct Shell {
|
||||
commands: std::collections::BTreeMap<String, Vec<CommandInfo>>,
|
||||
saved_pipelines: Vec<String>,
|
||||
flows: Vec<FlowInfo>,
|
||||
/// Throughput por flow socket (bytes_total + bytes/s).
|
||||
flow_throughput: Vec<FlowThroughputInfo>,
|
||||
/// History de RSS por workspace (últimas N samples).
|
||||
stats_history: std::collections::BTreeMap<String, std::collections::VecDeque<WorkspaceStatsInfo>>,
|
||||
/// Quota report fresco por workspace.
|
||||
@@ -81,6 +83,7 @@ impl Shell {
|
||||
me.commands = snap.commands;
|
||||
me.saved_pipelines = snap.saved_pipelines;
|
||||
me.flows = snap.flows;
|
||||
me.flow_throughput = snap.flow_throughput;
|
||||
me.quotas = snap.quotas;
|
||||
// Hidratar history server-side para workspaces
|
||||
// que no tenían history local (primer probe).
|
||||
@@ -122,6 +125,7 @@ impl Shell {
|
||||
me.commands.clear();
|
||||
me.saved_pipelines.clear();
|
||||
me.flows.clear();
|
||||
me.flow_throughput.clear();
|
||||
me.quotas.clear();
|
||||
me.caps = None;
|
||||
me.recent_log = None;
|
||||
@@ -142,6 +146,7 @@ impl Shell {
|
||||
commands: std::collections::BTreeMap::new(),
|
||||
saved_pipelines: Vec::new(),
|
||||
flows: Vec::new(),
|
||||
flow_throughput: Vec::new(),
|
||||
stats_history: std::collections::BTreeMap::new(),
|
||||
quotas: std::collections::BTreeMap::new(),
|
||||
caps: None,
|
||||
@@ -157,6 +162,7 @@ struct Snapshot {
|
||||
commands: std::collections::BTreeMap<String, Vec<CommandInfo>>,
|
||||
saved_pipelines: Vec<String>,
|
||||
flows: Vec<FlowInfo>,
|
||||
flow_throughput: Vec<FlowThroughputInfo>,
|
||||
/// Stats fresco por workspace (id.toString → stats).
|
||||
fresh_stats: std::collections::BTreeMap<String, WorkspaceStatsInfo>,
|
||||
/// Quota report fresco por workspace.
|
||||
@@ -254,6 +260,17 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
Response::FlowList { items } => items,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
// Throughput per-socket.
|
||||
write_frame(&mut stream, &Request::FlowThroughput)
|
||||
.await
|
||||
.map_err(|e| format!("write throughput: {e}"))?;
|
||||
let resp: Response = read_frame(&mut stream)
|
||||
.await
|
||||
.map_err(|e| format!("read throughput: {e}"))?;
|
||||
let flow_throughput = match resp {
|
||||
Response::FlowThroughput { items } => items,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// Live tail: log del comando más reciente con bytes>0.
|
||||
let recent_log = {
|
||||
@@ -330,6 +347,7 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
commands: commands_map,
|
||||
saved_pipelines,
|
||||
flows,
|
||||
flow_throughput,
|
||||
fresh_stats,
|
||||
quotas,
|
||||
hydrate_history,
|
||||
@@ -509,31 +527,38 @@ impl Render for Shell {
|
||||
"ws_suffix · recurso · uso > limit".to_string()
|
||||
};
|
||||
|
||||
// Flow channels (data plane).
|
||||
// Flow channels (data plane) con throughput.
|
||||
let flow_count: usize = self.flows.iter().map(|f| f.sockets.len()).sum();
|
||||
let flow_items: Vec<String> = self
|
||||
.flows
|
||||
.iter()
|
||||
.flat_map(|f| {
|
||||
let pipe = f.pipeline.to_string();
|
||||
let short = &pipe[pipe.len() - 6..];
|
||||
f.sockets
|
||||
.iter()
|
||||
.map(move |s| {
|
||||
format!(
|
||||
"{short} {}",
|
||||
s.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| s.display().to_string())
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
// Lookup helper que NO captura por ref (evita issue de borrow
|
||||
// en el closure de flat_map).
|
||||
let find_tp = |s: &std::path::PathBuf| -> (f64, f64) {
|
||||
for t in &self.flow_throughput {
|
||||
if t.socket == *s {
|
||||
return (t.bytes_total as f64 / 1024.0, t.bytes_per_sec / 1024.0);
|
||||
}
|
||||
}
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let mut flow_items: Vec<String> = Vec::new();
|
||||
for f in &self.flows {
|
||||
let pipe = f.pipeline.to_string();
|
||||
let short_pipe = &pipe[pipe.len() - 6..];
|
||||
for s in &f.sockets {
|
||||
let name = s
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| s.display().to_string());
|
||||
let (total_kib, rate_kib) = find_tp(s);
|
||||
flow_items.push(format!(
|
||||
"{short_pipe} {:<48} {:>7.1} KiB {:>6.2} KiB/s",
|
||||
name, total_kib, rate_kib
|
||||
));
|
||||
}
|
||||
}
|
||||
let flow_descr = if flow_count == 0 {
|
||||
"pipelines con --tap exponen sockets aquí".to_string()
|
||||
} else {
|
||||
"shipote flow tail <socket> para suscribirse".to_string()
|
||||
"pipe6 · socket · total · rate".to_string()
|
||||
};
|
||||
|
||||
let body = div()
|
||||
|
||||
Reference in New Issue
Block a user