changeset 51761:812a094a7477

profiling: add a py-spy profiling backend The recommended way to use this backend is by setting the config `profiling.output` to point to a file because py-spy output is not human-readable.
author Arseniy Alekseyev <aalekseyev@janestreet.com>
date Thu, 01 Aug 2024 13:07:13 +0100
parents 421c9b3f2f4e
children dcbe7fda53e4
files mercurial/configitems.toml mercurial/profiling.py
diffstat 2 files changed, 64 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/configitems.toml	Thu Aug 01 13:38:31 2024 +0100
+++ b/mercurial/configitems.toml	Thu Aug 01 13:07:13 2024 +0100
@@ -1811,6 +1811,20 @@
 default = "stat"
 
 [[items]]
+section = "profiling"
+name = "py-spy.exe"
+default = "py-spy"
+
+[[items]]
+section = "profiling"
+name = "py-spy.freq"
+default = 100
+
+[[items]]
+section = "profiling"
+name = "py-spy.format"
+
+[[items]]
 section = "progress"
 name = "assume-tty"
 default = false
--- a/mercurial/profiling.py	Thu Aug 01 13:38:31 2024 +0100
+++ b/mercurial/profiling.py	Thu Aug 01 13:07:13 2024 +0100
@@ -7,6 +7,9 @@
 
 
 import contextlib
+import os
+import signal
+import subprocess
 
 from .i18n import _
 from .pycompat import (
@@ -175,6 +178,50 @@
         fp.flush()
 
 
+@contextlib.contextmanager
+def pyspy_profile(ui, fp):
+    exe = ui.config(b'profiling', b'py-spy.exe')
+
+    freq = ui.configint(b'profiling', b'py-spy.freq')
+
+    format = ui.config(b'profiling', b'py-spy.format')
+
+    fd = fp.fileno()
+
+    output_path = "/dev/fd/%d" % (fd)
+
+    my_pid = os.getpid()
+
+    cmd = [
+        exe,
+        "record",
+        "--pid",
+        str(my_pid),
+        "--native",
+        "--rate",
+        str(freq),
+        "--output",
+        output_path,
+    ]
+
+    if format:
+        cmd.extend(["--format", format])
+
+    proc = subprocess.Popen(
+        cmd,
+        pass_fds={fd},
+        stdout=subprocess.PIPE,
+    )
+
+    _ = proc.stdout.readline()
+
+    try:
+        yield
+    finally:
+        os.kill(proc.pid, signal.SIGINT)
+        proc.communicate()
+
+
 class profile:
     """Start profiling.
 
@@ -214,7 +261,7 @@
         proffn = None
         if profiler is None:
             profiler = self._ui.config(b'profiling', b'type')
-        if profiler not in (b'ls', b'stat', b'flame'):
+        if profiler not in (b'ls', b'stat', b'flame', b'py-spy'):
             # try load profiler from extension with the same name
             proffn = _loadprofiler(self._ui, profiler)
             if proffn is None:
@@ -257,6 +304,8 @@
                 proffn = lsprofile
             elif profiler == b'flame':
                 proffn = flameprofile
+            elif profiler == b'py-spy':
+                proffn = pyspy_profile
             else:
                 proffn = statprofile