77st .set_page_config (
88 layout = "wide" , page_icon = ":chart_with_upwards_trend:" , page_title = "TPS Degradation" )
99
10- # Generic benchmark line parser
10+ DEFAULT_BENCH_DIR = "bench6"
11+
1112SERVICE_RE = re .compile (
1213 r'^(?P<bench>\S+)-(?P<workers>\d+)\s+'
1314 r'(?P<iterations>\d+)\s+'
1920 r'(?P<value>\d+)\s+ns/op\s+\((?P<name>p\d+)\)'
2021)
2122
22-
23- def ns2ms (ns ):
24- """Nano Seconds to Milliseconds"""
25- return ns / 1e6
26-
27-
28- # ----------------------------
29- # PARSER
30- # ----------------------------
3123LOCAL_RE = re .compile (
3224 r'^'
3325 r'(?P<bench>Benchmark[^\s/]+)' # Benchmark name
@@ -43,21 +35,26 @@ def ns2ms(ns):
4335)
4436
4537
38+ def ns2ms (ns ):
39+ """Nano Seconds to Milliseconds"""
40+ return ns / 1e6
41+
42+
4643def parse (path : Path , regex = SERVICE_RE ):
4744 rows = []
4845
49- for i , line in enumerate ( path .read_text ().splitlines () ):
46+ for line in path .read_text ().splitlines ():
5047 m = regex .match (line .strip ())
5148 if not m :
52- if i == 9 :
53- from IPython import embed
54- embed (colors = "Neutral" )
5549 continue
5650
5751 data = m .groupdict ()
5852 bench , * parts = data ["bench" ].split ("/" )
5953
60- p_dct = dict (p .split ("=" ) for p in parts )
54+ if data .get ("params" ):
55+ parts .extend (data ["params" ].split ("/" ))
56+
57+ p_dct = dict (p .split ("=" , 1 ) for p in parts if "=" in p )
6158
6259 row = {
6360 "bench" : bench ,
@@ -67,7 +64,6 @@ def parse(path: Path, regex=SERVICE_RE):
6764 ** p_dct
6865 }
6966
70- # Parse latency percentiles dynamically
7167 if "rest" in data :
7268 for p in RE_PERCENTILE .finditer (data ["rest" ]):
7369 row [f"{ p .group ('name' )} (ms)" ] = ns2ms (int (p .group ("value" )))
@@ -77,6 +73,108 @@ def parse(path: Path, regex=SERVICE_RE):
7773 return pd .DataFrame (rows )
7874
7975
76+ def try_parse (path : Path ):
77+ """Try SERVICE_RE first, fall back to LOCAL_RE."""
78+ df = parse (path , regex = SERVICE_RE )
79+ if df .empty :
80+ df = parse (path , regex = LOCAL_RE )
81+ return df
82+
83+
84+ # ----------------------------
85+ # STRUCTURED PARALLEL LOG PARSER
86+ # ----------------------------
87+ ANSI_ESCAPE_RE = re .compile (r'\x1b\[[0-9;]*m' )
88+
89+ PARALLEL_RUN_RE = re .compile (
90+ r'=== RUN\s+(\S+)/(.+?)_with_(\d+)_workers'
91+ )
92+
93+ PARALLEL_THROUGHPUT_RE = re .compile (
94+ r'Pure Throughput\s+([\d.]+)/s'
95+ )
96+
97+ PARALLEL_LATENCY_RE = re .compile (
98+ r'(Min|P50 \(Median\)|Average|P95|P99\.9|P99|P5|Max)\s+'
99+ r'([\d.]+(?:ms|[n\xb5\xc2]+s|s))'
100+ )
101+
102+
103+ def _parse_ms (s : str ) -> float :
104+ """Parse a duration string (e.g. '36.86ms') into milliseconds."""
105+ m = re .match (r'([\d.]+)(.*)' , s )
106+ val , unit = float (m .group (1 )), m .group (2 )
107+ if unit == 'ms' :
108+ return val
109+ if unit == 'ns' :
110+ return val / 1e6
111+ if unit == 's' :
112+ return val * 1e3
113+ raise ValueError (f"unknown unit: { unit } " )
114+
115+
116+ def parse_parallel_log (path : Path ) -> pd .DataFrame :
117+ text = ANSI_ESCAPE_RE .sub ('' , path .read_text ())
118+
119+ rows : list [dict ] = []
120+ current : dict = {}
121+
122+ for line in text .splitlines ():
123+ line = line .strip ()
124+
125+ m = PARALLEL_RUN_RE .match (line )
126+ if m :
127+ if current :
128+ rows .append (current )
129+ bench , params_raw , workers = m .group (
130+ 1 ), m .group (2 ), int (m .group (3 ))
131+ current = {'bench' : bench , 'workers' : workers }
132+ setup_m = re .search (r'Setup\((.+?)\)' , params_raw )
133+ if setup_m :
134+ for token in setup_m .group (1 ).split (',_' ):
135+ token = token .strip ('_' ).lstrip ('#' )
136+ if '_' in token :
137+ k , v = token .split ('_' , 1 )
138+ current [k ] = v
139+ continue
140+
141+ m = PARALLEL_THROUGHPUT_RE .match (line )
142+ if m :
143+ current ['tps' ] = float (m .group (1 ))
144+ continue
145+
146+ m = PARALLEL_LATENCY_RE .match (line )
147+ if m :
148+ name , raw = m .group (1 ), m .group (2 )
149+ ms_val = _parse_ms (raw )
150+ key_map = {
151+ # 'P50 (Median)': 'p50 (ms)',
152+ 'P5' : 'p5 (ms)' ,
153+ 'P95' : 'p95 (ms)' ,
154+ 'P99' : 'p99 (ms)' ,
155+ # 'P99.9': 'p99.9 (ms)',
156+ # 'Min': 'min (ms)',
157+ # 'Max': 'max (ms)',
158+ # 'Average': 'avg (ms)',
159+ }
160+ if name in key_map :
161+ current [key_map [name ]] = ms_val
162+ continue
163+
164+ m = re .match (r'Total Ops\s+(\d+)' , line )
165+ if m :
166+ current ['iterations' ] = int (m .group (1 ))
167+
168+ if current :
169+ rows .append (current )
170+
171+ return pd .DataFrame (rows )
172+
173+
174+ def has_multi_nc (df : pd .DataFrame ) -> bool :
175+ return "nc" in df .columns and df ["nc" ].nunique () > 1
176+
177+
80178# ----------------------------
81179# PLOTTING
82180# ----------------------------
@@ -95,7 +193,7 @@ def make_figures(df):
95193 cols = [
96194 c for c in param_cols
97195 if bdf [c ].notna ().any ()
98- and not c .endswith ("(ms)" ) # exclude latency
196+ and not c .endswith ("(ms)" )
99197 ]
100198 varying = [c for c in cols if bdf [c ].nunique () > 1 ]
101199 fixed = [c for c in cols if bdf [c ].nunique () <= 1 ]
@@ -105,14 +203,13 @@ def make_figures(df):
105203
106204 bdf = bdf .copy ()
107205
108- # Build legend label from varying params
206+ # Build legend label
109207 bdf ['series' ] = (
110208 bdf [varying ].astype (str ).apply (
111209 lambda r : ', ' .join (f'{ k } ={ v } ' for k , v in r .items ()), axis = 1 )
112210 if varying else bench
113211 )
114212
115- # Aggregate numeric columns
116213 numeric_cols = [
117214 c for c in bdf .select_dtypes (include = 'number' ).columns
118215 if c not in ('workers' ,)
@@ -121,7 +218,7 @@ def make_figures(df):
121218 bdf .groupby (['series' , 'workers' ])[numeric_cols ]
122219 .mean ()
123220 .reset_index ()
124- .sort_values ('workers' )
221+ .sort_values ('workers' , ascending = False )
125222 )
126223
127224 fixed_str = ', ' .join (f'{ bdf [c ].dropna ().iloc [0 ]} ' for c in fixed )
@@ -180,51 +277,7 @@ def make_figures(df):
180277 legend = dict (tracegroupgap = 10 ))
181278 figs .append (fig_lat )
182279
183- # -----------------
184- # LATENCY FIGURE 2
185- # -----------------
186- # latency_cols = [c for c in agg.columns if c.endswith('(ms)')]
187-
188- # if latency_cols:
189- # latency_df = agg.melt(
190- # id_vars=['series', 'workers'],
191- # value_vars=latency_cols,
192- # var_name='percentile',
193- # value_name='latency'
194- # )
195-
196- # # Clean up the percentile name (p5 (ms) → p5)
197- # latency_df['percentile'] = latency_df['percentile'].str.replace(
198- # r' \(ms\)', '', regex=True
199- # )
200-
201- # fig_lat = px.line(
202- # latency_df,
203- # x='workers',
204- # y='latency',
205- # color='series',
206- # line_dash='percentile',
207- # markers=True,
208- # title=f'{bench} - Latency{title_suffix}',
209- # labels={
210- # 'workers': 'Workers (GOMAXPROCS)',
211- # 'latency': 'Latency (ms)',
212- # 'percentile': ''
213- # }
214- # )
215-
216- # fig_lat.update_layout(template='plotly_white',
217- # hovermode='x unified',
218- # legend=dict(
219- # title='nc / Percentile',
220- # tracegroupgap=10
221- # ))
222- # figs.append(fig_lat)
223-
224280 return figs
225- # ----------------------------
226- # MAIN
227- # ----------------------------
228281
229282
230283def parse_combined (dfs : dict [str , pd .DataFrame ]):
@@ -249,7 +302,6 @@ def make_combined_figures(dfs: dict[str, pd.DataFrame],
249302
250303 df = df .copy ()
251304
252- # Aggregate numeric columns per bench + workers
253305 numeric_cols = [
254306 c for c in df .select_dtypes (include = "number" ).columns
255307 if c != "workers"
@@ -266,9 +318,13 @@ def make_combined_figures(dfs: dict[str, pd.DataFrame],
266318 # TPS FIGURE
267319 # -------------------------
268320 agg ["bench" ] += "-2machines"
269- for name , df in local_dfs .items ():
270- df ['bench' ] = name
271- agg = pd .concat ([agg , * local_dfs .values ()], ignore_index = True )
321+ local_parts = []
322+ for name , ldf in local_dfs .items ():
323+ ldf = ldf .copy ()
324+ ldf ['bench' ] = name
325+ local_parts .append (ldf )
326+ agg = pd .concat ([agg , * local_parts ],
327+ ignore_index = True ).sort_values (["workers" , "tps" ], ascending = [True , False ])
272328
273329 fig = px .line (
274330 agg ,
@@ -278,7 +334,7 @@ def make_combined_figures(dfs: dict[str, pd.DataFrame],
278334 markers = True ,
279335 title = f"TPS (nc={ nc } )" ,
280336 labels = {
281- "workers" : "Workers (GOMAXPROCS) " ,
337+ "workers" : "Workers" ,
282338 "tps" : "TPS" ,
283339 "bench" : ""
284340 }
@@ -320,49 +376,45 @@ def make_combined_figures(dfs: dict[str, pd.DataFrame],
320376 return figs
321377
322378
323- def cli ():
324- if len (sys .argv ) < 2 :
325- sys .exit (f"Usage: { sys .argv [0 ]} <log> [output.html]" )
326-
327- path = Path (sys .argv [1 ])
328- out = Path (sys .argv [2 ]) if len (sys .argv ) > 2 else path .with_suffix (".html" )
329-
330- df = parse (path )
331- figs = make_figures (df )
332-
333- html = [
334- fig .to_html (full_html = False ,
335- include_plotlyjs = ("cdn" if i == 0 else False ))
336- for i , fig in enumerate (figs )
337- ]
338-
339- out .write_text ("\n " .join (html ))
340- print (f"Saved { len (figs )} plots to { out } " )
341-
342-
343379if __name__ == "__main__" :
344380
345- directory = Path ("bench5" )
381+ directory = Path (sys . argv [ 1 ] if len ( sys . argv ) > 1 else DEFAULT_BENCH_DIR )
346382
347- for path in directory .glob ("*.txt" ):
348- with st .expander (f"`{ path .stem .strip ("res_transfer_" ).upper ()} `" ):
349- if path .name .startswith ("local" ):
350- df = parse (path , regex = LOCAL_RE )
351- else :
352- df = parse (path )
383+ all_dfs = {}
384+ for path in sorted (directory .glob ("*.txt" )):
385+ df = try_parse (path )
386+ if df .empty :
387+ continue
388+ all_dfs [path .stem ] = df
389+ with st .expander (f"`{ path .stem .removeprefix ('res_transfer_' ).upper ()} `" ):
353390 for fig in make_figures (df ):
354391 st .plotly_chart (fig )
355392
356- dfs = parse_combined ({p .stem .strip ("res_transfer_" ).upper (): parse (p ).copy ()
357- for p in directory .glob ("*.txt" ) if not p .name .startswith ("local" )})
358- figs = make_combined_figures (dfs , local_dfs = {p .stem : parse (p , regex = LOCAL_RE ).copy ()
359- for p in directory .glob ("*.txt" ) if p .name .startswith ("local" )})
393+ multi = {name : df .copy ()
394+ for name , df in all_dfs .items () if has_multi_nc (df )}
395+ single = {name : df .copy ()
396+ for name , df in all_dfs .items () if not has_multi_nc (df )}
397+
398+ dfs = parse_combined ({
399+ name .removeprefix ("res_transfer_" ).upper (): df
400+ for name , df in multi .items ()
401+ })
402+ for path in sorted (directory .glob ("*.csv" )):
403+ df = pd .read_csv (path )
404+ assert not df .empty
405+ with st .expander (f"`{ path .stem } `" ):
406+ st .dataframe (df )
407+ single [path .stem ] = df
408+ for path in sorted (directory .glob ("*.log" )):
409+ df = parse_parallel_log (path )
410+ assert not df .empty
411+ with st .expander (f"`{ path .stem } `" ):
412+ st .dataframe (df )
413+ single [path .stem ] = df
414+
415+ figs = make_combined_figures (dfs , local_dfs = single )
360416 st .subheader ("Combined TPS" )
361417 tabs = st .tabs (list (figs .keys ()))
362418 for fig , tab in zip (figs .values (), tabs ):
363419 with tab :
364420 st .plotly_chart (fig )
365-
366- #TODO Add allon snumbers
367- #TODO Write down average testdata file (token)
368- #Todo:
0 commit comments