Skip to content

Commit 2b61b70

Browse files
Effi-SAKRAM@il.ibm.com
authored andcommitted
Adding ZKP Benchmarking to test overhead of FSC nodes on TPS
Signed-off-by: Effi-S <effi.szt@gmail.com> Signed-off-by: AKRAM@il.ibm.com <akram@akramb.vpc.cloud9.ibm.com>
1 parent 138ec17 commit 2b61b70

20 files changed

Lines changed: 4034 additions & 11 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ go.work.sum
88
fabric-samples/
99
fabric-smart-client/
1010
/.bob/
11+
cmd/token_validation_service/example-*.txt
12+
cmd/token_validation_service/out/
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import re
2+
import os
3+
from pathlib import Path
4+
from typing import Any
5+
import pandas as pd
6+
import plotly.express as px
7+
import streamlit as st
8+
st.set_page_config(
9+
layout="wide", page_icon=":chart_with_upwards_trend:", page_title="TPS Degradation", initial_sidebar_state="collapsed")
10+
11+
IGNORE_COLS = {"bench", "workers", "tps",
12+
"iterations", "ns/op", "B/op", "allocs/op"}
13+
DEFAULT_BENCH_DIR = "bench"
14+
15+
16+
def _is_number(s: str) -> bool:
17+
try:
18+
float(s)
19+
return True
20+
except ValueError:
21+
return False
22+
23+
24+
def simple_parser(path: Path) -> pd.DataFrame:
25+
rows = []
26+
for ln in path.read_text().splitlines():
27+
if not ln.startswith("Benchmark"):
28+
continue
29+
first, *cols = ln.split()
30+
if not cols:
31+
continue
32+
33+
bench_name, *params = first.split("/")
34+
if params:
35+
parts = params[-1].rsplit("-", 1)
36+
if len(parts) == 2 and parts[1].isdigit():
37+
last_param, workers = parts[0], int(parts[1])
38+
else:
39+
last_param, workers = params[-1], 1
40+
row: dict[str, Any] = {"bench": bench_name, "workers": workers}
41+
for p in [*params[:-1], last_param]:
42+
if "=" in p:
43+
k, v = p.split("=", 1)
44+
row[k] = v
45+
else:
46+
parts = bench_name.rsplit("-", 1)
47+
if len(parts) == 2 and parts[1].isdigit():
48+
bench_name, workers = parts[0], int(parts[1])
49+
else:
50+
workers = 1
51+
row = {"bench": bench_name, "workers": workers}
52+
53+
row["iterations"] = int(cols.pop(0))
54+
55+
i = 0
56+
while i < len(cols):
57+
pval = cols[i]
58+
j = i + 1
59+
while j < len(cols) and not _is_number(cols[j]):
60+
j += 1
61+
pname = " ".join(cols[i + 1:j])
62+
row[pname] = float(pval)
63+
i = j
64+
65+
rows.append(row)
66+
67+
df = pd.DataFrame(rows)
68+
if df.empty:
69+
return df
70+
71+
if "TPS" in df.columns:
72+
df = df.rename(columns={"TPS": "tps"})
73+
for col in [c for c in df.columns if c.startswith("ns/op") and "(" in c]:
74+
label = col.split("(")[1].rstrip(")")
75+
df[f"{label} (ms)"] = df[col] / 1e6
76+
df = df.drop(columns=[col])
77+
78+
if 'tps' in df.columns and 'workers' in df.columns:
79+
# Little's Law (latency = concurrency / throughput)
80+
df['avg (ms)'] = df['workers'] * 1000 / df['tps']
81+
82+
return df
83+
84+
85+
# --- STRUCTURED PARALLEL LOG PARSER ---
86+
87+
ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-9;]*m')
88+
PARALLEL_RUN_RE = re.compile(r'=== RUN\s+(\S+)/(.+?)_with_(\d+)_workers')
89+
PARALLEL_THROUGHPUT_RE = re.compile(r'Pure Throughput\s+([\d.]+)/s')
90+
PARALLEL_LATENCY_RE = re.compile(
91+
r'(Min|P50 \(Median\)|Average|P95|P99\.9|P99|P5|Max)\s+'
92+
r'([\d.]+(?:ms|[n\xb5\xc2]+s|s))')
93+
94+
95+
def _parse_ms(s: str) -> float:
96+
m = re.match(r'([\d.]+)(.*)', s)
97+
val, unit = float(m.group(1)), m.group(2) # type: ignore
98+
if unit == 'ms':
99+
return val
100+
if unit == 'ns':
101+
return val / 1e6
102+
if unit == 's':
103+
return val * 1e3
104+
if '\xb5' in unit or 'µ' in unit:
105+
return val / 1e3
106+
raise ValueError(f"unknown unit: {unit}")
107+
108+
109+
LATENCY_KEYS = {
110+
'P50 (Median)': 'p50 (ms)',
111+
'P5': 'p5 (ms)',
112+
'P95': 'p95 (ms)',
113+
'P99': 'p99 (ms)',
114+
}
115+
DISPLAY_LATENCY = {'avg (ms)', 'p50 (ms)', 'p95 (ms)'}
116+
WORKERS_RE = re.compile(r'^Workers\s+(\d+)')
117+
118+
119+
def parse_parallel_log(path: Path, default_bench: str | None = None) -> pd.DataFrame:
120+
text = ANSI_ESCAPE_RE.sub('', path.read_text())
121+
rows: list[dict] = []
122+
current: dict = {}
123+
has_run_lines = bool(PARALLEL_RUN_RE.search(text))
124+
125+
for line in text.splitlines():
126+
line = line.strip()
127+
128+
m = PARALLEL_RUN_RE.match(line)
129+
if m:
130+
if current:
131+
rows.append(current)
132+
bench, params_raw, workers = m.group(
133+
1), m.group(2), int(m.group(3))
134+
current = {'bench': bench, 'workers': workers}
135+
setup_m = re.search(r'Setup\((.+?)\)', params_raw)
136+
if setup_m:
137+
for token in setup_m.group(1).split(',_'):
138+
token = token.strip('_').lstrip('#')
139+
if '_' in token:
140+
k, v = token.split('_', 1)
141+
current[k] = v
142+
continue
143+
144+
if not has_run_lines:
145+
m = WORKERS_RE.match(line)
146+
if m:
147+
workers = int(m.group(1))
148+
if current:
149+
rows.append(current)
150+
current = {
151+
'bench': default_bench or path.stem,
152+
'workers': workers,
153+
}
154+
continue
155+
156+
m = PARALLEL_THROUGHPUT_RE.match(line)
157+
if m:
158+
current['tps'] = float(m.group(1))
159+
continue
160+
161+
m = PARALLEL_LATENCY_RE.match(line)
162+
if m and m.group(1) in LATENCY_KEYS:
163+
current[LATENCY_KEYS[m.group(1)]] = _parse_ms(m.group(2))
164+
continue
165+
166+
m = re.match(r'Total Ops\s+(\d+)', line)
167+
if m:
168+
current['iterations'] = int(m.group(1))
169+
170+
if current:
171+
rows.append(current)
172+
return pd.DataFrame(rows)
173+
174+
175+
def has_multi_nc(df: pd.DataFrame) -> bool:
176+
return "nc" in df.columns and df["nc"].nunique() > 1
177+
178+
179+
# --- PLOTTING ---
180+
181+
182+
def _tps_fig(df, color_col, title):
183+
fig = px.line(df, x='workers', y='tps', color=color_col,
184+
symbol=color_col,
185+
markers=True, title=title,
186+
labels={'workers': 'Workers', 'tps': 'TPS', color_col: ''},
187+
symbol_sequence=SHAPES)
188+
fig.update_yaxes(
189+
nticks=25,
190+
)
191+
fig.update_layout(template='plotly_white',
192+
hovermode='x unified')
193+
return fig
194+
195+
196+
def _latency_fig(df, color_col, dash_col, title):
197+
latency_cols = [c for c in df.columns if c in DISPLAY_LATENCY]
198+
if not latency_cols:
199+
return None
200+
melted = df.melt(id_vars=[color_col, 'workers'], value_vars=latency_cols,
201+
var_name='percentile', value_name='latency')
202+
dash_map = {'avg (ms)': 'dash', 'p50 (ms)': 'solid', 'p95 (ms)': 'dot'}
203+
fig = px.line(melted, x='workers', y='latency',
204+
color=color_col, line_dash=dash_col, markers=True, title=title,
205+
labels={'workers': 'Workers',
206+
'latency': 'Latency (ms)', color_col: ''},
207+
line_dash_map=dash_map)
208+
fig.update_layout(template='plotly_white', hovermode='x unified')
209+
return fig
210+
211+
212+
def _aggregate(df, group_cols):
213+
numeric = [c for c in df.select_dtypes(
214+
include='number').columns if c != 'workers']
215+
return (df.groupby(group_cols)[numeric].mean().reset_index().sort_values('workers'))
216+
217+
218+
def make_figures(df):
219+
param_cols = [
220+
c for c in df.columns if c not in IGNORE_COLS and not c.endswith("(ms)")]
221+
figs = []
222+
223+
for bench, bdf in df.groupby('bench'):
224+
bdf = bdf.copy()
225+
varying = [c for c in param_cols if c in bdf and bdf[c].nunique() > 1]
226+
fixed = [c for c in param_cols if c in bdf and bdf[c].nunique()
227+
<= 1 and bdf[c].notna().any()]
228+
229+
bdf['series'] = (
230+
bdf[varying].astype(str).apply(
231+
lambda r: ', '.join(f'{k}={v}' for k, v in r.items()), axis=1)
232+
if varying else bench
233+
)
234+
235+
agg = _aggregate(bdf, ['series', 'workers'])
236+
fixed_str = ', '.join(str(bdf[c].dropna().iloc[0]) for c in fixed)
237+
suffix = f' ({fixed_str})' if fixed_str else ''
238+
239+
figs.append(_tps_fig(agg, 'series', f'{bench}{suffix}'))
240+
lat = _latency_fig(agg, 'series', 'percentile',
241+
f'{bench} - Latency{suffix}')
242+
if lat:
243+
figs.append(lat)
244+
245+
return figs
246+
247+
248+
def parse_combined(dfs: dict[str, pd.DataFrame]):
249+
for name, df in dfs.items():
250+
df['bench'] = name
251+
dct = {}
252+
for df in dfs.values():
253+
for key, group in df.groupby("nc"):
254+
dct[key] = pd.concat(
255+
[dct.get(key, pd.DataFrame()), group], ignore_index=True)
256+
return dct
257+
258+
259+
SHAPES = ['circle', 'square', 'diamond', 'cross', 'hexagon', 'star']
260+
261+
262+
def make_combined_figures(dfs, local_dfs):
263+
figs = {"_All": None}
264+
all_dfs = []
265+
for name, d in local_dfs.items():
266+
d = d.copy()
267+
d["bench"] = name
268+
all_dfs.append(_aggregate(d, ["bench", "workers"]))
269+
270+
for nc, df in sorted(dfs.items(), key=lambda x: x[0]):
271+
agg = _aggregate(df, ["bench", "workers"])
272+
d = agg.copy()
273+
d["bench"] = d["bench"] + f" (nc={nc})"
274+
all_dfs.append(d)
275+
local_parts = []
276+
for name, ldf in local_dfs.items():
277+
ldf = ldf.copy()
278+
ldf['bench'] = name
279+
local_parts.append(ldf)
280+
agg = _aggregate(
281+
pd.concat([agg, *local_parts], ignore_index=True),
282+
["bench", "workers"]
283+
)
284+
285+
figs[nc] = _tps_fig(agg, 'bench', f'TPS (nc={nc})')
286+
lat = _latency_fig(agg, 'bench', 'percentile', f'Latency (nc={nc})')
287+
if lat:
288+
figs[f"{nc}_latency"] = lat
289+
figs["_All"] = _tps_fig(
290+
pd.concat(all_dfs, ignore_index=True), 'bench', 'TPS (All)')
291+
return figs
292+
293+
294+
def _get_dir():
295+
def_dir = Path(os.environ.get("DEF_BENCH", DEFAULT_BENCH_DIR))
296+
297+
with st.sidebar:
298+
directory = st.text_input(
299+
"Directory", value=str(def_dir) if def_dir.exists() else "", key="benchdir")
300+
os.environ["DEF_BENCH"] = directory
301+
302+
if not directory:
303+
st.info(f"Please input a Folder in the Sidebar")
304+
return None
305+
directory = Path(directory)
306+
if not directory.exists():
307+
st.error(f"Directory `{directory}` does not exist")
308+
return None
309+
return directory
310+
311+
312+
def main():
313+
directory = _get_dir()
314+
if not directory:
315+
return
316+
single = {}
317+
318+
for path in sorted(directory.glob("*.log")):
319+
df = parse_parallel_log(path)
320+
if df.empty:
321+
continue
322+
with st.expander(f"`{path.stem}`"):
323+
for fig in make_figures(df):
324+
st.plotly_chart(fig)
325+
st.dataframe(df)
326+
single[path.stem] = df
327+
328+
all_dfs = {}
329+
for path in sorted(directory.glob("*.txt")):
330+
df = simple_parser(path)
331+
if df.empty:
332+
continue
333+
all_dfs[path.stem] = df
334+
with st.expander(f"`{path.stem}`"):
335+
for fig in make_figures(df):
336+
st.plotly_chart(fig)
337+
st.dataframe(df)
338+
339+
multi = {n: df.copy() for n, df in all_dfs.items() if has_multi_nc(df)}
340+
single.update({n: df.copy()
341+
for n, df in all_dfs.items() if not has_multi_nc(df)})
342+
343+
dfs = parse_combined({
344+
n: df for n, df in multi.items()
345+
})
346+
347+
figs = make_combined_figures(dfs, local_dfs=single)
348+
st.subheader("Combined TPS")
349+
tabs = st.tabs(list(figs.keys()))
350+
for fig, tab in zip(figs.values(), tabs):
351+
with tab:
352+
st.plotly_chart(fig)
353+
354+
355+
if __name__ == "__main__":
356+
main()

0 commit comments

Comments
 (0)