Skip to content

Commit 66c2731

Browse files
committed
contrib: plot getMemoryLoad()
Add Python script to make plots. Assisted-by: GitHub Copilot Assisted-by: OpenAI GPT-5.1-Codex
1 parent 4f5a87b commit 66c2731

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ target/
2626
/guix-build-*
2727

2828
/ci/scratch/
29+
30+
plots
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""Plot getMemoryLoad durations and template memory footprints over time."""
3+
from __future__ import annotations
4+
5+
import argparse
6+
import datetime as dt
7+
import re
8+
from pathlib import Path
9+
10+
import matplotlib.pyplot as plt
11+
from matplotlib.ticker import NullFormatter
12+
13+
MEMORY_LOAD_COMPLETED = re.compile(
14+
r"^(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z).*getMemoryLoad:.*completed \((?P<ms>\d+\.\d+)ms\)"
15+
)
16+
UPDATE_TIP = re.compile(
17+
r"^(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z).*UpdateTip:.*height=(?P<height>\d+) "
18+
)
19+
FOOTPRINT_ENTRY = re.compile(
20+
r"^(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})Z.*Template memory footprint (?P<mib>\d+(?:\.\d+)?) MiB"
21+
)
22+
23+
24+
def parse_args() -> argparse.Namespace:
25+
parser = argparse.ArgumentParser(description=__doc__)
26+
parser.add_argument(
27+
"bitcoin_log",
28+
type=Path,
29+
help="Path to bitcoin debug log containing getMemoryLoad entries",
30+
)
31+
parser.add_argument(
32+
"--output",
33+
type=Path,
34+
default=Path("getmemoryload-scatter.png"),
35+
help="Destination image path (default: ./getmemoryload-scatter.png)",
36+
)
37+
parser.add_argument(
38+
"--show",
39+
action="store_true",
40+
help="Display the plot interactively instead of saving it",
41+
)
42+
parser.add_argument(
43+
"footprint_log",
44+
type=Path,
45+
nargs="?",
46+
help="Optional SV2 log path with 'Template memory footprint' lines",
47+
)
48+
return parser.parse_args()
49+
50+
51+
def parse_timestamp(raw: str) -> dt.datetime:
52+
return dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
53+
54+
55+
def load_events(log_path: Path):
56+
mem_points = []
57+
tip_events = []
58+
with log_path.open("r", encoding="utf-8") as handle:
59+
for line in handle:
60+
mem_match = MEMORY_LOAD_COMPLETED.search(line)
61+
if mem_match:
62+
mem_points.append(
63+
(parse_timestamp(mem_match.group("ts")), float(mem_match.group("ms")))
64+
)
65+
continue
66+
tip_match = UPDATE_TIP.search(line)
67+
if tip_match:
68+
tip_events.append(
69+
(parse_timestamp(tip_match.group("ts")), int(tip_match.group("height")))
70+
)
71+
mem_points.sort(key=lambda item: item[0])
72+
tip_events.sort(key=lambda item: item[0])
73+
return mem_points, tip_events
74+
75+
76+
def load_footprints(log_path: Path | None):
77+
footprints = []
78+
if log_path is None:
79+
return footprints
80+
81+
if not log_path.exists():
82+
raise SystemExit(f"Footprint log '{log_path}' not found")
83+
84+
with log_path.open("r", encoding="utf-8") as handle:
85+
for line in handle:
86+
match = FOOTPRINT_ENTRY.search(line)
87+
if match:
88+
mib_val = float(match.group("mib"))
89+
footprints.append((parse_timestamp(match.group("ts")), mib_val))
90+
91+
footprints.sort(key=lambda item: item[0])
92+
return footprints
93+
94+
95+
def plot_memory_load(
96+
mem_points,
97+
tip_events,
98+
footprint_points,
99+
output: Path,
100+
show_plot: bool,
101+
) -> None:
102+
if not mem_points:
103+
raise SystemExit("No getMemoryLoad() completion records found in log")
104+
105+
times, durations = zip(*mem_points)
106+
fig, ax = plt.subplots(figsize=(10, 5))
107+
ax.scatter(
108+
times,
109+
durations,
110+
s=5,
111+
color="tab:blue",
112+
label="getMemoryLoad duration",
113+
zorder=3,
114+
)
115+
116+
ax.set_ylabel("Duration (ms)")
117+
ax.set_xlabel("Time")
118+
ax.set_title("")
119+
ax.xaxis.set_major_formatter(NullFormatter())
120+
ax.set_ylim(bottom=0)
121+
122+
twin_ax = None
123+
if footprint_points:
124+
footprint_times, footprint_mib = zip(*footprint_points)
125+
twin_ax = ax.twinx()
126+
twin_ax.vlines(
127+
footprint_times,
128+
[0] * len(footprint_times),
129+
footprint_mib,
130+
color="#4fbf73",
131+
alpha=0.5,
132+
linewidth=1,
133+
zorder=1,
134+
label="Template memory footprint",
135+
)
136+
twin_ax.set_ylabel("Memory footprint (MiB)", color="#2b8a46")
137+
twin_ax.tick_params(axis="y", colors="#2b8a46")
138+
twin_ax.set_ylim(bottom=0)
139+
140+
if tip_events:
141+
tip_color = "tab:red"
142+
for ts, height in tip_events:
143+
ax.axvline(ts, linestyle=":", color=tip_color, alpha=0.8)
144+
ax.annotate(
145+
str(height),
146+
xy=(ts, 1),
147+
xycoords=("data", "axes fraction"),
148+
xytext=(0, 3),
149+
textcoords="offset points",
150+
rotation=90,
151+
ha="center",
152+
va="bottom",
153+
color=tip_color,
154+
fontsize=8,
155+
)
156+
157+
handles, labels = ax.get_legend_handles_labels()
158+
if twin_ax is not None:
159+
twin_handles, twin_labels = twin_ax.get_legend_handles_labels()
160+
handles += twin_handles
161+
labels += twin_labels
162+
ax.legend(handles, labels, loc="upper left")
163+
ax.grid(True, axis="y", linestyle="--", alpha=0.3)
164+
165+
if show_plot:
166+
plt.show()
167+
else:
168+
output.parent.mkdir(parents=True, exist_ok=True)
169+
fig.savefig(output, bbox_inches="tight", dpi=150)
170+
print(f"Wrote {output}")
171+
172+
plt.close(fig)
173+
174+
175+
def main() -> None:
176+
args = parse_args()
177+
mem_points, tip_events = load_events(args.bitcoin_log)
178+
footprint_points = load_footprints(args.footprint_log)
179+
plot_memory_load(mem_points, tip_events, footprint_points, args.output, args.show)
180+
181+
182+
if __name__ == "__main__":
183+
main()

0 commit comments

Comments
 (0)