git » git-arr » commit 182c807

Add optional debug tracing to help with performance analysis

author Alberto Bertogli
2025-05-18 17:32:52 UTC
committer Alberto Bertogli
2025-05-18 20:48:43 UTC
parent 6b440988781999187058b041248a60e49aaa0245

Add optional debug tracing to help with performance analysis

It can be very time consuming to identify why git-arr is taking time to
run.

While end users are not expected to do that kind of debugging, it can be
very useful for specific situations.

Today, git-arr performance is quite opaque and it's hard to know what is
it doing or how long different calls are taking.

This patch introduces some basic tracing, that is shown to stderr when
the GIT_ARR_DEBUG env variable is set.

This is a debugging aid and subject to change in the future in
backwards-incompatible ways.

git-arr +5 -0
git.py +24 -0
utils.py +30 -0

diff --git a/git-arr b/git-arr
index 337912a..9148d92 100755
--- a/git-arr
+++ b/git-arr
@@ -6,7 +6,9 @@ git-arr: A git web html generator.
 import configparser
 import math
 import optparse
+import functools
 import os
+import time
 import re
 import sys
 from typing import Union
@@ -217,6 +219,7 @@ def with_utils(f):
     return wrapped
 
 
+@utils.log_timing()
 @bottle.route("/")
 @bottle.view("index")
 @with_utils
@@ -224,6 +227,7 @@ def index():
     return dict(repos=repos)
 
 
+@utils.log_timing()
 @bottle.route("/r/<repo:repo>/")
 @bottle.view("summary")
 @with_utils
@@ -332,6 +336,7 @@ def is_404(e):
 def generate(output: str, only=None):
     """Generate static html to the output directory."""
 
+    @utils.log_timing("path")
     def write_to(path: str, func_or_str, args=(), mtime=None):
         path = output + "/" + path
         dirname = os.path.dirname(path)
diff --git a/git.py b/git.py
index 8402c8c..0314f07 100644
--- a/git.py
+++ b/git.py
@@ -9,6 +9,8 @@ parameters.
 import functools
 import sys
 import io
+import time
+import os
 import subprocess
 from collections import defaultdict
 import email.utils
@@ -30,6 +32,28 @@ def run_git(
     This function invokes git with the given parameters, and returns a
     file-like object with the output (from a pipe).
     """
+    start = time.time()
+    out = _run_git(
+        repo_path, params, stdin, silent_stderr=silent_stderr, raw=raw
+    )
+    end = time.time()
+
+    if os.environ.get("GIT_ARR_DEBUG"):
+        sys.stderr.write(
+            "%.4fs  %s  %s\n"
+            % (end - start, repo_path[-30:], " ".join(params))
+        )
+
+    return out
+
+
+def _run_git(
+    repo_path: str, params, stdin: bytes = None, silent_stderr=False, raw=False
+) -> Union[IO[str], IO[bytes]]:
+    """Invokes git with the given parameters.
+
+    This is the real run_git function, which is called by run_git().
+    """
     params = [GIT_BIN, "--git-dir=%s" % repo_path] + list(params)
 
     stderr = None
diff --git a/utils.py b/utils.py
index 9fb6544..e12c477 100644
--- a/utils.py
+++ b/utils.py
@@ -30,6 +30,10 @@ import base64
 import functools
 import mimetypes
 import string
+import inspect
+import sys
+import time
+import os
 import os.path
 
 import git
@@ -194,3 +198,29 @@ else:
     @functools.lru_cache
     def markdown_blob(s: str) -> str:
         raise RuntimeError("markdown_blob() called without markdown support")
+
+
+def log_timing(*log_args):
+    "Decorator to log how long a function call took."
+    if not os.environ.get("GIT_ARR_DEBUG"):
+        return lambda f: f
+
+    def log_timing_decorator(f):
+        argspec = inspect.getfullargspec(f)
+        idxs = [argspec.args.index(arg) for arg in log_args]
+
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            start = time.time()
+            result = f(*args, **kwargs)
+            end = time.time()
+
+            f_args = [args[i] for i in idxs]
+            sys.stderr.write(
+                "%.4fs  %s %s\n" % (end - start, f.__name__, " ".join(f_args))
+            )
+            return result
+
+        return wrapper
+
+    return log_timing_decorator