mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-09-04 00:46:17 +00:00
Switch to perf for profiling from gperftools
This commit is contained in:
parent
a432ef3774
commit
f5ae245f65
4 changed files with 117 additions and 35 deletions
105
benchmark.py
105
benchmark.py
|
|
@ -5,19 +5,74 @@ import argparse
|
|||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
from pty import CHILD, fork
|
||||
|
||||
from kitty.constants import kitten_exe
|
||||
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')
|
||||
PERF_OUTPUT = '/tmp/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-thread CPU breakdown ---\n')
|
||||
subprocess.run(
|
||||
[
|
||||
perf_exe,
|
||||
'report',
|
||||
'--stdio',
|
||||
'-n',
|
||||
'--sort',
|
||||
'overhead,tid,comm,symbol',
|
||||
'--percent-limit',
|
||||
'1.0',
|
||||
'-i',
|
||||
PERF_OUTPUT,
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
|
||||
print(f'\n{sep}\n')
|
||||
|
||||
|
||||
def run_parsing_benchmark(
|
||||
|
|
@ -90,6 +145,35 @@ def run_parsing_benchmark(
|
|||
sys.stdout.write(str(screen.linebuf))
|
||||
|
||||
|
||||
def exec_under_perf(perf_exe: str) -> 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 we run
|
||||
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)
|
||||
run_perf_reports(perf_exe)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description='Run kitty parsing benchmarks')
|
||||
p.add_argument(
|
||||
|
|
@ -106,7 +190,26 @@ def main() -> None:
|
|||
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, '
|
||||
'then prints per-thread CPU breakdown and call-graph hotspots before benchmark results. '
|
||||
'Requires perf in PATH with setcap cap_sys_admin,cap_sys_ptrace,cap_syslog=ep /usr/bin/perf'
|
||||
),
|
||||
)
|
||||
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)
|
||||
return
|
||||
|
||||
benchmarks = tuple(args.benchmarks) if args.benchmarks else ALL_BENCHMARKS
|
||||
run_parsing_benchmark(benchmarks=benchmarks, with_scrollback=args.with_scrollback)
|
||||
|
||||
|
|
|
|||
|
|
@ -142,9 +142,15 @@ admittedly biased, eyes).
|
|||
Instrumenting kitty
|
||||
-----------------------
|
||||
|
||||
You can generate detailed per-function performance data using
|
||||
`gperftools <https://github.com/gperftools/gperftools>`__. Build |kitty| with
|
||||
``make profile``. Run kitty and perform the task you want to analyse, for
|
||||
example, scrolling a large file with :program:`less`. After you quit, function
|
||||
call statistics will be displayed in *KCachegrind*. Hence, profiling is best done
|
||||
on Linux which has these tools easily available.
|
||||
To profile kitty performance on Linux, first install ``perf`` and run::
|
||||
|
||||
sudo setcap cap_sys_admin,cap_sys_ptrace,cap_syslog=ep /usr/bin/perf
|
||||
|
||||
Then build kitty with::
|
||||
|
||||
make profile
|
||||
|
||||
Finally get benchmark and profiling results using::
|
||||
|
||||
./benchmark.py --perf
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,6 @@
|
|||
#include <stdio.h>
|
||||
#include <locale.h>
|
||||
|
||||
#ifdef WITH_PROFILER
|
||||
#include <gperftools/profiler.h>
|
||||
#endif
|
||||
|
||||
#include "monotonic.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
|
@ -351,22 +347,6 @@ pyset_iutf8(PyObject UNUSED *self, PyObject *args) {
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#ifdef WITH_PROFILER
|
||||
static PyObject *
|
||||
start_profiler(PyObject UNUSED *self, PyObject *args) {
|
||||
char *path;
|
||||
if (!PyArg_ParseTuple(args, "s", &path)) return NULL;
|
||||
ProfilerStart(path);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
stop_profiler(PyObject UNUSED *self, PyObject *args UNUSED) {
|
||||
ProfilerStop();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool
|
||||
put_tty_in_raw_mode(int fd, const struct termios *termios_p, bool read_with_timeout, int optional_actions) {
|
||||
struct termios raw_termios = *termios_p;
|
||||
|
|
@ -880,10 +860,6 @@ static PyMethodDef module_methods[] = {
|
|||
#ifdef __APPLE__
|
||||
METHODB(user_cache_dir, METH_NOARGS),
|
||||
METHODB(process_group_map, METH_NOARGS),
|
||||
#endif
|
||||
#ifdef WITH_PROFILER
|
||||
{"start_profiler", (PyCFunction)start_profiler, METH_VARARGS, ""},
|
||||
{"stop_profiler", (PyCFunction)stop_profiler, METH_NOARGS, ""},
|
||||
#endif
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
|
|
|||
5
setup.py
5
setup.py
|
|
@ -675,7 +675,6 @@ def init_env(
|
|||
if profile:
|
||||
cppflags.append('-DWITH_PROFILER')
|
||||
cflags.append('-g3')
|
||||
ldflags.append('-lprofiler')
|
||||
|
||||
if debug or profile:
|
||||
cflags.append('-fno-omit-frame-pointer')
|
||||
|
|
@ -1602,8 +1601,6 @@ def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 's
|
|||
cflags.extend(sanitize_args)
|
||||
ldflags.extend(sanitize_args)
|
||||
libs += ['-lasan'] if not is_macos and env.compiler_type is not CompilerType.clang else []
|
||||
if args.profile:
|
||||
libs.append('-lprofiler')
|
||||
else:
|
||||
cflags.append('-g3' if args.debug else '-O3')
|
||||
if bundle_type.endswith('-freeze'):
|
||||
|
|
@ -2299,7 +2296,7 @@ def option_parser() -> argparse.ArgumentParser: # {{{
|
|||
' the Python used to run setup.py is queried for these.',
|
||||
)
|
||||
p.add_argument('--full', dest='incremental', default=Options.incremental, action='store_false', help='Do a full build, even for unchanged files')
|
||||
p.add_argument('--profile', default=Options.profile, action='store_true', help='Use the -pg compile flag to add profiling information')
|
||||
p.add_argument('--profile', default=Options.profile, action='store_true', help='Use compile flags to add profiling information')
|
||||
p.add_argument(
|
||||
'--libdir-name', default=Options.libdir_name, help='The name of the directory inside --prefix in which to store compiled files. Defaults to "lib"'
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue