Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
9 Cu.import("resource://gre/modules/Log.jsm", this);
10 Cu.import("resource://gre/modules/osfile.jsm", this)
11 Cu.import("resource://gre/modules/Promise.jsm", this);
12 Cu.import("resource://gre/modules/Services.jsm", this);
13 Cu.import("resource://gre/modules/Task.jsm", this);
14 Cu.import("resource://gre/modules/Timer.jsm", this);
15 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
16 Cu.import("resource://services-common/utils.js", this);
18 this.EXPORTED_SYMBOLS = [
19 "CrashManager",
20 ];
22 /**
23 * How long to wait after application startup before crash event files are
24 * automatically aggregated.
25 *
26 * We defer aggregation for performance reasons, as we don't want too many
27 * services competing for I/O immediately after startup.
28 */
29 const AGGREGATE_STARTUP_DELAY_MS = 57000;
31 const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
33 // Converts Date to days since UNIX epoch.
34 // This was copied from /services/metrics.storage.jsm. The implementation
35 // does not account for leap seconds.
36 function dateToDays(date) {
37 return Math.floor(date.getTime() / MILLISECONDS_IN_DAY);
38 }
41 /**
42 * A gateway to crash-related data.
43 *
44 * This type is generic and can be instantiated any number of times.
45 * However, most applications will typically only have one instance
46 * instantiated and that instance will point to profile and user appdata
47 * directories.
48 *
49 * Instances are created by passing an object with properties.
50 * Recognized properties are:
51 *
52 * pendingDumpsDir (string) (required)
53 * Where dump files that haven't been uploaded are located.
54 *
55 * submittedDumpsDir (string) (required)
56 * Where records of uploaded dumps are located.
57 *
58 * eventsDirs (array)
59 * Directories (defined as strings) where events files are written. This
60 * instance will collects events from files in the directories specified.
61 *
62 * storeDir (string)
63 * Directory we will use for our data store. This instance will write
64 * data files into the directory specified.
65 *
66 * telemetryStoreSizeKey (string)
67 * Telemetry histogram to report store size under.
68 */
69 this.CrashManager = function (options) {
70 for (let k of ["pendingDumpsDir", "submittedDumpsDir", "eventsDirs",
71 "storeDir"]) {
72 if (!(k in options)) {
73 throw new Error("Required key not present in options: " + k);
74 }
75 }
77 this._log = Log.repository.getLogger("Crashes.CrashManager");
79 for (let k in options) {
80 let v = options[k];
82 switch (k) {
83 case "pendingDumpsDir":
84 this._pendingDumpsDir = v;
85 break;
87 case "submittedDumpsDir":
88 this._submittedDumpsDir = v;
89 break;
91 case "eventsDirs":
92 this._eventsDirs = v;
93 break;
95 case "storeDir":
96 this._storeDir = v;
97 break;
99 case "telemetryStoreSizeKey":
100 this._telemetryStoreSizeKey = v;
101 break;
103 default:
104 throw new Error("Unknown property in options: " + k);
105 }
106 }
108 // Promise for in-progress aggregation operation. We store it on the
109 // object so it can be returned for in-progress operations.
110 this._aggregatePromise = null;
112 // The CrashStore currently attached to this object.
113 this._store = null;
115 // The timer controlling the expiration of the CrashStore instance.
116 this._storeTimer = null;
118 // This is a semaphore that prevents the store from being freed by our
119 // timer-based resource freeing mechanism.
120 this._storeProtectedCount = 0;
121 };
123 this.CrashManager.prototype = Object.freeze({
124 DUMP_REGEX: /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.dmp$/i,
125 SUBMITTED_REGEX: /^bp-(?:hr-)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.txt$/i,
126 ALL_REGEX: /^(.*)$/,
128 // How long the store object should persist in memory before being
129 // automatically garbage collected.
130 STORE_EXPIRATION_MS: 60 * 1000,
132 // Number of days after which a crash with no activity will get purged.
133 PURGE_OLDER_THAN_DAYS: 180,
135 // The following are return codes for individual event file processing.
136 // File processed OK.
137 EVENT_FILE_SUCCESS: "ok",
138 // The event appears to be malformed.
139 EVENT_FILE_ERROR_MALFORMED: "malformed",
140 // The type of event is unknown.
141 EVENT_FILE_ERROR_UNKNOWN_EVENT: "unknown-event",
143 /**
144 * Obtain a list of all dumps pending upload.
145 *
146 * The returned value is a promise that resolves to an array of objects
147 * on success. Each element in the array has the following properties:
148 *
149 * id (string)
150 * The ID of the crash (a UUID).
151 *
152 * path (string)
153 * The filename of the crash (<UUID.dmp>)
154 *
155 * date (Date)
156 * When this dump was created
157 *
158 * The returned arry is sorted by the modified time of the file backing
159 * the entry, oldest to newest.
160 *
161 * @return Promise<Array>
162 */
163 pendingDumps: function () {
164 return this._getDirectoryEntries(this._pendingDumpsDir, this.DUMP_REGEX);
165 },
167 /**
168 * Obtain a list of all dump files corresponding to submitted crashes.
169 *
170 * The returned value is a promise that resolves to an Array of
171 * objects. Each object has the following properties:
172 *
173 * path (string)
174 * The path of the file this entry comes from.
175 *
176 * id (string)
177 * The crash UUID.
178 *
179 * date (Date)
180 * The (estimated) date this crash was submitted.
181 *
182 * The returned array is sorted by the modified time of the file backing
183 * the entry, oldest to newest.
184 *
185 * @return Promise<Array>
186 */
187 submittedDumps: function () {
188 return this._getDirectoryEntries(this._submittedDumpsDir,
189 this.SUBMITTED_REGEX);
190 },
192 /**
193 * Aggregates "loose" events files into the unified "database."
194 *
195 * This function should be called periodically to collect metadata from
196 * all events files into the central data store maintained by this manager.
197 *
198 * Once events have been stored in the backing store the corresponding
199 * source files are deleted.
200 *
201 * Only one aggregation operation is allowed to occur at a time. If this
202 * is called when an existing aggregation is in progress, the promise for
203 * the original call will be returned.
204 *
205 * @return promise<int> The number of event files that were examined.
206 */
207 aggregateEventsFiles: function () {
208 if (this._aggregatePromise) {
209 return this._aggregatePromise;
210 }
212 return this._aggregatePromise = Task.spawn(function* () {
213 if (this._aggregatePromise) {
214 return this._aggregatePromise;
215 }
217 try {
218 let unprocessedFiles = yield this._getUnprocessedEventsFiles();
220 let deletePaths = [];
221 let needsSave = false;
223 this._storeProtectedCount++;
224 for (let entry of unprocessedFiles) {
225 try {
226 let result = yield this._processEventFile(entry);
228 switch (result) {
229 case this.EVENT_FILE_SUCCESS:
230 needsSave = true;
231 // Fall through.
233 case this.EVENT_FILE_ERROR_MALFORMED:
234 deletePaths.push(entry.path);
235 break;
237 case this.EVENT_FILE_ERROR_UNKNOWN_EVENT:
238 break;
240 default:
241 Cu.reportError("Unhandled crash event file return code. Please " +
242 "file a bug: " + result);
243 }
244 } catch (ex if ex instanceof OS.File.Error) {
245 this._log.warn("I/O error reading " + entry.path + ": " +
246 CommonUtils.exceptionStr(ex));
247 } catch (ex) {
248 // We should never encounter an exception. This likely represents
249 // a coding error because all errors should be detected and
250 // converted to return codes.
251 //
252 // If we get here, report the error and delete the source file
253 // so we don't see it again.
254 Cu.reportError("Exception when processing crash event file: " +
255 CommonUtils.exceptionStr(ex));
256 deletePaths.push(entry.path);
257 }
258 }
260 if (needsSave) {
261 let store = yield this._getStore();
262 yield store.save();
263 }
265 for (let path of deletePaths) {
266 try {
267 yield OS.File.remove(path);
268 } catch (ex) {
269 this._log.warn("Error removing event file (" + path + "): " +
270 CommonUtils.exceptionStr(ex));
271 }
272 }
274 return unprocessedFiles.length;
276 } finally {
277 this._aggregatePromise = false;
278 this._storeProtectedCount--;
279 }
280 }.bind(this));
281 },
283 /**
284 * Prune old crash data.
285 *
286 * @param date
287 * (Date) The cutoff point for pruning. Crashes without data newer
288 * than this will be pruned.
289 */
290 pruneOldCrashes: function (date) {
291 return Task.spawn(function* () {
292 let store = yield this._getStore();
293 store.pruneOldCrashes(date);
294 yield store.save();
295 }.bind(this));
296 },
298 /**
299 * Run tasks that should be periodically performed.
300 */
301 runMaintenanceTasks: function () {
302 return Task.spawn(function* () {
303 yield this.aggregateEventsFiles();
305 let offset = this.PURGE_OLDER_THAN_DAYS * MILLISECONDS_IN_DAY;
306 yield this.pruneOldCrashes(new Date(Date.now() - offset));
307 }.bind(this));
308 },
310 /**
311 * Schedule maintenance tasks for some point in the future.
312 *
313 * @param delay
314 * (integer) Delay in milliseconds when maintenance should occur.
315 */
316 scheduleMaintenance: function (delay) {
317 let deferred = Promise.defer();
319 setTimeout(() => {
320 this.runMaintenanceTasks().then(deferred.resolve, deferred.reject);
321 }, delay);
323 return deferred.promise;
324 },
326 /**
327 * Obtain the paths of all unprocessed events files.
328 *
329 * The promise-resolved array is sorted by file mtime, oldest to newest.
330 */
331 _getUnprocessedEventsFiles: function () {
332 return Task.spawn(function* () {
333 let entries = [];
335 for (let dir of this._eventsDirs) {
336 for (let e of yield this._getDirectoryEntries(dir, this.ALL_REGEX)) {
337 entries.push(e);
338 }
339 }
341 entries.sort((a, b) => { return a.date - b.date; });
343 return entries;
344 }.bind(this));
345 },
347 // See docs/crash-events.rst for the file format specification.
348 _processEventFile: function (entry) {
349 return Task.spawn(function* () {
350 let data = yield OS.File.read(entry.path);
351 let store = yield this._getStore();
353 let decoder = new TextDecoder();
354 data = decoder.decode(data);
356 let type, time, payload;
357 let start = 0;
358 for (let i = 0; i < 2; i++) {
359 let index = data.indexOf("\n", start);
360 if (index == -1) {
361 return this.EVENT_FILE_ERROR_MALFORMED;
362 }
364 let sub = data.substring(start, index);
365 switch (i) {
366 case 0:
367 type = sub;
368 break;
369 case 1:
370 time = sub;
371 try {
372 time = parseInt(time, 10);
373 } catch (ex) {
374 return this.EVENT_FILE_ERROR_MALFORMED;
375 }
376 }
378 start = index + 1;
379 }
380 let date = new Date(time * 1000);
381 let payload = data.substring(start);
383 return this._handleEventFilePayload(store, entry, type, date, payload);
384 }.bind(this));
385 },
387 _handleEventFilePayload: function (store, entry, type, date, payload) {
388 // The payload types and formats are documented in docs/crash-events.rst.
389 // Do not change the format of an existing type. Instead, invent a new
390 // type.
392 let eventMap = {
393 "crash.main.1": "addMainProcessCrash",
394 "crash.plugin.1": "addPluginCrash",
395 "hang.plugin.1": "addPluginHang",
396 };
398 if (type in eventMap) {
399 let lines = payload.split("\n");
400 if (lines.length > 1) {
401 this._log.warn("Multiple lines unexpected in payload for " +
402 entry.path);
403 return this.EVENT_FILE_ERROR_MALFORMED;
404 }
406 store[eventMap[type]](payload, date);
407 return this.EVENT_FILE_SUCCESS;
408 }
410 // DO NOT ADD NEW TYPES WITHOUT DOCUMENTING!
412 return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
413 },
415 /**
416 * The resolved promise is an array of objects with the properties:
417 *
418 * path -- String filename
419 * id -- regexp.match()[1] (likely the crash ID)
420 * date -- Date mtime of the file
421 */
422 _getDirectoryEntries: function (path, re) {
423 return Task.spawn(function* () {
424 try {
425 yield OS.File.stat(path);
426 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
427 return [];
428 }
430 let it = new OS.File.DirectoryIterator(path);
431 let entries = [];
433 try {
434 yield it.forEach((entry, index, it) => {
435 if (entry.isDir) {
436 return;
437 }
439 let match = re.exec(entry.name);
440 if (!match) {
441 return;
442 }
444 return OS.File.stat(entry.path).then((info) => {
445 entries.push({
446 path: entry.path,
447 id: match[1],
448 date: info.lastModificationDate,
449 });
450 });
451 });
452 } finally {
453 it.close();
454 }
456 entries.sort((a, b) => { return a.date - b.date; });
458 return entries;
459 }.bind(this));
460 },
462 _getStore: function () {
463 return Task.spawn(function* () {
464 if (!this._store) {
465 yield OS.File.makeDir(this._storeDir, {
466 ignoreExisting: true,
467 unixMode: OS.Constants.libc.S_IRWXU,
468 });
470 let store = new CrashStore(this._storeDir, this._telemetryStoreSizeKey);
471 yield store.load();
473 this._store = store;
474 this._storeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
475 }
477 // The application can go long periods without interacting with the
478 // store. Since the store takes up resources, we automatically "free"
479 // the store after inactivity so resources can be returned to the system.
480 // We do this via a timer and a mechanism that tracks when the store
481 // is being accessed.
482 this._storeTimer.cancel();
484 // This callback frees resources from the store unless the store
485 // is protected from freeing by some other process.
486 let timerCB = function () {
487 if (this._storeProtectedCount) {
488 this._storeTimer.initWithCallback(timerCB, this.STORE_EXPIRATION_MS,
489 this._storeTimer.TYPE_ONE_SHOT);
490 return;
491 }
493 // We kill the reference that we hold. GC will kill it later. If
494 // someone else holds a reference, that will prevent GC until that
495 // reference is gone.
496 this._store = null;
497 this._storeTimer = null;
498 }.bind(this);
500 this._storeTimer.initWithCallback(timerCB, this.STORE_EXPIRATION_MS,
501 this._storeTimer.TYPE_ONE_SHOT);
503 return this._store;
504 }.bind(this));
505 },
507 /**
508 * Obtain information about all known crashes.
509 *
510 * Returns an array of CrashRecord instances. Instances are read-only.
511 */
512 getCrashes: function () {
513 return Task.spawn(function* () {
514 let store = yield this._getStore();
516 return store.crashes;
517 }.bind(this));
518 },
520 getCrashCountsByDay: function () {
521 return Task.spawn(function* () {
522 let store = yield this._getStore();
524 return store._countsByDay;
525 }.bind(this));
526 },
527 });
529 let gCrashManager;
531 /**
532 * Interface to storage of crash data.
533 *
534 * This type handles storage of crash metadata. It exists as a separate type
535 * from the crash manager for performance reasons: since all crash metadata
536 * needs to be loaded into memory for access, we wish to easily dispose of all
537 * associated memory when this data is no longer needed. Having an isolated
538 * object whose references can easily be lost faciliates that simple disposal.
539 *
540 * When metadata is updated, the caller must explicitly persist the changes
541 * to disk. This prevents excessive I/O during updates.
542 *
543 * The store has a mechanism for ensuring it doesn't grow too large. A ceiling
544 * is placed on the number of daily events that can occur for events that can
545 * occur with relatively high frequency, notably plugin crashes and hangs
546 * (plugins can enter cycles where they repeatedly crash). If we've reached
547 * the high water mark and new data arrives, it's silently dropped.
548 * However, the count of actual events is always preserved. This allows
549 * us to report on the severity of problems beyond the storage threshold.
550 *
551 * Main process crashes are excluded from limits because they are both
552 * important and should be rare.
553 *
554 * @param storeDir (string)
555 * Directory the store should be located in.
556 * @param telemetrySizeKey (string)
557 * The telemetry histogram that should be used to store the size
558 * of the data file.
559 */
560 function CrashStore(storeDir, telemetrySizeKey) {
561 this._storeDir = storeDir;
562 this._telemetrySizeKey = telemetrySizeKey;
564 this._storePath = OS.Path.join(storeDir, "store.json.mozlz4");
566 // Holds the read data from disk.
567 this._data = null;
569 // Maps days since UNIX epoch to a Map of event types to counts.
570 // This data structure is populated when the JSON file is loaded
571 // and is also updated when new events are added.
572 this._countsByDay = new Map();
573 }
575 CrashStore.prototype = Object.freeze({
576 // A crash that occurred in the main process.
577 TYPE_MAIN_CRASH: "main-crash",
579 // A crash in a plugin process.
580 TYPE_PLUGIN_CRASH: "plugin-crash",
582 // A hang in a plugin process.
583 TYPE_PLUGIN_HANG: "plugin-hang",
585 // Maximum number of events to store per day. This establishes a
586 // ceiling on the per-type/per-day records that will be stored.
587 HIGH_WATER_DAILY_THRESHOLD: 100,
589 /**
590 * Load data from disk.
591 *
592 * @return Promise
593 */
594 load: function () {
595 return Task.spawn(function* () {
596 // Loading replaces data. So reset data structures.
597 this._data = {
598 v: 1,
599 crashes: new Map(),
600 corruptDate: null,
601 };
602 this._countsByDay = new Map();
604 try {
605 let decoder = new TextDecoder();
606 let data = yield OS.File.read(this._storePath, {compression: "lz4"});
607 data = JSON.parse(decoder.decode(data));
609 if (data.corruptDate) {
610 this._data.corruptDate = new Date(data.corruptDate);
611 }
613 // actualCounts is used to validate that the derived counts by
614 // days stored in the payload matches up to actual data.
615 let actualCounts = new Map();
617 for (let id in data.crashes) {
618 let crash = data.crashes[id];
619 let denormalized = this._denormalize(crash);
621 this._data.crashes.set(id, denormalized);
623 let key = dateToDays(denormalized.crashDate) + "-" + denormalized.type;
624 actualCounts.set(key, (actualCounts.get(key) || 0) + 1);
625 }
627 // The validation in this loop is arguably not necessary. We perform
628 // it as a defense against unknown bugs.
629 for (let dayKey in data.countsByDay) {
630 let day = parseInt(dayKey, 10);
631 for (let type in data.countsByDay[day]) {
632 this._ensureCountsForDay(day);
634 let count = data.countsByDay[day][type];
635 let key = day + "-" + type;
637 // If the payload says we have data for a given day but we
638 // don't, the payload is wrong. Ignore it.
639 if (!actualCounts.has(key)) {
640 continue;
641 }
643 // If we encountered more data in the payload than what the
644 // data structure says, use the proper value.
645 count = Math.max(count, actualCounts.get(key));
647 this._countsByDay.get(day).set(type, count);
648 }
649 }
650 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
651 // Missing files (first use) are allowed.
652 } catch (ex) {
653 // If we can't load for any reason, mark a corrupt date in the instance
654 // and swallow the error.
655 //
656 // The marking of a corrupted file is intentionally not persisted to
657 // disk yet. Instead, we wait until the next save(). This is to give
658 // non-permanent failures the opportunity to recover on their own.
659 this._data.corruptDate = new Date();
660 }
661 }.bind(this));
662 },
664 /**
665 * Save data to disk.
666 *
667 * @return Promise<null>
668 */
669 save: function () {
670 return Task.spawn(function* () {
671 if (!this._data) {
672 return;
673 }
675 let normalized = {
676 // The version should be incremented whenever the format
677 // changes.
678 v: 1,
679 // Maps crash IDs to objects defining the crash.
680 crashes: {},
681 // Maps days since UNIX epoch to objects mapping event types to
682 // counts. This is a mirror of this._countsByDay. e.g.
683 // {
684 // 15000: {
685 // "main-crash": 2,
686 // "plugin-crash": 1
687 // }
688 // }
689 countsByDay: {},
691 // When the store was last corrupted.
692 corruptDate: null,
693 };
695 if (this._data.corruptDate) {
696 normalized.corruptDate = this._data.corruptDate.getTime();
697 }
699 for (let [id, crash] of this._data.crashes) {
700 let c = this._normalize(crash);
701 normalized.crashes[id] = c;
702 }
704 for (let [day, m] of this._countsByDay) {
705 normalized.countsByDay[day] = {};
706 for (let [type, count] of m) {
707 normalized.countsByDay[day][type] = count;
708 }
709 }
711 let encoder = new TextEncoder();
712 let data = encoder.encode(JSON.stringify(normalized));
713 let size = yield OS.File.writeAtomic(this._storePath, data, {
714 tmpPath: this._storePath + ".tmp",
715 compression: "lz4"});
716 if (this._telemetrySizeKey) {
717 Services.telemetry.getHistogramById(this._telemetrySizeKey).add(size);
718 }
719 }.bind(this));
720 },
722 /**
723 * Normalize an object into one fit for serialization.
724 *
725 * This function along with _denormalize() serve to hack around the
726 * default handling of Date JSON serialization because Date serialization
727 * is undefined by JSON.
728 *
729 * Fields ending with "Date" are assumed to contain Date instances.
730 * We convert these to milliseconds since epoch on output and back to
731 * Date on input.
732 */
733 _normalize: function (o) {
734 let normalized = {};
736 for (let k in o) {
737 let v = o[k];
738 if (v && k.endsWith("Date")) {
739 normalized[k] = v.getTime();
740 } else {
741 normalized[k] = v;
742 }
743 }
745 return normalized;
746 },
748 /**
749 * Convert a serialized object back to its native form.
750 */
751 _denormalize: function (o) {
752 let n = {};
754 for (let k in o) {
755 let v = o[k];
756 if (v && k.endsWith("Date")) {
757 n[k] = new Date(parseInt(v, 10));
758 } else {
759 n[k] = v;
760 }
761 }
763 return n;
764 },
766 /**
767 * Prune old crash data.
768 *
769 * Crashes without recent activity are pruned from the store so the
770 * size of the store is not unbounded. If there is activity on a crash,
771 * that activity will keep the crash and all its data around for longer.
772 *
773 * @param date
774 * (Date) The cutoff at which data will be pruned. If an entry
775 * doesn't have data newer than this, it will be pruned.
776 */
777 pruneOldCrashes: function (date) {
778 for (let crash of this.crashes) {
779 let newest = crash.newestDate;
780 if (!newest || newest.getTime() < date.getTime()) {
781 this._data.crashes.delete(crash.id);
782 }
783 }
784 },
786 /**
787 * Date the store was last corrupted and required a reset.
788 *
789 * May be null (no corruption has ever occurred) or a Date instance.
790 */
791 get corruptDate() {
792 return this._data.corruptDate;
793 },
795 /**
796 * The number of distinct crashes tracked.
797 */
798 get crashesCount() {
799 return this._data.crashes.size;
800 },
802 /**
803 * All crashes tracked.
804 *
805 * This is an array of CrashRecord.
806 */
807 get crashes() {
808 let crashes = [];
809 for (let [id, crash] of this._data.crashes) {
810 crashes.push(new CrashRecord(crash));
811 }
813 return crashes;
814 },
816 /**
817 * Obtain a particular crash from its ID.
818 *
819 * A CrashRecord will be returned if the crash exists. null will be returned
820 * if the crash is unknown.
821 */
822 getCrash: function (id) {
823 for (let crash of this.crashes) {
824 if (crash.id == id) {
825 return crash;
826 }
827 }
829 return null;
830 },
832 _ensureCountsForDay: function (day) {
833 if (!this._countsByDay.has(day)) {
834 this._countsByDay.set(day, new Map());
835 }
836 },
838 /**
839 * Ensure the crash record is present in storage.
840 *
841 * Returns the crash record if we're allowed to store it or null
842 * if we've hit the high water mark.
843 *
844 * @param id
845 * (string) The crash ID.
846 * @param type
847 * (string) One of the this.TYPE_* constants describing the crash type.
848 * @param date
849 * (Date) When this crash occurred.
850 *
851 * @return null | object crash record
852 */
853 _ensureCrashRecord: function (id, type, date) {
854 let day = dateToDays(date);
855 this._ensureCountsForDay(day);
857 let count = (this._countsByDay.get(day).get(type) || 0) + 1;
858 this._countsByDay.get(day).set(type, count);
860 if (count > this.HIGH_WATER_DAILY_THRESHOLD && type != this.TYPE_MAIN_CRASH) {
861 return null;
862 }
864 if (!this._data.crashes.has(id)) {
865 this._data.crashes.set(id, {
866 id: id,
867 type: type,
868 crashDate: date,
869 });
870 }
872 let crash = this._data.crashes.get(id);
873 crash.type = type;
874 crash.date = date;
876 return crash;
877 },
879 /**
880 * Record the occurrence of a crash in the main process.
881 *
882 * @param id (string) Crash ID. Likely a UUID.
883 * @param date (Date) When the crash occurred.
884 */
885 addMainProcessCrash: function (id, date) {
886 this._ensureCrashRecord(id, this.TYPE_MAIN_CRASH, date);
887 },
889 /**
890 * Record the occurrence of a crash in a plugin process.
891 *
892 * @param id (string) Crash ID. Likely a UUID.
893 * @param date (Date) When the crash occurred.
894 */
895 addPluginCrash: function (id, date) {
896 this._ensureCrashRecord(id, this.TYPE_PLUGIN_CRASH, date);
897 },
899 /**
900 * Record the occurrence of a hang in a plugin process.
901 *
902 * @param id (string) Crash ID. Likely a UUID.
903 * @param date (Date) When the hang was reported.
904 */
905 addPluginHang: function (id, date) {
906 this._ensureCrashRecord(id, this.TYPE_PLUGIN_HANG, date);
907 },
909 get mainProcessCrashes() {
910 let crashes = [];
911 for (let crash of this.crashes) {
912 if (crash.isMainProcessCrash) {
913 crashes.push(crash);
914 }
915 }
917 return crashes;
918 },
920 get pluginCrashes() {
921 let crashes = [];
922 for (let crash of this.crashes) {
923 if (crash.isPluginCrash) {
924 crashes.push(crash);
925 }
926 }
928 return crashes;
929 },
931 get pluginHangs() {
932 let crashes = [];
933 for (let crash of this.crashes) {
934 if (crash.isPluginHang) {
935 crashes.push(crash);
936 }
937 }
939 return crashes;
940 },
941 });
943 /**
944 * Represents an individual crash with metadata.
945 *
946 * This is a wrapper around the low-level anonymous JS objects that define
947 * crashes. It exposes a consistent and helpful API.
948 *
949 * Instances of this type should only be constructured inside this module,
950 * not externally. The constructor is not considered a public API.
951 *
952 * @param o (object)
953 * The crash's entry from the CrashStore.
954 */
955 function CrashRecord(o) {
956 this._o = o;
957 }
959 CrashRecord.prototype = Object.freeze({
960 get id() {
961 return this._o.id;
962 },
964 get crashDate() {
965 return this._o.crashDate;
966 },
968 /**
969 * Obtain the newest date in this record.
970 *
971 * This is a convenience getter. The returned value is used to determine when
972 * to expire a record.
973 */
974 get newestDate() {
975 // We currently only have 1 date, so this is easy.
976 return this._o.crashDate;
977 },
979 get oldestDate() {
980 return this._o.crashDate;
981 },
983 get type() {
984 return this._o.type;
985 },
987 get isMainProcessCrash() {
988 return this._o.type == CrashStore.prototype.TYPE_MAIN_CRASH;
989 },
991 get isPluginCrash() {
992 return this._o.type == CrashStore.prototype.TYPE_PLUGIN_CRASH;
993 },
995 get isPluginHang() {
996 return this._o.type == CrashStore.prototype.TYPE_PLUGIN_HANG;
997 },
998 });
1000 /**
1001 * Obtain the global CrashManager instance used by the running application.
1002 *
1003 * CrashManager is likely only ever instantiated once per application lifetime.
1004 * The main reason it's implemented as a reusable type is to facilitate testing.
1005 */
1006 XPCOMUtils.defineLazyGetter(this.CrashManager, "Singleton", function () {
1007 if (gCrashManager) {
1008 return gCrashManager;
1009 }
1011 let crPath = OS.Path.join(OS.Constants.Path.userApplicationDataDir,
1012 "Crash Reports");
1013 let storePath = OS.Path.join(OS.Constants.Path.profileDir, "crashes");
1015 gCrashManager = new CrashManager({
1016 pendingDumpsDir: OS.Path.join(crPath, "pending"),
1017 submittedDumpsDir: OS.Path.join(crPath, "submitted"),
1018 eventsDirs: [OS.Path.join(crPath, "events"), OS.Path.join(storePath, "events")],
1019 storeDir: storePath,
1020 telemetryStoreSizeKey: "CRASH_STORE_COMPRESSED_BYTES",
1021 });
1023 // Automatically aggregate event files shortly after startup. This
1024 // ensures it happens with some frequency.
1025 //
1026 // There are performance considerations here. While this is doing
1027 // work and could negatively impact performance, the amount of work
1028 // is kept small per run by periodically aggregating event files.
1029 // Furthermore, well-behaving installs should not have much work
1030 // here to do. If there is a lot of work, that install has bigger
1031 // issues beyond reduced performance near startup.
1032 gCrashManager.scheduleMaintenance(AGGREGATE_STARTUP_DELAY_MS);
1034 return gCrashManager;
1035 });