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

mozilla / relman-auto-nag / #4668

pending completion
#4668

push

coveralls-python

suhaibmujahid
[file_crash_bug] Don't check status flags when `regressed_by` is missed

716 of 3564 branches covered (20.09%)

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

1924 of 8714 relevant lines covered (22.08%)

0.22 hits per line

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

0.0
/bugbot/crash/analyzer.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
import itertools
×
6
import re
×
7
from collections import defaultdict
×
8
from datetime import date, timedelta
×
9
from functools import cached_property
×
10
from typing import Iterable, Iterator
×
11

12
from libmozdata import bugzilla, clouseau, connection, socorro
×
13
from libmozdata import utils as lmdutils
×
14
from libmozdata.bugzilla import Bugzilla
×
15
from libmozdata.connection import Connection
×
16

17
from bugbot import logger, utils
×
18
from bugbot.bug.analyzer import BugAnalyzer, BugsStore
×
19
from bugbot.components import ComponentName
×
20
from bugbot.crash import socorro_util
×
21

22
# The max offset from a memory address to be considered "near".
23
OFFSET_64_BIT = 0x1000
×
24
OFFSET_32_BIT = 0x100
×
25
# Allocator poison value addresses.
26
ALLOCATOR_ADDRESSES_64_BIT = (
×
27
    (0xE5E5E5E5E5E5E5E5, OFFSET_64_BIT),
28
    # On 64-bit windows, sometimes it could be doing something with a 32-bit
29
    # value gotten from freed memory, so it'll be 0X00000000E5E5E5E5 +/-, and
30
    # because of the address limitation, quite often it will be
31
    # 0X0000E5E5E5E5E5E5 +/-.
32
    (0x00000000E5E5E5E5, OFFSET_32_BIT),
33
    (0x0000E5E5E5E5E5E5, OFFSET_64_BIT),
34
    (0x4B4B4B4B4B4B4B4B, OFFSET_64_BIT),
35
)
36
ALLOCATOR_ADDRESSES_32_BIT = (
×
37
    (0xE5E5E5E5, OFFSET_32_BIT),
38
    (0x4B4B4B4B, OFFSET_32_BIT),
39
)
40
# Ranges where addresses are considered near allocator poison values.
41
ALLOCATOR_RANGES_64_BIT = (
×
42
    (addr - offset, addr + offset) for addr, offset in ALLOCATOR_ADDRESSES_64_BIT
43
)
44
ALLOCATOR_RANGES_32_BIT = (
×
45
    (addr - offset, addr + offset) for addr, offset in ALLOCATOR_ADDRESSES_32_BIT
46
)
47

48

49
def is_near_null_address(str_address) -> bool:
×
50
    """Check if the address is near null.
51

52
    Args:
53
        str_address: The memory address to check.
54

55
    Returns:
56
        True if the address is near null, False otherwise.
57
    """
58
    address = int(str_address, 0)
×
59
    is_64_bit = len(str_address) >= 18
×
60

61
    if is_64_bit:
×
62
        return -OFFSET_64_BIT <= address <= OFFSET_64_BIT
×
63

64
    return -OFFSET_32_BIT <= address <= OFFSET_32_BIT
×
65

66

67
def is_near_allocator_address(str_address) -> bool:
×
68
    """Check if the address is near an allocator poison value.
69

70
    Args:
71
        str_address: The memory address to check.
72

73
    Returns:
74
        True if the address is near an allocator poison value, False otherwise.
75
    """
76
    address = int(str_address, 0)
×
77
    is_64_bit = len(str_address) >= 18
×
78

79
    return any(
×
80
        low <= address <= high
81
        for low, high in (
82
            ALLOCATOR_RANGES_64_BIT if is_64_bit else ALLOCATOR_RANGES_32_BIT
83
        )
84
    )
85

86

87
# TODO: Move this to libmozdata
88
def generate_signature_page_url(params: dict, tab: str) -> str:
×
89
    """Generate a URL to the signature page on Socorro
90

91
    Args:
92
        params: the parameters for the search query.
93
        tab: the page tab that should be selected.
94

95
    Returns:
96
        The URL of the signature page on Socorro
97
    """
98
    web_url = socorro.Socorro.CRASH_STATS_URL
×
99
    query = lmdutils.get_params_for_url(params)
×
100
    return f"{web_url}/signature/{query}#{tab}"
×
101

102

