Files
vterm/benchmarks/profiling.py
2026-09-11 12:33:57 +02:00

130 lines
3.1 KiB
Python

"""Profile the feed/parse path, then micro-bench the per-char steps of VtScreen.draw.
cProfile ranks the hotspots on the bench workloads; the micro loops put numbers
on the per-char candidates inside the draw path.
Run with: python3 benchmarks/profiling.py
"""
import cProfile
import os
import pstats
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "tests"))
import _bootstrap # noqa: F401 sets sys.path for the imports below
from bench_vs_terminus import colour_flood, plain_flood, tui_repaint # noqa: E402
from _bootstrap import FakeProcess # noqa: E402
from pyte.screens import wcwidth as pyte_wcwidth # noqa: E402 lru_cache'd
from wcwidth import wcwidth as raw_wcwidth # noqa: E402
from vterm.emulator import VtScreen, VtStream # noqa: E402
# ------------------------------------------------------------ feed hotspots
def build():
screen = VtScreen(100, 40, process=FakeProcess(), history=10000)
return VtStream(screen)
def run(name, data):
stream = build()
t0 = time.perf_counter()
for i in range(0, len(data), 32768):
stream.feed(data[i:i + 32768])
dt = (time.perf_counter() - t0) * 1000
print("{:<20} {:.1f} ms".format(name, dt))
def profiled(name, data):
stream = build()
pr = cProfile.Profile()
pr.enable()
for i in range(0, len(data), 32768):
stream.feed(data[i:i + 32768])
pr.disable()
s = pstats.Stats(pr)
print("\n==== {} ====".format(name))
s.sort_stats("cumulative").print_stats(18)
# ------------------------------------------- per-char micro-benches of draw
data = ("x" * 50 + "\x1b[38;5;100m") * 40000 # ~2M printable chars
def t(label, fn, n=3):
best = 1e9
for _ in range(n):
t0 = time.perf_counter()
fn()
best = min(best, time.perf_counter() - t0)
print("{:<42} {:8.3f} ms".format(label, best * 1000))
def loop_wcwidth_pyte():
for c in data:
w = pyte_wcwidth(c)
def loop_wcwidth_raw():
for c in data:
w = raw_wcwidth(c)
def loop_iter_only():
for c in data:
pass
def loop_min():
x = 0
for c in data:
x = min(x + 1, 80)
def loop_dict_get_set():
d = {}
line = {}
for i, c in enumerate(data):
v = d.get(c)
if v is None:
d[c] = c
line[i % 80] = c
def loop_min_vs_cmp():
x = 0
cols = 80
for c in data:
nx = x + 1
x = cols if nx > cols else nx
def main():
floods = {
"colour": colour_flood(),
"plain": plain_flood(),
"tui": tui_repaint(),
}
for n, d in floods.items():
run(n, d)
profiled("colour", colour_flood())
profiled("tui", tui_repaint())
print("==== per-char micro-benches ====")
t("iter only", loop_iter_only)
t("wcwidth (pyte lru_cache)", loop_wcwidth_pyte)
t("wcwidth (raw)", loop_wcwidth_raw)
t("min(x+w, cols) per char", loop_min)
t("min -> branch per char", loop_min_vs_cmp)
t("dict get/miss/set per char", loop_dict_get_set)
if __name__ == "__main__":
main()