diff --git a/scenarios/python_greenlet_multithreaded_3.11/Dockerfile b/scenarios/python_greenlet_multithreaded_3.11/Dockerfile new file mode 100644 index 0000000..95320dc --- /dev/null +++ b/scenarios/python_greenlet_multithreaded_3.11/Dockerfile @@ -0,0 +1,19 @@ +ARG BASE_IMAGE="prof-python-3.11" +FROM $BASE_IMAGE + +COPY ./scenarios/python_greenlet_multithreaded_3.11/requirements.txt /app/ +RUN chmod 644 /app/* + +WORKDIR /app + +RUN pip install --no-cache-dir -r requirements.txt + +COPY ./scenarios/python_greenlet_multithreaded_3.11/main.py /app/ + +ENV DD_PROFILING_ENABLED=true +ENV DD_TRACE_ENABLED=false +ENV DD_TRACE_DEBUG=false +ENV DD_PROFILING_OUTPUT_PPROF="/app/data/profiles" +ENV EXECUTION_TIME_SEC="10" + +CMD ddtrace-run python main.py diff --git a/scenarios/python_greenlet_multithreaded_3.11/expected_profile.json b/scenarios/python_greenlet_multithreaded_3.11/expected_profile.json new file mode 100644 index 0000000..fe34a70 --- /dev/null +++ b/scenarios/python_greenlet_multithreaded_3.11/expected_profile.json @@ -0,0 +1,102 @@ +{ + "test_name": "python_greenlet_multithreaded_3.11", + "scale_by_duration": false, + "pprof-regex": "", + "stacks": [ + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*MathWorker.*work_fibonacci.*", + "percent": 25, + "error_margin": 10, + "labels": [ + { + "key": "thread name", + "values": [ + "MathWorker" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*MathWorker.*work_prime.*", + "percent": 10, + "error_margin": 10, + "labels": [ + { + "key": "thread name", + "values": [ + "MathWorker" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*MathWorker.*work_prime;.*genexpr.*", + "percent": 7, + "error_margin": 10, + "labels": [ + { + "key": "thread name", + "values": [ + "MathWorker" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*UtilWorker.*work_string.*", + "percent": 15, + "error_margin": 10, + "labels": [ + { + "key": "thread name", + "values": [ + "UtilWorker" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*UtilWorker.*work_list.*", + "percent": 20, + "error_margin": 10, + "labels": [ + { + "key": "thread name", + "values": [ + "UtilWorker" + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/scenarios/python_greenlet_multithreaded_3.11/main.py b/scenarios/python_greenlet_multithreaded_3.11/main.py new file mode 100644 index 0000000..93c940c --- /dev/null +++ b/scenarios/python_greenlet_multithreaded_3.11/main.py @@ -0,0 +1,109 @@ +"""Test scenario for greenlet profiling with multiple threads.""" + +import random +import threading +import time + +import greenlet + + +class MathWorker: + def __init__(self) -> None: + self.end_time = time.monotonic() + 10 + + self.fib_greenlet = greenlet.greenlet(self.work_fibonacci) + self.prime_greenlet = greenlet.greenlet(self.work_prime) + + self.last_fib = 0 + self.last_prime = 0 + + def do_work(self) -> None: + self.fib_greenlet.switch() + print(f"Fibonacci: {self.last_fib}, Prime: {self.last_prime}") + + def work_fibonacci(self) -> None: + while True: + # Find the next Fibonacci number + self.last_fib = self.last_fib + 1 + + a, b = 0, 1 + for _ in range(self.last_fib): + a, b = b, a + b + + if self.end_time < time.monotonic(): + return + + self.prime_greenlet.switch() + + def work_prime(self) -> None: + while True: + for _ in range(5): + # Find the next prime number after self.last_prime + candidate = self.last_prime + 1 + while True: + is_prime = candidate > 1 and all(candidate % i != 0 for i in range(2, int(candidate**0.5) + 1)) + if is_prime: + self.last_prime = candidate + break + candidate += 1 + + if self.end_time < time.monotonic(): + return + + self.fib_greenlet.switch() + + +class UtilWorker: + def __init__(self) -> None: + self.end_time = time.monotonic() + 10 + + self.string_greenlet = greenlet.greenlet(self.work_string) + self.list_greenlet = greenlet.greenlet(self.work_list) + + self.string_iterations = 0 + self.list_iterations = 0 + + def do_work(self) -> None: + self.string_greenlet.switch() + print(f"String iterations: {self.string_iterations}, List iterations: {self.list_iterations}") + + def work_string(self) -> None: + while True: + result = "" + for _ in range(100): + result += str(random.randint(0, 100)) # noqa: S311 + + self.string_iterations += 1 + + if self.end_time < time.monotonic(): + return + + self.list_greenlet.switch() + + def work_list(self) -> None: + while True: + sums = [1] + for _ in range(100): + sums.append(sum(sums)) + + self.list_iterations += 1 + + if self.end_time < time.monotonic(): + return + + self.string_greenlet.switch() + + +def main() -> None: + t = threading.Thread(target=lambda: MathWorker().do_work(), name="MathWorker") + u = threading.Thread(target=lambda: UtilWorker().do_work(), name="UtilWorker") + + t.start() + u.start() + + t.join() + u.join() + + +if __name__ == "__main__": + main() diff --git a/scenarios/python_greenlet_multithreaded_3.11/requirements.txt b/scenarios/python_greenlet_multithreaded_3.11/requirements.txt new file mode 100644 index 0000000..ca2deb4 --- /dev/null +++ b/scenarios/python_greenlet_multithreaded_3.11/requirements.txt @@ -0,0 +1,2 @@ +ddtrace +greenlet