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

mozilla / relman-auto-nag / #3908

pending completion
#3908

push

coveralls-python

web-flow
Merge branch 'master' into topcrash-old

542 of 3008 branches covered (18.02%)

26 of 26 new or added lines in 1 file covered. (100.0%)

1761 of 7774 relevant lines covered (22.65%)

0.23 hits per line

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

35.97
/auto_nag/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 auto_nag.scripts.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
            "minimum_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
            "minimum_installations": 3,
122
            "tc_limit": 5,
123
        },
124
        {
125
            "name": "desktop browser crashes on Linux",
126
            "product": "Firefox",
127
            "channel": "release",
128
            "platform": "Linux",
129
            "minimum_installations": 3,
130
            "tc_limit": 5,
131
        },
132
        {
133
            "name": "desktop browser crashes on Mac",
134
            "product": "Firefox",
135
            "channel": "beta",
136
            "platform": "Mac OS X",
137
            "minimum_installations": 3,
138
            "tc_limit": 5,
139
        },
140
        {
141
            "name": "desktop browser crashes on Mac",
142
            "product": "Firefox",
143
            "channel": "release",
144
            "platform": "Mac OS X",
145
            "minimum_installations": 3,
146
            "tc_limit": 5,
147
        },
148
        {
149
            "name": "desktop browser crashes on Windows",
150
            "product": "Firefox",
151
            "channel": "beta",
152
            "platform": "Windows",
153
            "minimum_installations": 3,
154
            "tc_limit": 5,
155
        },
156
        {
157
            "name": "desktop browser crashes on Windows",
158
            "product": "Firefox",
159
            "channel": "release",
160
            "platform": "Windows",
161
            "minimum_installations": 3,
162
            "tc_limit": 5,
163
        },
164
        # -----
165
        # Fenix
166
        # -----
167
        {
168
            "name": "AArch64 and ARM crashes",
169
            "product": "Fenix",
170
            "channel": "nightly",
171
            "cpu_arch": ["arm64", "arm"],
172
            "tc_limit": 10,
173
        },
174
        {
175
            "name": "AArch64 and ARM crashes",
176
            "product": "Fenix",
177
            "channel": "beta",
178
            "cpu_arch": ["arm64", "arm"],
179
            "tc_limit": 10,
180
        },
181
        {
182
            "name": "AArch64 and ARM crashes",
183
            "product": "Fenix",
184
            "channel": "release",
185
            "cpu_arch": ["arm64", "arm"],
186
            "tc_limit": 10,
187
        },
188
    ]
189
)
190

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

204

205
class Topcrash:
1✔
206
    def __init__(
1✔
207
        self,
208
        date: Union[str, datetime] = "today",
209
        duration: int = 7,
210
        minimum_crashes: int = 15,
211
        signature_block_patterns: list = CRASH_SIGNATURE_BLOCK_PATTERNS,
212
        criteria: Iterable[dict] = TOP_CRASH_IDENTIFICATION_CRITERIA,
213
    ) -> None:
214
        """Constructor
215

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

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

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

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

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

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

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

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

265
        return signatures
1✔
266

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

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

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

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

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

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

309
        for search in searches:
×
310
            search.wait()
×
311

312
        return signature_volume
×
313

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

321
        return self._blocked_signatures
1✔
322

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

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

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

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

346
        for search in searches:
×
347
            search.wait()
×
348

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

358
        return result
×
359

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

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

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

406
        return self.__version_constrains[channel]
×
407

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

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

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

423
            blocked_signatures = self.get_blocked_signatures()
×
424

425
            signatures = search_resp["facets"]["signature"]
×
426
            tc_limit = criterion["tc_limit"]
×
427
            tc_startup_limit = criterion.get("tc_startup_limit", tc_limit)
×
428
            minimum_installations = criterion.get("minimum_installations", 3)
×
429
            assert tc_startup_limit >= tc_limit
×
430

431
            rank = 0
×
432
            for signature in signatures:
×
433
                if (
×
434
                    rank >= tc_startup_limit
435
                    or signature["count"] < self.minimum_crashes
436
                ):
437
                    return
×
438

439
                name = signature["term"]
×
440
                installations = signature["facets"]["cardinality_install_time"]["value"]
×
441
                if installations < minimum_installations or name in blocked_signatures:
×
442
                    continue
×
443

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

451
                rank += 1
×
452

453
        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