Switch to perf for profiling from gperftools

This commit is contained in:
Kovid Goyal 2026-08-16 11:18:06 +05:30
parent a432ef3774
commit f5ae245f65
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 117 additions and 35 deletions

View file

@ -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)

View file

@ -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

View file

@ -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 */
};

View file

@ -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"'
)