Skip to content

Commit 1bf5033

Browse files
author
pseusys
committed
better plots
1 parent f46e994 commit 1bf5033

13 files changed

Lines changed: 182 additions & 36 deletions

File tree

evaluation/src/typhoon_eval/flow_plot.py

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,24 +52,35 @@
5252
}
5353

5454

55-
def _parse_lines(lines: list[str]) -> list[dict]:
56-
"""Extract capture JSONL records from env_logger output lines."""
57-
records = []
55+
def _parse_lines(lines: list[str]) -> tuple[list[dict], list[dict]]:
56+
"""
57+
Extract capture JSONL records from env_logger output lines.
58+
59+
Returns (packet_records, config_records) separated by ``kind``.
60+
Config records (kind="Config") carry flow configuration metadata;
61+
all other records are per-packet measurements.
62+
"""
63+
packets: list[dict] = []
64+
configs: list[dict] = []
5865
for line in lines:
5966
if "typhoon::capture" not in line:
6067
continue
6168
brace = line.find("{")
6269
if brace == -1:
6370
continue
6471
try:
65-
records.append(json.loads(line[brace:]))
72+
rec = json.loads(line[brace:])
6673
except json.JSONDecodeError:
67-
pass
68-
return records
74+
continue
75+
if rec.get("kind") == "Config":
76+
configs.append(rec)
77+
else:
78+
packets.append(rec)
79+
return packets, configs
6980

7081

