| OLD | NEW |
| (Empty) |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 from collections import defaultdict | |
| 6 from collections import namedtuple | |
| 7 import logging | |
| 8 | |
| 9 | |
| 10 class _FrozenDict(dict): | |
| 11 """An immutable ``dict`` (or some approximation thereof). | |
| 12 | |
| 13 The goal of this class is to render a ``dict`` hashable, so | |
| 14 that it can be used as a key in other ``dict``s, so that in | |
| 15 turn we can use our ``MemoizedFunction`` on functions taking | |
| 16 ``CrashReportWithDependencies`` as an argument. | |
| 17 | |
| 18 For now, we simply define the ``__hash__`` method and assume clients | |
| 19 will not try to mutate instances after they have been stored as keys | |
| 20 in another ``dict``. In the future it may be worth taking further | |
| 21 steps to make it more difficult for clients to do such mutation. | |
| 22 | |
| 23 N.B., the ``__init__`` method will clone the ``dict`` argument. There | |
| 24 doesn't seem to be an easy way around this. | |
| 25 """ | |
| 26 def __hash__(self): | |
| 27 return hash(tuple(sorted(self.items()))) | |
| 28 | |
| 29 | |
| 30 class CrashReportWithDependencies( | |
| 31 namedtuple('CrashReportWithDependencies', [ | |
| 32 'crashed_version', 'signature', 'platform', 'stacktrace', | |
| 33 'last_good_version', 'first_bad_version', 'dependencies', | |
| 34 'dependency_rolls'])): | |
| 35 """A crash report annotated with extra information for the CL classifier. | |
| 36 | |
| 37 This class extends ``CrashReport`` with some additional information | |
| 38 about deps. In particular, this is useful for the CL-classifier since | |
| 39 this information is feature-independent and suspect-independent, | |
| 40 so we can compute it just once and store the results. In addition, | |
| 41 this class happens to be the "X" for the loglinear model underlying | |
| 42 ``LogLinearChangelistClassifier``. | |
| 43 | |
| 44 Properties: | |
| 45 crashed_version (str): The version of Chrome in which the crash occurred. | |
| 46 signature (str): The signature of the crash on the Chrome crash server. | |
| 47 platform (str): The platform affected by the crash; e.g., 'win', | |
| 48 'mac', 'linux', 'android', 'ios', etc. | |
| 49 stacktrace (Stacktrace): The stacktrace of the crash. N.B., this is | |
| 50 an object generated by parsing the string containing the stack trace; | |
| 51 we do not store the string itself. | |
| 52 last_good_version (str): the last known-good revision for this crash. | |
| 53 first_bad_version (str): the first known-bad revision for this crash. | |
| 54 regression_range (pair of str): a pair of ``last_good_version`` and | |
| 55 ``first_bad_version``. Offered for convenience, to match the API of | |
| 56 ``CrashReport``. Note that, unlike ``CrashReport``, we do not permit | |
| 57 ``None`` as a regression range. | |
| 58 dependencies (_FrozenDict): An immutable dict from dependency paths to | |
| 59 ``Dependency`` objects. The keys are all those deps which are | |
| 60 used by both the ``crashed_version`` of the code, and at least | |
| 61 one frame in the ``stacktrace.crash_stack``. | |
| 62 dependency_rolls (_FrozenDict) An immutable dict from dependency | |
| 63 paths to ``DependencyRoll`` objects. The keys are all those | |
| 64 dependencies which (1) occur in the regression range for the | |
| 65 ``platform`` where the crash occurred, (2) neither add nor delete | |
| 66 a dependency, and (3) are also keys of ``dependencies``. | |
| 67 """ | |
| 68 __slots__ = () | |
| 69 | |
| 70 def __new__(cls, report, dependency_fetcher): | |
| 71 """Annotate a crash report with dependencies. | |
| 72 | |
| 73 N.B., because the CL-classifier does not currenly support missing | |
| 74 regression ranges, if the regression range of the ``report`` is | |
| 75 ``None`` then this constructor will return ``None`` rather than | |
| 76 constructing the new annotated crash report. If one (or both) of the | |
| 77 components of the regression range are ``None``, we will log a warning | |
| 78 but nevertheless construct the annotated crash report to the extent | |
| 79 we can. This is to maintain compatibility with the current unittests, | |
| 80 but really we should return ``None`` since the CL-classifiers do not | |
| 81 support open or half-open regression ranges either. In the future | |
| 82 it would probably be better to raise a ``ValueError`` rather than | |
| 83 returning ``None`` | |
| 84 | |
| 85 Args: | |
| 86 report (CrashReport): The original crash report given to Predator, | |
| 87 which we extend. | |
| 88 dependency_fetcher (ChromeDependencyFetcher): For getting dep information. | |
| 89 """ | |
| 90 if not report.regression_range: | |
| 91 logging.warning('%s.__new__: Missing regression range for report: %s', | |
| 92 cls.__name__, str(report)) | |
| 93 # Give up. | |
| 94 return None | |
| 95 | |
| 96 last_good_version = report.regression_range[0] | |
| 97 if not last_good_version: | |
| 98 logging.warning('%s.__new__: Missing last-good version for report: %s', | |
| 99 cls.__name__, str(report)) | |
| 100 # Proceed as if everything was normal. | |
| 101 | |
| 102 first_bad_version = report.regression_range[1] | |
| 103 if not first_bad_version: | |
| 104 logging.warning('%s.__new__: Missing first-bad version for report: %s', | |
| 105 cls.__name__, str(report)) | |
| 106 # Proceed as if everything was normal. | |
| 107 | |
| 108 logging.info('%s.__new__: Regression range %s:%s', | |
| 109 cls.__name__, last_good_version, first_bad_version) | |
| 110 | |
| 111 # Short-circuit when we know the deps must be empty. | |
| 112 if not report.stacktrace.crash_stack: | |
| 113 logging.warning('%s.__new__: Missing or empty crash stack for report: %s', | |
| 114 cls.__name__, str(report)) | |
| 115 dependencies = _FrozenDict() | |
| 116 else: | |
| 117 # Get all the dependencies used by the version that crashed. | |
| 118 crashed_version_deps = dependency_fetcher.GetDependency( | |
| 119 report.crashed_version, report.platform) | |
| 120 # Filter them to only retain those which are used by some frame in | |
| 121 # the callstack causing the crash (and which are truthy). | |
| 122 dependencies = _FrozenDict({ | |
| 123 dep_path: crashed_version_deps[dep_path] | |
| 124 for dep_path in ( | |
| 125 # N.B., returning duplicate dep paths works just fine. | |
| 126 frame.dep_path | |
| 127 for frame in report.stacktrace.crash_stack) | |
| 128 if dep_path and dep_path in crashed_version_deps | |
| 129 }) | |
| 130 | |
| 131 # Short-circuit when we know the deprolls must be empty. | |
| 132 if not dependencies: | |
| 133 logging.warning('%s.__new__: Empty deps and dep rolls for report: %s', | |
| 134 cls.__name__, str(report)) | |
| 135 dependency_rolls = _FrozenDict() | |
| 136 else: | |
| 137 # Get ``DependencyRoll` objects for all dependencies in the regression | |
| 138 # range (for the particular platform that crashed). | |
| 139 regression_range_dep_rolls = dependency_fetcher.GetDependencyRollsDict( | |
| 140 last_good_version, first_bad_version, report.platform) | |
| 141 # Filter out the ones which add or delete a dependency, because we | |
| 142 # can't really be sure whether to blame them or not. This rarely | |
| 143 # happens, so our inability to decide shouldn't be too much of a problem. | |
| 144 def HasBothRevisions(dep_path, dep_roll): | |
| 145 has_both_revisions = bool(dep_roll.old_revision) and bool( | |
| 146 dep_roll.new_revision) | |
| 147 if not has_both_revisions: | |
| 148 logging.info( | |
| 149 'Skip %s dependency %s', | |
| 150 'added' if dep_roll.new_revision else 'deleted', | |
| 151 dep_path) | |
| 152 return has_both_revisions | |
| 153 # Apply the above filter, and also filter to only retain those | |
| 154 # which occur in ``crashed_stack_deps``. | |
| 155 dependency_rolls = _FrozenDict({ | |
| 156 dep_path: dep_roll | |
| 157 for dep_path, dep_roll in regression_range_dep_rolls.iteritems() | |
| 158 if HasBothRevisions(dep_path, dep_roll) and dep_path in dependencies | |
| 159 }) | |
| 160 | |
| 161 return super(cls, CrashReportWithDependencies).__new__( | |
| 162 cls, | |
| 163 crashed_version = report.crashed_version, | |
| 164 signature = report.signature, | |
| 165 platform = report.platform, | |
| 166 stacktrace = report.stacktrace, | |
| 167 last_good_version = last_good_version, | |
| 168 first_bad_version = first_bad_version, | |
| 169 dependencies = dependencies, | |
| 170 dependency_rolls = dependency_rolls, | |
| 171 ) | |
| 172 | |
| 173 @property | |
| 174 def regression_range(self): | |
| 175 """Returns a pair of the last-good and first-bad revisions. | |
| 176 | |
| 177 Note that even if both revisions are ``None``, this property still | |
| 178 returns a pair. It never returns ``None``. | |
| 179 """ | |
| 180 return self.last_good_version, self.first_bad_version | |
| OLD | NEW |