| 
13 | 13 | from ddtrace.profiling.collector import stack  | 
14 | 14 | from ddtrace.settings.profiling import config  | 
15 | 15 | from tests.profiling.collector import pprof_utils  | 
 | 16 | +from tests.profiling.collector import test_collector  | 
16 | 17 | 
 
  | 
17 | 18 | 
 
  | 
18 | 19 | # Python 3.11.9 is not compatible with gevent, https://github.com/gevent/gevent/issues/2040  | 
 | 
24 | 25 | )  | 
25 | 26 | 
 
  | 
26 | 27 | 
 
  | 
 | 28 | +# Use subprocess as ddup config persists across tests.  | 
 | 29 | +@pytest.mark.subprocess(  | 
 | 30 | +    env=dict(  | 
 | 31 | +        DD_PROFILING_MAX_FRAMES="5",  | 
 | 32 | +        DD_PROFILING_OUTPUT_PPROF="/tmp/test_collect_truncate",  | 
 | 33 | +        DD_PROFILING_STACK_V2_ENABLED="1",  | 
 | 34 | +    )  | 
 | 35 | +)  | 
 | 36 | +@pytest.mark.skipif(sys.version_info[:2] == (3, 7), reason="stack_v2 is not supported on Python 3.7")  | 
 | 37 | +def test_collect_truncate():  | 
 | 38 | +    import os  | 
 | 39 | + | 
 | 40 | +    from ddtrace.profiling import profiler  | 
 | 41 | +    from tests.profiling.collector import pprof_utils  | 
 | 42 | +    from tests.profiling.collector.test_stack import func1  | 
 | 43 | + | 
 | 44 | +    pprof_prefix = os.environ["DD_PROFILING_OUTPUT_PPROF"]  | 
 | 45 | +    output_filename = pprof_prefix + "." + str(os.getpid())  | 
 | 46 | + | 
 | 47 | +    max_nframes = int(os.environ["DD_PROFILING_MAX_FRAMES"])  | 
 | 48 | + | 
 | 49 | +    p = profiler.Profiler()  | 
 | 50 | +    p.start()  | 
 | 51 | + | 
 | 52 | +    func1()  | 
 | 53 | + | 
 | 54 | +    p.stop()  | 
 | 55 | + | 
 | 56 | +    profile = pprof_utils.parse_profile(output_filename)  | 
 | 57 | +    samples = pprof_utils.get_samples_with_value_type(profile, "wall-time")  | 
 | 58 | +    assert len(samples) > 0  | 
 | 59 | +    for sample in samples:  | 
 | 60 | +        # stack v2 adds one extra frame for "%d frames omitted" message  | 
 | 61 | +        # Also, it allows max_nframes + 1 frames, so we add 2 here.  | 
 | 62 | +        assert len(sample.location_id) <= max_nframes + 2, len(sample.location_id)  | 
 | 63 | + | 
 | 64 | + | 
27 | 65 | @pytest.mark.parametrize("stack_v2_enabled", [True, False])  | 
28 | 66 | def test_stack_locations(stack_v2_enabled, tmp_path):  | 
29 | 67 |     if sys.version_info[:2] == (3, 7) and stack_v2_enabled:  | 
@@ -651,8 +689,23 @@ def _dofib():  | 
651 | 689 |     assert checked_thread, "No samples found for the expected threads"  | 
652 | 690 | 
 
  | 
653 | 691 | 
 
  | 
 | 692 | +def test_max_time_usage():  | 
 | 693 | +    with pytest.raises(ValueError):  | 
 | 694 | +        stack.StackCollector(None, max_time_usage_pct=0)  | 
 | 695 | + | 
 | 696 | + | 
 | 697 | +def test_max_time_usage_over():  | 
 | 698 | +    with pytest.raises(ValueError):  | 
 | 699 | +        stack.StackCollector(None, max_time_usage_pct=200)  | 
 | 700 | + | 
 | 701 | + | 
