Index: tools/findit/stacktrace.py |
diff --git a/tools/findit/stacktrace.py b/tools/findit/stacktrace.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4d7664d468dcf540110dce64395391ffb3114dc0 |
--- /dev/null |
+++ b/tools/findit/stacktrace.py |
@@ -0,0 +1,279 @@ |
+# Copyright (c) 2014 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import os |
+import re |
+ |
+import crash_utils |
+ |
+ |
+class StackFrame(object): |
+ """Represents a frame in stacktrace. |
+ |
+ Attributes: |
+ index: An index of the stack frame. |
+ component_path: The path of the component this line represents. |
+ component_name: The name of the component this line represents. |
+ file_name: The name of the file that crashed. |
+ function: The function that caused the crash. |
+ file_path: The path of the crashed file. |
+ crashed_line_number: The line of the file that caused the crash. |
+ """ |
+ |
+ def __init__(self, stack_frame_index, component_path, component_name, |
+ file_name, function, file_path, crashed_line_number): |
+ self.index = stack_frame_index |
+ self.component_path = component_path |
+ self.component_name = component_name |
+ self.file_name = file_name |
+ self.function = function |
+ self.file_path = file_path |
+ self.crashed_line_number = crashed_line_number |
+ |
+ |
+class CallStack(object): |
+ """Represents a call stack within a stacktrace. |
+ |
+ It is a list of StackFrame object, and the object keeps track of whether |
+ the stack is crash stack, freed or previously-allocated. |
+ """ |
+ |
+ def __init__(self, stack_priority): |
+ self.frame_list = [] |
+ self.priority = stack_priority |
+ |
+ def Add(self, stacktrace_line): |
+ self.frame_list.append(stacktrace_line) |
+ |
+ def GetTopNFrames(self, n): |
+ return self.frame_list[:n] |
+ |
+ |
+class Stacktrace(object): |
+ """Represents Stacktrace object. |
+ |
+ Contains a list of callstacks, because one stacktrace might have more than |
+ one callstacks. |
+ """ |
+ |
+ def __init__(self, stacktrace, build_type, parsed_deps): |
+ self.stack_list = None |
+ self.parsed_deps = parsed_deps |
+ self.ParseStacktrace(stacktrace, build_type) |
+ |
+ def ParseStacktrace(self, stacktrace, build_type): |
+ """Parses stacktrace and normalizes it. |
+ |
+ If there are multiple callstacks within the stacktrace, |
+ it will parse each of them separately, and store them in the stack_list |
+ variable. |
+ |
+ Args: |
+ stacktrace: A string containing stacktrace. |
+ build_type: A string containing the build type of the crash. |
+ """ |
+ # If the passed in string is empty, the object does not represent anything. |
+ if not stacktrace: |
+ return |
+ |
+ # Reset the stack list. |
+ self.stack_list = [] |
+ reached_new_callstack = False |
+ # Note that we do not need exact stack frame index, we only need relative |
+ # position of a frame within a callstack. The reason for not extracting |
+ # index from a line is that some stack frames do not have index. |
+ stack_frame_index = 0 |
+ current_stack = None |
+ |
+ for line in stacktrace: |
+ (is_new_callstack, stack_priority) = self.__IsStartOfNewCallStack( |
+ line, build_type) |
+ |
+ if is_new_callstack: |
+ |
+ # If this callstack is crash stack, update the boolean. |
+ if not reached_new_callstack: |
+ reached_new_callstack = True |
+ current_stack = CallStack(stack_priority) |
+ |
+ # If this is from freed or allocation, add the callstack we have |
+ # to the list of callstacks, and increment the stack priority. |
+ else: |
+ stack_frame_index = 0 |
+ if current_stack and current_stack.frame_list: |
+ self.stack_list.append(current_stack) |
+ current_stack = CallStack(stack_priority) |
+ |
+ # Generate stack frame object from the line. |
+ parsed_stack_frame = self.__GenerateStackFrame( |
+ stack_frame_index, line, build_type) |
+ |
+ # If the line does not represent the stack frame, ignore this line. |
+ if not parsed_stack_frame: |
+ continue |
+ |
+ # Add the parsed stack frame object to the current stack. |
+ current_stack.Add(parsed_stack_frame) |
+ stack_frame_index += 1 |
+ |
+ # Add the current callstack only if there are frames in it. |
+ if current_stack and current_stack.frame_list: |
+ self.stack_list.append(current_stack) |
+ |
+ def __IsStartOfNewCallStack(self, line, build_type): |
+ """Check if this line is the start of the new callstack. |
+ |
+ Since each builds have different format of stacktrace, the logic for |
+ checking the line for all builds is handled in here. |
+ |
+ Args: |
+ line: Line to check for. |
+ build_type: The name of the build. |
+ |
+ Returns: |
+ True if the line is the start of new callstack, False otherwise. If True, |
+ it also returns the priority of the line. |
+ """ |
+ # Currently not supported. |
+ if 'android' in build_type: |
+ pass |
+ |
+ elif 'syzyasan' in build_type: |
+ # In syzyasan build, new stack starts with 'crash stack:', |
+ # 'freed stack:', etc. |
+ callstack_start_pattern = re.compile(r'^(.*) stack:$') |
+ match = callstack_start_pattern.match(line) |
+ |
+ # If the line matches the callstack start pattern. |
+ if match: |
+ # Check the type of the new match. |
+ stack_type = match.group(1) |
+ |
+ # Crash stack gets priority 0. |
+ if stack_type == 'Crash': |
+ return (True, 0) |
+ |
+ # Other callstacks all get priority 1. |
+ else: |
+ return (True, 1) |
+ |
+ elif 'tsan' in build_type: |
+ # Create patterns for each callstack type. |
+ crash_callstack_start_pattern = re.compile( |
+ r'^(Read|Write) of size \d+') |
+ |
+ allocation_callstack_start_pattern = re.compile( |
+ r'^Previous (write|read) of size \d+') |
+ |
+ location_callstack_start_pattern = re.compile( |
+ r'^Location is heap block of size \d+') |
+ |
+ # Crash stack gets priority 0. |
+ if crash_callstack_start_pattern.match(line): |
+ return (True, 0) |
+ |
+ # All other stacks get priority 1. |
+ if allocation_callstack_start_pattern.match(line): |
+ return (True, 1) |
+ |
+ if location_callstack_start_pattern.match(line): |
+ return (True, 1) |
+ |
+ else: |
+ # In asan and other build types, crash stack can start |
+ # in two different ways. |
+ crash_callstack_start_pattern1 = re.compile(r'^==\d+== ?ERROR:') |
+ crash_callstack_start_pattern2 = re.compile( |
+ r'^(READ|WRITE) of size \d+ at') |
+ |
+ freed_callstack_start_pattern = re.compile( |
+ r'^freed by thread T\d+ (.* )?here:') |
+ |
+ allocation_callstack_start_pattern = re.compile( |
+ r'^previously allocated by thread T\d+ (.* )?here:') |
+ |
+ other_callstack_start_pattern = re.compile( |
+ r'^Thread T\d+ (.* )?created by') |
+ |
+ # Crash stack gets priority 0. |
+ if (crash_callstack_start_pattern1.match(line) or |
+ crash_callstack_start_pattern2.match(line)): |
+ return (True, 0) |
+ |
+ # All other callstack gets priority 1. |
+ if freed_callstack_start_pattern.match(line): |
+ return (True, 1) |
+ |
+ if allocation_callstack_start_pattern.match(line): |
+ return (True, 1) |
+ |
+ if other_callstack_start_pattern.match(line): |
+ return (True, 1) |
+ |
+ # If the line does not match any pattern, return false and a dummy for |
+ # stack priority. |
+ return (False, -1) |
+ |
+ def __GenerateStackFrame(self, stack_frame_index, line, build_type): |
+ """Extracts information from a line in stacktrace. |
+ |
+ Args: |
+ stack_frame_index: A stack frame index of this line. |
+ line: A stacktrace string to extract data from. |
+ build_type: A string containing the build type |
+ of this crash (e.g. linux_asan_chrome_mp). |
+ |
+ Returns: |
+ A triple containing the name of the function, the path of the file and |
+ the crashed line number. |
+ """ |
+ line_parts = line.split() |
+ |
+ try: |
+ # Filter out lines that are not stack frame. |
+ stack_frame_index_pattern = re.compile(r'#(\d+)') |
+ if not stack_frame_index_pattern.match(line_parts[0]): |
+ return None |
+ |
+ # Tsan has different stack frame style from other builds. |
+ if build_type.startswith('linux_tsan'): |
+ file_path_and_line = line_parts[-2] |
+ function = ' '.join(line_parts[1:-2]) |
+ |
+ else: |
+ file_path_and_line = line_parts[-1] |
+ function = ' '.join(line_parts[3:-1]) |
+ |
+ # Get file path and line info from the line. |
+ file_path_and_line = file_path_and_line.split(':') |
+ file_path = file_path_and_line[0] |
+ crashed_line_number = int(file_path_and_line[1]) |
+ |
+ # Return None if the line is malformed. |
+ except IndexError: |
+ return None |
+ except ValueError: |
+ return None |
+ |
+ # Normalize the file path so that it can be compared to repository path. |
+ file_name = os.path.basename(file_path) |
+ (component_path, component_name, file_path) = ( |
+ crash_utils.NormalizePathLinux(file_path, self.parsed_deps)) |
+ |
+ # If this component is not supported, ignore this line. |
+ if not component_path: |
+ return None |
+ |
+ # Return a new stack frame object with the parsed information. |
+ return StackFrame(stack_frame_index, component_path, component_name, |
+ file_name, function, file_path, crashed_line_number) |
+ |
+ def __getitem__(self, index): |
+ return self.stack_list[index] |
+ |
+ def GetCrashStack(self): |
+ """Returns the callstack with the lowest priority.""" |
+ sorted_stacklist = sorted(self.stack_list, |
+ key=lambda callstack: callstack.priority) |
+ return sorted_stacklist[0] |