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
243296def _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
431486if __name__ == "__main__" :
0 commit comments