Skip to content

Commit e98d568

Browse files
committed
Add enhanced live vfsreadlat.py monitor BCC example with rich library
1 parent c07707a commit e98d568

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

BCC-Examples/vfsreadlat_rich.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
2+
from pythonbpf.helper import ktime, pid
3+
from pythonbpf.maps import HashMap, PerfEventArray
4+
from ctypes import c_void_p, c_uint64
5+
6+
from rich.console import Console
7+
from rich.live import Live
8+
from rich.table import Table
9+
from rich.panel import Panel
10+
from rich.layout import Layout
11+
import numpy as np
12+
import threading
13+
import time
14+
from collections import Counter
15+
16+
# ==================== BPF Setup ====================
17+
18+
19+
@bpf
20+
@struct
21+
class latency_event:
22+
pid: c_uint64
23+
delta_us: c_uint64
24+
25+
26+
@bpf
27+
@map
28+
def start() -> HashMap:
29+
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
30+
31+
32+
@bpf
33+
@map
34+
def events() -> PerfEventArray:
35+
return PerfEventArray(key_size=c_uint64, value_size=c_uint64)
36+
37+
38+
@bpf
39+
@section("kprobe/vfs_read")
40+
def do_entry(ctx: c_void_p) -> c_uint64:
41+
p, ts = pid(), ktime()
42+
start.update(p, ts)
43+
return 0 # type: ignore [return-value]
44+
45+
46+
@bpf
47+
@section("kretprobe/vfs_read")
48+
def do_return(ctx: c_void_p) -> c_uint64:
49+
p = pid()
50+
tsp = start.lookup(p)
51+
52+
if tsp:
53+
delta_ns = ktime() - tsp
54+
55+
if delta_ns > 1000:
56+
evt = latency_event()
57+
evt.pid, evt.delta_us = p, delta_ns // 1000
58+
events.output(evt)
59+
60+
start.delete(p)
61+
62+
return 0 # type: ignore [return-value]
63+
64+
65+
@bpf
66+
@bpfglobal
67+
def LICENSE() -> str:
68+
return "GPL"
69+
70+
71+
console = Console()
72+
console.print("[bold green]Loading BPF program...[/]")
73+
74+
b = BPF()
75+
b.load()
76+
b.attach_all()
77+
78+
# ==================== Data Collection ====================
79+
80+
all_latencies = []
81+
histogram_buckets = Counter() # type: ignore [var-annotated]
82+
83+
84+
def callback(cpu, event):
85+
all_latencies.append(event.delta_us)
86+
# Create log2 bucket
87+
bucket = int(np.floor(np.log2(event.delta_us + 1)))
88+
histogram_buckets[bucket] += 1
89+
90+
91+
b["events"].open_perf_buffer(callback, struct_name="latency_event")
92+
93+
94+
def poll_events():
95+
while True:
96+
b["events"].poll(100)
97+
98+
99+
poll_thread = threading.Thread(target=poll_events, daemon=True)
100+
poll_thread.start()
101+
102+
# ==================== Live Display ====================
103+
104+
105+
def generate_display():
106+
layout = Layout()
107+
layout.split_column(
108+
Layout(name="header", size=3),
109+
Layout(name="stats", size=8),
110+
Layout(name="histogram", size=20),
111+
)
112+
113+
# Header
114+
layout["header"].update(
115+
Panel("[bold cyan]🔥 VFS Read Latency Monitor[/]", style="bold white on blue")
116+
)
117+
118+
# Stats
119+
if len(all_latencies) > 0:
120+
lats = np.array(all_latencies)
121+
stats_table = Table(show_header=False, box=None, padding=(0, 2))
122+
stats_table.add_column(style="bold cyan")
123+
stats_table.add_column(style="bold yellow")
124+
125+
stats_table.add_row("📊 Total Samples:", f"{len(lats):,}")
126+
stats_table.add_row("⚡ Mean Latency:", f"{np.mean(lats):.2f} µs")
127+
stats_table.add_row("📉 Min Latency:", f"{np.min(lats):.2f} µs")
128+
stats_table.add_row("📈 Max Latency:", f"{np.max(lats):.2f} µs")
129+
stats_table.add_row("🎯 P95 Latency:", f"{np.percentile(lats, 95):.2f} µs")
130+
stats_table.add_row("🔥 P99 Latency:", f"{np.percentile(lats, 99):.2f} µs")
131+
132+
layout["stats"].update(
133+
Panel(stats_table, title="Statistics", border_style="green")
134+
)
135+
else:
136+
layout["stats"].update(
137+
Panel("[yellow]Waiting for data...[/]", border_style="yellow")
138+
)
139+
140+
# Histogram
141+
if histogram_buckets:
142+
hist_table = Table(title="Latency Distribution", box=None)
143+
hist_table.add_column("Range", style="cyan", no_wrap=True)
144+
hist_table.add_column("Count", justify="right", style="yellow")
145+
hist_table.add_column("Distribution", style="green")
146+
147+
max_count = max(histogram_buckets.values())
148+
149+
for bucket in sorted(histogram_buckets.keys()):
150+
count = histogram_buckets[bucket]
151+
lower = 2**bucket
152+
upper = 2 ** (bucket + 1)
153+
154+
# Create bar
155+
bar_width = int((count / max_count) * 40)
156+
bar = "█" * bar_width
157+
158+
hist_table.add_row(
159+
f"{lower:5d}-{upper:5d} µs",
160+
f"{count:6d}",
161+
f"[green]{bar}[/] {count / len(all_latencies) * 100:.1f}%",
162+
)
163+
164+
layout["histogram"].update(Panel(hist_table, border_style="green"))
165+
166+
return layout
167+
168+
169+
try:
170+
with Live(generate_display(), refresh_per_second=2, console=console) as live:
171+
while True:
172+
time.sleep(0.5)
173+
live.update(generate_display())
174+
except KeyboardInterrupt:
175+
console.print("\n[bold red]Stopping...[/]")
176+
177+
if all_latencies:
178+
console.print(f"\n[bold green]✅ Collected {len(all_latencies):,} samples[/]")

0 commit comments

Comments
 (0)