Skip to content

Commit 798f079

Browse files
committed
Add enhanced vfsreadlat BCC example with live plotly and dash graphs on browser
1 parent e98d568 commit 798f079

File tree

3 files changed

+479
-0
lines changed

3 files changed

+479
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""BPF program for tracing VFS read latency."""
2+
3+
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
4+
from pythonbpf.helper import ktime, pid
5+
from pythonbpf.maps import HashMap, PerfEventArray
6+
from ctypes import c_void_p, c_uint64
7+
import argparse
8+
from data_collector import LatencyCollector
9+
from dashboard import LatencyDashboard
10+
11+
12+
@bpf
13+
@struct
14+
class latency_event:
15+
pid: c_uint64
16+
delta_us: c_uint64
17+
18+
19+
@bpf
20+
@map
21+
def start() -> HashMap:
22+
"""Map to store start timestamps by PID."""
23+
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
24+
25+
26+
@bpf
27+
@map
28+
def events() -> PerfEventArray:
29+
"""Perf event array for sending latency events to userspace."""
30+
return PerfEventArray(key_size=c_uint64, value_size=c_uint64)
31+
32+
33+
@bpf
34+
@section("kprobe/vfs_read")
35+
def do_entry(ctx: c_void_p) -> c_uint64:
36+
"""Record start time when vfs_read is called."""
37+
p, ts = pid(), ktime()
38+
start.update(p, ts)
39+
return 0 # type: ignore [return-value]
40+
41+
42+
@bpf
43+
@section("kretprobe/vfs_read")
44+
def do_return(ctx: c_void_p) -> c_uint64:
45+
"""Calculate and record latency when vfs_read returns."""
46+
p = pid()
47+
tsp = start.lookup(p)
48+
49+
if tsp:
50+
delta_ns = ktime() - tsp
51+
52+
# Only track latencies > 1 microsecond
53+
if delta_ns > 1000:
54+
evt = latency_event()
55+
evt.pid, evt.delta_us = p, delta_ns // 1000
56+
events.output(evt)
57+
58+
start.delete(p)
59+
60+
return 0 # type: ignore [return-value]
61+
62+
63+
@bpf
64+
@bpfglobal
65+
def LICENSE() -> str:
66+
return "GPL"
67+
68+
69+
def parse_args():
70+
"""Parse command line arguments."""
71+
parser = argparse.ArgumentParser(
72+
description="Monitor VFS read latency with live dashboard"
73+
)
74+
parser.add_argument(
75+
"--host", default="0.0.0.0", help="Dashboard host (default: 0.0.0.0)"
76+
)
77+
parser.add_argument(
78+
"--port", type=int, default=8050, help="Dashboard port (default: 8050)"
79+
)
80+
parser.add_argument(
81+
"--buffer", type=int, default=10000, help="Recent data buffer size"
82+
)
83+
return parser.parse_args()
84+
85+
86+
args = parse_args()
87+
88+
# Load BPF program
89+
print("Loading BPF program...")
90+
b = BPF()
91+
b.load()
92+
b.attach_all()
93+
print("✅ BPF program loaded and attached")
94+
95+
# Setup data collector
96+
collector = LatencyCollector(b, buffer_size=args.buffer)
97+
collector.start()
98+
99+
# Create and run dashboard
100+
dashboard = LatencyDashboard(collector)
101+
dashboard.run(host=args.host, port=args.port)
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
"""Plotly Dash dashboard for visualizing latency data."""
2+
3+
import dash
4+
from dash import dcc, html
5+
from dash.dependencies import Input, Output
6+
import plotly.graph_objects as go
7+
from plotly.subplots import make_subplots
8+
import numpy as np
9+
10+
11+
class LatencyDashboard:
12+
"""Interactive dashboard for latency visualization."""
13+
14+
def __init__(self, collector, title: str = "VFS Read Latency Monitor"):
15+
self.collector = collector
16+
self.app = dash.Dash(__name__)
17+
self.app.title = title
18+
self._setup_layout()
19+
self._setup_callbacks()
20+
21+
def _setup_layout(self):
22+
"""Create dashboard layout."""
23+
self.app.layout = html.Div(
24+
[
25+
html.H1(
26+
"🔥 VFS Read Latency Dashboard",
27+
style={
28+
"textAlign": "center",
29+
"color": "#2c3e50",
30+
"marginBottom": 20,
31+
},
32+
),
33+
# Stats cards
34+
html.Div(
35+
[
36+
self._create_stat_card(
37+
"total-samples", "📊 Total Samples", "#3498db"
38+
),
39+
self._create_stat_card(
40+
"mean-latency", "⚡ Mean Latency", "#e74c3c"
41+
),
42+
self._create_stat_card(
43+
"p99-latency", "🔥 P99 Latency", "#f39c12"
44+
),
45+
],
46+
style={
47+
"display": "flex",
48+
"justifyContent": "space-around",
49+
"marginBottom": 30,
50+
},
51+
),
52+
# Graphs - ✅ Make sure these IDs match the callback outputs
53+
dcc.Graph(id="dual-histogram", style={"height": "450px"}),
54+
dcc.Graph(id="log2-buckets", style={"height": "350px"}),
55+
dcc.Graph(id="timeseries-graph", style={"height": "300px"}),
56+
# Auto-update
57+
dcc.Interval(id="interval-component", interval=1000, n_intervals=0),
58+
],
59+
style={"padding": 20, "fontFamily": "Arial, sans-serif"},
60+
)
61+
62+
def _create_stat_card(self, id_name: str, title: str, color: str):
63+
"""Create a statistics card."""
64+
return html.Div(
65+
[
66+
html.H3(title, style={"color": color}),
67+
html.H2(id=id_name, style={"fontSize": 48, "color": "#2c3e50"}),
68+
],
69+
className="stat-box",
70+
style={
71+
"background": "white",
72+
"padding": 20,
73+
"borderRadius": 10,
74+
"boxShadow": "0 4px 6px rgba(0,0,0,0.1)",
75+
"textAlign": "center",
76+
"flex": 1,
77+
"margin": "0 10px",
78+
},
79+
)
80+
81+
def _setup_callbacks(self):
82+
"""Setup dashboard callbacks."""
83+
84+
@self.app.callback(
85+
[
86+
Output("total-samples", "children"),
87+
Output("mean-latency", "children"),
88+
Output("p99-latency", "children"),
89+
Output("dual-histogram", "figure"), # ✅ Match layout IDs
90+
Output("log2-buckets", "figure"), # ✅ Match layout IDs
91+
Output("timeseries-graph", "figure"), # ✅ Match layout IDs
92+
],
93+
[Input("interval-component", "n_intervals")],
94+
)
95+
def update_dashboard(n):
96+
stats = self.collector.get_stats()
97+
98+
if stats.total == 0:
99+
return self._empty_state()
100+
101+
return (
102+
f"{stats.total:,}",
103+
f"{stats.mean:.1f} µs",
104+
f"{stats.p99:.1f} µs",
105+
self._create_dual_histogram(),
106+
self._create_log2_buckets(),
107+
self._create_timeseries(),
108+
)
109+
110+
def _empty_state(self):
111+
"""Return empty state for dashboard."""
112+
empty_fig = go.Figure()
113+
empty_fig.update_layout(
114+
title="Waiting for data... Generate some disk I/O!", template="plotly_white"
115+
)
116+
# ✅ Return 6 values (3 stats + 3 figures)
117+
return "0", "0 µs", "0 µs", empty_fig, empty_fig, empty_fig
118+
119+
def _create_dual_histogram(self) -> go.Figure:
120+
"""Create side-by-side linear and log2 histograms."""
121+
latencies = self.collector.get_all_latencies()
122+
123+
# Create subplots
124+
fig = make_subplots(
125+
rows=1,
126+
cols=2,
127+
subplot_titles=("Linear Scale", "Log2 Scale"),
128+
horizontal_spacing=0.12,
129+
)
130+
131+
# Linear histogram
132+
fig.add_trace(
133+
go.Histogram(
134+
x=latencies,
135+
nbinsx=50,
136+
marker_color="rgb(55, 83, 109)",
137+
opacity=0.75,
138+
name="Linear",
139+
),
140+
row=1,
141+
col=1,
142+
)
143+
144+
# Log2 histogram
145+
log2_latencies = np.log2(latencies + 1) # +1 to avoid log2(0)
146+
fig.add_trace(
147+
go.Histogram(
148+
x=log2_latencies,
149+
nbinsx=30,
150+
marker_color="rgb(243, 156, 18)",
151+
opacity=0.75,
152+
name="Log2",
153+
),
154+
row=1,
155+
col=2,
156+
)
157+
158+
# Update axes
159+
fig.update_xaxes(title_text="Latency (µs)", row=1, col=1)
160+
fig.update_xaxes(title_text="log2(Latency in µs)", row=1, col=2)
161+
fig.update_yaxes(title_text="Count", row=1, col=1)
162+
fig.update_yaxes(title_text="Count", row=1, col=2)
163+
164+
fig.update_layout(
165+
title_text="📊 Latency Distribution (Linear vs Log2)",
166+
template="plotly_white",
167+
showlegend=False,
168+
height=450,
169+
)
170+
171+
return fig
172+
173+
def _create_log2_buckets(self) -> go.Figure:
174+
"""Create bar chart of log2 buckets (like BCC histogram)."""
175+
buckets = self.collector.get_histogram_buckets()
176+
177+
if not buckets:
178+
fig = go.Figure()
179+
fig.update_layout(
180+
title="🔥 Log2 Histogram - Waiting for data...", template="plotly_white"
181+
)
182+
return fig
183+
184+
# Sort buckets
185+
sorted_buckets = sorted(buckets.keys())
186+
counts = [buckets[b] for b in sorted_buckets]
187+
188+
# Create labels (e.g., "8-16µs", "16-32µs")
189+
labels = []
190+
hover_text = []
191+
for bucket in sorted_buckets:
192+
lower = 2**bucket
193+
upper = 2 ** (bucket + 1)
194+
labels.append(f"{lower}-{upper}")
195+
196+
# Calculate percentage
197+
total = sum(counts)
198+
pct = (buckets[bucket] / total) * 100 if total > 0 else 0
199+
hover_text.append(
200+
f"Range: {lower}-{upper} µs<br>"
201+
f"Count: {buckets[bucket]:,}<br>"
202+
f"Percentage: {pct:.2f}%"
203+
)
204+
205+
# Create bar chart
206+
fig = go.Figure()
207+
208+
fig.add_trace(
209+
go.Bar(
210+
x=labels,
211+
y=counts,
212+
marker=dict(
213+
color=counts,
214+
colorscale="YlOrRd",
215+
showscale=True,
216+
colorbar=dict(title="Count"),
217+
),
218+
text=counts,
219+
textposition="outside",
220+
hovertext=hover_text,
221+
hoverinfo="text",
222+
)
223+
)
224+
225+
fig.update_layout(
226+
title="🔥 Log2 Histogram (BCC-style buckets)",
227+
xaxis_title="Latency Range (µs)",
228+
yaxis_title="Count",
229+
template="plotly_white",
230+
height=350,
231+
xaxis=dict(tickangle=-45),
232+
)
233+
234+
return fig
235+
236+
def _create_timeseries(self) -> go.Figure:
237+
"""Create time series figure."""
238+
recent = self.collector.get_recent_latencies()
239+
240+
if not recent:
241+
fig = go.Figure()
242+
fig.update_layout(
243+
title="⏱️ Real-time Latency - Waiting for data...",
244+
template="plotly_white",
245+
)
246+
return fig
247+
248+
times = [d["time"] for d in recent]
249+
lats = [d["latency"] for d in recent]
250+
251+
fig = go.Figure()
252+
fig.add_trace(
253+
go.Scatter(
254+
x=times,
255+
y=lats,
256+
mode="lines",
257+
line=dict(color="rgb(231, 76, 60)", width=2),
258+
fill="tozeroy",
259+
fillcolor="rgba(231, 76, 60, 0.2)",
260+
)
261+
)
262+
263+
fig.update_layout(
264+
title="⏱️ Real-time Latency (Last 10,000 samples)",
265+
xaxis_title="Time (seconds)",
266+
yaxis_title="Latency (µs)",
267+
template="plotly_white",
268+
height=300,
269+
)
270+
271+
return fig
272+
273+
def run(self, host: str = "0.0.0.0", port: int = 8050, debug: bool = False):
274+
"""Run the dashboard server."""
275+
print(f"\n{'=' * 60}")
276+
print(f"🚀 Dashboard running at: http://{host}:{port}")
277+
print(" Access from your browser to see live graphs")
278+
print(
279+
" Generate disk I/O to see data: dd if=/dev/zero of=/tmp/test bs=1M count=100"
280+
)
281+
print(f"{'=' * 60}\n")
282+
self.app.run(debug=debug, host=host, port=port)

0 commit comments

Comments
 (0)