71-
def _run_example(example: str, typhoon_dir: Path, timeout: int) -> list[dict]:
72-
"""Build and run a TYPHOON example with capture logging; return parsed records."""
82+
def _run_example(example: str, typhoon_dir: Path, timeout: int) -> tuple[list[dict], list[dict]]:
83+
"""Build and run a TYPHOON example with capture logging; return (packets, configs)."""
7384
env = {**os.environ, "RUST_LOG": "typhoon::capture=trace"}
7485
try:
7586
result = subprocess.run(
@@ -229,15 +240,57 @@ def _compute_xpos(timestamps: list[int]) -> np.ndarray:
229240
return xpos
230241

231242

232-
def _subplot_title(c2s_addr: str | None, s2c_addr: str | None) -> str:
243+
def _config_annotation(
244+
c2s_addr: str | None,
245+
s2c_addr: str | None,
246+
configs: list[dict],
247+
) -> str:
248+
"""
249+
Build a compact config annotation string for a subplot.
250+
251+
Looks up Config records matching each flow address and direction,
252+
then formats: ``body=… header=…B decoy=…`` per direction.
253+
Returns an empty string if no config records are found.
254+
"""
255+
by_key: dict[tuple[str, str], dict] = {
256+
(r.get("flow", ""), r.get("dir", "")): r for r in configs
257+
}
258+
259+
def _fmt(addr: str | None, direction: str) -> str:
260+
if addr is None:
261+
return ""
262+
rec = by_key.get((addr, direction))
263+
if rec is None:
264+
return ""
265+
body = rec.get("body_mode", "?")
266+
hdr = rec.get("header_len", "?")
267+
decoy = rec.get("decoy", "?")
268+
return f"{direction}: body={body} header={hdr}B decoy={decoy}"
269+
270+
parts = [p for p in (_fmt(c2s_addr, "c2s"), _fmt(s2c_addr, "s2c")) if p]
271+
return "\n".join(parts)
272+
273+
274+
def _subplot_title(
275+
c2s_addr: str | None,
276+
s2c_addr: str | None,
277+
configs: list[dict] | None = None,
278+
) -> str:
233279
if c2s_addr == s2c_addr:
234-
return c2s_addr or "unknown"
235-
parts = []
236-
if c2s_addr:
237-
parts.append(f"→ {c2s_addr} (c2s)")
238-
if s2c_addr:
239-
parts.append(f"← {s2c_addr} (s2c)")
240-
return " ".join(parts)
280+
base = c2s_addr or "unknown"
281+
else:
282+
parts = []
283+
if c2s_addr:
284+
parts.append(f"→ {c2s_addr} (c2s)")
285+
if s2c_addr:
286+
parts.append(f"← {s2c_addr} (s2c)")
287+
base = " ".join(parts)
288+
289+
if configs:
290+
annotation = _config_annotation(c2s_addr, s2c_addr, configs)
291+
if annotation:
292+
return f"{base}\n{annotation}"
293+
return base
241294

242295

243296
def _draw_bars(ax, times_or_xpos, values_by_direction: dict[str, dict[int | float, dict]]) -> None:
@@ -279,6 +332,7 @@ def _plot_all(
279332
out_dir: Path,
280333
bucket_ms: int,
281334
name: str,
335+
configs: list[dict] | None = None,
282336
) -> None:
283337
"""Render all paired flows as bucketed stacked-bar subplots in a single PNG."""
284338
pairs = [(c, s, b) for c, s, b in pairs if b]
@@ -309,7 +363,7 @@ def _empty_dirs() -> dict:
309363
ax.axhline(0, color="black", linewidth=0.8)
310364
ax.set_xlabel(f"Time ({bucket_ms} ms buckets)")
311365
ax.set_ylabel("Bytes")
312-
ax.set_title(_subplot_title(c2s_addr, s2c_addr))
366+
ax.set_title(_subplot_title(c2s_addr, s2c_addr, configs), fontsize=8)
313367
ax.legend(handles=component_patches + direction_patches, loc="upper right", fontsize=8)
314368

315369
fig.suptitle(name, fontsize=13, fontweight="bold")
@@ -326,6 +380,7 @@ def _plot_all_per_packet(
326380
pairs_with_records: list[tuple[str | None, str | None, list[dict]]],
327381
out_dir: Path,
328382
name: str,
383+
configs: list[dict] | None = None,
329384
) -> None:
330385
"""
331386
Render all paired flows as per-packet stacked-bar subplots in a single PNG.
@@ -359,7 +414,7 @@ def _plot_all_per_packet(
359414

360415
ax.axhline(0, color="black", linewidth=0.8)
361416
ax.set_ylabel("Bytes")
362-
ax.set_title(_subplot_title(c2s_addr, s2c_addr))
417+
ax.set_title(_subplot_title(c2s_addr, s2c_addr, configs), fontsize=8)
363418
ax.legend(handles=component_patches + direction_patches, loc="upper right", fontsize=8)
364419

365420
# Place ~10 time-reference ticks labelled with ms-since-start.
@@ -399,13 +454,13 @@ def main(example: str, log_file: str, out_dir: str, typhoon_dir: str, timeout: i
399454
raise click.UsageError("--example and --log are mutually exclusive.")
400455

401456
if example:
402-
records = _run_example(example, Path(typhoon_dir), timeout)
457+
records, configs = _run_example(example, Path(typhoon_dir), timeout)
403458
name = example
404459
elif log_file == "-":
405-
records = _parse_lines(sys.stdin.readlines())
460+
records, configs = _parse_lines(sys.stdin.readlines())
406461
name = "capture"
407462
else:
408-
records = _parse_lines(Path(log_file).read_text().splitlines())
463+
records, configs = _parse_lines(Path(log_file).read_text().splitlines())
409464
name = Path(log_file).stem
410465

411466
if not records:
@@ -420,12 +475,12 @@ def main(example: str, log_file: str, out_dir: str, typhoon_dir: str, timeout: i
420475
out = Path(out_dir)
421476
if per_packet:
422477
pairs = _pair_records(records)
423-
_plot_all_per_packet(pairs, out, name)
478+
_plot_all_per_packet(pairs, out, name, configs)
424479
else:
425480
bms = bucket_ms if bucket_ms > 0 else _auto_bucket_ms(records)
426481
flows = _bucket(records, bms)
427482
pairs = _pair_flows(flows)
428-
_plot_all(pairs, out, bms, name)
483+
_plot_all(pairs, out, bms, name, configs)
429484

430485

431486
if __name__ == "__main__":

typhoon/src/capture.rs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
//!
1010
//! Each line is a self-contained JSON object with fields:
1111
//! `t` (unix ms), `dir` (`c2s`/`s2c`), `flow` (addr), `kind`
12-
//! (`Data`/`Service`/`Decoy`), `tailor`, `crypto`, `header`, `payload`, `body`.
12+
//! (`Data`/`Service`/`Decoy`/`Config`), `tailor`, `crypto`, `header`, `payload`, `body`.
13+
//!
14+
//! All capture functions accept a lazy closure so that argument computation —
15+
//! string formatting, allocations, etc. — is entirely skipped at zero cost
16+
//! when the `capture` feature is disabled.
1317
1418
use std::net::SocketAddr;
1519

@@ -49,8 +53,16 @@ impl CaptureContext {
4953
}
5054

5155
/// Emit a c2s (client-to-server) packet record.
56+
///
57+
/// `f` is called only when the `capture` feature is enabled; its body
58+
/// (including any string construction or arithmetic) is never executed
59+
/// otherwise, giving true zero overhead.
5260
#[cfg(feature = "capture")]
53-
pub(crate) fn record_send(&self, kind: &str, tailor: usize, crypto: usize, header: usize, payload: usize, body: usize) {
61+
pub(crate) fn record_send<F>(&self, f: F)
62+
where
63+
F: FnOnce() -> (&'static str, usize, usize, usize, usize, usize),
64+
{
65+
let (kind, tailor, crypto, header, payload, body) = f();
5466
trace!(
5567
target: "typhoon::capture",
5668
"{{\"t\":{},\"dir\":\"c2s\",\"flow\":\"{}\",\"kind\":\"{kind}\",\"tailor\":{tailor},\"crypto\":{crypto},\"header\":{header},\"payload\":{payload},\"body\":{body}}}",
@@ -61,12 +73,47 @@ impl CaptureContext {
6173

6274
#[cfg(not(feature = "capture"))]
6375
#[inline(always)]
64-
pub(crate) fn record_send(&self, _: &str, _: usize, _: usize, _: usize, _: usize, _: usize) {}
76+
pub(crate) fn record_send<F>(&self, _: F)
77+
where
78+
F: FnOnce() -> (&'static str, usize, usize, usize, usize, usize),
79+
{
80+
}
81+
}
82+
83+
/// Emit a configuration record when a flow is established.
84+
///
85+
/// `f` is called only when the `capture` feature is enabled.
86+
/// It should return `(body_mode_description, header_len_bytes, decoy_name)`.
87+
#[cfg(feature = "capture")]
88+
pub(crate) fn record_flow_config<F>(flow_addr: SocketAddr, dir: &str, f: F)
89+
where
90+
F: FnOnce() -> (String, usize, &'static str),
91+
{
92+
let (body_mode, header_len, decoy) = f();
93+
trace!(
94+
target: "typhoon::capture",
95+
"{{\"t\":{},\"kind\":\"Config\",\"dir\":\"{dir}\",\"flow\":\"{flow_addr}\",\"body_mode\":\"{body_mode}\",\"header_len\":{header_len},\"decoy\":\"{decoy}\"}}",
96+
unix_timestamp_ms(),
97+
);
98+
}
99+
100+
#[cfg(not(feature = "capture"))]
101+
#[inline(always)]
102+
pub(crate) fn record_flow_config<F>(_: SocketAddr, _: &str, _: F)
103+
where
104+
F: FnOnce() -> (String, usize, &'static str),
105+
{
65106
}
66107

67108
/// Emit an s2c (server-to-client) packet record from the server send path.
109+
///
110+
/// `f` is called only when the `capture` feature is enabled.
68111
#[cfg(feature = "capture")]
69-
pub(crate) fn record_server_send(addr: SocketAddr, kind: &str, tailor: usize, crypto: usize, header: usize, payload: usize, body: usize) {
112+
pub(crate) fn record_server_send<F>(addr: SocketAddr, f: F)
113+
where
114+
F: FnOnce() -> (&'static str, usize, usize, usize, usize, usize),
115+
{
116+
let (kind, tailor, crypto, header, payload, body) = f();
70117
trace!(
71118
target: "typhoon::capture",
72119
"{{\"t\":{},\"dir\":\"s2c\",\"flow\":\"{addr}\",\"kind\":\"{kind}\",\"tailor\":{tailor},\"crypto\":{crypto},\"header\":{header},\"payload\":{payload},\"body\":{body}}}",
@@ -76,4 +123,8 @@ pub(crate) fn record_server_send(addr: SocketAddr, kind: &str, tailor: usize, cr
76123

77124
#[cfg(not(feature = "capture"))]
78125
#[inline(always)]
79-
pub(crate) fn record_server_send(_: SocketAddr, _: &str, _: usize, _: usize, _: usize, _: usize, _: usize) {}
126+
pub(crate) fn record_server_send<F>(_: SocketAddr, _: F)
127+
where
128+
F: FnOnce() -> (&'static str, usize, usize, usize, usize, usize),
129+
{
130+
}

typhoon/src/flow/client.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::sync::{Arc, Weak};
44

55
use crate::bytes::DynamicByteBuffer;
66
use crate::cache::CachedValue;
7-
use crate::capture::CaptureContext;
7+
use crate::capture::{CaptureContext, record_flow_config};
88
use crate::crypto::ClientCryptoTool;
99
use crate::flow::common::{FlowManager, FlowReceiveInternal, FlowSendInternal};
1010
use crate::flow::config::FlowConfig;
@@ -35,6 +35,9 @@ impl<T: IdentityType + Clone + 'static, AE: AsyncExecutor + 'static> ClientFlowM
3535
let manager_ref = Arc::new_cyclic(|m: &Weak<ClientFlowManager<T, AE>>| {
3636
let mgr: Weak<dyn DecoyFlowSender> = m.clone();
3737
let decoy = factory(mgr, settings.clone(), identity);
38+
record_flow_config(addr, "c2s", || {
39+
(config.fake_body_mode.description(), config.fake_header_mode.len(), decoy.name())
40+
});
3841
ClientFlowManager {
3942
decoy_provider: Mutex::new(decoy),
4043
send_internal: Mutex::new(FlowSendInternal {

typhoon/src/flow/common.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,10 @@ impl<CP: FlowCryptoProvider> FlowSendInternal<CP> {
111111
self.config.fake_header_mode.fill(full_packet.rebuffer_end(fake_header_len));
112112
get_rng().fill(&mut full_packet.rebuffer_both(fake_header_len, full_packet_len));
113113

114-
let kind = if packet_flags.is_discardable() { "Decoy" } else if packet_flags.is_service() { "Service" } else { "Data" };
115-
self.capture.record_send(kind, full_tailor_len, CP::tailor_overhead(), fake_header_len, data_len, full_packet_len - fake_header_len);
114+
self.capture.record_send(|| {
115+
let kind = if packet_flags.is_discardable() { "Decoy" } else if packet_flags.is_service() { "Service" } else { "Data" };
116+
(kind, full_tailor_len, CP::tailor_overhead(), fake_header_len, data_len, full_packet_len - fake_header_len)
117+
});
116118

117119
Ok(full_packet)
118120
}

typhoon/src/flow/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ pub enum FakeBodyMode {
3737
}
3838

3939
impl FakeBodyMode {
40+
/// Human-readable description of this mode for capture log records.
41+
#[inline]
42+
pub(crate) fn description(&self) -> String {
43+
match self {
44+
FakeBodyMode::Empty => "Empty".to_string(),
45+
FakeBodyMode::Random { min_length, max_length, service } => format!("Random({min_length}..{max_length},svc={service})"),
46+
FakeBodyMode::Constant { packet_length } => format!("Constant({packet_length})"),
47+
}
48+
}
49+
4050
/// Maximum fake body length this mode can produce — used to bound MTU calculations.
4151
pub fn max_len(&self) -> usize {
4252
match self {

typhoon/src/flow/decoy/common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ impl<T: FlowManager + Send + Sync> DecoyFlowSender for T {
227227
/// flow managers. All async methods are boxed automatically by `async_trait`.
228228
#[async_trait]
229229
pub trait DecoyProvider: Send + Sync {
230+
/// Short display name of this provider (e.g. "SparseDecoyProvider").
231+
fn name(&self) -> &'static str;
232+
230233
/// Start the background decoy generation timer.
231234
async fn start(&mut self);
232235

typhoon/src/flow/decoy/heavy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ impl<T: IdentityType + Clone, AE: AsyncExecutor> HeavyDecoyProvider<T, AE> {
9999

100100
#[async_trait]
101101
impl<T: IdentityType + Clone + 'static, AE: AsyncExecutor + 'static> DecoyProvider for HeavyDecoyProvider<T, AE> {
102+
#[inline]
103+
fn name(&self) -> &'static str { "HeavyDecoyProvider" }
104+
102105
async fn start(&mut self) {
103106
let executor = {
104107
let lock = self.state.read().await;

typhoon/src/flow/decoy/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ where
4242
DP: DecoyCommunicationMode<T, AE> + 'static,
4343
{
4444
Arc::new(|manager, settings, identity| {
45-
info!("decoy provider: {}", DP::name());
45+
info!("decoy provider: {}", <DP as DecoyCommunicationMode<T, AE>>::name());
4646
Box::new(DP::new(manager, settings, identity))
4747
})
4848
}

typhoon/src/flow/decoy/noisy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ impl<T: IdentityType + Clone, AE: AsyncExecutor> NoisyDecoyProvider<T, AE> {
9797

9898
#[async_trait]
9999
impl<T: IdentityType + Clone + 'static, AE: AsyncExecutor + 'static> DecoyProvider for NoisyDecoyProvider<T, AE> {
100+
#[inline]
101+
fn name(&self) -> &'static str { "NoisyDecoyProvider" }
102+
100103
async fn start(&mut self) {
101104
let executor = {
102105
let lock = self.state.read().await;

typhoon/src/flow/decoy/simple.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub struct SimpleDecoyProvider;
1414

1515
#[async_trait]
1616
impl DecoyProvider for SimpleDecoyProvider {
17+
#[inline]
18+
fn name(&self) -> &'static str { "SimpleDecoyProvider" }
19+
1720
async fn start(&mut self) {}
1821

1922
async fn feed_input(&mut self, packet: DynamicByteBuffer) -> Option<DynamicByteBuffer> {

0 commit comments

Comments
 (0)