-
Notifications
You must be signed in to change notification settings - Fork 469
feat(profiling): track asyncio.wait
#15338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
releasenotes/notes/profiling-track-asyncio-wait-5c7c5f7e06760c9f.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| features: | ||
| - | | ||
| profiling: This introduces tracking for ``asyncio.wait`` in the Profiler. | ||
| This makes it possible to track dependencies between Tasks/Coroutines that await/are awaited through ``asyncio.wait``. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| @pytest.mark.subprocess( | ||
| env=dict( | ||
| DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_utils_gather", | ||
| ), | ||
| err=None, | ||
| ) | ||
| # For macOS: err=None ignores expected stderr from tracer failing to connect to agent (not relevant to this test) | ||
| def test_asyncio_gather() -> None: | ||
| import asyncio | ||
| import os | ||
| import time | ||
| import uuid | ||
|
|
||
| from ddtrace import ext | ||
| from ddtrace.internal.datadog.profiling import stack_v2 | ||
| from ddtrace.profiling import profiler | ||
| from ddtrace.trace import tracer | ||
| from tests.profiling.collector import pprof_utils | ||
|
|
||
| assert stack_v2.is_available, stack_v2.failure_msg | ||
|
|
||
| sleep_time = 0.2 | ||
| loop_run_time = 3 | ||
|
|
||
| async def inner1() -> None: | ||
| start_time = time.time() | ||
| while time.time() < start_time + loop_run_time: | ||
| await asyncio.sleep(sleep_time) | ||
|
|
||
| async def inner2() -> None: | ||
| start_time = time.time() | ||
| while time.time() < start_time + loop_run_time: | ||
| await asyncio.sleep(sleep_time) | ||
|
|
||
| async def outer() -> None: | ||
| t1 = asyncio.create_task(inner1(), name="inner 1") | ||
| t2 = asyncio.create_task(inner2(), name="inner 2") | ||
| await asyncio.gather(t1, t2) | ||
|
|
||
| resource = str(uuid.uuid4()) | ||
| span_type = ext.SpanTypes.WEB | ||
|
|
||
| p = profiler.Profiler(tracer=tracer) | ||
| p.start() | ||
| with tracer.trace("test_asyncio", resource=resource, span_type=span_type) as span: | ||
| span_id = span.span_id | ||
| local_root_span_id = span._local_root.span_id | ||
|
|
||
| loop = asyncio.new_event_loop() | ||
| asyncio.set_event_loop(loop) | ||
| main_task = loop.create_task(outer(), name="outer") | ||
| loop.run_until_complete(main_task) | ||
|
|
||
| p.stop() | ||
|
|
||
| output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid()) | ||
|
|
||
| profile = pprof_utils.parse_newest_profile(output_filename) | ||
|
|
||
| samples_with_span_id = pprof_utils.get_samples_with_label_key(profile, "span id") | ||
| assert len(samples_with_span_id) > 0 | ||
|
|
||
| # get samples with task_name | ||
| samples = pprof_utils.get_samples_with_label_key(profile, "task name") | ||
| # The next fails if stack_v2 is not properly configured with asyncio task | ||
| # tracking via ddtrace.profiling._asyncio | ||
| assert len(samples) > 0 | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", filename="test_asyncio_gather.py", line_no=outer.__code__.co_firstlineno + 3 | ||
| ), | ||
| # TODO: We should add the locations of the gathered Tasks here as they should be in the same Stack | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| try: | ||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", # TODO: This is a bug and we need to fix it, it should be "inner 1" | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner2", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=inner2.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="inner 1", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner1", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=inner1.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
| except AssertionError: | ||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="inner 2", # TODO: This is a bug and we need to fix it, it should be "inner 1" | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner2", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=inner2.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner1", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=inner1.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_gather.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| @pytest.mark.subprocess( | ||
| env=dict( | ||
| DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_wait", | ||
| ), | ||
| err=None, | ||
| ) | ||
| # For macOS: err=None ignores expected stderr from tracer failing to connect to agent (not relevant to this test) | ||
| def test_asyncio_wait() -> None: | ||
| import asyncio | ||
| import os | ||
| import time | ||
| import uuid | ||
|
|
||
| from ddtrace import ext | ||
| from ddtrace.internal.datadog.profiling import stack_v2 | ||
| from ddtrace.profiling import profiler | ||
| from ddtrace.trace import tracer | ||
| from tests.profiling.collector import pprof_utils | ||
|
|
||
| assert stack_v2.is_available, stack_v2.failure_msg | ||
|
|
||
| sleep_time = 0.2 | ||
| loop_run_time = 3 | ||
|
|
||
| async def inner1() -> None: | ||
| start_time = time.time() | ||
| while time.time() < start_time + loop_run_time: | ||
| await asyncio.sleep(sleep_time) | ||
|
|
||
| async def inner2() -> None: | ||
| start_time = time.time() | ||
| while time.time() < start_time + loop_run_time: | ||
| await asyncio.sleep(sleep_time) | ||
|
|
||
| async def outer() -> None: | ||
| t1 = asyncio.create_task(inner1(), name="inner 1") | ||
| t2 = asyncio.create_task(inner2(), name="inner 2") | ||
| await asyncio.wait(fs=(t1, t2), return_when=asyncio.ALL_COMPLETED) | ||
|
|
||
| resource = str(uuid.uuid4()) | ||
| span_type = ext.SpanTypes.WEB | ||
|
|
||
| p = profiler.Profiler(tracer=tracer) | ||
| p.start() | ||
| with tracer.trace("test_asyncio", resource=resource, span_type=span_type) as span: | ||
| span_id = span.span_id | ||
| local_root_span_id = span._local_root.span_id | ||
|
|
||
| loop = asyncio.new_event_loop() | ||
| asyncio.set_event_loop(loop) | ||
| main_task = loop.create_task(outer(), name="outer") | ||
| loop.run_until_complete(main_task) | ||
|
|
||
| p.stop() | ||
|
|
||
| output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid()) | ||
|
|
||
| profile = pprof_utils.parse_newest_profile(output_filename) | ||
|
|
||
| samples_with_span_id = pprof_utils.get_samples_with_label_key(profile, "span id") | ||
| assert len(samples_with_span_id) > 0 | ||
|
|
||
| # get samples with task_name | ||
| samples = pprof_utils.get_samples_with_label_key(profile, "task name") | ||
| # The next fails if stack_v2 is not properly configured with asyncio task | ||
| # tracking via ddtrace.profiling._asyncio | ||
| assert len(samples) > 0 | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", filename="test_asyncio_wait.py", line_no=outer.__code__.co_firstlineno + 3 | ||
| ), | ||
| # TODO: We should add the locations of the gathered Tasks here as they should be in the same Stack | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| # Note: there currently is a bug somewhere that makes one of the Tasks show up under the parent Task and the | ||
| # other Tasks be under their own Task name. We need to fix this. | ||
| # For the time being, though, which Task is "independent" is non-deterministic which means we must | ||
| # test both possibilities ("inner 2" is part of "outer" or "inner 1" is part of "outer"). | ||
| try: | ||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", # TODO: This is a bug and we need to fix it, it should be "inner 1" | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner1", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=inner1.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="inner 2", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner2", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=inner2.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
| except AssertionError: | ||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="outer", # TODO: This is a bug and we need to fix it, it should be "inner 1" | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner2", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=inner2.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) | ||
|
|
||
| pprof_utils.assert_profile_has_sample( | ||
| profile, | ||
| samples, | ||
| expected_sample=pprof_utils.StackEvent( | ||
| thread_name="MainThread", | ||
| task_name="inner 1", | ||
| span_id=span_id, | ||
| local_root_span_id=local_root_span_id, | ||
| locations=[ | ||
| pprof_utils.StackLocation( | ||
| function_name="inner1", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=inner1.__code__.co_firstlineno + 3, | ||
| ), | ||
| pprof_utils.StackLocation( | ||
| function_name="outer", | ||
| filename="test_asyncio_wait.py", | ||
| line_no=outer.__code__.co_firstlineno + 3, | ||
| ), | ||
| ], | ||
| ), | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.