wolfpld / tracy

Frame profiler
https://tracy.nereid.pl/
Other
9.87k stars 658 forks source link

Added basic Python Support #754

Closed Chekov2k closed 6 months ago

Chekov2k commented 6 months ago

Hello!

I just discovered this profiler a few days ago and I am loving it :) Thank you very much for this awesome tool!

I noticed that there is currently no support for Python (as far as I could tell). We are using pybind11 to create python-bindings for our C++ code and were looking for a way to gain performance insights into C++ and Python at the same time. Tracy was the obvious choice and I added some basic Python support using pybind11.

Currently supported functionality is:

Missing support so far:

To just test the Python code you can use the script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from time import sleep

import numpy as np

from tracy_client import *

def main():
    index = 0
    app_info("this is a python app")
    set_thread_name("python")
    plot_config("plot", PlotFormatType.Number, False, True, 0)
    while True:
        with ScopedZone("test", ColorType.Coral) as zone:
            index += 1

            frame_mark()
            image = np.full([400, 400, 4], index, dtype=np.uint8)
            frame_image(image.tobytes(), 400, 400, 0, False)

            zone.text(index)

            message(f"we are at index {index}")
            message(f"we are at index {index}", ColorType.Coral)

            plot("plot", index)

            sleep(0.1)

if __name__ == "__main__":
    main()

and run it via

export PYTHONPATH=tracy/python:$PYTHONPATH
./test.py

and connect with the UI.

Screenshot 2024-03-17 at 11 02 32

I'm not sure if you are interested at all in adding Python support or if they way I adjusted things suit you...

Please let me know what you think!

wolfpld commented 6 months ago

Looks cool. You seem to be mixing two things there: Python support and changes to add color support. Please make these separate. Also, please provide some documentation in the manual. The writeup you provided here that shows how to use this would be a good start.

Chekov2k commented 6 months ago

Thanks for the quick feedback! I will split the request in two and have a go at updating the documentation as well.

simonvanbernem commented 6 months ago

Since you already added colors to AllocSourceLocation, could you also propagate it to ___tracy_alloc_srcloc/___tracy_alloc_srcloc_name? That way the C api would also have access to your change.

Chekov2k commented 6 months ago

Since you already added colors to AllocSourceLocation, could you also propagate it to ___tracy_alloc_srcloc/___tracy_alloc_srcloc_name? That way the C api would also have access to your change.

Will do!

Chekov2k commented 6 months ago

I have added support for named frames and memory allocation. As requests, I added documentation section for the Python bindings with a little script to demonstrate the API usage as well. Moreover, example build instructions for the Python package are documented. Please let me know what you think.

Chekov2k commented 6 months ago

Fixed linux compile issues and CMake parameter handling

gedalia commented 6 months ago

couple of quick thoughts, I wound up adding an overload patch to tracy's publishing code:


--- a/public/client/TracyProfiler.cpp
+++ b/public/client/TracyProfiler.cpp
@@ -401,6 +401,10 @@ static const char* GetProcessName()
     auto buf = getprogname();
     if( buf ) processName = buf;
 #endif
+    const char* processNameOverride = GetEnvVar( "TRACY_PROCESS_NAME" );
+    if( processNameOverride ){
+        return processNameOverride;
+    }
     return processName;
 }

which let me do this in python:

os.environ["TRACY_PROCESS_NAME"] = process_name

so that if there were multiple python processes I could tell them apart. I bound a c++ function that could return true or false if tracy enable was # defed on or off

bool TracyPresent() {
#ifdef TRACY_ENABLE
    return true;
#else
    return false;
#endif
}

This worked as a generic logging mapper:


     # Connect to the Python Logging system:
    # Define a custom formatter with the desired format
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    class MyHandler(logging.Handler):

        def emit(self, record):
            formatted_string = formatter.format(record)
            profiler.TracyMarkerMessage(formatted_string, len(formatted_string), record.levelno)

    logger = logging.getLogger()
    logger.addHandler(MyHandler())

Sample decorators:

def SCOPED_FUNCTION():

    def decorator(func):
        if not HAS_BINDINGS:
            return func
        if not profiler.TracyPresent():
            return func

        @wraps(func)
        def inner(*args, **kwargs):
            full_function_name = func.__name__
            profiler.BeginScopedZone(full_function_name, func.__code__.co_filename,
                                     func.__code__.co_firstlineno)
            result = func(*args, **kwargs)
            profiler.EndZone()
            return result

        return inner

    return decorator

    def SCOPED_CLASS_FUNCTION(class_name):

    def decorator(func):
        if not HAS_BINDINGS:
            return func
        if not profiler.TracyPresent():
            return func

        @wraps(func)
        def inner(*args, **kwargs):
            full_function_name = f"{class_name}:{func.__name__}"
            profiler.BeginScopedZone(full_function_name, func.__code__.co_filename,
                                     func.__code__.co_firstlineno)
            result = func(*args, **kwargs)
            profiler.EndZone()
            return result

        return inner

    return decorator

def SCOPED_CLASS(class_name=None):
    """
    A class decorator to apply a decorator to all methods within a class.
    """
    decorator = SCOPED_CLASS_FUNCTION(class_name)

    def decorate(cls):
        if not HAS_BINDINGS:
            return cls

        if not profiler.TracyPresent():
            return cls

        for name, method in vars(cls).items():
            # Ensure it's a method, not a class variable
            if callable(method) and not name.startswith("__") and not isinstance(
                    method, staticmethod):
                # Check if the method has already been decorated
                if hasattr(method, '__wrapped__'):
                    # Skip decorating if already decorated
                    continue
                setattr(cls, name, decorator(method))
        return cls

    return decorate
Chekov2k commented 6 months ago

Those look like nice suggestions. I think we would need another pull request for the CPP code change for the env-var and then another to add the Python extensions in? I can also merge the python stuff in this one and wait for the other two MRs to go in.

Chekov2k commented 6 months ago

Added proper support for TRACY_ENABLE=OFF

Chekov2k commented 6 months ago

Added ScopedZoneDecorator and ScopedFrameDecorator and fixed bug that colour was not applied to ScopedZone.

Chekov2k commented 6 months ago

Added support to set the program name

gedalia commented 6 months ago

btw it sends an email every time you force push the branch. you could probably also just merge master. Just making it hard to tell when you are actually modifying the code.

Chekov2k commented 6 months ago

Ah... I was hoping it wouldn't... Will add commits on top. Apologies for the spam

Chekov2k commented 6 months ago

This is now in a state I am happy with. It supports both states of TRACY_ENABLE in a sane way and as far as I can tell everything works just fine (tested with a mixed C++/Python code base that was instrumented).

There is another option to integrate the Python bindings by having a completely separate repository, i.e. tracy-python. Which would you prefer @wolfpld ?

Any other comments on the pull request? Things that need changing?