remove benchmarking code

This commit is contained in:
Vaclav Tvrdik
2026-09-08 22:15:00 +02:00
parent 026f4f472b
commit fb598227d1
4 changed files with 0 additions and 556 deletions

View File

@@ -161,7 +161,6 @@ python3 tests/test_render.py # render layer on a fake view: trimming, select
python3 tests/test_commands.py # theme picker, live restyle, generated scheme
python3 tests/harness_live.py CMD SECONDS [--send TEXT] # run a real app in the emulator, dump the screen
python3 tools/gen_keymap.py # regenerate Default.sublime-keymap
python3 tools/bench_vs_terminus.py # render cost per frame, against Terminus on the same input
```
`harness_live.py` is the useful one: it drives a real `pi` or `claude` through the

View File

@@ -1,69 +0,0 @@
"""Micro-bench the per-char steps of VtScreen.draw."""
import os
import time
import sys
ST_LIB = os.path.expanduser("~/Library/Application Support/Sublime Text/Lib/python38")
sys.path.insert(0, ST_LIB)
sys.path.insert(0, "tests/stubs")
from pyte.screens import wcwidth as pyte_wcwidth # lru_cache'd
from wcwidth import wcwidth as raw_wcwidth
from functools import lru_cache
data = ("x" * 50 + "\x1b[38;5;100m") * 40000 # ~1.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
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)

View File

@@ -1,50 +0,0 @@
"""One-shot profile of the feed/parse path, to rank hotspots."""
import cProfile
import pstats
import sys
import time
sys.path.insert(0, "tools")
sys.path.insert(0, ".")
from tools.bench_vs_terminus import colour_flood, plain_flood, tui_repaint, _bootstrap # noqa
from vterm.emulator import VtScreen, VtStream
from tests._bootstrap import FakeProcess # noqa
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)
if __name__ == "__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())

View File

@@ -1,436 +0,0 @@
""" Render-cost comparison between vterm and Terminus
Both plugins turn pty output into view text plus add_regions overlays, so what
they cost the editor is the number of Sublime calls a frame makes -- the plugin
host is a separate process (see Architecture.md). This drives both render
commands against a fake view that counts every call, on the same input, with
each plugin's own render gate deciding when a frame happens.
Terminus is read straight out of its .sublime-package by zipimport; it is
skipped when not installed. Parse times are real, call counts stand in for the
cross-process cost, which a fake view cannot show.
Run with: python3 tools/bench_vs_terminus.py
"""
import collections
import os
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "tests"))
import _bootstrap # noqa: F401 sets sys.path for the imports below
TERMINUS = os.path.expanduser(
"~/Library/Application Support/Sublime Text/Installed Packages/Terminus.sublime-package")
import sublime
ROWS, COLUMNS = 40, 100
SCROLLBACK = 10000
class FakeSelection(list):
def clear(self):
del self[:]
def add(self, region):
self.append(region)
class CountingView:
"""A view that answers with real line arithmetic and tallies every call."""
_ids = [0]
def __init__(self, counter):
CountingView._ids[0] += 1
self._id = CountingView._ids[0]
self.c = counter
self.text = ""
self.selection = FakeSelection()
self.regions = {}
self.folds = []
self._name = ""
self._settings = sublime.Settings()
# so a plugin that renders through a second command has it counted too
self.commands = {}
def _t(self, name):
self.c[name] += 1
def id(self):
return self._id
def size(self):
self._t("size")
return len(self.text)
def rowcol(self, point):
self._t("rowcol")
head = self.text[:point]
return (head.count("\n"), point - (head.rfind("\n") + 1))
def text_point(self, row, col):
self._t("text_point")
lines = self.text.split("\n")
start = sum(len(l) + 1 for l in lines[:row])
return start + min(col, len(lines[row]) if row < len(lines) else 0)
def _line(self, point):
start = self.text.rfind("\n", 0, point) + 1
end = self.text.find("\n", point)
return sublime.Region(start, len(self.text) if end == -1 else end)
def line(self, point):
self._t("line")
return self._line(point)
def full_line(self, point):
self._t("full_line")
region = self._line(point)
return sublime.Region(region.begin(), min(region.end() + 1, len(self.text)))
def lines(self, region):
self._t("lines")
out = []
point = region.begin()
while point <= region.end():
line = self._line(point)
out.append(line)
point = line.end() + 1
return out
def substr(self, region):
self._t("substr")
return self.text[region.begin():region.end()]
def erase(self, edit, region):
self._t("erase")
self.text = self.text[:region.begin()] + self.text[region.end():]
def replace(self, edit, region, value):
self._t("replace")
self.text = self.text[:region.begin()] + value + self.text[region.end():]
def insert(self, edit, point, value):
self._t("insert")
self.text = self.text[:point] + value + self.text[point:]
return len(value)
def fold(self, region):
self._t("fold")
self.folds.append((region.begin(), region.end()))
return True
def unfold(self, region):
self._t("unfold")
folded, self.folds = self.folds, []
return folded
def folded_regions(self):
self._t("folded_regions")
return [sublime.Region(a, b) for a, b in self.folds]
def add_regions(self, key, regions, scope="", *args, **kwargs):
self._t("add_regions")
# what crosses the process boundary is the regions, not just the call
self.c["regions.sent"] += len(regions)
self.c["regions.sent.merged" if "@" in key else "regions.sent.row"] += len(regions)
self.regions[key] = (scope, [(r.begin(), r.end()) for r in regions])
def get_regions(self, key):
self._t("get_regions")
_, spans = self.regions.get(key, ("", []))
self.c["regions.read"] += len(spans)
return [sublime.Region(a, b) for a, b in spans]
def erase_regions(self, key):
self._t("erase_regions")
self.regions.pop(key, None)
def settings(self):
self._t("settings")
return CountingSettings(self._settings, self.c)
def viewport_position(self):
self._t("viewport_position")
return (0, 0)
def viewport_extent(self):
self._t("viewport_extent")
return (800, ROWS * 20)
def layout_extent(self):
self._t("layout_extent")
return (800, len(self.text))
def text_to_layout(self, point):
self._t("text_to_layout")
return (0, point)
def set_viewport_position(self, position, animate=True):
self._t("set_viewport_position")
def line_height(self):
self._t("line_height")
return 20
def em_width(self):
self._t("em_width")
return 8
def name(self):
self._t("name")
return self._name
def set_name(self, name):
self._t("set_name")
self._name = name
def window(self):
self._t("window")
return object()
def sel(self):
self._t("sel")
return self.selection
def run_command(self, name, args=None):
self._t("run_command:" + name)
command = self.commands.get(name)
if command:
command.run(None, **(args or {}))
class CountingSettings:
"""Settings reads and writes are round trips too, and Terminus does one per run."""
def __init__(self, settings, counter):
self._settings = settings
self._c = counter
def get(self, key, default=None):
self._c["settings.get"] += 1
return self._settings.get(key, default)
def set(self, key, value):
self._c["settings.set"] += 1
self._settings[key] = value
def erase(self, key):
self._c["settings.erase"] += 1
self._settings.pop(key, None)
def has(self, key):
return key in self._settings
def add_on_change(self, *args):
pass
def clear_on_change(self, *args):
pass
class FakeProcess:
def __init__(self):
self.writes = []
def write(self, data):
self.writes.append(data)
# ------------------------------------------------------------------ workloads
def colour_flood(lines=2000, runs=6, palette=200):
"""Coloured output: `runs` SGR changes per line, drawn from `palette` colours."""
width = COLUMNS // runs
out = []
for i in range(lines):
parts = []
for r in range(runs):
parts.append("\x1b[38;5;{}m".format(16 + (i * runs + r) % palette))
parts.append(str((i + r) % 10) * width)
parts.append("\x1b[0m\r\n")
out.append("".join(parts))
return "".join(out)
def plain_flood(lines=2000):
return "".join("{:04d} ".format(i) + "x" * (COLUMNS - 5) + "\r\n" for i in range(lines))
def tui_repaint(frames=60):
"""A full-screen coloured app redrawing itself; one cell differs per frame."""
out = []
for f in range(frames):
parts = ["\x1b[H"]
for row in range(ROWS):
for r in range(4):
parts.append("\x1b[38;5;{}m".format(16 + (row * 4 + r) % 200))
parts.append(str((row + r) % 10) * (COLUMNS // 4))
parts.append("\x1b[0m")
if row < ROWS - 1:
parts.append("\r\n")
parts.append("\x1b[1;1H\x1b[38;5;99m{:04d}\x1b[0m".format(f))
out.append("".join(parts))
return "".join(out)
# ------------------------------------------------------------------- drivers
def drive(feed, need_to_render, render, data, tick):
"""Feed `data` in tick-sized reads, rendering whenever the plugin asks to."""
parse = 0.0
frames = 0
for i in range(0, len(data), tick):
start = time.perf_counter()
feed(data[i:i + tick])
parse += time.perf_counter() - start
if need_to_render():
render()
frames += 1
return parse, frames
def run_vterm(data, tick):
from vterm.emulator import VtScreen, VtStream
from vterm.render import VtermRenderCommand
from vterm.terminal import Terminal
counter = collections.Counter()
view = CountingView(counter)
terminal = Terminal(view)
terminal.scrollback_size = SCROLLBACK
terminal.process = FakeProcess()
terminal.screen = VtScreen(COLUMNS, ROWS, process=terminal.process, history=SCROLLBACK)
Terminal._terminals[view.id()] = terminal
stream = VtStream(terminal.screen)
command = VtermRenderCommand(view)
def render():
counter["run_command:vterm_render"] += 1
command.run(None)
try:
parse, frames = drive(stream.feed, terminal._need_to_render, render, data, tick)
finally:
del Terminal._terminals[view.id()]
return counter, parse, frames, len(view.regions)
def run_terminus(data, tick):
from terminus.ptty import TerminalScreen, TerminalStream
from terminus.render import TerminusRenderCommand, TerminusShowCursorCommand
from terminus.terminal import Terminal
counter = collections.Counter()
view = CountingView(counter)
terminal = Terminal(view)
terminal.detached = False
terminal.offset = 0
terminal.title = None
terminal.default_title = "Terminus"
terminal.screen = TerminalScreen(
COLUMNS, ROWS, process=FakeProcess(), clear_callback=lambda: None,
reset_callback=lambda: None, history=SCROLLBACK)
Terminal._terminals[view.id()] = terminal
stream = TerminalStream(terminal.screen)
command = TerminusRenderCommand(view)
command.scrollback_history_size = SCROLLBACK
view.commands["terminus_show_cursor"] = TerminusShowCursorCommand(view)
def render():
counter["run_command:terminus_render"] += 1
command.run(None)
# terminus clears dirty in its render loop, not in the command
terminal.screen.dirty.clear()
try:
parse, frames = drive(stream.feed, terminal._need_to_render, render, data, tick)
finally:
del Terminal._terminals[view.id()]
return counter, parse, frames, len(view.regions)
# -------------------------------------------------------------------- report
def measure(runner, data, tick):
counter, parse, frames, keys = runner(data, tick)
calls = sum(v for k, v in counter.items() if not k.startswith("regions."))
return {
"parse": parse * 1000,
"frames": frames,
"calls": calls,
"per_frame": calls / max(frames, 1),
"sent": counter["regions.sent"],
"sent_row": counter["regions.sent.row"],
"sent_merged": counter["regions.sent.merged"],
"read": counter["regions.read"],
"keys": keys,
"counter": counter,
}
ROW_FORMAT = " {:<26}{:>14}{:>14}{:>10}"
def report(title, data, tick, both):
print("=" * 66)
print("{}\n {:.0f} KiB fed in {} B reads".format(title, len(data) / 1024, tick))
print("=" * 66)
ours = measure(run_vterm, data, tick)
theirs = measure(run_terminus, data, tick) if both else None
def line(label, key, fmt="{:.0f}"):
mine = fmt.format(ours[key])
if not theirs:
print(ROW_FORMAT.format(label, mine, "-", "-"))
return
other = fmt.format(theirs[key])
ratio = theirs[key] / ours[key] if ours[key] else 0
print(ROW_FORMAT.format(label, mine, other, "{:.1f}x".format(ratio)))
print(ROW_FORMAT.format("", "vterm", "Terminus", "ratio"))
line("parse ms", "parse", "{:.1f}")
line("frames", "frames")
line("sublime calls", "calls")
line("calls / frame", "per_frame", "{:.1f}")
line("regions sent", "sent")
line(" of them merges", "sent_merged")
line("regions read back", "read")
line("live region keys", "keys")
top = sorted(((v, k) for k, v in ours["counter"].items() if not k.startswith("regions.")),
reverse=True)[:6]
print(" vterm top: " + ", ".join("{}={}".format(k, v) for v, k in top))
if theirs:
top = sorted(((v, k) for k, v in theirs["counter"].items() if not k.startswith("regions.")),
reverse=True)[:6]
print(" Terminus top: " + ", ".join("{}={}".format(k, v) for v, k in top))
print()
def main():
both = os.path.exists(TERMINUS)
if both:
sys.path.insert(0, TERMINUS)
else:
print("Terminus is not installed; reporting vterm alone.\n")
burst = 32 * 1024 # what a 30 ms tick collects during a flood
trickle = 512 # a few rows a frame, as a build log arrives
report("A. colour flood, 6 runs/line, 200 colours",
colour_flood(), burst, both)
report("B. colour flood, 6 runs/line, 8 colours",
colour_flood(palette=8), burst, both)
report("C. colour trickle, a few rows a frame",
colour_flood(lines=600), trickle, both)
report("D. plain flood, no colour",
plain_flood(), burst, both)
report("E. TUI repaint, 60 full-screen frames, one cell changes",
tui_repaint(), burst, both)
if __name__ == "__main__":
main()