103
# NOTE: At this point, we will file bugs on bugzilla-dev. Once we are confident
104
# that the bug filing is working as expected, we can switch to filing bugs in
105
# the production instance of Bugzilla.
106
class DevBugzilla(Bugzilla):
×
107
    URL = "https://bugzilla-dev.allizom.org"
×
108
    API_URL = URL + "/rest/bug"
×
109
    ATTACHMENT_API_URL = API_URL + "/attachment"
×
110
    TOKEN = utils.get_login_info()["bz_api_key_dev"]
×
111

112

113
class NoCrashReportFoundError(Exception):
×
114
    """There are no crash reports that meet the required criteria."""
115

116

117
class ClouseauDataAnalyzer:
×
118
    """Analyze the data returned by Crash Clouseau about a specific crash
119
    signature.
120
    """
121

122
    MINIMUM_CLOUSEAU_SCORE_THRESHOLD: int = 8
×
123
    DEFAULT_CRASH_COMPONENT = ComponentName("Core", "General")
×
124

125
    def __init__(self, reports: Iterable[dict], bugs_store: BugsStore):
×
126
        self._clouseau_reports = reports
×
127
        self.bugs_store = bugs_store
×
128

129
    @cached_property
×
130
    def max_clouseau_score(self):
×
131
        """The maximum Clouseau score in the crash reports."""
132
        if not self._clouseau_reports:
×
133
            return 0
×
134
        return max(report["max_score"] for report in self._clouseau_reports)
×
135

136
    @cached_property
×
137
    def regressed_by_potential_bug_ids(self) -> set[int]:
×
138
        """The IDs for the bugs that their patches could have caused the crash."""
139
        minimum_accepted_score = max(
×
140
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
141
        )
142
        return {
×
143
            changeset["bug_id"]
144
            for report in self._clouseau_reports
145
            if report["max_score"] >= minimum_accepted_score
146
            for changeset in report["changesets"]
147
            if changeset["max_score"] >= minimum_accepted_score
148
            and not changeset["is_merge"]
149
            and not changeset["is_backedout"]
150
        }
151

152
    @cached_property
×
153
    def regressed_by_patch(self) -> str | None:
×
154
        """The hash of the patch that could have caused the crash."""
155
        minimum_accepted_score = max(
×
156
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
157
        )
158
        potential_patches = {
×
159
            changeset["changeset"]
160
            for report in self._clouseau_reports
161
            if report["max_score"] >= minimum_accepted_score
162
            for changeset in report["changesets"]
163
            if changeset["max_score"] >= minimum_accepted_score
164
            and not changeset["is_merge"]
165
            and not changeset["is_backedout"]
166
        }
167
        if len(potential_patches) == 1:
×
168
            return next(iter(potential_patches))
×
169
        return None
×
170

171
    @cached_property
×
172
    def regressed_by(self) -> int | None:
×
173
        """The ID of the bug that one of its patches could have caused
174
        the crash.
175

176
        If there are multiple bugs, the value will be `None`.
177
        """
178
        bug_ids = self.regressed_by_potential_bug_ids
×
179
        if len(bug_ids) == 1:
×
180
            return next(iter(bug_ids))
×
181
        return None
×
182

183
    @cached_property
×
184
    def regressed_by_potential_bugs(self) -> list[BugAnalyzer]:
×
185
        """The bugs whose patches could have caused the crash."""
186
        self.bugs_store.fetch_bugs(
×
187
            self.regressed_by_potential_bug_ids,
188
            [
189
                "id",
190
                "groups",
191
                "assigned_to",
192
                "product",
193
                "component",
194
            ],
195
        )
196
        return [
×
197
            self.bugs_store.get_bug_by_id(bug_id)
198
            for bug_id in self.regressed_by_potential_bug_ids
199
        ]
200

201
    @cached_property
×
202
    def regressed_by_author(self) -> dict | None:
×
203
        """The author of the patch that could have caused the crash.
204

205
        If there are multiple regressors, the value will be `None`.
206

207
        The regressor bug assignee is considered as the author, even if the
208
        assignee is not the patch author.
209
        """
210

211
        if not self.regressed_by:
×
212
            return None
×
213

214
        bug = self.regressed_by_potential_bugs[0]
×
215
        assert bug.id == self.regressed_by
×
216
        return bug.get_field("assigned_to_detail")
×
217

218
    @cached_property
×
219
    def crash_component(self) -> ComponentName:
