Chromium Code Reviews| Index: appengine/findit/crash/stacktrace.py |
| diff --git a/appengine/findit/crash/stacktrace.py b/appengine/findit/crash/stacktrace.py |
| index 45dcc4241e69dc8aa9981d4fc3d7fa4fb9a39fac..043323e844653f8ce18c6920d2c70409a8e79146 100644 |
| --- a/appengine/findit/crash/stacktrace.py |
| +++ b/appengine/findit/crash/stacktrace.py |
| @@ -3,6 +3,7 @@ |
| # found in the LICENSE file. |
| from collections import namedtuple |
| +import copy |
| import logging |
| import re |
| @@ -22,6 +23,9 @@ CALLSTACK_FORMAT_TO_PATTERN = { |
| FRAME_INDEX_PATTERN = re.compile(r'\s*#(\d+)\s.*') |
| +_DEFAULT_FORMAT_TYPE = CallStackFormatType.DEFAULT |
| +_DEFAULT_LANGUAGE_TYPE = CallStackLanguageType.CPP |
| + |
| class StackFrame(namedtuple('StackFrame', |
| ['index', 'dep_path', 'function', 'file_path', 'raw_file_path', |
| @@ -46,6 +50,7 @@ class StackFrame(namedtuple('StackFrame', |
| def __new__(cls, index, dep_path, function, file_path, raw_file_path, |
| crashed_line_numbers, repo_url=None): |
| + assert index is not None, TypeError("The index must be an int") |
|
Martin Barbella
2016/12/08 22:30:55
Nit: single quotes for consistency with other stri
wrengr
2016/12/08 23:42:37
Done.
|
| return super(cls, StackFrame).__new__(cls, |
| index, dep_path, function, file_path, raw_file_path, |
| crashed_line_numbers, repo_url) |
| @@ -76,12 +81,97 @@ class StackFrame(namedtuple('StackFrame', |
| return self.ToString() |
| -class CallStack(list): |
| - """Represents a call stack within a stacktrace. A list of StackFrame objects. |
| +# TODO(wrengr): this should be a method on ``format_type``, or a class |
| +# method on ``StackFrame``. |
|
Sharu Jiang
2016/12/08 22:23:44
I think in ``StackFrame`` and change the name to P
Martin Barbella
2016/12/08 22:30:55
Is there any reason not to just do this (my usual
stgao
2016/12/08 23:42:17
Let's make this a method into the class in this CL
wrengr
2016/12/08 23:42:37
Mainly just that I ran into issues getting it to w
|
| +def ParseStackFrame(language_type, format_type, line, deps, |
| + default_stack_frame_index=None): |
| + """Parse line into a StackFrame instance, if possible. |
| + |
| + Args: |
| + language_type (CallStackLanguageType): the language the line is in. |
| + format_type (CallStackFormatType): the format the line is in. |
| + line (str): The line to be parsed. |
| + deps (dict): Map dependency path to its corresponding Dependency. |
| + |
| + Returns: |
| + A ``StackFrame`` or ``None``. |
| + """ |
| + # TODO(wrengr): how can we avoid duplicating this logic from ``CallStack``? |
| + if format_type is None: # pragma: no cover |
| + format_type = _DEFAULT_FORMAT_TYPE |
| + |
| + if language_type is None: |
| + language_type = _DEFAULT_LANGUAGE_TYPE |
| + |
| + if format_type == CallStackFormatType.JAVA: |
| + language_type = CallStackLanguageType.JAVA |
| + |
| + line = line.strip() |
| + line_pattern = CALLSTACK_FORMAT_TO_PATTERN[format_type] |
| + |
| + if format_type == CallStackFormatType.JAVA: |
| + match = line_pattern.match(line) |
| + if not match: |
| + return None |
| + |
| + function = match.group(1) |
| + raw_file_path = parse_util.GetFullPathForJavaFrame(function) |
| + crashed_line_numbers = [int(match.group(3))] |
| + |
| + elif format_type == CallStackFormatType.SYZYASAN: |
| + match = line_pattern.match(line) |
| + if not match: |
| + return None |
| + |
| + function = match.group(2).strip() |
| + raw_file_path = match.group(5) |
| + crashed_line_numbers = [int(match.group(6))] |
| + |
| + else: |
| + line_parts = line.split() |
| + if not line_parts or not line_parts[0].startswith('#'): |
| + return None |
| + |
| + match = line_pattern.match(line_parts[-1]) |
| + if not match: # pragma: no cover |
| + return None |
| + |
| + function = ' '.join(line_parts[3:-1]) |
| + |
| + raw_file_path = match.group(1) |
| + # Fracas java stack has default format type. |
| + if language_type == CallStackLanguageType.JAVA: |
| + raw_file_path = parse_util.GetFullPathForJavaFrame(function) |
| + |
| + crashed_line_numbers = parse_util.GetCrashedLineRange( |
| + match.group(2) + (match.group(3) if match.group(3) else '')) |
| + # Normalize the file path so that it can be compared to repository path. |
| + dep_path, file_path, repo_url = parse_util.GetDepPathAndNormalizedFilePath( |
| + raw_file_path, deps, language_type == CallStackLanguageType.JAVA) |
| + |
| + # If we have the common stack frame index pattern, then use it |
| + # since it is more reliable. |
| + index_match = FRAME_INDEX_PATTERN.match(line) |
| + if index_match: |
| + stack_frame_index = int(index_match.group(1)) |
| + else: |
| + stack_frame_index = int(default_stack_frame_index or 0) |
| + |
| + return StackFrame(stack_frame_index, dep_path, function, file_path, |
| + raw_file_path, crashed_line_numbers, repo_url) |
| + |
| + |
| +# N.B., because ``list`` is mutable it isn't hashable, thus cannot be |
| +# used as a key in a dict. Because we want to usecallstacks as keys (for |
| +# memoization) we has-a tuple rather than is-a list. |
|
Sharu Jiang
2016/12/08 22:23:44
I think it's not necessary to make every parameter
stgao
2016/12/08 23:42:17
With the current implementation of MemoizedFunctio
wrengr
2016/12/08 23:42:37
Even though the old code did do a lot of mutation,
Sharu Jiang
2016/12/09 00:13:58
We can do, that's one alternative, but this will n
Sharu Jiang
2016/12/09 00:13:58
For those frames we don't even look into, we shoul
Sharu Jiang
2016/12/09 00:29:20
*Only parse top_n frames after the signature frame
Sharu Jiang
2016/12/09 00:57:15
Follow up on "redesign the stacktrace to only pars
wrengr
2016/12/09 18:58:38
All tests pass with 100% coverage. The only modifi
wrengr
2016/12/09 18:58:38
The code using MemoizedFunction is:
https://coder
|
| +class CallStack(namedtuple('CallStack', |
| + ['priority', 'frames', 'format_type', 'language_type'])): |
| + """A stack (sequence of ``StackFrame`` objects) in a ``Stacktrace``. |
| Attributes: |
| priority (int): The smaller the number, the higher the priority beginning |
| with 0. |
| + frames (tuple of StackFrame): the frames in order from bottom to top. |
| format_type (CallStackFormatType): Represents the type of line format |
| within a callstack. For example: |
| @@ -95,77 +185,83 @@ class CallStack(list): |
| '#0 0x32b5982 in get third_party/WebKit/Source/wtf/RefPtr.h:61:43' |
| language_type (CallStackLanguageType): Either CPP or JAVA language. |
| """ |
| - def __init__(self, priority, format_type=CallStackFormatType.DEFAULT, |
| - language_type=CallStackLanguageType.CPP, |
| - frame_list=None): |
| - super(CallStack, self).__init__(frame_list or []) |
| - |
| - self.priority = priority |
| - self.format_type = format_type |
| - self.language_type = ( |
| - CallStackLanguageType.JAVA if format_type == CallStackFormatType.JAVA |
| - else language_type) |
| - |
| - def ParseLine(self, line, deps): |
| - """Parse line into StackFrame instance and append it if successfully |
| - parsed.""" |
| - line = line.strip() |
| - line_pattern = CALLSTACK_FORMAT_TO_PATTERN[self.format_type] |
| - |
| - if self.format_type == CallStackFormatType.JAVA: |
| - match = line_pattern.match(line) |
| - if not match: |
| - return |
| - |
| - function = match.group(1) |
| - raw_file_path = parse_util.GetFullPathForJavaFrame(function) |
| - crashed_line_numbers = [int(match.group(3))] |
| - |
| - elif self.format_type == CallStackFormatType.SYZYASAN: |
| - match = line_pattern.match(line) |
| - if not match: |
| - return |
| - |
| - function = match.group(2).strip() |
| - raw_file_path = match.group(5) |
| - crashed_line_numbers = [int(match.group(6))] |
| - |
| - else: |
| - line_parts = line.split() |
| - if not line_parts or not line_parts[0].startswith('#'): |
| - return |
| - |
| - match = line_pattern.match(line_parts[-1]) |
| - if not match: |
| - return |
| - |
| - function = ' '.join(line_parts[3:-1]) |
| - |
| - raw_file_path = match.group(1) |
| - # Fracas java stack has default format type. |
| - if self.language_type == CallStackLanguageType.JAVA: |
| - raw_file_path = parse_util.GetFullPathForJavaFrame(function) |
| - |
| - crashed_line_numbers = parse_util.GetCrashedLineRange( |
| - match.group(2) + (match.group(3) if match.group(3) else '')) |
| - # Normalize the file path so that it can be compared to repository path. |
| - dep_path, file_path, repo_url = parse_util.GetDepPathAndNormalizedFilePath( |
| - raw_file_path, deps, self.language_type == CallStackLanguageType.JAVA) |
| - |
| - # If we have the common stack frame index pattern, then use it |
| - # since it is more reliable. |
| - index_match = FRAME_INDEX_PATTERN.match(line) |
| - if index_match: |
| - stack_frame_index = int(index_match.group(1)) |
| - else: |
| - stack_frame_index = len(self) |
| - |
| - self.append(StackFrame(stack_frame_index, dep_path, function, file_path, |
| - raw_file_path, crashed_line_numbers, repo_url)) |
| - |
| + __slots__ = () |
| + def __new__(cls, priority, format_type=None, language_type=None, |
| + frame_list=None): |
| + """Construct a new ``CallStack``. |
| + |
| + N.B., we use ``None`` as the default value of the optional arguments |
| + so that if callers need to explicitly provide those arguments but |
| + don't have an explicit value, they can pass ``None`` to get at the |
| + default without needing to be kept in sync with this constructor. For |
| + example, the ``ChromeCrashParser.Parse`` constructs a stack and they |
| + need to keep track of all the arguments to be passed to this function. |
| + |
| + Args: |
| + priority (int): The priority of this stack in its ``Stacktrace``. |
| + format_type (CallStackFormatType): Optional. The stack's format. |
| + language_type (CallStackLanguageType): Optional. The stack's language. |
| + frame_list (iterable of StackFrame): Optional. The frames in the stack. |
| + """ |
| + if format_type is None: |
| + format_type = _DEFAULT_FORMAT_TYPE |
| + |
| + if language_type is None: |
| + language_type = _DEFAULT_LANGUAGE_TYPE |
| + |
| + if format_type == CallStackFormatType.JAVA: # pragma: no cover |
|
Martin Barbella
2016/12/08 22:30:55
Not too important, but do we really need no cover
wrengr
2016/12/08 23:42:37
We don't *need* the pragma; rather, the old versio
|
| + language_type = CallStackLanguageType.JAVA |
| + |
| + if frame_list is None: |
| + frame_list = [] |
| + |
| + return super(cls, CallStack).__new__(cls, |
| + priority, tuple(frame_list), format_type, language_type) |
| + |
| + def __len__(self): |
| + """Returns the number of frames in this stack.""" |
| + return len(self.frames) |
| + |
| + # TODO: we do actually have unittests for this, but for some reason |
|
Martin Barbella
2016/12/08 22:30:55
Strange. Is there a bug opened for this? might be
wrengr
2016/12/08 23:42:37
Done. crbug.com/672641
|
| + # coverage isn't seeing them. |
| + def __bool__(self): # pragma: no cover |
| + """Returns whether this stack is empty.""" |
| + return bool(self.frames) |
| + |
| + def __iter__(self): |
| + """Iterator over the frames in this stack.""" |
| + return iter(self.frames) |
| + |
| + def SliceFrames(self, low_index, high_index): |
| + """Returns a new ``CallStack`` keeping only the specified frames. |
| + |
| + Args: |
| + low_index (int or None): the lowest index to keep. If ``None`` |
| + then defaults to 0. |
| + high_index (int or None): the index after the highest one to |
| + keep. If ``None`` then defaults to one after the highest index. |
| + |
| + Returns: |
| + A new ``CallStack`` instance. If both arguments are ``None`` then |
| + we return the original stack object, because they are equal and |
| + due to immutability there's no reason to clone the instance. |
| + """ |
| + if low_index is None and high_index is None: |
| + return self |
| + |
| + # TODO(wrengr): can we use ``_replace`` without running into TypeErrors? |
|
Sharu Jiang
2016/12/08 22:23:44
what TypeErrors you encountered?
wrengr
2016/12/08 23:42:37
The TypeError complains about _make not being call
|
| + return CallStack(self.priority, |
| + format_type=self.format_type, |
| + language_type=self.language_type, |
| + frame_list=self.frames[low_index:high_index]) |
| + |
| + |
| +# N.B., because ``list`` is mutable it isn't hashable, thus cannot be |
| +# used as a key in a dict. Because we want to usecallstacks as keys (for |
| +# memoization) we has-a tuple rather than is-a list. |
| # TODO(http://crbug.com/644476): this class needs a better name. |
| -class Stacktrace(list): |
| +class Stacktrace(object): |
| """A collection of callstacks which together provide a trace of what happened. |
| For instance, when doing memory debugging we will have callstacks for |
| @@ -174,8 +270,7 @@ class Stacktrace(list): |
| use-after-free crashes), etc. What callstacks are included in the |
| trace is unspecified, since this differs for different tools.""" |
| def __init__(self, stack_list=None, signature=None): |
| - super(Stacktrace, self).__init__(stack_list or []) |
| - |
| + self.stacks = stack_list or [] |
| self._crash_stack = None |
| self._signature_parts = None |
| if signature: |
| @@ -185,12 +280,23 @@ class Stacktrace(list): |
| # usually the top 3 important stack frames separated by '\n'. |
| self._signature_parts = signature.split('\n') |
| + def __getitem__(self, i): # pragma: no cover |
| + return self.stacks[i] |
| + |
| + def __len__(self): |
| + return len(self.stacks) |
| + |
| + def __bool__(self): # pragma: no cover |
| + return bool(self.stacks) |
| + |
| + def __iter__(self): |
| + return iter(self.stacks) |
| @property |
| def crash_stack(self): |
| """Get the callstack with the highest priority (i.e., whose priority |
| field is numerically the smallest) in the stacktrace.""" |
| - if not self: |
| + if not self.stacks: |
| logging.warning('Cannot get crash stack for empty stacktrace: %s', self) |
| return None |
| @@ -204,17 +310,17 @@ class Stacktrace(list): |
| return False, 0 |
| # Set the crash stack using signature callstack. |
| - for callstack in self: |
| + for callstack in self.stacks: |
| is_signature_callstack, index = _IsSignatureCallstack(callstack) |
| if is_signature_callstack: |
| # Filter all the stack frames before signature. |
| - callstack[:] = callstack[index:] |
| - self._crash_stack = callstack |
| + self._crash_stack = callstack.SliceFrames(index, None) |
| break |
| # If there is no signature callstack, fall back to set crash stack using |
| # the first least priority callstack. |
| if self._crash_stack is None: |
| - self._crash_stack = sorted(self, key=lambda stack: stack.priority)[0] |
| + self._crash_stack = sorted(self.stacks, |
| + key=lambda stack: stack.priority)[0] |
| return self._crash_stack |