-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix(explorer): better profile thread selection logic #103656
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
Changes from all commits
beb8c78
bc1c2ac
7eddf7c
3ef6be5
30c982e
58ebae5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,8 +48,9 @@ def normalize_description(description: str) -> str: | |
|
|
||
| def _convert_profile_to_execution_tree(profile_data: dict) -> list[dict]: | ||
| """ | ||
| Converts profile data into a hierarchical representation of code execution, | ||
| including only items from the MainThread and app frames. | ||
| Converts profile data into a hierarchical representation of code execution. | ||
| Selects the thread with the most in_app frames, or falls back to MainThread if no | ||
| in_app frames exist (showing all frames including system frames). | ||
| Calculates accurate durations for all nodes based on call stack transitions. | ||
| """ | ||
| profile = profile_data.get( | ||
|
|
@@ -65,15 +66,47 @@ def _convert_profile_to_execution_tree(profile_data: dict) -> list[dict]: | |
| frames = profile.get("frames") | ||
| stacks = profile.get("stacks") | ||
| samples = profile.get("samples") | ||
| thread_metadata = profile.get("thread_metadata", {}) | ||
| if not all([frames, stacks, samples]): | ||
| return [] | ||
|
|
||
| # Use the thread ID from the first sample as this should be the one with the most application logic | ||
| if samples: | ||
| main_thread_id = str(samples[0]["thread_id"]) | ||
| # Count in_app frames per thread | ||
| thread_in_app_counts: dict[str, int] = {} | ||
| for sample in samples: | ||
| thread_id = str(sample["thread_id"]) | ||
| thread_in_app_counts.setdefault(thread_id, 0) | ||
|
|
||
| stack_index = sample.get("stack_id") | ||
| if stack_index is not None and stack_index < len(stacks): | ||
| for idx in stacks[stack_index]: | ||
| if idx < len(frames) and frames[idx].get("in_app", False): | ||
| thread_in_app_counts[thread_id] += 1 | ||
|
|
||
| # Select thread with most in_app frames | ||
| selected_thread_id: str | None = None | ||
| max_in_app_count = max(thread_in_app_counts.values()) if thread_in_app_counts else 0 | ||
|
|
||
| if max_in_app_count > 0: | ||
| # Find thread with most in_app frames | ||
| selected_thread_id = max(thread_in_app_counts.items(), key=lambda x: x[1])[0] | ||
| show_all_frames = False | ||
| else: | ||
| # No samples - can't determine thread | ||
| main_thread_id = None | ||
| # No in_app frames found, try to find MainThread | ||
| main_thread_id_from_metadata = next( | ||
| ( | ||
| str(thread_id) | ||
| for thread_id, metadata in thread_metadata.items() | ||
| if metadata.get("name") == "MainThread" | ||
| ), | ||
| None, | ||
| ) | ||
|
|
||
| selected_thread_id = main_thread_id_from_metadata or ( | ||
| str(samples[0]["thread_id"]) if samples else None | ||
| ) | ||
| show_all_frames = ( | ||
| True # Show all frames including system frames when no in_app frames exist | ||
| ) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: MainThread selection ignores whether thread has samplesWhen no threads have in_app frames, the code attempts to select MainThread from metadata without verifying it actually contains any samples. If MainThread exists in metadata but never appears in the samples data, |
||
|
|
||
| def _get_elapsed_since_start_ns( | ||
| sample: dict[str, Any], all_samples: list[dict[str, Any]] | ||
|
|
@@ -139,55 +172,74 @@ def find_or_create_child(parent: dict[str, Any], frame_data: dict[str, Any]) -> | |
| parent["children"].append(frame_data) | ||
| return frame_data | ||
|
|
||
| def process_stack(stack_index): | ||
| """Extract app frames from a stack trace""" | ||
| def is_valid_frame(frame: dict[str, Any], include_all_frames: bool) -> bool: | ||
| """Check if a frame should be included in the execution tree.""" | ||
| filename = frame.get("filename", "") | ||
| is_generated = filename.startswith("<") and filename.endswith(">") | ||
| if is_generated: | ||
| return False | ||
| return include_all_frames or frame.get("in_app", False) | ||
|
|
||
| def process_stack(stack_index: int, include_all_frames: bool = False) -> list[dict[str, Any]]: | ||
| """Extract frames from a stack trace. | ||
|
|
||
| Args: | ||
| stack_index: Index into the stacks array | ||
| include_all_frames: If True, include all frames (including system frames). | ||
| If False, only include in_app frames. | ||
| """ | ||
| frame_indices = stacks[stack_index] | ||
| if not frame_indices: | ||
| return [] | ||
|
|
||
| # Create nodes for app frames only, maintaining order (bottom to top) | ||
| # Create nodes for frames, maintaining order (bottom to top) | ||
| nodes = [] | ||
| for idx in reversed(frame_indices): | ||
| frame = frames[idx] | ||
| if frame.get("in_app", False) and not ( | ||
| frame.get("filename", "").startswith("<") | ||
| and frame.get("filename", "").endswith(">") | ||
| ): | ||
| if idx >= len(frames): | ||
| continue | ||
| if is_valid_frame(frames[idx], include_all_frames): | ||
| nodes.append(create_frame_node(idx)) | ||
|
|
||
| return nodes | ||
|
|
||
| if not selected_thread_id: | ||
| return [] | ||
|
|
||
| # Build the execution tree and track call stacks | ||
| execution_tree: list[dict[str, Any]] = [] | ||
| call_stack_history: list[tuple[list[str], int]] = [] # [(node_ids, timestamp), ...] | ||
| node_registry: dict[str, dict[str, Any]] = {} # {node_id: node_reference} | ||
|
|
||
| for sample in sorted_samples: | ||
| if str(sample["thread_id"]) != str(main_thread_id): | ||
| if str(sample["thread_id"]) != selected_thread_id: | ||
| continue | ||
|
|
||
| timestamp_ns = _get_elapsed_since_start_ns(sample, sorted_samples) | ||
| stack_frames = process_stack(sample["stack_id"]) | ||
| stack_frames = process_stack(sample["stack_id"], include_all_frames=show_all_frames) | ||
| if not stack_frames: | ||
| continue | ||
|
|
||
| # Process this stack sample | ||
| current_stack_ids = [] | ||
|
|
||
| # Find or create root node | ||
| root = None | ||
| for existing_root in execution_tree: | ||
| if ( | ||
| existing_root["function"] == stack_frames[0]["function"] | ||
| and existing_root["module"] == stack_frames[0]["module"] | ||
| and existing_root["filename"] == stack_frames[0]["filename"] | ||
| and existing_root["lineno"] == stack_frames[0]["lineno"] | ||
| ): | ||
| root = existing_root | ||
| break | ||
| root_frame = stack_frames[0] | ||
| root = next( | ||
| ( | ||
| existing_root | ||
| for existing_root in execution_tree | ||
| if ( | ||
| existing_root["function"] == root_frame["function"] | ||
| and existing_root["module"] == root_frame["module"] | ||
| and existing_root["filename"] == root_frame["filename"] | ||
| and existing_root["lineno"] == root_frame["lineno"] | ||
| ) | ||
| ), | ||
| None, | ||
| ) | ||
|
|
||
| if root is None: | ||
| root = stack_frames[0] | ||
| root = root_frame | ||
| execution_tree.append(root) | ||
|
|
||
| # Process root node | ||
|
|
@@ -302,8 +354,9 @@ def apply_durations(node): | |
|
|
||
| def convert_profile_to_execution_tree(profile_data: dict) -> list[ExecutionTreeNode]: | ||
| """ | ||
| Converts profile data into a hierarchical representation of code execution, | ||
| including only items from the MainThread and app frames. | ||
| Converts profile data into a hierarchical representation of code execution. | ||
| Selects the thread with the most in_app frames, or falls back to MainThread if no | ||
| in_app frames exist (showing all frames including system frames). | ||
| Calculates accurate durations for all nodes based on call stack transitions. | ||
| """ | ||
| dict_tree = _convert_profile_to_execution_tree(profile_data) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.