×
220
        """The component that the crash belongs to.
221

222
        If there are multiple components, the value will be the default one.
223
        """
224
        potential_components = {
×
225
            bug.component for bug in self.regressed_by_potential_bugs
226
        }
227
        if len(potential_components) == 1:
×
228
            return next(iter(potential_components))
×
229
        return self.DEFAULT_CRASH_COMPONENT
×
230

231

232
class SocorroDataAnalyzer(socorro_util.SignatureStats):
×
233
    """Analyze the data returned by Socorro."""
234

235
    _bugzilla_os_legal_values = None
×
236
    _bugzilla_cpu_legal_values_map = None
×
237
    _platforms = [
×
238
        {"short_name": "win", "name": "Windows"},
239
        {"short_name": "mac", "name": "Mac OS X"},
240
        {"short_name": "lin", "name": "Linux"},
241
        {"short_name": "and", "name": "Android"},
242
        {"short_name": "unknown", "name": "Unknown"},
243
    ]
244

245
    def __init__(
×
246
        self,
247
        signature: dict,
248
        num_total_crashes: int,
249
    ):
250
        super().__init__(signature, num_total_crashes, platforms=self._platforms)
×
251

252
    @classmethod
×
253
    def to_bugzilla_op_sys(cls, op_sys: str) -> str:
×
254
        """Return the corresponding OS name in Bugzilla for the provided OS name
255
        from Socorro.
256

257
        If the OS name is not recognized, return "Other".
258
        """
259
        if cls._bugzilla_os_legal_values is None:
×
260
            cls._bugzilla_os_legal_values = set(
×
261
                bugzilla.BugFields.fetch_field_values("op_sys")
262
            )
263

264
        if op_sys in cls._bugzilla_os_legal_values:
×
265
            return op_sys
×
266

267
        if op_sys.startswith("OS X ") or op_sys.startswith("macOS "):
×
268
            op_sys = "macOS"
×
269
        elif op_sys.startswith("Windows"):
×
270
            op_sys = "Windows"
×
271
        elif "Linux" in op_sys or op_sys.startswith("Ubuntu"):
×
272
            op_sys = "Linux"
×
273
        else:
274
            op_sys = "Other"
×
275

276
        return op_sys
×
277

278
    @property
×
279
    def bugzilla_op_sys(self) -> str:
×
280
        """The name of the OS where the crash happens.
281

282
        The value is one of the legal values for Bugzilla's `op_sys` field.
283

284
        - If no OS name is found, the value will be "Unspecified".
285
        - If the OS name is not recognized, the value will be "Other".
286
        - If multiple OS names are found, the value will be "All". Unless the OS
287
          names can be resolved to a common name without a version. For example,
288
          "Windows 10" and "Windows 7" will become "Windows".
289
        """
290
        all_op_sys = {
×
291
            self.to_bugzilla_op_sys(op_sys["term"])
292
            for op_sys in self.signature["facets"]["platform_pretty_version"]
293
        }
294

295
        if len(all_op_sys) > 1:
×
296
            # Resolve to root OS name by removing the version number.
297
            all_op_sys = {op_sys.split(" ")[0] for op_sys in all_op_sys}
×
298

299
        if len(all_op_sys) == 2 and "Other" in all_op_sys:
×
300
            # TODO: explain this workaround.
301
            all_op_sys.remove("Other")
×
302

303
        if len(all_op_sys) == 1:
×
304
            return next(iter(all_op_sys))
×
305

306
        if len(all_op_sys) == 0:
×
307
            return "Unspecified"
×
308

309
        return "All"
×
310

311
    @classmethod
×
312
    def to_bugzilla_cpu(cls, cpu: str) -> str:
×
313
        """Return the corresponding CPU name in Bugzilla for the provided name
314
        from Socorro.
315

316
        If the CPU is not recognized, return "Other".
317
        """
318
        if cls._bugzilla_cpu_legal_values_map is None:
×
319
            cls._bugzilla_cpu_legal_values_map = {
×
320
                value.lower(): value
321
                for value in bugzilla.BugFields.fetch_field_values("rep_platform")
322
            }
323

324
        return cls._bugzilla_cpu_legal_values_map.get(cpu, "Other")
×
325

326
    @property
×
327
    def bugzilla_cpu_arch(self) -> str:
×
328
        """The CPU architecture of the devices where the crash happens.
329

330
        The value is one of the legal values for Bugzilla's `rep_platform` field.
331

332
        - If no CPU architecture is found, the value will be "Unspecified".
333
        - If the CPU architecture is not recognized, the value will be "Other".
334
        - If multiple CPU architectures are found, the value will "All".
335
        """
336
        all_cpu_arch = {
×
337
            self.to_bugzilla_cpu(cpu["term"])
338
            for cpu in self.signature["facets"]["cpu_arch"]
339
        }
340

341
        if len(all_cpu_arch) == 2 and "Other" in all_cpu_arch:
×
342
            all_cpu_arch.remove("Other")
×
343

344
        if len(all_cpu_arch) == 1:
×
345
            return next(iter(all_cpu_arch))
×
346

347
        if len(all_cpu_arch) == 0:
×
348
            return "Unspecified"
×
349

350
        return "All"
×
351

352
    @property
×
353
    def user_comments_page_url(self) -> str:
×
354
        """The URL to the Signature page on Socorro where the Comments tab is
355
        selected.
356
        """
357
        start_date = date.today() - timedelta(weeks=26)
×
358
        params = {
×
359
            "signature": self.signature_term,
360
            "date": socorro.SuperSearch.get_search_date(start_date),
361
        }
362
        return generate_signature_page_url(params, "comments")
×
363

364
    @property
×
365
    def num_user_comments(self) -> int:
×
366
        """The number of crash reports with user comments."""
367
        # TODO: count useful/interesting user comments (e.g., exclude one word comments)
368
        return self.signature["facets"]["cardinality_user_comments"]["value"]
×
369

370
    @property
×
371
    def has_user_comments(self) -> bool:
×
372
        """Whether the crash signature has any reports with a user comment."""
373
        return self.num_user_comments > 0
×
374

375
    @property
×
376
    def top_proto_signature(self) -> str:
×
377
        """The proto signature that occurs the most."""
378
        return self.signature["facets"]["proto_signature"][0]["term"]
×
379

380
    @property
×
381
    def num_top_proto_signature_crashes(self) -> int:
×
382
        """The number of crashes for the most occurring proto signature."""
383
        return self.signature["facets"]["proto_signature"][0]["count"]
×
384

385
    def _build_ids(self) -> Iterator[int]:
×
386
        """Yields the build IDs where the crash occurred."""
387
        for build_id in self.signature["facets"]["build_id"]:
×
388
            yield build_id["term"]
×
389

390
    @property
×
391
    def top_build_id(self) -> int:
×
392
        """The build ID where most crashes occurred."""
393
        return self.signature["facets"]["build_id"][0]["term"]
×
394

395
    @cached_property
×
396
    def num_near_null_crashes(self) -> int:
×
397
        """The number of crashes that occurred on addresses near null."""
398
        return sum(
×
399
            address["count"]
400
            for address in self.signature["facets"]["address"]
401
            if is_near_null_address(address["term"])
402
        )
403

404
    @property
×
405
    def is_near_null_crash(self) -> bool:
×
406
        """Whether all crashes occurred on addresses near null."""
407
        return self.num_near_null_crashes == self.num_crashes
×
408

409
    @property
×
410
    def is_potential_near_null_crash(self) -> bool:
×
411
        """Whether the signature is a potential near null crash.
412

413
        The value will be True if some but not all crashes occurred on addresses
414
        near null.
415
        """
416
        return not self.is_near_null_crash and self.num_near_null_crashes > 0
×
417

418
    @property
×
419
    def is_near_null_related_crash(self) -> bool:
×
420
        """Whether the signature is related to near null crashes.
421

422
        The value will be True if any of the crashes occurred on addresses near
423
        null.
424
        """
425
        return self.is_near_null_crash or self.is_potential_near_null_crash
×
426

427
    @cached_property
×
428
    def num_near_allocator_crashes(self) -> int:
×
429
        """The number of crashes that occurred on addresses near an allocator
430
        poison value.
431
        """
432
        return sum(
×
433
            address["count"]
434
            for address in self.signature["facets"]["address"]
435
            if is_near_allocator_address(address["term"])
436
        )
437

438
    @property
×
439
    def is_near_allocator_crash(self) -> bool:
×
440
        """Whether all crashes occurred on addresses near an allocator poison
441
        value.
442
        """
443
        return self.num_near_allocator_crashes == self.num_crashes
×
444

445
    @property
×
446
    def is_potential_near_allocator_crash(self) -> bool:
×
447
        """Whether the signature is a potential near allocator poison value
448
        crash.
449

450
        The value will be True if some but not all crashes occurred on addresses
451
        near an allocator poison value.
452
        """
453
        return not self.is_near_allocator_crash and self.num_near_allocator_crashes > 0
×
454

455
    @property
×
456
    def is_near_allocator_related_crash(self) -> bool:
×
457
        """Whether the signature is related to near allocator poison value
458
        crashes.
459

460
        The value will be True if any of the crashes occurred on addresses near
461
        an allocator poison value.
462
        """
463
        return self.is_near_allocator_crash or self.is_potential_near_allocator_crash
×
464

465

466
class SignatureAnalyzer(SocorroDataAnalyzer, ClouseauDataAnalyzer):
×
467
    """Analyze the data related to a signature.
468

469
    This includes data from Socorro and Clouseau.
470
    """
471

472
    def __init__(
×
473
        self,
474
        socorro_signature: dict,
475
        num_total_crashes: int,
476
        clouseau_reports: list[dict],
477
        bugs_store: BugsStore,
478
    ):
479
        SocorroDataAnalyzer.__init__(self, socorro_signature, num_total_crashes)
×
480
        ClouseauDataAnalyzer.__init__(self, clouseau_reports, bugs_store)
×
481

482
    def _fetch_crash_reports(
×
483
        self,
484
        proto_signature: str,
485
        build_id: int | Iterable[int],
486
        limit: int = 1,
487
    ) -> Iterator[dict]:
488
        params = {
×
489
            "proto_signature": "=" + proto_signature,
490
            "build_id": build_id,
491
            "_columns": [
492
                "uuid",
493
            ],
494
            "_results_number": limit,
495
        }
496

497
        def handler(res: dict, data: dict):
×
498
            data.update(res)
×
499

500
        data: dict = {}
×
501
        socorro.SuperSearch(params=params, handler=handler, handlerdata=data).wait()
×
502

503
        yield from data["hits"]
×
504

505
    def fetch_representative_processed_crash(self) -> dict:
×
506
        """Fetch a processed crash to represent the signature.
507

508
        This could fetch multiple processed crashes and return the one that is
509
        most likely to be useful.
510
        """
511
        limit_to_top_proto_signature = (
×
512
            self.num_top_proto_signature_crashes / self.num_crashes > 0.6
513
        )
514

515
        reports = itertools.chain(
×
516
            # Reports with a higher score from clouseau are more likely to be
517
            # useful.
518
            sorted(
519
                self._clouseau_reports,
520
                key=lambda report: report["max_score"],
521
                reverse=True,
522
            ),
523
            # Next we try find reports from the top crashing build because they
524
            # are likely to be representative.
525
            self._fetch_crash_reports(self.top_proto_signature, self.top_build_id),
526
            self._fetch_crash_reports(self.top_proto_signature, self._build_ids()),
527
        )
528
        for report in reports:
×
529
            uuid = report["uuid"]
×
530
            processed_crash = socorro.ProcessedCrash.get_processed(uuid)[uuid]
×
531
            if (
×
532
                not limit_to_top_proto_signature
533
                or processed_crash["proto_signature"] == self.top_proto_signature
534
            ):
535
                # TODO(investigate): maybe we should check if the stack is
536
                # corrupted (ask gsvelto or willkg about how to detect that)
537
                return processed_crash
×
538

539
        raise NoCrashReportFoundError(
×
540
            f"No crash report found with the most frequent proto signature for {self.signature_term}."
541
        )
542

543
    @cached_property
×
544
    def is_potential_security_crash(self) -> bool:
×
545
        """Whether the crash is related to a potential security bug.
546

547
        The value will be True if:
548
            - the signature is related to near allocator poison value crashes, or
549
            - one of the potential regressors is a security bug
550
        """
551
        return self.is_near_allocator_related_crash or any(
×
552
            bug.is_security for bug in self.regressed_by_potential_bugs
553
        )
554

555

556
class SignaturesDataFetcher:
×
557
    """Fetch the data related to the given signatures."""
558

559
    MEMORY_ACCESS_ERROR_REASONS = (
×
560
        # On Windows:
561
        "EXCEPTION_ACCESS_VIOLATION_READ",
562
        "EXCEPTION_ACCESS_VIOLATION_WRITE",
563
        "EXCEPTION_ACCESS_VIOLATION_EXEC"
564
        # On Linux:
565
        "SIGSEGV / SEGV_MAPERR",
566
        "SIGSEGV / SEGV_ACCERR",
567
    )
