• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #4831

30 Nov 2023 11:51PM CUT coverage: 21.938% (-0.01%) from 21.95%
#4831

push

coveralls-python

suhaibmujahid
[topcrash] Ignore signatures that start with "bad hardware"

716 of 3588 branches covered (0.0%)

0 of 1 new or added line in 1 file covered. (0.0%)

2 existing lines in 2 files now uncovered.

1924 of 8770 relevant lines covered (21.94%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.83
/bugbot/topcrash.py
1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
from collections import defaultdict
1✔
6
from datetime import datetime, timedelta
1✔
7
from typing import Dict, Iterable, List, Optional, Set, Union
1✔
8

9
import libmozdata.socorro as socorro
1✔
10
from libmozdata import utils as lmdutils
1✔
11
from libmozdata import versions as lmdversions
1✔
12

13
from bugbot.rules.no_crashes import NoCrashes
1✔
14

15

16
class SocorroError(Exception):
1✔
17
    pass
1✔
18

19

20
def _format_criteria_names(criteria: List[dict]):
1✔
21
    return [
1✔
22
        {
23
            **criterion,
24
            "name": "Top {tc_limit} {name} on {channel}".format(**criterion),
25
        }
26
        for criterion in criteria
27
    ]
28

29

30
# Top crash identification criteria as defined on Mozilla Wiki.
31
#
32
# Wiki page: https://wiki.mozilla.org/CrashKill/Topcrash
33

34
TOP_CRASH_IDENTIFICATION_CRITERIA = _format_criteria_names(
1✔
35
    [
36
        # -------
37
        # Firefox
38
        # -------
39
        {
40
            "name": "desktop browser crashes",
41
            "product": "Firefox",
42
            "channel": "release",
43
            "tc_limit": 20,
44
            "tc_startup_limit": 30,
45
        },
46
        {
47
            "name": "desktop browser crashes",
48
            "product": "Firefox",
49
            "channel": "beta",
50
            "tc_limit": 20,
51
            "tc_startup_limit": 30,
52
        },
53
        {
54
            "name": "desktop browser crashes",
55
            "product": "Firefox",
56
            "channel": "nightly",
57
            "min_installations": 5,
58
            "tc_limit": 10,
59
        },
60
        {
61
            "name": "content process crashes",
62
            "product": "Firefox",
63
            "channel": "beta",
64
            "process_type": "content",
65
            "tc_limit": 10,
66
        },
67
        {
68
            "name": "content process crashes",
69
            "product": "Firefox",
70
            "channel": "release",
71
            "process_type": "content",
72
            "tc_limit": 10,
73
        },
74
        {
75
            "name": "GPU process crashes",
76
            "product": "Firefox",
77
            "channel": "beta",
78
            "process_type": "gpu",
79
            "tc_limit": 5,
80
        },
81
        {
82
            "name": "GPU process crashes",
83
            "product": "Firefox",
84
            "channel": "release",
85
            "process_type": "gpu",
86
            "tc_limit": 5,
87
        },
88
        {
89
            "name": "RDD process crashes",
90
            "product": "Firefox",
91
            "channel": "beta",
92
            "process_type": "rdd",
93
            "tc_limit": 5,
94
        },
95
        {
96
            "name": "RDD process crashes",
97
            "product": "Firefox",
98
            "channel": "release",
99
            "process_type": "rdd",
100
            "tc_limit": 5,
101
        },
102
        {
103
            "name": "socket and utility process crashes",
104
            "product": "Firefox",
105
            "channel": "beta",
106
            "process_type": ["socket", "utility"],
107
            "tc_limit": 5,
108
        },
109
        {
110
            "name": "socket and utility process crashes",
111
            "product": "Firefox",
112
            "channel": "release",
113
            "process_type": ["socket", "utility"],
114
            "tc_limit": 5,
115
        },
116
        {
117
            "name": "desktop browser crashes on Linux",
118
            "product": "Firefox",
119
            "channel": "beta",
120
            "platform": "Linux",
121
            "tc_limit": 5,
122
        },
123
        {
124
            "name": "desktop browser crashes on Linux",
125
            "product": "Firefox",
126
            "channel": "release",
127
            "platform": "Linux",
128
            "tc_limit": 5,
129
        },
130
        {
131
            "name": "desktop browser crashes on Mac",
132
            "product": "Firefox",
133
            "channel": "beta",
134
            "platform": "Mac OS X",
135
            "tc_limit": 5,
136
        },
137
        {
138
            "name": "desktop browser crashes on Mac",
139
            "product": "Firefox",
140
            "channel": "release",
141
            "platform": "Mac OS X",
142
            "tc_limit": 5,
143
        },
144
        {
145
            "name": "desktop browser crashes on Windows",
146
            "product": "Firefox",
147
            "channel": "beta",
148
            "platform": "Windows",
149
            "tc_limit": 5,
150
        },
151
        {
152
            "name": "desktop browser crashes on Windows",
153
            "product": "Firefox",
154
            "channel": "release",
155
            "platform": "Windows",
156
            "tc_limit": 5,
157
        },
158
        # -----
159
        # Fenix
160
        # -----
161
        {
162
            "name": "AArch64 and ARM crashes",
163
            "product": "Fenix",
164
            "channel": "nightly",
165
            "cpu_arch": ["arm64", "arm"],
166
            "tc_limit": 10,
167
        },
168
        {
169
            "name": "AArch64 and ARM crashes",
170
            "product": "Fenix",
171
            "channel": "beta",
172
            "cpu_arch": ["arm64", "arm"],
173
            "tc_limit": 10,
174
        },
175
        {
176
            "name": "AArch64 and ARM crashes",
177
            "product": "Fenix",
178
            "channel": "release",
179
            "cpu_arch": ["arm64", "arm"],
180
            "tc_limit": 10,
181
        },
182
    ]
183
)
184

185
# The crash signature block patterns are based on the criteria defined on
186
# Mozilla Wiki. However, the matching roles (e.g., `!=` and `!^`) are based on
187
# the SuperSearch docs.
188
#
189
# Wiki page: https://wiki.mozilla.org/CrashKill/Topcrash
190
# Docs: https://crash-stats.mozilla.org/documentation/supersearch/#operators
191
CRASH_SIGNATURE_BLOCK_PATTERNS = [
1✔
192
    "!^EMPTY: ",
193
    "!^OOM ",
194
    "!=IPCError-browser | ShutDownKill",
195
    "!^java.lang.OutOfMemoryError",
196
]
197

198

199
class Topcrash:
1✔
200
    def __init__(
1✔
201
        self,
202
        date: Union[str, datetime] = "today",
203
        duration: int = 7,
204
        min_crashes: int = 15,
205
        default_min_installations: int = 3,
206
        signature_block_patterns: list = CRASH_SIGNATURE_BLOCK_PATTERNS,
207
        criteria: Iterable[dict] = TOP_CRASH_IDENTIFICATION_CRITERIA,
208
    ) -> None:
209
        """Constructor
210

211
        Args:
212
            date: the final date. If not provided, the value will be today.
213
            duration: the number of days to retrieve the crash data.
214
            min_crashes: the minimum number of crashes to consider a signature
215
                in the top crashes.
216
            default_min_installations: the minimum number of installations to
217
                consider a signature in the top crashes.
218
            signature_block_list: a list of crash signature to be ignored.
219
            criteria: the list of criteria to be used to query the top crashes.
220
        """
221
        self.min_crashes = min_crashes
1✔
222
        self.default_min_installations = default_min_installations
1✔
223
        self.signature_block_patterns = signature_block_patterns
1✔
224
        self.criteria = criteria
1✔
225

226
        end_date = lmdutils.get_date_ymd(date)
1✔
227
        self.start_date = lmdutils.get_date_ymd(end_date - timedelta(duration))
1✔
228
        self.date_range = socorro.SuperSearch.get_search_date(self.start_date, end_date)
1✔
229

230
        self._blocked_signatures: Optional[Set[str]] = None
1✔
231
        self.__version_constrains: Optional[Dict[str, str]] = None
1✔
232

233
    def _fetch_signatures_from_patterns(self, patterns) -> Set[str]:
1✔
234
        MAX_SIGNATURES_IN_REQUEST = 1000
1✔
235

236
        signatures: Set[str] = set()
1✔
237
        params = {
1✔
238
            "date": self.date_range,
239
            "signature": [
240
                pattern if pattern[0] != "!" else pattern[1:] for pattern in patterns
241
            ],
242
            "_results_number": 0,
243
            "_facets_size": MAX_SIGNATURES_IN_REQUEST,
244
        }
245

246
        def handler(search_resp: dict, data: set):
1✔
247
            data.update(
1✔
248
                signature["term"]
249
                for signature in search_resp["facets"]["signature"]
250
                if signature["count"] >= self.min_crashes
251
            )
252

253
        socorro.SuperSearch(
1✔
254
            params=params,
255
            handler=handler,
256
            handlerdata=signatures,
257
        ).wait()
258

259
        assert (
1✔
260
            len(signatures) < MAX_SIGNATURES_IN_REQUEST
261
        ), "the patterns match more signatures than what the request could return, consider to increase the threshold"
262

263
        return signatures
1✔
264

265
    def fetch_signature_volume(
1✔
266
        self,
267
        signatures: Iterable[str],
268
    ) -> dict:
269
        """Fetch the volume of crashes for each crash signature.
270

271
        Returns:
272
            A dictionary with the crash signature as key and the volume as value.
273
        """
274

275
        def handler(search_resp: dict, data: dict):
×
276
            if search_resp["errors"]:
×
277
                raise SocorroError(search_resp["errors"])
×
278

279
            data.update(
×
280
                {
281
                    signature["term"]: signature["count"]
282
                    for signature in search_resp["facets"]["signature"]
283
                }
284
            )
285

286
        signature_volume: dict = {signature: 0 for signature in signatures}
×
287
        assert len(signature_volume) > 0, "no signatures provided"
×
288

289
        chunks, size = NoCrashes.chunkify(
×
290
            ["=" + signature for signature in signature_volume]
291
        )
292
        searches = [
×
293
            socorro.SuperSearch(
294
                params={
295
                    "date": self.date_range,
296
                    "signature": _signatures,
297
                    "_facets": "signature",
298
                    "_results_number": 0,
299
                    "_facets_size": size,
300
                },
301
                handler=handler,
302
                handlerdata=signature_volume,
303
            )
304
            for _signatures in chunks
305
        ]
306

307
        for search in searches:
×
308
            search.wait()
×
309

310
        return signature_volume
×
311

312
    def get_blocked_signatures(self) -> Set[str]:
1✔
313
        """Return the list of signatures to be ignored."""
314
        if self._blocked_signatures is None:
1!
315
            self._blocked_signatures = self._fetch_signatures_from_patterns(
1✔
316
                self.signature_block_patterns
317
            )
318

319
        return self._blocked_signatures
1✔
320

321
    def get_signatures(
1✔
322
        self,
323
    ) -> Dict[str, List[dict]]:
324
        """Fetch the top crashes from socorro.
325

326
        Top crashes will be queried twice for each release channel, one that
327
        targets startup crashes and another that targets all crashes.
328

329
        Returns:
330
            A dictionary where the keys are crash signatures, and the values are
331
            list of criteria that the crash signature matches.
332
        """
333

334
        data: dict = defaultdict(dict)
×
335
        searches = [
×
336
            socorro.SuperSearch(
337
                params=self.__get_params_from_criterion(criterion),
338
                handler=self.__signatures_handler(criterion),
339
                handlerdata=data[criterion["name"]],
340
            )
341
            for criterion in self.criteria
342
        ]
343

344
        for search in searches:
×
345
            search.wait()
×
346

347
        # We merge the results after finishing all queries to avoid race conditions
348
        result = {}
×
349
        for _, signatures in data.items():
×
350
            for signature_name, signature_info in signatures.items():
×
351
                if signature_name not in result:
×
352
                    result[signature_name] = [signature_info]
×
353
                else:
354
                    result[signature_name].append(signature_info)
×
355

356
        return result
×
357

358
    def __get_params_from_criterion(self, criterion: dict):
1✔
359
        params = {
×
360
            "product": criterion["product"],
361
            "release_channel": criterion["channel"],
362
            "major_version": self._get_major_version_constrain(criterion["channel"]),
363
            "process_type": criterion.get("process_type"),
364
            "cpu_arch": criterion.get("cpu_arch"),
365
            "platform": criterion.get("platform"),
366
            "date": self.date_range,
367
            "_aggs.signature": [
368
                "_cardinality.install_time",
369
                "startup_crash",
370
            ],
371
            "_results_number": 0,
372
            "_facets_size": (
373
                criterion.get("tc_startup_limit", criterion["tc_limit"])
374
                # Because of the limitation in https://bugzilla.mozilla.org/show_bug.cgi?id=1257376#c9,
375
                # we cannot ignore the generic signatures in the Socorro side, thus we ignore them
376
                # in the client side. We add here the maximum number of signatures that could be
377
                # ignored to stay in the safe side.
378
                + len(self.get_blocked_signatures())
379
            ),
380
        }
381
        return params
×
382

383
    def _get_major_version_constrain(self, channel: str) -> str:
1✔
384
        """Return the major version constrain for the given channel."""
385
        if self.__version_constrains is None:
×
386
            versions = lmdversions.get(base=True)
×
387
            last_release_date = lmdversions.getMajorDate(versions["release"])
×
388

389
            if last_release_date > self.start_date:
×
390
                # If the release date is newer than the start date in the query, we
391
                # include an extra version to have enough data.
392
                self.__version_constrains = {
×
393
                    "nightly": f""">={versions["nightly"]-1}""",
394
                    "beta": f""">={versions["beta"]-1}""",
395
                    "release": f""">={versions["release"]-1}""",
396
                }
397
            else:
398
                self.__version_constrains = {
×
399
                    "nightly": f""">={versions["nightly"]}""",
400
                    "beta": f""">={versions["beta"]}""",
401
                    "release": f""">={versions["release"]}""",
402
                }
403

404
        return self.__version_constrains[channel]
×
405

406
    @staticmethod
1✔
407
    def __is_startup_crash(signature: dict):
1✔
408
        return any(
×
409
            startup["term"] == "T" for startup in signature["facets"]["startup_crash"]
410
        )
411

412
    def __signatures_handler(self, criterion: dict):
1✔
413
        def handler(search_resp: dict, data: dict):
×
414
            """
415
            Handle and merge crash signatures from different queries.
416

417
            Only startup crashes will be considered after exceeding `tc_limit`
418
            and up to `tc_startup_limit`.
419
            """
420

421
            blocked_signatures = self.get_blocked_signatures()
×
422

423
            signatures = search_resp["facets"]["signature"]
×
424
            tc_limit = criterion["tc_limit"]
×
425
            tc_startup_limit = criterion.get("tc_startup_limit", tc_limit)
×
426
            min_installations = criterion.get(
×
427
                "min_installations", self.default_min_installations
428
            )
429
            assert tc_startup_limit >= tc_limit
×
430

431
            rank = 0
×
432
            for signature in signatures:
×
433
                if rank >= tc_startup_limit or signature["count"] < self.min_crashes:
×
434
                    return
×
435

436
                name = signature["term"]
×
437
                installations = signature["facets"]["cardinality_install_time"]["value"]
×
NEW
438
                if (
×
439
                    installations < min_installations
440
                    or name in blocked_signatures
441
                    or name.startswith("bad hardware | ")
442
                ):
UNCOV
443
                    continue
×
444

445
                is_startup = self.__is_startup_crash(signature)
×
446
                if is_startup or rank < tc_limit:
×
447
                    data[name] = {
×
448
                        "criterion_name": criterion["name"],
449
                        "is_startup": is_startup,
450
                    }
451

452
                rank += 1
×
453

454
        return handler
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc