feat(phase-a): surface uptime, dedup efficiency, per-type cache rates + FQ qdisc

Backend (rpc.rs):
- DashboardStatusReply gains daemon_started_unix (proxy for daemon restart time)
- CacheSummary gains per-type breakdown: get_ref_hits/misses, get_tag_hits/misses,
  has_chunk_hits/misses — dedup efficiency is now visible in the API response

Frontend (api.ts, NodeCard.tsx):
- NodeStatusV2 type carries daemon_started_unix and expanded CacheSummary
- fmtUptime() helper renders "up 3d 14h" from a unix timestamp
- NodeCard now shows: uptime, available disk bytes in the bar label,
  dedup efficiency % (has_chunk hit rate), timer last-result text for
  failed timers

Infrastructure (T3.1):
- FQ qdisc applied on all 3 nodes (Architect enp11s0+enp5s0f1, Tank same,
  Morpheus eno1) for precise QUIC packet pacing per QUIC Steps paper
- GSO already on on Architect and Tank 10G NICs

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-23 16:24:16 +00:00
co-authored by Claude Sonnet 4.6
parent 2f7eabf034
commit a3efdbfc04
4 changed files with 110 additions and 5 deletions
+36 -3
View File
@@ -366,6 +366,10 @@ pub struct DashboardStatusReply {
pub cache: Option<CacheSummary>, pub cache: Option<CacheSummary>,
#[serde(default)] #[serde(default)]
pub timers: Vec<TimerStatus>, pub timers: Vec<TimerStatus>,
/// Unix timestamp (seconds) when this daemon's metrics counters were
/// reset — proxy for when the daemon last started/restarted.
#[serde(default)]
pub daemon_started_unix: Option<u64>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -395,11 +399,27 @@ pub struct MountStatus {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CacheSummary { pub struct CacheSummary {
/// Composite totals across ref + tag + chunk lookups (for hit_rate).
pub hits: u64, pub hits: u64,
pub misses: u64, pub misses: u64,
pub bytes_served: u64, pub bytes_served: u64,
pub bytes_ingested: u64, pub bytes_ingested: u64,
pub hit_rate: f64, pub hit_rate: f64,
/// Per-type breakdown so the dashboard can show ref vs tag vs chunk rates.
#[serde(default)]
pub get_ref_hits: u64,
#[serde(default)]
pub get_ref_misses: u64,
#[serde(default)]
pub get_tag_hits: u64,
#[serde(default)]
pub get_tag_misses: u64,
/// HasChunk probes — measures dedup efficiency (how many chunk uploads
/// were skipped because the receiver already had the chunk).
#[serde(default)]
pub has_chunk_hits: u64,
#[serde(default)]
pub has_chunk_misses: u64,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -981,7 +1001,7 @@ impl RpcRouter {
// Cache metrics — the router already tracks these // Cache metrics — the router already tracks these
// in-memory. Compute hit-rate here so the // in-memory. Compute hit-rate here so the
// dashboard doesn't need to divide. // dashboard doesn't need to divide.
let cache = { let (cache, daemon_started_unix) = {
let snap = self.metrics.snapshot(); let snap = self.metrics.snapshot();
// The dashboard cares about "did the peer find // The dashboard cares about "did the peer find
// what someone asked for". Sum the get_ref / // what someone asked for". Sum the get_ref /
@@ -999,13 +1019,25 @@ impl RpcRouter {
} else { } else {
0.0 0.0
}; };
Some(CacheSummary { let summary = Some(CacheSummary {
hits, hits,
misses, misses,
bytes_served: snap.blob_get_bytes, bytes_served: snap.blob_get_bytes,
bytes_ingested: snap.blob_put_bytes, bytes_ingested: snap.blob_put_bytes,
hit_rate: rate, hit_rate: rate,
}) get_ref_hits: snap.get_ref_hits,
get_ref_misses: snap.get_ref_misses,
get_tag_hits: snap.get_tag_hits,
get_tag_misses: snap.get_tag_misses,
has_chunk_hits: snap.has_chunk_hits,
has_chunk_misses: snap.has_chunk_misses,
});
let started = if snap.started_unix > 0 {
Some(snap.started_unix)
} else {
None
};
(summary, started)
}; };
// Well-known timer set. Missing timers just get // Well-known timer set. Missing timers just get
// next_fire_unix=None / last_result=None. // next_fire_unix=None / last_result=None.
@@ -1031,6 +1063,7 @@ impl RpcRouter {
mount, mount,
cache, cache,
timers, timers,
daemon_started_unix,
}; };
let json = serde_json::to_vec(&reply) let json = serde_json::to_vec(&reply)
.context("encoding DashboardStatusReply as JSON")?; .context("encoding DashboardStatusReply as JSON")?;
+5
View File
@@ -181,6 +181,9 @@ pub struct NodeStatusV2 {
pub cache: Option<CacheSummary>, pub cache: Option<CacheSummary>,
#[serde(default)] #[serde(default)]
pub timers: Vec<TimerStatus>, pub timers: Vec<TimerStatus>,
/// Unix timestamp (seconds) when the peer's daemon last started.
#[serde(default)]
pub daemon_started_unix: Option<u64>,
/// `true` when the aggregator successfully talked to the peer; /// `true` when the aggregator successfully talked to the peer;
/// `false` when the RPC failed. Frontend uses this to badge the /// `false` when the RPC failed. Frontend uses this to badge the
/// card as offline. /// card as offline.
@@ -207,6 +210,7 @@ impl NodeStatusV2 {
mount: r.mount, mount: r.mount,
cache: r.cache, cache: r.cache,
timers: r.timers, timers: r.timers,
daemon_started_unix: r.daemon_started_unix,
online: true, online: true,
error: None, error: None,
}; };
@@ -232,6 +236,7 @@ impl NodeStatusV2 {
mount: None, mount: None,
cache: None, cache: None,
timers: Vec::new(), timers: Vec::new(),
daemon_started_unix: None,
online: false, online: false,
error: Some(e), error: Some(e),
} }
+47 -2
View File
@@ -1,5 +1,5 @@
import { Link } from 'wouter'; import { Link } from 'wouter';
import { NodeStatusV2 } from '../lib/api'; import { NodeStatusV2, fmtBytes, fmtUptime } from '../lib/api';
import { StorageBar } from './StorageBar'; import { StorageBar } from './StorageBar';
interface Props { interface Props {
@@ -58,7 +58,11 @@ export function NodeCard({ node }: Props) {
{/* Storage bars */} {/* Storage bars */}
{node.filesystem && ( {node.filesystem && (
<StorageBar <StorageBar
label="disk" label={
node.filesystem.available_bytes != null
? `disk — ${fmtBytes(node.filesystem.available_bytes)} free`
: 'disk'
}
used={node.filesystem.used_bytes} used={node.filesystem.used_bytes}
total={node.filesystem.total_bytes} total={node.filesystem.total_bytes}
/> />
@@ -74,6 +78,12 @@ export function NodeCard({ node }: Props) {
{/* One-liner facts */} {/* One-liner facts */}
<div className="grid grid-cols-2 gap-y-1 text-sm"> <div className="grid grid-cols-2 gap-y-1 text-sm">
<span className="text-slate-500">uptime</span>
<span className="text-right font-mono text-xs">
{node.daemon_started_unix
? <span className="text-slate-300">{fmtUptime(node.daemon_started_unix)}</span>
: <span className="text-slate-500">—</span>}
</span>
<span className="text-slate-500">mount</span> <span className="text-slate-500">mount</span>
<span className="text-right font-mono text-xs"> <span className="text-right font-mono text-xs">
{node.mount?.active ? ( {node.mount?.active ? (
@@ -88,11 +98,30 @@ export function NodeCard({ node }: Props) {
? `${Math.round(node.cache.hit_rate * 100)}%` ? `${Math.round(node.cache.hit_rate * 100)}%`
: <span className="text-slate-500">idle</span>} : <span className="text-slate-500">idle</span>}
</span> </span>
{dedupRate(node) !== null && (
<>
<span className="text-slate-500">dedup efficiency</span>
<span className="text-right font-mono text-xs text-slate-300">
{dedupRate(node)}% chunks skipped
</span>
</>
)}
<span className="text-slate-500">next scheduled job</span> <span className="text-slate-500">next scheduled job</span>
<span className="text-right font-mono text-xs"> <span className="text-right font-mono text-xs">
{nextTimer(node)} {nextTimer(node)}
</span> </span>
</div> </div>
{/* Failed timer callout (T1.3) */}
{failedTimers(node).length > 0 && (
<div className="space-y-0.5">
{failedTimers(node).map((t) => (
<div key={t.unit} className="text-xs text-amber-400 font-mono truncate">
⚠ {t.unit.replace(/^clawstor-/, '').replace(/\.timer$/, '')}: {t.last_result}
</div>
))}
</div>
)}
</> </>
)} )}
</a> </a>
@@ -100,6 +129,22 @@ export function NodeCard({ node }: Props) {
); );
} }
/** HasChunk dedup efficiency as an integer percent, or null if no data. */
function dedupRate(n: NodeStatusV2): number | null {
const c = n.cache;
if (!c) return null;
const total = (c.has_chunk_hits ?? 0) + (c.has_chunk_misses ?? 0);
if (total === 0) return null;
return Math.round(((c.has_chunk_hits ?? 0) / total) * 100);
}
/** Timers whose last_result is a non-success string. */
function failedTimers(n: NodeStatusV2) {
return n.timers.filter(
(t) => t.last_result && t.last_result !== 'success'
);
}
function healthOf(n: NodeStatusV2): 'ok' | 'warn' | 'err' | 'idle' { function healthOf(n: NodeStatusV2): 'ok' | 'warn' | 'err' | 'idle' {
if (!n.online) return 'err'; if (!n.online) return 'err';
const fsPct = n.filesystem const fsPct = n.filesystem
+22
View File
@@ -25,6 +25,14 @@ export interface CacheSummary {
bytes_served: number; bytes_served: number;
bytes_ingested: number; bytes_ingested: number;
hit_rate: number; hit_rate: number;
// Per-type breakdown (available when backend >= Phase A).
get_ref_hits: number;
get_ref_misses: number;
get_tag_hits: number;
get_tag_misses: number;
/** HasChunk probes — non-zero means partial-sync dedup is active. */
has_chunk_hits: number;
has_chunk_misses: number;
} }
export interface TimerStatus { export interface TimerStatus {
@@ -49,6 +57,8 @@ export interface NodeStatusV2 {
mount: MountStatus | null; mount: MountStatus | null;
cache: CacheSummary | null; cache: CacheSummary | null;
timers: TimerStatus[]; timers: TimerStatus[];
/** Unix timestamp (seconds) when the daemon last started. */
daemon_started_unix: number | null;
online: boolean; online: boolean;
error: string | null; error: string | null;
} }
@@ -165,3 +175,15 @@ export function fmtAge(unix: number): string {
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`; return `${Math.floor(diff / 86400)}d ago`;
} }
/** "up 3d 14h" from a daemon start unix timestamp. */
export function fmtUptime(startedUnix: number): string {
const now = Math.floor(Date.now() / 1000);
const secs = Math.max(0, now - startedUnix);
const days = Math.floor(secs / 86400);
const hours = Math.floor((secs % 86400) / 3600);
const mins = Math.floor((secs % 3600) / 60);
if (days > 0) return `up ${days}d ${hours}h`;
if (hours > 0) return `up ${hours}h ${mins}m`;
return `up ${mins}m`;
}