568

569
    EXCLUDED_MOZ_REASON_STRINGS = (
×
570
        "MOZ_CRASH(OOM)",
571
        "MOZ_CRASH(Out of memory)",
572
        "out of memory",
573
        "Shutdown hanging",
574
        # TODO(investigate): do we need to exclude signatures that their reason
575
        # contains `[unhandlable oom]`?
576
        # Example: arena_t::InitChunk | arena_t::AllocRun | arena_t::MallocLarge | arena_t::Malloc | BaseAllocator::malloc | Allocator::malloc | PageMalloc
577
        # "[unhandlable oom]",
578
    )
579

580
    # If any of the crash reason starts with any of the following, then it is
581
    # Network or I/O error.
582
    EXCLUDED_IO_ERROR_REASON_PREFIXES = (
×
583
        "EXCEPTION_IN_PAGE_ERROR_READ",
584
        "EXCEPTION_IN_PAGE_ERROR_WRITE",
585
        "EXCEPTION_IN_PAGE_ERROR_EXEC",
586
    )
587

588
    # TODO(investigate): do we need to exclude all these signatures prefixes?
589
    EXCLUDED_SIGNATURE_PREFIXES = (
×
590
        "OOM | ",
591
        "bad hardware | ",
592
        "shutdownhang | ",
593
    )
594

595
    def __init__(
×
596
        self,
597
        signatures: Iterable[str],
598
        product: str = "Firefox",
599
        channel: str = "nightly",
600
    ):
601
        self._signatures = set(signatures)
×
602
        self._product = product
×
603
        self._channel = channel
×
604

605
    @classmethod
×
606
    def find_new_actionable_crashes(
×
607
        cls,
608
        product: str,
609
        channel: str,
610
        days_to_check: int = 7,
611
        days_without_crashes: int = 7,
612
    ) -> "SignaturesDataFetcher":
613
        """Find new actionable crashes.
614

615
        Args:
616
            product: The product to check.
617
            channel: The release channel to check.
618
            days_to_check: The number of days to check for crashes.
619
            days_without_crashes: The number of days without crashes before the
620
                `days_to_check` to consider the signature new.
621

622
        Returns:
623
            A list of actionable signatures.
624
        """
625
        duration = days_to_check + days_without_crashes
×
626
        end_date = lmdutils.get_date_ymd("today")
×
627
        start_date = end_date - timedelta(duration)
×
628
        earliest_allowed_date = lmdutils.get_date_str(
×
629
            end_date - timedelta(days_to_check)
630
        )
631
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
632

633
        params = {
×
634
            "product": product,
635
            "release_channel": channel,
636
            "date": date_range,
637
            # TODO(investigate): should we do a local filter instead of the
638
            # following (should we exclude the signature if one of the crashes
639
            # is a shutdown hang?):
640
            # If the `ipc_shutdown_state` or `shutdown_progress` field are
641
            # non-empty then it's a shutdown hang.
642
            "ipc_shutdown_state": "__null__",
643
            "shutdown_progress": "__null__",
644
            # TODO(investigate): should we use the following instead of the
645
            # local filter.
646
            # "oom_allocation_size": "!__null__",
647
            "_aggs.signature": [
648
                "moz_crash_reason",
649
                "reason",
650
                "_histogram.date",
651
                "_cardinality.install_time",
652
                "_cardinality.oom_allocation_size",
653
            ],
654
            "_results_number": 0,
655
            "_facets_size": 10000,
656
        }
657

658
        def handler(search_resp: dict, data: list):
×
659
            logger.debug(
×
660
                "Total of %d signatures received from Socorro",
661
                len(search_resp["facets"]["signature"]),
662
            )
663

664
            for crash in search_resp["facets"]["signature"]:
×
665
                signature = crash["term"]
×
666
                if any(
×
667
                    signature.startswith(excluded_prefix)
668
                    for excluded_prefix in cls.EXCLUDED_SIGNATURE_PREFIXES
669
                ):
670
                    # Ignore signatures that start with any of the excluded prefixes.
671
                    continue
×
672

673
                facets = crash["facets"]
×
674
                installations = facets["cardinality_install_time"]["value"]
×
675
                if installations <= 1:
×
676
                    # Ignore crashes that only happen on one installation.
677
                    continue
×
678

679
                first_date = facets["histogram_date"][0]["term"]
×
680
                if first_date < earliest_allowed_date:
×
681
                    # The crash is not new, skip it.
682
                    continue
×
683

684
                if any(
×
685
                    reason["term"].startswith(io_error_prefix)
686
                    for reason in facets["reason"]
687
                    for io_error_prefix in cls.EXCLUDED_IO_ERROR_REASON_PREFIXES
688
                ):
689
                    # Ignore Network or I/O error crashes.
690
                    continue
×
691

692
                if crash["count"] < 20:
×
693
                    # For signatures with low volume, having multiple types of
694
                    # memory errors indicates potential bad hardware crashes.
695
                    num_memory_error_types = sum(
×
696
                        reason["term"] in cls.MEMORY_ACCESS_ERROR_REASONS
697
                        for reason in facets["reason"]
698
                    )
699
                    if num_memory_error_types > 1:
×
700
                        # Potential bad hardware crash, skip it.
701
                        continue
×
702

703
                # TODO: Add a filter using the `possible_bit_flips_max_confidence`
704
                # field to exclude bad hardware crashes. The filed is not available yet.
705
                # See: https://bugzilla.mozilla.org/show_bug.cgi?id=1816669#c3
706

707
                # TODO(investigate): is this needed since we are already
708
                # filtering signatures that start with "OOM | "
709
                if facets["cardinality_oom_allocation_size"]["value"]:
×
710
                    # If one of the crashes is an OOM crash, skip it.
711
                    continue
×
712

713
                # TODO(investigate): do we need to check for the `moz_crash_reason`
714
                moz_crash_reasons = facets["moz_crash_reason"]
×
715
                if moz_crash_reasons and any(
×
716
                    excluded_reason in reason["term"]
717
                    for reason in moz_crash_reasons
718
                    for excluded_reason in cls.EXCLUDED_MOZ_REASON_STRINGS
719
                ):
720
                    continue
×
721

722
                data.append(signature)
×
723

724
        signatures: list = []
×
725
        socorro.SuperSearch(
×
726
            params=params,
727
            handler=handler,
728
            handlerdata=signatures,
729
        ).wait()
730

731
        logger.debug(
×
732
            "Total of %d signatures left after applying the filtering criteria",
733
            len(signatures),
734
        )
735

736
        return cls(signatures, product, channel)
×
737

738
    def fetch_clouseau_crash_reports(self) -> dict[str, list]:
×
739
        """Fetch the crash reports data from Crash Clouseau."""
740
        if not self._signatures:
×
741
            return {}
×
742

743
        logger.debug(
×
744
            "Fetch from Clouseau: requesting reports for %d signatures",
745
            len(self._signatures),
746
        )
747

748
        signature_reports = clouseau.Reports.get_by_signatures(
×
749
            self._signatures,
750
            product=self._product,
751
            channel=self._channel,
752
        )
753

754
        logger.debug(
×
755
            "Fetch from Clouseau: received reports for %d signatures",
756
            len(signature_reports),
757
        )
758

759
        return signature_reports
×
760

761
    def fetch_socorro_info(self) -> tuple[list[dict], int]:
×
762
        """Fetch the signature data from Socorro."""
763
        if not self._signatures:
×
764
            return [], 0
×
765

766
        # TODO(investigate): should we increase the duration to 6 months?
767
        duration = timedelta(weeks=1)
×
768
        end_date = lmdutils.get_date_ymd("today")
×
769
        start_date = end_date - duration
×
770
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
771

772
        params = {
×
773
            "product": self._product,
774
            # TODO(investigate): should we included all release channels?
775
            "release_channel": self._channel,
776
            # TODO(investigate): should we limit based on the build date as well?
777
            "date": date_range,
778
            # TODO: split signatures into chunks to avoid very long query URLs
779
            "signature": ["=" + signature for signature in self._signatures],
780
            "_aggs.signature": [
781
                "address",
782
                "build_id",
783
                "cpu_arch",
784
                "proto_signature",
785
                "_cardinality.user_comments",
786
                "cpu_arch",
787
                "platform_pretty_version",
788
                # The following are needed for SignatureStats:
789
                "platform",
790
                "is_garbage_collecting",
791
                "_cardinality.install_time",
792
                "startup_crash",
793
                "_histogram.uptime",
794
                "process_type",
795
            ],
796
            "_results_number": 0,
797
            "_facets_size": 10000,
798
        }