654 | 702 | @pytest.mark.parametrize(  | 
655 |  | -    ("stack_v2_enabled", "ignore_profiler"), [(True, True), (True, False), (False, True), (False, False)]  | 
 | 703 | +    "stack_v2_enabled",  | 
 | 704 | +    [True, False],  | 
 | 705 | +)  | 
 | 706 | +@pytest.mark.parametrize(  | 
 | 707 | +    "ignore_profiler",  | 
 | 708 | +    [True, False],  | 
656 | 709 | )  | 
657 | 710 | def test_ignore_profiler(stack_v2_enabled, ignore_profiler, tmp_path):  | 
658 | 711 |     if sys.version_info[:2] == (3, 7) and stack_v2_enabled:  | 
@@ -691,3 +744,79 @@ def test_ignore_profiler(stack_v2_enabled, ignore_profiler, tmp_path):  | 
691 | 744 |         assert collector_worker_thread_id in thread_ids  | 
692 | 745 |     else:  | 
693 | 746 |         assert collector_worker_thread_id not in thread_ids  | 
 | 747 | + | 
 | 748 | + | 
 | 749 | +# TODO: support ignore profiler with stack_v2 and update this test  | 
 | 750 | +@pytest.mark.skipif(not TESTING_GEVENT, reason="Not testing gevent")  | 
 | 751 | +@pytest.mark.subprocess(  | 
 | 752 | +    ddtrace_run=True,  | 
 | 753 | +    env=dict(DD_PROFILING_IGNORE_PROFILER="1", DD_PROFILING_OUTPUT_PPROF="/tmp/test_ignore_profiler_gevent_task"),  | 
 | 754 | +)  | 
 | 755 | +def test_ignore_profiler_gevent_task():  | 
 | 756 | +    import gevent.monkey  | 
 | 757 | + | 
 | 758 | +    gevent.monkey.patch_all()  | 
 | 759 | + | 
 | 760 | +    import os  | 
 | 761 | +    import time  | 
 | 762 | +    import typing  | 
 | 763 | + | 
 | 764 | +    from ddtrace.profiling import collector  | 
 | 765 | +    from ddtrace.profiling import event as event_mod  | 
 | 766 | +    from ddtrace.profiling import profiler  | 
 | 767 | +    from ddtrace.profiling.collector import stack  | 
 | 768 | +    from tests.profiling.collector import pprof_utils  | 
 | 769 | + | 
 | 770 | +    def _fib(n):  | 
 | 771 | +        if n == 1:  | 
 | 772 | +            return 1  | 
 | 773 | +        elif n == 0:  | 
 | 774 | +            return 0  | 
 | 775 | +        else:  | 
 | 776 | +            return _fib(n - 1) + _fib(n - 2)  | 
 | 777 | + | 
 | 778 | +    class CollectorTest(collector.PeriodicCollector):  | 
 | 779 | +        def collect(self) -> typing.Iterable[typing.Iterable[event_mod.Event]]:  | 
 | 780 | +            _fib(22)  | 
 | 781 | +            return []  | 
 | 782 | + | 
 | 783 | +    output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"]  | 
 | 784 | + | 
 | 785 | +    p = profiler.Profiler()  | 
 | 786 | + | 
 | 787 | +    p.start()  | 
 | 788 | + | 
 | 789 | +    for c in p._profiler._collectors:  | 
 | 790 | +        if isinstance(c, stack.StackCollector):  | 
 | 791 | +            c.ignore_profiler  | 
 | 792 | + | 
 | 793 | +    c = CollectorTest(None, interval=0.00001)  | 
 | 794 | +    c.start()  | 
 | 795 | + | 
 | 796 | +    time.sleep(3)  | 
 | 797 | + | 
 | 798 | +    worker_ident = c._worker.ident  | 
 | 799 | + | 
 | 800 | +    c.stop()  | 
 | 801 | +    p.stop()  | 
 | 802 | + | 
 | 803 | +    profile = pprof_utils.parse_profile(output_filename + "." + str(os.getpid()))  | 
 | 804 | + | 
 | 805 | +    samples = pprof_utils.get_samples_with_value_type(profile, "cpu-time")  | 
 | 806 | + | 
 | 807 | +    thread_ids = set()  | 
 | 808 | +    for sample in samples:  | 
 | 809 | +        thread_id_label = pprof_utils.get_label_with_key(profile.string_table, sample, "thread id")  | 
 | 810 | +        thread_id = int(thread_id_label.num)  | 
 | 811 | +        thread_ids.add(thread_id)  | 
 | 812 | + | 
 | 813 | +    assert worker_ident not in thread_ids  | 
 | 814 | + | 
 | 815 | + | 
 | 816 | +def test_repr():  | 
 | 817 | +    test_collector._test_repr(  | 
 | 818 | +        stack.StackCollector,  | 
 | 819 | +        "StackCollector(status=<ServiceStatus.STOPPED: 'stopped'>, "  | 
 | 820 | +        "recorder=Recorder(default_max_events=16384, max_events={}), min_interval_time=0.01, max_time_usage_pct=1.0, "  | 
 | 821 | +        "nframes=64, ignore_profiler=False, endpoint_collection_enabled=None, tracer=None)",  | 
 | 822 | +    )  | 
0 commit comments