mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-09-01 06:41:30 +00:00
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
242 lines
7.1 KiB
Python
Executable file
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()
|