799

800
        def handler(search_results: dict, data: dict):
×
801
            data["num_total_crashes"] = search_results["total"]
×
802
            data["signatures"] = search_results["facets"]["signature"]
×
803

804
        logger.debug(
×
805
            "Fetch from Socorro: requesting info for %d signatures",
806
            len(self._signatures),
807
        )
808

809
        data: dict = {}
×
810
        socorro.SuperSearchUnredacted(
×
811
            params=params,
812
            handler=handler,
813
            handlerdata=data,
814
        ).wait()
815

816
        logger.debug(
×
817
            "Fetch from Socorro: received info for %d signatures",
818
            len(data["signatures"]),
819
        )
820

821
        return data["signatures"], data["num_total_crashes"]
×
822

823
    def fetch_bugs(self, include_fields: list[str] = None) -> dict[str, list[dict]]:
×
824
        """Fetch bugs that are filed against the given signatures."""
825
        if not self._signatures:
×
826
            return {}
×
827

828
        params_base: dict = {
×
829
            "include_fields": [
830
                "cf_crash_signature",
831
            ],
832
        }
833

834
        if include_fields:
×
835
            params_base["include_fields"].extend(include_fields)
×
836

837
        params_list = []
×
838
        for signatures_chunk in Connection.chunks(list(self._signatures), 30):
×
839
            params = params_base.copy()
×
840
            n = int(utils.get_last_field_num(params))
×
841
            params[f"f{n}"] = "OP"
×
842
            params[f"j{n}"] = "OR"
×
843
            for signature in signatures_chunk:
×
844
                n += 1
×
845
                params[f"f{n}"] = "cf_crash_signature"
×
846
                params[f"o{n}"] = "regexp"
×
847
                params[f"v{n}"] = rf"\[(@ |@){re.escape(signature)}( \]|\])"
×
848
            params[f"f{n+1}"] = "CP"
×
849
            params_list.append(params)
×
850

851
        signatures_bugs: dict = defaultdict(list)
×
852

853
        def handler(res, data):
×
854
            for bug in res["bugs"]:
×
855
                for signature in utils.get_signatures(bug["cf_crash_signature"]):
×
856
                    if signature in self._signatures:
×
857
                        data[signature].append(bug)
×
858

859
        logger.debug(
×
860
            "Fetch from Bugzilla: requesting bugs for %d signatures",
861
            len(self._signatures),
862
        )
863
        timeout = utils.get_config("common", "bz_query_timeout")
×
864
        Bugzilla(
×
865
            timeout=timeout,
866
            queries=[
867
                connection.Query(Bugzilla.API_URL, params, handler, signatures_bugs)
868
                for params in params_list
869
            ],
870
        ).wait()
871

872
        # TODO: remove the call to DevBugzilla after moving to production
873
        DevBugzilla(
×
874
            timeout=timeout,
875
            queries=[
876
                connection.Query(DevBugzilla.API_URL, params, handler, signatures_bugs)
877
                for params in params_list
878
            ],
879
        ).wait()
880

881
        logger.debug(
×
882
            "Fetch from Bugzilla: received bugs for %d signatures", len(signatures_bugs)
883
        )
884

885
        return signatures_bugs
×
886

887
    def analyze(self) -> list[SignatureAnalyzer]:
×
888
        """Analyze the data related to the signatures."""
889
        bugs = self.fetch_bugs()
×
890
        # TODO(investigate): For now, we are ignoring signatures that have bugs
891
        # filed even if they are closed long time ago. We should investigate
892
        # whether we should include the ones with closed bugs. For example, if
893
        # the bug was closed as Fixed years ago.
894
        self._signatures.difference_update(bugs.keys())
×
895

896
        clouseau_reports = self.fetch_clouseau_crash_reports()
×
897
        # TODO(investigate): For now, we are ignoring signatures that are not
898
        # analyzed by clouseau. We should investigate why they are not analyzed
899
        # and whether we should include them.
900
        self._signatures.intersection_update(clouseau_reports.keys())
×
901

902
        signatures, num_total_crashes = self.fetch_socorro_info()
×
903
        bugs_store = BugsStore()
×
904

905
        return [
×
906
            SignatureAnalyzer(
907
                signature,
908
                num_total_crashes,
909
                clouseau_reports[signature["term"]],
910
                bugs_store,
911
            )
912
            for signature in signatures
913
        ]
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