Skip to content

Commit b8e8a5d

Browse files
committed
Minor additions to plotting
Signed-off-by: Effi-S <effi.szt@gmail.com>
1 parent b066fde commit b8e8a5d

2 files changed

Lines changed: 159 additions & 104 deletions

File tree

cmd/benchmarking/plotly_plot_node.py

Lines changed: 156 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
st.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+
1112
SERVICE_RE = re.compile(
1213
r'^(?P<bench>\S+)-(?P<workers>\d+)\s+'
1314
r'(?P<iterations>\d+)\s+'
@@ -19,15 +20,6 @@
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-
# ----------------------------
3123
LOCAL_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+
4643
def 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

230283
def 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-
343379
if __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:

token/core/zkatdlog/nogh/v1/validator/bench/transfer_service/transfer_service_bench.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (p *trandferServiceParams) PublicParamsRaw() ([]byte, error) {
5252
if err != nil {
5353
return nil, fmt.Errorf("failed to base64-decode params file: %w", err)
5454
}
55+
5556
return ppRaw, nil
5657
}
5758

@@ -60,6 +61,7 @@ func (p *trandferServiceParams) PublicParams() (*v1setup.PublicParams, error) {
6061
if err != nil {
6162
return nil, err
6263
}
64+
6365
return v1setup.NewPublicParamsFromBytes(ppRaw, v1setup.DLogNoGHDriverName, v1setup.ProtocolV1)
6466
}
6567

@@ -69,6 +71,7 @@ func (p *trandferServiceParams) NumInputs() int {
6971
n, _ := strconv.Atoi(m[1])
7072
return n
7173
}
74+
7275
return -1
7376
}
7477

0 commit comments

Comments
 (0)