1- """
2- pulsebeam_viz.py
3- ----------------
4- Lightning-Fast Interactive WebRTC SFU Analytics (Plotly Engine)
5- Manually windowed via --offset-ms to skip warmup phases instantly.
6- """
7-
81import argparse
92import numpy as np
103import pandas as pd
1710TEXT_MUTED = "#94a3b8"
1811TEXT_MAIN = "#f8fafc"
1912
13+ COLOR_P9999 = "#ef4444" # Vibrant Red for P99.99 Tail Latency
2014COLOR_P999 = "#0ea5e9"
2115COLOR_P99 = "#d946ef"
2216COLOR_P50 = "#64748b"
2317COLOR_TPUT = "#3b82f6"
18+ COLOR_RTT_P99 = "#f59e0b" # Amber for P99 RTT
19+ COLOR_RTT_P999 = "#10b981" # Emerald for P99.9 RTT
2420
2521
2622def process_data (lat_csv : str , snap_csv : str , window_secs : float , offset_ms : float , duration_ms : float ):
@@ -32,7 +28,6 @@ def process_data(lat_csv: str, snap_csv: str, window_secs: float, offset_ms: flo
3228 lat_df = pd .read_csv (lat_csv )
3329 lat_df .columns = lat_df .columns .str .strip ()
3430
35- # Filter immediately to save CPU cycles
3631 lat_df = lat_df [(lat_df ["elapsed_ms" ] >= offset_ms ) & (lat_df ["elapsed_ms" ] <= end_ms )].copy ()
3732 if lat_df .empty :
3833 raise ValueError ("No latency data found in the specified --offset-ms and --duration-ms window." )
@@ -45,46 +40,53 @@ def process_data(lat_csv: str, snap_csv: str, window_secs: float, offset_ms: flo
4540 active_agents .rename (columns = {"agent_id" : "agents" }, inplace = True )
4641
4742 # ---------------------------------------------------------
48- # 2. PROCESS SNAPSHOTS (O(N) Fast Smearing)
43+ # 2. PROCESS SNAPSHOTS (O(N) Fast Smearing + RTT Multi-Percentile Tracking )
4944 # ---------------------------------------------------------
5045 snap_df = pd .read_csv (snap_csv )
5146 snap_df .columns = snap_df .columns .str .strip ()
5247 snap_df = snap_df .sort_values (['agent_id' , 'elapsed_ms' ])
5348
54- # We must calculate diffs BEFORE filtering, so the first snapshot in our window knows its history
5549 snap_df ['tx_diff' ] = snap_df .groupby ('agent_id' )['tx_bytes' ].diff ().clip (lower = 0 )
5650 snap_df ['rx_diff' ] = snap_df .groupby ('agent_id' )['rx_bytes' ].diff ().clip (lower = 0 )
5751 snap_df ['dt_sec' ] = snap_df .groupby ('agent_id' )['elapsed_ms' ].diff () / 1000.0
5852
59- # Now filter to our window
6053 snap_df = snap_df [(snap_df ["elapsed_ms" ] >= offset_ms ) & (snap_df ["elapsed_ms" ] <= end_ms )].copy ()
6154
6255 valid_snaps = snap_df [snap_df ['dt_sec' ] > 0 ].copy ()
6356 valid_snaps ['bytes_total' ] = valid_snaps ['tx_diff' ] + valid_snaps ['rx_diff' ]
6457 valid_snaps ['rate_mbps' ] = (valid_snaps ['bytes_total' ] * 8 ) / (valid_snaps ['dt_sec' ] * 1_000_000.0 )
65-
58+ valid_snaps ['rtt_ms' ] = valid_snaps ['rtt_us' ] / 1000.0
59+
6660 valid_snaps ['end_rel' ] = (valid_snaps ['elapsed_ms' ] - offset_ms ) / 1000.0
6761 valid_snaps ['start_rel' ] = valid_snaps ['end_rel' ] - valid_snaps ['dt_sec' ]
62+ valid_snaps ['window_rel' ] = (valid_snaps ['end_rel' ] // window_secs ) * window_secs
63+
64+ # Extract both P99 and P99.9 RTT from snapshots per time-slice window
65+ rtt_stats = valid_snaps .groupby ('window_rel' )['rtt_ms' ].agg (
66+ rtt_p99 = lambda x : np .percentile (x , 99 ),
67+ rtt_p999 = lambda x : np .percentile (x , 99.9 )
68+ ).reset_index ()
6869
69- # O(N) Fast Accumulation (No more slow boolean masks!)
7070 max_t = lat_df ["window_rel" ].max ()
7171 num_bins = int ((max_t // window_secs ) + 2 )
7272 mbps_totals = np .zeros (num_bins , dtype = float )
7373
7474 for row in valid_snaps .itertuples (index = False ):
7575 start_idx = max (0 , int (row .start_rel // window_secs ))
7676 end_idx = min (num_bins , int (row .end_rel // window_secs ) + 1 )
77- # Smear the mbps rate across the bins it touches
7877 mbps_totals [start_idx :end_idx ] += row .rate_mbps
7978
8079 times = np .arange (0 , num_bins * window_secs , window_secs )
8180 tput = pd .DataFrame ({'window_rel' : times , 'mbps_raw' : mbps_totals })
8281
8382 tput = pd .merge (tput , active_agents , on = "window_rel" , how = "left" ).fillna ({'agents' : 0 })
84- # Smooth throughput display
85- tput ['mbps' ] = tput ['mbps_raw' ].rolling (window = 3 , center = True , min_periods = 1 ).median ()
8683
87- # Crop to actual data bounds
84+ # CONTINUOUS FIX: Merge RTT, then forward fill and backward fill instead of using fillna(0)
85+ tput = pd .merge (tput , rtt_stats , on = "window_rel" , how = "left" )
86+ tput ['rtt_p99' ] = tput ['rtt_p99' ].ffill ().bfill ().fillna (0 )
87+ tput ['rtt_p999' ] = tput ['rtt_p999' ].ffill ().bfill ().fillna (0 )
88+
89+ tput ['mbps' ] = tput ['mbps_raw' ].rolling (window = 3 , center = True , min_periods = 1 ).median ()
8890 tput = tput [tput ['window_rel' ] <= max_t ].copy ()
8991
9092 return lat_df , tput
@@ -93,51 +95,66 @@ def process_data(lat_csv: str, snap_csv: str, window_secs: float, offset_ms: flo
9395def plot_benchmark (lat_csv : str , snap_csv : str , label : str , window_secs : float , offset_ms : float , duration_ms : float ):
9496 lat_df , tput = process_data (lat_csv , snap_csv , window_secs , offset_ms , duration_ms )
9597
96- # Calculate Percentiles
98+ # Calculate Latency Percentiles (Including P99.99)
9799 percentiles = lat_df .groupby ("window_rel" )["delay_ms" ].agg (
98100 p50 = lambda x : np .percentile (x , 50 ),
99101 p99 = lambda x : np .percentile (x , 99 ),
100102 p999 = lambda x : np .percentile (x , 99.9 ),
103+ p9999 = lambda x : np .percentile (x , 99.99 ),
101104 pmax = lambda x : np .max (x )
102105 ).reset_index ()
103106
104- # Calculate Summary Stats for the Subtitle
107+ # CONTINUOUS FIX: Keep timeline continuous during percentiles merge using ffill/bfill
108+ percentiles = pd .merge (percentiles , tput [['window_rel' , 'rtt_p99' , 'rtt_p999' ]], on = "window_rel" , how = "left" )
109+ percentiles ['rtt_p99' ] = percentiles ['rtt_p99' ].ffill ().bfill ().fillna (0 )
110+ percentiles ['rtt_p999' ] = percentiles ['rtt_p999' ].ffill ().bfill ().fillna (0 )
111+
112+ # Summary Stats calculation (Including P99.99)
105113 max_agents = int (tput ["agents" ].max ())
106114 p50_med = percentiles ["p50" ].median ()
107115 p99_med = percentiles ["p99" ].median ()
108116 p999_med = percentiles ["p999" ].median ()
117+ p9999_med = percentiles ["p9999" ].median ()
118+ rtt_p99_med = percentiles ["rtt_p99" ].median ()
119+ rtt_p999_med = percentiles ["rtt_p999" ].median ()
109120 max_lat = percentiles ["pmax" ].max ()
110121 tput_avg = tput ["mbps" ].mean ()
111122 tput_max = tput ["mbps" ].max ()
112123 duration_s = int (tput ["window_rel" ].max ())
113124
114- # Build the rich HTML title
115125 title_html = f"""
116126 <span style="font-size: 22px; font-weight: bold; color: { TEXT_MAIN } ;">SFU Runtime Jitter Benchmark — { label } </span><br>
117127 <span style="font-size: 13px; color: { TEXT_MUTED } ;">
118128 Peak Concurrency: { max_agents } Agents • Window: { window_secs } s • Sliced Duration: { duration_s } s (Offset: { offset_ms } ms)<br>
119- Transit Latency • Med: { p50_med :.2f} ms • P99: { p99_med :.2f} ms • P99.9: { p999_med :.2f} ms • Max: { max_lat :.2f} ms<br>
120- Edge Throughput • Avg : { tput_avg :.2f} Mbps • Peak : { tput_max :.2f} Mbps
129+ Transit Latency • Med: { p50_med :.2f} ms • P99: { p99_med :.2f} ms • P99.9: { p999_med :.2f} ms • P99.99: { p9999_med :.2f } ms • Max: { max_lat :.2f} ms<br>
130+ Network RTT • P99 RTT : { rtt_p99_med :.2f} ms • P99.9 RTT : { rtt_p999_med :.2f } ms • Edge Throughput • Avg: { tput_avg :.2f} Mbps
121131 </span>
122132 """
123133
124- # --- PLOTLY FIGURE SETUP ---
125134 fig = make_subplots (
126135 rows = 2 , cols = 1 ,
127136 shared_xaxes = True ,
128137 row_heights = [0.75 , 0.25 ],
129138 vertical_spacing = 0.08
130139 )
131140
132- # 1. P99.9 Line
141+ # 1. Transit Latency: P99.99 Line
142+ fig .add_trace (go .Scatter (
143+ x = percentiles ["window_rel" ], y = percentiles ["p9999" ],
144+ name = "P99.99 Transit Latency" ,
145+ line = dict (color = COLOR_P9999 , width = 1.5 ),
146+ opacity = 0.9
147+ ), row = 1 , col = 1 )
148+
149+ # 2. Transit Latency: P99.9 Line
133150 fig .add_trace (go .Scatter (
134151 x = percentiles ["window_rel" ], y = percentiles ["p999" ],
135- name = "P99.9 (Extreme Tail) " ,
152+ name = "P99.9 Transit Latency " ,
136153 line = dict (color = COLOR_P999 , width = 1.5 ),
137154 opacity = 0.8
138155 ), row = 1 , col = 1 )
139156
140- # 2. P99 Line (Glowing)
157+ # 3. Transit Latency: P99 Line (Glowing effect )
141158 fig .add_trace (go .Scatter (
142159 x = percentiles ["window_rel" ], y = percentiles ["p99" ],
143160 showlegend = False , hoverinfo = 'skip' ,
@@ -147,18 +164,32 @@ def plot_benchmark(lat_csv: str, snap_csv: str, label: str, window_secs: float,
147164
148165 fig .add_trace (go .Scatter (
149166 x = percentiles ["window_rel" ], y = percentiles ["p99" ],
150- name = "P99 (Tail) " ,
167+ name = "P99 Transit Latency " ,
151168 line = dict (color = COLOR_P99 , width = 2.5 )
152169 ), row = 1 , col = 1 )
153170
154- # 3. P50 Line
171+ # 4. Network RTT: P99.9 Trace (Emerald)
172+ fig .add_trace (go .Scatter (
173+ x = percentiles ["window_rel" ], y = percentiles ["rtt_p999" ],
174+ name = "P99.9 Round Trip Time (RTT)" ,
175+ line = dict (color = COLOR_RTT_P999 , width = 1.5 , dash = 'dash' )
176+ ), row = 1 , col = 1 )
177+
178+ # 5. Network RTT: P99 Trace (Amber)
179+ fig .add_trace (go .Scatter (
180+ x = percentiles ["window_rel" ], y = percentiles ["rtt_p99" ],
181+ name = "P99 Round Trip Time (RTT)" ,
182+ line = dict (color = COLOR_RTT_P99 , width = 2.0 , dash = 'dashdot' )
183+ ), row = 1 , col = 1 )
184+
185+ # 6. Transit Latency: P50 Line
155186 fig .add_trace (go .Scatter (
156187 x = percentiles ["window_rel" ], y = percentiles ["p50" ],
157- name = "P50 (Median)" ,
188+ name = "P50 Transit (Median)" ,
158189 line = dict (color = COLOR_P50 , width = 1.5 , dash = 'dot' )
159190 ), row = 1 , col = 1 )
160191
161- # 4 . Throughput Fill
192+ # 7 . Throughput Fill
162193 fig .add_trace (go .Scatter (
163194 x = tput ["window_rel" ], y = tput ["mbps" ],
164195 name = "Throughput (Mbps)" ,
@@ -167,10 +198,9 @@ def plot_benchmark(lat_csv: str, snap_csv: str, label: str, window_secs: float,
167198 fillcolor = f"rgba(59, 130, 246, 0.15)"
168199 ), row = 2 , col = 1 )
169200
170- # --- LAYOUT & STYLING ---
171201 fig .update_layout (
172202 title = dict (text = title_html , x = 0.5 , xanchor = 'center' , y = 0.96 ),
173- margin = dict (t = 120 , b = 40 , l = 60 , r = 40 ),
203+ margin = dict (t = 140 , b = 40 , l = 60 , r = 40 ),
174204 paper_bgcolor = BG_COLOR ,
175205 plot_bgcolor = BG_COLOR ,
176206 font = dict (family = "Inter, -apple-system, sans-serif" , color = TEXT_MAIN ),
@@ -179,9 +209,12 @@ def plot_benchmark(lat_csv: str, snap_csv: str, label: str, window_secs: float,
179209 legend = dict (orientation = "h" , yanchor = "bottom" , y = 1.02 , xanchor = "right" , x = 1 , bgcolor = "rgba(0,0,0,0)" ),
180210 )
181211
182- lat_max = min (percentiles ["p999" ].max () * 1.2 , 150.0 )
212+ # Auto-adjust scaling metrics for the primary viewport limits (Accounting for P99.99)
213+ max_y_val = max (percentiles ["p9999" ].max (), percentiles ["rtt_p999" ].max ())
214+ lat_max = min (max_y_val * 1.2 , 500.0 ) # Elevated max cutoff since P99.99 catches high peaks
215+
183216 fig .update_yaxes (
184- title_text = "Latency [ms]" , title_font = dict (color = TEXT_MUTED , size = 12 ),
217+ title_text = "Latency / RTT [ms]" , title_font = dict (color = TEXT_MUTED , size = 12 ),
185218 range = [0 , lat_max ],
186219 showgrid = True , gridwidth = 1 , gridcolor = GRID_COLOR , zerolinecolor = GRID_COLOR ,
187220 showspikes = True , spikemode = "across" , spikethickness = 1 , spikedash = "dash" , spikecolor = TEXT_MUTED ,
@@ -207,19 +240,18 @@ def plot_benchmark(lat_csv: str, snap_csv: str, label: str, window_secs: float,
207240 row = 2 , col = 1
208241 )
209242
210- print (f"✅ Loaded window instantly. Plotting { max_agents } peak concurrent agents..." )
243+ print (f"✅ Loaded window instantly. Plotting { max_agents } peak concurrent agents with multi-percentile RTT analytics ..." )
211244 fig .show ()
212245
246+
213247if __name__ == "__main__" :
214248 parser = argparse .ArgumentParser (description = "Interactive SFU Analytics Plotter" )
215249 parser .add_argument ("--latency-csv" , required = True )
216250 parser .add_argument ("--snapshots-csv" , required = True )
217- parser .add_argument ("--label" , required = True , help = "e.g., 'Thread-per-Core'" )
218- parser .add_argument ("--window" , type = float , default = 1.0 , help = "Bin resolution in seconds" )
219-
220- # User-defined overrides
221- parser .add_argument ("--offset-ms" , type = float , required = True , help = "Skip this many milliseconds of warmup data" )
222- parser .add_argument ("--duration-ms" , type = float , default = None , help = "Stop plotting after this many milliseconds" )
251+ parser .add_argument ("--label" , required = True )
252+ parser .add_argument ("--window" , type = float , default = 1.0 )
253+ parser .add_argument ("--offset-ms" , type = float , required = True )
254+ parser .add_argument ("--duration-ms" , type = float , default = None )
223255
224256 args = parser .parse_args ()
225257 plot_benchmark (args .latency_csv , args .snapshots_csv , args .label , args .window , args .offset_ms , args .duration_ms )
0 commit comments