kitty/benchmark.py
Kovid Goyal e62ec6d540
Throughput improvements when processing large blocks of text
Mostly comes from detecting the case of runs of ASCII printable
characters and using a special fast path for them. Improves benchmarks
as per benchmark.py by 15-30% for text heavy workloads
2026-08-16 17:47:56 +05:30

242 lines
7.1 KiB
Python
Executable file

#!./kitty/launcher/kitty +launch
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import argparse
import fcntl
import os
import select
import shutil
import signal
import struct
import subprocess
import sys
import tempfile
import termios
import time
from pty import CHILD, fork
from kitty.constants import kitten_exe, kitty_exe
from kitty.fast_data_types import ChildMonitor, Screen, safe_pipe
from kitty.utils import read_screen_size
BENCHMARK_WINDOW_ID = 1
ALL_BENCHMARKS = ('ascii', 'unicode', 'unique_unicode', 'csi', 'images', 'long_escape_codes')
def perf_output() -> str:
return os.path.join(tempfile.gettempdir(), 'kitty-benchmark.perf')
# Set by the re-exec wrapper so we don't recurse when --perf is in argv.
_UNDER_PERF_ENV = '_KITTY_BENCHMARK_UNDER_PERF'
def find_perf() -> str | None:
return shutil.which('perf')
def run_perf_reports(perf_exe: str) -> None:
sep = '=' * 70
print(f'\n{sep}')
print('PERF PROFILING RESULTS')
print(sep)
print(f'Profile data saved to: {perf_output()}')
print(f'Re-run interactively: perf report -i {perf_output()}\n')
print('--- Top CPU hotspots (call graph, >=0.5% threshold) ---\n')
subprocess.run(
[
perf_exe,
'report',
'--stdio',
'-n',
'--call-graph',
'fractal,0.5',
'--percent-limit',
'0.5',
'-i',
perf_output(),
],
check=False,
)
print('\n--- Per-process CPU breakdown ---\n')
subprocess.run(
[
perf_exe,
'report',
'--stdio',
'-n',
'--sort',
'overhead,pid,comm,symbol',
'--percent-limit',
'1.0',
'-i',
perf_output(),
],
check=False,
)
print(f'\n{sep}\n')
def run_parsing_benchmark(
benchmarks: tuple[str, ...] = ALL_BENCHMARKS,
with_scrollback: bool = True,
cell_width: int = 10,
cell_height: int = 20,
scrollback: int = 20000,
repetitions: int | None = None,
) -> None:
isatty = sys.stdout.isatty()
if isatty:
sz = read_screen_size()
columns, rows = sz.cols, sz.rows
else:
columns, rows = 80, 25
child_pid, master_fd = fork()
is_child = child_pid == CHILD
# we add render as we arent rendering anyway and it means the synchronized
# escape codes are no longer needed.
argv = [kitten_exe(), '__benchmark__', '--render']
if with_scrollback:
argv.append('--with-scrollback')
if repetitions is not None:
argv.extend(['--repetitions', str(repetitions)])
argv.extend(benchmarks)
if is_child:
while read_screen_size().width != columns * cell_width:
time.sleep(0.01)
signal.pthread_sigmask(signal.SIG_SETMASK, ())
os.execvp(argv[0], argv)
x_pixels = columns * cell_width
y_pixels = rows * cell_height
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
fcntl.ioctl(master_fd, termios.TIOCSWINSZ, s)
child_died = False
def on_child_death(window_id: int, died: bool, exit_status: int) -> None:
nonlocal child_died
child_died = True
child_monitor = ChildMonitor(on_child_death, None)
# r_pipe: benchmark polls this; w_pipe: io_thread writes here on data ready
r_pipe, w_pipe = safe_pipe(False)
child_monitor.set_wakeup_fd(w_pipe)
screen = Screen(None, rows, columns, scrollback, cell_width, cell_height, BENCHMARK_WINDOW_ID)
child_monitor.add_child(BENCHMARK_WINDOW_ID, child_pid, master_fd, screen)
child_monitor.start()
try:
while not child_died:
rd, _, _ = select.select([r_pipe], [], [], 1.0)
if rd:
# drain all accumulated wakeup bytes
try:
os.read(r_pipe, 256)
except OSError:
pass
child_monitor.parse_input_once()
finally:
child_monitor.shutdown_monitor() # io_loop closes master_fd via cleanup_child
os.close(r_pipe)
os.close(w_pipe)
if isatty:
lines: list[str] = []
screen.linebuf.as_ansi(lines.append)
sys.stdout.write(''.join(lines))
else:
sys.stdout.write(str(screen.linebuf))
def exec_under_perf(perf_exe: str, print_report: bool = False) -> None:
"""Re-exec this script as a child of perf record.
perf becomes the outer process so it can profile the entire benchmark
run without any subprocess/SIGCHLD conflicts with ChildMonitor.
After the benchmark exits perf finalises its output, then optionally
runs perf report to print the results.
"""
script = os.path.abspath(__file__)
env = {**os.environ, _UNDER_PERF_ENV: '1'}
cmd = [
perf_exe,
'record',
'-g',
'-F',
'999',
'--call-graph',
'dwarf',
'-o',
perf_output(),
'--',
kitty_exe(),
'+launch',
script,
] + sys.argv[1:]
subprocess.run(cmd, env=env, check=False)
if print_report:
run_perf_reports(perf_exe)
else:
print(f'Profile data saved to: {perf_output()}')
def main() -> None:
p = argparse.ArgumentParser(description='Run kitty parsing benchmarks')
p.add_argument(
'benchmarks',
nargs='*',
choices=list(ALL_BENCHMARKS),
metavar='BENCHMARK',
help=f'Benchmarks to run (default: all). Choose from: {", ".join(ALL_BENCHMARKS)}',
)
p.add_argument(
'--with-scrollback',
dest='with_scrollback',
action=argparse.BooleanOptionalAction,
default=True,
help='Use the main screen instead of the alt screen so scrollback speed is also tested (default: enabled)',
)
p.add_argument(
'--perf',
action='store_true',
default=False,
help=(
'Profile with Linux perf: records at 999 Hz with DWARF call graphs '
'and saves raw data to ' + perf_output() + '. '
'Requires perf in PATH with setcap cap_sys_admin,cap_sys_ptrace,cap_syslog=ep /usr/bin/perf'
),
)
p.add_argument(
'--perf-report',
action='store_true',
default=False,
help='After --perf recording, print call-graph hotspots and per-process CPU breakdown to stdout (default: only print path to raw data)',
)
p.add_argument(
'--repetitions',
type=int,
default=None,
metavar='N',
help='Number of repetitions of each benchmark (default: kitten default of 100)',
)
args = p.parse_args()
if args.perf and not os.environ.get(_UNDER_PERF_ENV):
perf_exe = find_perf()
if perf_exe is None:
print('Warning: perf not found in PATH, running without profiling', file=sys.stderr)
else:
exec_under_perf(perf_exe, print_report=args.perf_report)
return
benchmarks = tuple(args.benchmarks) if args.benchmarks else ALL_BENCHMARKS
run_parsing_benchmark(benchmarks=benchmarks, with_scrollback=args.with_scrollback, repetitions=args.repetitions)
if __name__ == '__main__':
main()