Source code

Revision control

Other Tools

1
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
"use strict";
7
8
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
9
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
10
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
11
ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
12
const { AppConstants } = ChromeUtils.import(
14
);
15
16
XPCOMUtils.defineLazyModuleGetters(this, {
17
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
23
TelemetryReportingPolicy:
26
});
27
28
const Utils = TelemetryUtils;
29
30
const myScope = this;
31
32
// When modifying the payload in incompatible ways, please bump this version number
33
const PAYLOAD_VERSION = 4;
34
const PING_TYPE_MAIN = "main";
35
const PING_TYPE_SAVED_SESSION = "saved-session";
36
37
const REASON_ABORTED_SESSION = "aborted-session";
38
const REASON_DAILY = "daily";
39
const REASON_SAVED_SESSION = "saved-session";
40
const REASON_GATHER_PAYLOAD = "gather-payload";
41
const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
42
const REASON_TEST_PING = "test-ping";
43
const REASON_ENVIRONMENT_CHANGE = "environment-change";
44
const REASON_SHUTDOWN = "shutdown";
45
46
const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
47
48
const MIN_SUBSESSION_LENGTH_MS =
49
Services.prefs.getIntPref("toolkit.telemetry.minSubsessionLength", 5 * 60) *
50
1000;
51
52
const LOGGER_NAME = "Toolkit.Telemetry";
53
const LOGGER_PREFIX =
54
"TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::");
55
56
// Whether the FHR/Telemetry unification features are enabled.
57
// Changing this pref requires a restart.
58
const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
59
TelemetryUtils.Preferences.Unified,
60
false
61
);
62
63
var gWasDebuggerAttached = false;
64
65
XPCOMUtils.defineLazyServiceGetters(this, {
66
Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
67
});
68
69
function generateUUID() {
70
let str = Cc["@mozilla.org/uuid-generator;1"]
71
.getService(Ci.nsIUUIDGenerator)
72
.generateUUID()
73
.toString();
74
// strip {}
75
return str.substring(1, str.length - 1);
76
}
77
78
/**
79
* This is a policy object used to override behavior for testing.
80
*/
81
var Policy = {
82
now: () => new Date(),
83
monotonicNow: Utils.monotonicNow,
84
generateSessionUUID: () => generateUUID(),
85
generateSubsessionUUID: () => generateUUID(),
86
};
87
88
/**
89
* Get the ping type based on the payload.
90
* @param {Object} aPayload The ping payload.
91
* @return {String} A string representing the ping type.
92
*/
93
function getPingType(aPayload) {
94
// To remain consistent with server-side ping handling, set "saved-session" as the ping
95
// type for "saved-session" payload reasons.
96
if (aPayload.info.reason == REASON_SAVED_SESSION) {
97
return PING_TYPE_SAVED_SESSION;
98
}
99
100
return PING_TYPE_MAIN;
101
}
102
103
/**
104
* Annotate the current session ID with the crash reporter to map potential
105
* crash pings with the related main ping.
106
*/
107
function annotateCrashReport(sessionId) {
108
try {
109
const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
110
if (cr) {
111
cr.getService(Ci.nsICrashReporter).setTelemetrySessionId(sessionId);
112
}
113
} catch (e) {
114
// Ignore errors when crash reporting is disabled
115
}
116
}
117
118
/**
119
* Read current process I/O counters.
120
*/
121
var processInfo = {
122
_initialized: false,
123
_IO_COUNTERS: null,
124
_kernel32: null,
125
_GetProcessIoCounters: null,
126
_GetCurrentProcess: null,
127
getCounters() {
128
let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
129
if (isWindows) {
130
return this.getCounters_Windows();
131
}
132
return null;
133
},
134
getCounters_Windows() {
135
if (!this._initialized) {
136
var { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
137
this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [
138
{ readOps: ctypes.unsigned_long_long },
139
{ writeOps: ctypes.unsigned_long_long },
140
{ otherOps: ctypes.unsigned_long_long },
141
{ readBytes: ctypes.unsigned_long_long },
142
{ writeBytes: ctypes.unsigned_long_long },
143
{ otherBytes: ctypes.unsigned_long_long },
144
]);
145
try {
146
this._kernel32 = ctypes.open("Kernel32.dll");
147
this._GetProcessIoCounters = this._kernel32.declare(
148
"GetProcessIoCounters",
149
ctypes.winapi_abi,
150
ctypes.bool, // return
151
ctypes.voidptr_t, // hProcess
152
this._IO_COUNTERS.ptr
153
); // lpIoCounters
154
this._GetCurrentProcess = this._kernel32.declare(
155
"GetCurrentProcess",
156
ctypes.winapi_abi,
157
ctypes.voidptr_t
158
); // return
159
this._initialized = true;
160
} catch (err) {
161
return null;
162
}
163
}
164
let io = new this._IO_COUNTERS();
165
if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address())) {
166
return null;
167
}
168
return [parseInt(io.readBytes), parseInt(io.writeBytes)];
169
},
170
};
171
172
var EXPORTED_SYMBOLS = ["TelemetrySession"];
173
174
var TelemetrySession = Object.freeze({
175
/**
176
* Send a ping to a test server. Used only for testing.
177
*/
178
testPing() {
179
return Impl.testPing();
180
},
181
/**
182
* Returns the current telemetry payload.
183
* @param reason Optional, the reason to trigger the payload.
184
* @param clearSubsession Optional, whether to clear subsession specific data.
185
* @returns Object
186
*/
187
getPayload(reason, clearSubsession = false) {
188
return Impl.getPayload(reason, clearSubsession);
189
},
190
/**
191
* Save the session state to a pending file.
192
* Used only for testing purposes.
193
*/
194
testSavePendingPing() {
195
return Impl.testSavePendingPing();
196
},
197
/**
198
* Collect and store information about startup.
199
*/
200
gatherStartup() {
201
return Impl.gatherStartup();
202
},
203
/**
204
* Inform the ping which AddOns are installed.
205
*
206
* @param aAddOns - The AddOns.
207
*/
208
setAddOns(aAddOns) {
209
return Impl.setAddOns(aAddOns);
210
},
211
/**
212
* Descriptive metadata
213
*
214
* @param reason
215
* The reason for the telemetry ping, this will be included in the
216
* returned metadata,
217
* @return The metadata as a JS object
218
*/
219
getMetadata(reason) {
220
return Impl.getMetadata(reason);
221
},
222
223
/**
224
* Reset the subsession and profile subsession counter.
225
* This should only be called when the profile should be considered completely new,
226
* e.g. after opting out of sending Telemetry
227
*/
228
resetSubsessionCounter() {
229
Impl._subsessionCounter = 0;
230
Impl._profileSubsessionCounter = 0;
231
},
232
233
/**
234
* Used only for testing purposes.
235
*/
236
testReset() {
237
Impl._newProfilePingSent = false;
238
Impl._sessionId = null;
239
Impl._subsessionId = null;
240
Impl._previousSessionId = null;
241
Impl._previousSubsessionId = null;
242
Impl._subsessionCounter = 0;
243
Impl._profileSubsessionCounter = 0;
244
Impl._subsessionStartActiveTicks = 0;
245
Impl._sessionActiveTicks = 0;
246
Impl._isUserActive = true;
247
Impl._subsessionStartTimeMonotonic = 0;
248
Impl._lastEnvironmentChangeDate = Policy.monotonicNow();
249
this.testUninstall();
250
},
251
/**
252
* Triggers shutdown of the module.
253
*/
254
shutdown() {
255
return Impl.shutdownChromeProcess();
256
},
257
/**
258
* Used only for testing purposes.
259
*/
260
testUninstall() {
261
try {
262
Impl.uninstall();
263
} catch (ex) {
264
// Ignore errors
265
}
266
},
267
/**
268
* Lightweight init function, called as soon as Firefox starts.
269
*/
270
earlyInit(aTesting = false) {
271
return Impl.earlyInit(aTesting);
272
},
273
/**
274
* Does the "heavy" Telemetry initialization later on, so we
275
* don't impact startup performance.
276
* @return {Promise} Resolved when the initialization completes.
277
*/
278
delayedInit() {
279
return Impl.delayedInit();
280
},
281
/**
282
* Send a notification.
283
*/
284
observe(aSubject, aTopic, aData) {
285
return Impl.observe(aSubject, aTopic, aData);
286
},
287
/**
288
* Marks the "new-profile" ping as sent in the telemetry state file.
289
* @return {Promise} A promise resolved when the new telemetry state is saved to disk.
290
*/
291
markNewProfilePingSent() {
292
return Impl.markNewProfilePingSent();
293
},
294
/**
295
* Returns if the "new-profile" ping has ever been sent for this profile.
296
* Please note that the returned value is trustworthy only after the delayed setup.
297
*
298
* @return {Boolean} True if the new profile ping was sent on this profile,
299
* false otherwise.
300
*/
301
get newProfilePingSent() {
302
return Impl._newProfilePingSent;
303
},
304
305
saveAbortedSessionPing(aProvidedPayload) {
306
return Impl._saveAbortedSessionPing(aProvidedPayload);
307
},
308
309
sendDailyPing() {
310
return Impl._sendDailyPing();
311
},
312
});
313
314
var Impl = {
315
_initialized: false,
316
_logger: null,
317
_slowSQLStartup: {},
318
// The activity state for the user. If false, don't count the next
319
// active tick. Otherwise, increment the active ticks as usual.
320
_isUserActive: true,
321
_startupIO: {},
322
// The previous build ID, if this is the first run with a new build.
323
// Null if this is the first run, or the previous build ID is unknown.
324
_previousBuildId: null,
325
// Unique id that identifies this session so the server can cope with duplicate
326
// submissions, orphaning and other oddities. The id is shared across subsessions.
327
_sessionId: null,
328
// Random subsession id.
329
_subsessionId: null,
330
// Session id of the previous session, null on first run.
331
_previousSessionId: null,
332
// Subsession id of the previous subsession (even if it was in a different session),
333
// null on first run.
334
_previousSubsessionId: null,
335
// The running no. of subsessions since the start of the browser session
336
_subsessionCounter: 0,
337
// The running no. of all subsessions for the whole profile life time
338
_profileSubsessionCounter: 0,
339
// Date of the last session split
340
_subsessionStartDate: null,
341
// Start time of the current subsession using a monotonic clock for the subsession
342
// length measurements.
343
_subsessionStartTimeMonotonic: 0,
344
// The active ticks counted when the subsession starts
345
_subsessionStartActiveTicks: 0,
346
// Active ticks in the whole session.
347
_sessionActiveTicks: 0,
348
// A task performing delayed initialization of the chrome process
349
_delayedInitTask: null,
350
_testing: false,
351
// An accumulator of total memory across all processes. Only valid once the final child reports.
352
_lastEnvironmentChangeDate: 0,
353
// We save whether the "new-profile" ping was sent yet, to
354
// survive profile refresh and migrations.
355
_newProfilePingSent: false,
356
// Keep track of the active observers
357
_observedTopics: new Set(),
358
359
addObserver(aTopic) {
360
Services.obs.addObserver(this, aTopic);
361
this._observedTopics.add(aTopic);
362
},
363
364
removeObserver(aTopic) {
365
Services.obs.removeObserver(this, aTopic);
366
this._observedTopics.delete(aTopic);
367
},
368
369
get _log() {
370
if (!this._logger) {
371
this._logger = Log.repository.getLoggerWithMessagePrefix(
372
LOGGER_NAME,
373
LOGGER_PREFIX
374
);
375
}
376
return this._logger;
377
},
378
379
/**
380
* Gets a series of simple measurements (counters). At the moment, this
381
* only returns startup data from nsIAppStartup.getStartupInfo().
382
* @param {Boolean} isSubsession True if this is a subsession, false otherwise.
383
* @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise.
384
*
385
* @return simple measurements as a dictionary.
386
*/
387
getSimpleMeasurements: function getSimpleMeasurements(
388
forSavedSession,
389
isSubsession,
390
clearSubsession
391
) {
392
let si = Services.startup.getStartupInfo();
393
394
// Measurements common to chrome and content processes.
395
let elapsedTime = Date.now() - si.process;
396
var ret = {
397
totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds
398
};
399
400
// Look for app-specific timestamps
401
var appTimestamps = {};
402
try {
403
let o = {};
404
ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", o);
405
appTimestamps = o.TelemetryTimestamps.get();
406
} catch (ex) {}
407
408
// Only submit this if the extended set is enabled.
409
if (!Utils.isContentProcess && Telemetry.canRecordExtended) {
410
try {
411
ret.addonManager = AddonManagerPrivate.getSimpleMeasures();
412
} catch (ex) {}
413
}
414
415
if (si.process) {
416
for (let field of Object.keys(si)) {
417
if (field == "process") {
418
continue;
419
}
420
ret[field] = si[field] - si.process;
421
}
422
423
for (let p in appTimestamps) {
424
if (!(p in ret) && appTimestamps[p]) {
425
ret[p] = appTimestamps[p] - si.process;
426
}
427
}
428
}
429
430
ret.startupInterrupted = Number(Services.startup.interrupted);
431
432
let maximalNumberOfConcurrentThreads =
433
Telemetry.maximalNumberOfConcurrentThreads;
434
if (maximalNumberOfConcurrentThreads) {
435
ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads;
436
}
437
438
if (Utils.isContentProcess) {
439
return ret;
440
}
441
442
// Measurements specific to chrome process
443
444
// Update debuggerAttached flag
445
let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(
446
Ci.nsIDebug2
447
);
448
let isDebuggerAttached = debugService.isDebuggerAttached;
449
gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached;
450
ret.debuggerAttached = Number(gWasDebuggerAttached);
451
452
let shutdownDuration = Telemetry.lastShutdownDuration;
453
if (shutdownDuration) {
454
ret.shutdownDuration = shutdownDuration;
455
}
456
457
let failedProfileLockCount = Telemetry.failedProfileLockCount;
458
if (failedProfileLockCount) {
459
ret.failedProfileLockCount = failedProfileLockCount;
460
}
461
462
for (let ioCounter in this._startupIO) {
463
ret[ioCounter] = this._startupIO[ioCounter];
464
}
465
466
let activeTicks = this._sessionActiveTicks;
467
if (isSubsession) {
468
activeTicks = this._sessionActiveTicks - this._subsessionStartActiveTicks;
469
}
470
471
if (clearSubsession) {
472
this._subsessionStartActiveTicks = this._sessionActiveTicks;
473
}
474
475
ret.activeTicks = activeTicks;
476
477
return ret;
478
},
479
480
getHistograms: function getHistograms(clearSubsession) {
481
return Telemetry.getSnapshotForHistograms(
482
"main",
483
clearSubsession,
484
!this._testing
485
);
486
},
487
488
getKeyedHistograms(clearSubsession) {
489
return Telemetry.getSnapshotForKeyedHistograms(
490
"main",
491
clearSubsession,
492
!this._testing
493
);
494
},
495
496
/**
497
* Get a snapshot of the scalars and clear them.
498
* @param {subsession} If true, then we collect the data for a subsession.
499
* @param {clearSubsession} If true, we need to clear the subsession.
500
* @param {keyed} Take a snapshot of keyed or non keyed scalars.
501
* @return {Object} The scalar data as a Javascript object, including the
502
* data from child processes, in the following format:
503
* {'content': { 'scalarName': ... }, 'gpu': { ... } }
504
*/
505
getScalars(subsession, clearSubsession, keyed) {
506
if (!subsession) {
507
// We only support scalars for subsessions.
508
this._log.trace("getScalars - We only support scalars in subsessions.");
509
return {};
510
}
511
512
let scalarsSnapshot = keyed
513
? Telemetry.getSnapshotForKeyedScalars(
514
"main",
515
clearSubsession,
516
!this._testing
517
)
518
: Telemetry.getSnapshotForScalars(
519
"main",
520
clearSubsession,
521
!this._testing
522
);
523
524
return scalarsSnapshot;
525
},
526
527
/**
528
* Descriptive metadata
529
*
530
* @param reason
531
* The reason for the telemetry ping, this will be included in the
532
* returned metadata,
533
* @return The metadata as a JS object
534
*/
535
getMetadata: function getMetadata(reason) {
536
const sessionStartDate = Utils.toLocalTimeISOString(
537
Utils.truncateToHours(this._sessionStartDate)
538
);
539
const subsessionStartDate = Utils.toLocalTimeISOString(
540
Utils.truncateToHours(this._subsessionStartDate)
541
);
542
const monotonicNow = Policy.monotonicNow();
543
544
let ret = {
545
reason,
546
revision: AppConstants.SOURCE_REVISION_URL,
547
548
// Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of
549
// UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here.
550
timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(),
551
previousBuildId: this._previousBuildId,
552
553
sessionId: this._sessionId,
554
subsessionId: this._subsessionId,
555
previousSessionId: this._previousSessionId,
556
previousSubsessionId: this._previousSubsessionId,
557
558
subsessionCounter: this._subsessionCounter,
559
profileSubsessionCounter: this._profileSubsessionCounter,
560
561
sessionStartDate,
562
subsessionStartDate,
563
564
// Compute the session and subsession length in seconds.
565
// We use monotonic clocks as Date() is affected by jumping clocks (leading
566
// to negative lengths and other issues).
567
sessionLength: Math.floor(monotonicNow / 1000),
568
subsessionLength: Math.floor(
569
(monotonicNow - this._subsessionStartTimeMonotonic) / 1000
570
),
571
};
572
573
// TODO: Remove this when bug 1201837 lands.
574
if (this._addons) {
575
ret.addons = this._addons;
576
}
577
578
// TODO: Remove this when bug 1201837 lands.
579
let flashVersion = this.getFlashVersion();
580
if (flashVersion) {
581
ret.flashVersion = flashVersion;
582
}
583
584
return ret;
585
},
586
587
/**
588
* Get the current session's payload using the provided
589
* simpleMeasurements and info, which are typically obtained by a call
590
* to |this.getSimpleMeasurements| and |this.getMetadata|,
591
* respectively.
592
*/
593
assemblePayloadWithMeasurements(
594
simpleMeasurements,
595
info,
596
reason,
597
clearSubsession
598
) {
599
const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason);
600
clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession;
601
this._log.trace(
602
"assemblePayloadWithMeasurements - reason: " +
603
reason +
604
", submitting subsession data: " +
605
isSubsession
606
);
607
608
// This allows wrapping data retrieval calls in a try-catch block so that
609
// failures don't break the rest of the ping assembly.
610
const protect = (fn, defaultReturn = null) => {
611
try {
612
return fn();
613
} catch (ex) {
614
this._log.error(
615
"assemblePayloadWithMeasurements - caught exception",
616
ex
617
);
618
return defaultReturn;
619
}
620
};
621
622
// Payload common to chrome and content processes.
623
let payloadObj = {
624
ver: PAYLOAD_VERSION,
625
simpleMeasurements,
626
};
627
628
// Add extended set measurements common to chrome & content processes
629
if (Telemetry.canRecordExtended) {
630
payloadObj.log = [];
631
}
632
633
if (Utils.isContentProcess) {
634
return payloadObj;
635
}
636
637
// Additional payload for chrome process.
638
let measurements = {
639
histograms: protect(() => this.getHistograms(clearSubsession), {}),
640
keyedHistograms: protect(
641
() => this.getKeyedHistograms(clearSubsession),
642
{}
643
),
644
scalars: protect(
645
() => this.getScalars(isSubsession, clearSubsession),
646
{}
647
),
648
keyedScalars: protect(
649
() => this.getScalars(isSubsession, clearSubsession, true),
650
{}
651
),
652
};
653
654
let measurementsContainGPU = Object.keys(measurements).some(
655
key => "gpu" in measurements[key]
656
);
657
658
let measurementsContainSocket = Object.keys(measurements).some(
659
key => "socket" in measurements[key]
660
);
661
662
payloadObj.processes = {};
663
let processTypes = ["parent", "content", "extension", "dynamic"];
664
// Only include the GPU process if we've accumulated data for it.
665
if (measurementsContainGPU) {
666
processTypes.push("gpu");
667
}
668
if (measurementsContainSocket) {
669
processTypes.push("socket");
670
}
671
672
// Collect per-process measurements.
673
for (const processType of processTypes) {
674
let processPayload = {};
675
676
for (const key in measurements) {
677
let payloadLoc = processPayload;
678
// Parent histograms are added to the top-level payload object instead of the process payload.
679
if (
680
processType == "parent" &&
681
(key == "histograms" || key == "keyedHistograms")
682
) {
683
payloadLoc = payloadObj;
684
}
685
// The Dynamic process only collects scalars and keyed scalars.
686
if (
687
processType == "dynamic" &&
688
key !== "scalars" &&
689
key !== "keyedScalars"
690
) {
691
continue;
692
}
693
694
// Process measurements can be empty, set a default value.
695
payloadLoc[key] = measurements[key][processType] || {};
696
}
697
698
// Add process measurements to payload.
699
payloadObj.processes[processType] = processPayload;
700
}
701
702
payloadObj.info = info;
703
704
// Add extended set measurements for chrome process.
705
if (Telemetry.canRecordExtended) {
706
payloadObj.slowSQL = protect(() => Telemetry.slowSQL);
707
payloadObj.fileIOReports = protect(() => Telemetry.fileIOReports);
708
payloadObj.lateWrites = protect(() => Telemetry.lateWrites);
709
710
payloadObj.addonDetails = protect(() =>
711
AddonManagerPrivate.getTelemetryDetails()
712
);
713
714
let clearUIsession = !(
715
reason == REASON_GATHER_PAYLOAD ||
716
reason == REASON_GATHER_SUBSESSION_PAYLOAD
717
);
718
719
if (AppConstants.platform == "android") {
720
payloadObj.UIMeasurements = protect(() =>
721
UITelemetry.getUIMeasurements(clearUIsession)
722
);
723
}
724
725
if (
726
this._slowSQLStartup &&
727
!!Object.keys(this._slowSQLStartup).length &&
728
(Object.keys(this._slowSQLStartup.mainThread).length ||
729
Object.keys(this._slowSQLStartup.otherThreads).length)
730
) {
731
payloadObj.slowSQLStartup = this._slowSQLStartup;
732
}
733
734
if (!this._isClassicReason(reason)) {
735
payloadObj.processes.parent.gc = protect(() =>
736
GCTelemetry.entries("main", clearSubsession)
737
);
738
payloadObj.processes.content.gc = protect(() =>
739
GCTelemetry.entries("content", clearSubsession)
740
);
741
}
742
743
// Adding captured stacks to the payload only if any exist and clearing
744
// captures for this sub-session.
745
let stacks = protect(() => Telemetry.snapshotCapturedStacks(true));
746
if (stacks && "captures" in stacks && stacks.captures.length) {
747
payloadObj.processes.parent.capturedStacks = stacks;
748
}
749
}
750
751
return payloadObj;
752
},
753
754
/**
755
* Start a new subsession.
756
*/
757
startNewSubsession() {
758
this._subsessionStartDate = Policy.now();
759
this._subsessionStartTimeMonotonic = Policy.monotonicNow();
760
this._previousSubsessionId = this._subsessionId;
761
this._subsessionId = Policy.generateSubsessionUUID();
762
this._subsessionCounter++;
763
this._profileSubsessionCounter++;
764
},
765
766
getSessionPayload: function getSessionPayload(reason, clearSubsession) {
767
this._log.trace(
768
"getSessionPayload - reason: " +
769
reason +
770
", clearSubsession: " +
771
clearSubsession
772
);
773
774
let payload;
775
try {
776
const isMobile = AppConstants.platform == "android";
777
const isSubsession = isMobile ? false : !this._isClassicReason(reason);
778
779
if (isMobile) {
780
clearSubsession = false;
781
}
782
783
let measurements = this.getSimpleMeasurements(
784
reason == REASON_SAVED_SESSION,
785
isSubsession,
786
clearSubsession
787
);
788
let info = !Utils.isContentProcess ? this.getMetadata(reason) : null;
789
payload = this.assemblePayloadWithMeasurements(
790
measurements,
791
info,
792
reason,
793
clearSubsession
794
);
795
} catch (ex) {
796
Telemetry.getHistogramById("TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION").add(1);
797
throw ex;
798
} finally {
799
if (!Utils.isContentProcess && clearSubsession) {
800
this.startNewSubsession();
801
// Persist session data to disk (don't wait until it completes).
802
let sessionData = this._getSessionDataObject();
803
TelemetryStorage.saveSessionData(sessionData);
804
805
// Notify that there was a subsession split in the parent process. This is an
806
// internal topic and is only meant for internal Telemetry usage.
807
Services.obs.notifyObservers(
808
null,
809
"internal-telemetry-after-subsession-split"
810
);
811
}
812
}
813
814
return payload;
815
},
816
817
/**
818
* Send data to the server. Record success/send-time in histograms
819
*/
820
send: async function send(reason) {
821
this._log.trace("send - Reason " + reason);
822
// populate histograms one last time
823
await Services.telemetry.gatherMemory();
824
825
const isSubsession = !this._isClassicReason(reason);
826
let payload = this.getSessionPayload(reason, isSubsession);
827
let options = {
828
addClientId: true,
829
addEnvironment: true,
830
};
831
return TelemetryController.submitExternalPing(
832
getPingType(payload),
833
payload,
834
options
835
);
836
},
837
838
/**
839
* Attaches the needed observers during Telemetry early init, in the
840
* chrome process.
841
*/
842
attachEarlyObservers() {
843
this.addObserver("sessionstore-windows-restored");
844
if (AppConstants.platform === "android") {
845
this.addObserver("application-background");
846
}
847
this.addObserver("xul-window-visible");
848
849
// Attach the active-ticks related observers.
850
this.addObserver("user-interaction-active");
851
this.addObserver("user-interaction-inactive");
852
},
853
854
/**
855
* Lightweight init function, called as soon as Firefox starts.
856
*/
857
earlyInit(testing) {
858
this._log.trace("earlyInit");
859
860
this._initStarted = true;
861
this._testing = testing;
862
863
if (this._initialized && !testing) {
864
this._log.error("earlyInit - already initialized");
865
return;
866
}
867
868
if (!Telemetry.canRecordBase && !testing) {
869
this._log.config(
870
"earlyInit - Telemetry recording is disabled, skipping Chrome process setup."
871
);
872
return;
873
}
874
875
// Generate a unique id once per session so the server can cope with duplicate
876
// submissions, orphaning and other oddities. The id is shared across subsessions.
877
this._sessionId = Policy.generateSessionUUID();
878
this.startNewSubsession();
879
// startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
880
// the very same value for |_sessionStartDate|.
881
this._sessionStartDate = this._subsessionStartDate;
882
883
annotateCrashReport(this._sessionId);
884
885
// Record old value and update build ID preference if this is the first
886
// run with a new build ID.
887
let previousBuildId = Services.prefs.getStringPref(
888
TelemetryUtils.Preferences.PreviousBuildID,
889
null
890
);
891
let thisBuildID = Services.appinfo.appBuildID;
892
// If there is no previousBuildId preference, we send null to the server.
893
if (previousBuildId != thisBuildID) {
894
this._previousBuildId = previousBuildId;
895
Services.prefs.setStringPref(
896
TelemetryUtils.Preferences.PreviousBuildID,
897
thisBuildID
898
);
899
}
900
901
this.attachEarlyObservers();
902
},
903
904
/**
905
* Does the "heavy" Telemetry initialization later on, so we
906
* don't impact startup performance.
907
* @return {Promise} Resolved when the initialization completes.
908
*/
909
delayedInit() {
910
this._log.trace("delayedInit");
911
912
this._delayedInitTask = (async () => {
913
try {
914
this._initialized = true;
915
916
await this._loadSessionData();
917
// Update the session data to keep track of new subsessions created before
918
// the initialization.
919
await TelemetryStorage.saveSessionData(this._getSessionDataObject());
920
921
this.addObserver("idle-daily");
922
await Services.telemetry.gatherMemory();
923
924
Telemetry.asyncFetchTelemetryData(function() {});
925
926
if (IS_UNIFIED_TELEMETRY) {
927
// Check for a previously written aborted session ping.
928
await TelemetryController.checkAbortedSessionPing();
929
930
// Write the first aborted-session ping as early as possible. Just do that
931
// if we are not testing, since calling Telemetry.reset() will make a previous
932
// aborted ping a pending ping.
933
if (!this._testing) {
934
await this._saveAbortedSessionPing();
935
}
936
937
// The last change date for the environment, used to throttle environment changes.
938
this._lastEnvironmentChangeDate = Policy.monotonicNow();
939
TelemetryEnvironment.registerChangeListener(
940
ENVIRONMENT_CHANGE_LISTENER,
941
(reason, data) => this._onEnvironmentChange(reason, data)
942
);
943
944
// Start the scheduler.
945
// We skip this if unified telemetry is off, so we don't
946
// trigger the new unified ping types.
947
TelemetryScheduler.init();
948
}
949
950
this._delayedInitTask = null;
951
} catch (e) {
952
this._delayedInitTask = null;
953
throw e;
954
}
955
})();
956
957
return this._delayedInitTask;
958
},
959
960
getFlashVersion: function getFlashVersion() {
961
if (AppConstants.MOZ_APP_NAME == "firefox") {
962
let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
963
let tags = host.getPluginTags();
964
965
for (let i = 0; i < tags.length; i++) {
966
if (tags[i].name == "Shockwave Flash") {
967
return tags[i].version;
968
}
969
}
970
}
971
972
return null;
973
},
974
975
/**
976
* On Desktop: Save the "shutdown" ping to disk.
977
* On Android: Save the "saved-session" ping to disk.
978
* This needs to be called after TelemetrySend shuts down otherwise pings
979
* would be sent instead of getting persisted to disk.
980
*/
981
saveShutdownPings() {
982
this._log.trace("saveShutdownPings");
983
984
// We append the promises to this list and wait
985
// on all pings to be saved after kicking off their collection.
986
let p = [];
987
988
if (IS_UNIFIED_TELEMETRY) {
989
let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false);
990
991
// Only send the shutdown ping using the pingsender from the second
992
// browsing session on, to mitigate issues with "bot" profiles (see bug 1354482).
993
const sendOnThisSession =
994
Services.prefs.getBoolPref(
995
Utils.Preferences.ShutdownPingSenderFirstSession,
996
false
997
) || !TelemetryReportingPolicy.isFirstRun();
998
let sendWithPingsender =
999
Services.prefs.getBoolPref(
1000
TelemetryUtils.Preferences.ShutdownPingSender,
1001
false
1002
) && sendOnThisSession;
1003
1004
let options = {
1005
addClientId: true,
1006
addEnvironment: true,
1007
usePingSender: sendWithPingsender,
1008
};
1009
p.push(
1010
TelemetryController.submitExternalPing(
1011
getPingType(shutdownPayload),
1012
shutdownPayload,
1013
options
1014
).catch(e =>
1015
this._log.error(
1016
"saveShutdownPings - failed to submit shutdown ping",
1017
e
1018
)
1019
)
1020
);
1021
1022
// Send a duplicate of first-shutdown pings as a new ping type, in order to properly
1023
// evaluate first session profiles (see bug 1390095).
1024
const sendFirstShutdownPing =
1025
Services.prefs.getBoolPref(
1026
Utils.Preferences.ShutdownPingSender,
1027
false
1028
) &&
1029
Services.prefs.getBoolPref(
1030
Utils.Preferences.FirstShutdownPingEnabled,
1031
false
1032
) &&
1033
TelemetryReportingPolicy.isFirstRun();
1034
1035
if (sendFirstShutdownPing) {
1036
let options = {
1037
addClientId: true,
1038
addEnvironment: true,
1039
usePingSender: true,
1040
};
1041
p.push(
1042
TelemetryController.submitExternalPing(
1043
"first-shutdown",
1044
shutdownPayload,
1045
options
1046
).catch(e =>
1047
this._log.error(
1048
"saveShutdownPings - failed to submit first shutdown ping",
1049
e
1050
)
1051
)
1052
);
1053
}
1054
}
1055
1056
if (AppConstants.platform == "android" && Telemetry.canRecordExtended) {
1057
let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
1058
1059
let options = {
1060
addClientId: true,
1061
addEnvironment: true,
1062
};
1063
p.push(
1064
TelemetryController.submitExternalPing(
1065
getPingType(payload),
1066
payload,
1067
options
1068
).catch(e =>
1069
this._log.error(
1070
"saveShutdownPings - failed to submit saved-session ping",
1071
e
1072
)
1073
)
1074
);
1075
}
1076
1077
// Wait on pings to be saved.
1078
return Promise.all(p);
1079
},
1080
1081
testSavePendingPing() {
1082
let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
1083
let options = {
1084
addClientId: true,
1085
addEnvironment: true,
1086
overwrite: true,
1087
};
1088
return TelemetryController.addPendingPing(
1089
getPingType(payload),
1090
payload,
1091
options
1092
);
1093
},
1094
1095
/**
1096
* Do some shutdown work that is common to all process types.
1097
*/
1098
uninstall() {
1099
for (let topic of this._observedTopics) {
1100
try {
1101
// Tests may flip Telemetry.canRecordExtended on and off. It can be the case
1102
// that the observer TOPIC_CYCLE_COLLECTOR_BEGIN was not added.
1103
this.removeObserver(topic);
1104
} catch (e) {
1105
this._log.warn("uninstall - Failed to remove " + topic, e);
1106
}
1107
}
1108
},
1109
1110
getPayload: function getPayload(reason, clearSubsession) {
1111
this._log.trace("getPayload - clearSubsession: " + clearSubsession);
1112
reason = reason || REASON_GATHER_PAYLOAD;
1113
// This function returns the current Telemetry payload to the caller.
1114
// We only gather startup info once.
1115
if (!Object.keys(this._slowSQLStartup).length) {
1116
this._slowSQLStartup = Telemetry.slowSQL;
1117
}
1118
Services.telemetry.gatherMemory();
1119
return this.getSessionPayload(reason, clearSubsession);
1120
},
1121
1122
gatherStartup: function gatherStartup() {
1123
this._log.trace("gatherStartup");
1124
let counters = processInfo.getCounters();
1125
if (counters) {
1126
[
1127
this._startupIO.startupSessionRestoreReadBytes,
1128
this._startupIO.startupSessionRestoreWriteBytes,
1129
] = counters;
1130
}
1131
this._slowSQLStartup = Telemetry.slowSQL;
1132
},
1133
1134
setAddOns: function setAddOns(aAddOns) {
1135
this._addons = aAddOns;
1136
},
1137
1138
testPing: function testPing() {
1139
return this.send(REASON_TEST_PING);
1140
},
1141
1142
/**
1143
* Tracks the number of "ticks" the user was active in.
1144
*/
1145
_onActiveTick(aUserActive) {
1146
const needsUpdate = aUserActive && this._isUserActive;
1147
this._isUserActive = aUserActive;
1148
1149
// Don't count the first active tick after we get out of
1150
// inactivity, because it is just the start of this active tick.
1151
if (needsUpdate) {
1152
this._sessionActiveTicks++;
1153
Telemetry.scalarAdd("browser.engagement.active_ticks", 1);
1154
}
1155
},
1156
1157
/**
1158
* This observer drives telemetry.
1159
*/
1160
observe(aSubject, aTopic, aData) {
1161
this._log.trace("observe - " + aTopic + " notified.");
1162
1163
switch (aTopic) {
1164
case "xul-window-visible":
1165
this.removeObserver("xul-window-visible");
1166
var counters = processInfo.getCounters();
1167
if (counters) {
1168
[
1169
this._startupIO.startupWindowVisibleReadBytes,
1170
this._startupIO.startupWindowVisibleWriteBytes,
1171
] = counters;
1172
}
1173
break;
1174
case "sessionstore-windows-restored":
1175
this.removeObserver("sessionstore-windows-restored");
1176
// Check whether debugger was attached during startup
1177
let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(
1178
Ci.nsIDebug2
1179
);
1180
gWasDebuggerAttached = debugService.isDebuggerAttached;
1181
this.gatherStartup();
1182
break;
1183
case "idle-daily":
1184
// Enqueue to main-thread, otherwise components may be inited by the
1185
// idle-daily category and miss the gather-telemetry notification.
1186
Services.tm.dispatchToMainThread(function() {
1187
// Notify that data should be gathered now.
1188
// TODO: We are keeping this behaviour for now but it will be removed as soon as
1189
// bug 1127907 lands.
1190
Services.obs.notifyObservers(null, "gather-telemetry");
1191
});
1192
break;
1193
1194
case "application-background":
1195
if (AppConstants.platform !== "android") {
1196
break;
1197
}
1198
// On Android, we can get killed without warning once we are in the background,
1199
// but we may also submit data and/or come back into the foreground without getting
1200
// killed. To deal with this, we save the current session data to file when we are
1201
// put into the background. This handles the following post-backgrounding scenarios:
1202
// 1) We are killed immediately. In this case the current session data (which we
1203
// save to a file) will be loaded and submitted on a future run.
1204
// 2) We submit the data while in the background, and then are killed. In this case
1205
// the file that we saved will be deleted by the usual process in
1206
// finishPingRequest after it is submitted.
1207
// 3) We submit the data, and then come back into the foreground. Same as case (2).
1208
// 4) We do not submit the data, but come back into the foreground. In this case
1209
// we have the option of either deleting the file that we saved (since we will either
1210
// send the live data while in the foreground, or create the file again on the next
1211
// backgrounding), or not (in which case we will delete it on submit, or overwrite
1212
// it on the next backgrounding). Not deleting it is faster, so that's what we do.
1213
let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
1214
let options = {
1215
addClientId: true,
1216
addEnvironment: true,
1217
overwrite: true,
1218
};
1219
TelemetryController.addPendingPing(
1220
getPingType(payload),
1221
payload,
1222
options
1223
);
1224
break;
1225
case "user-interaction-active":
1226
this._onActiveTick(true);
1227
break;
1228
case "user-interaction-inactive":
1229
this._onActiveTick(false);
1230
break;
1231
}
1232
return undefined;
1233
},
1234
1235
/**
1236
* This tells TelemetrySession to uninitialize and save any pending pings.
1237
*/
1238
shutdownChromeProcess() {
1239
this._log.trace("shutdownChromeProcess");
1240
1241
let cleanup = () => {
1242
if (IS_UNIFIED_TELEMETRY) {
1243
TelemetryEnvironment.unregisterChangeListener(
1244
ENVIRONMENT_CHANGE_LISTENER
1245
);
1246
TelemetryScheduler.shutdown();
1247
}
1248
this.uninstall();
1249
1250
let reset = () => {
1251
this._initStarted = false;
1252
this._initialized = false;
1253
};
1254
1255
return (async () => {
1256
await this.saveShutdownPings();
1257
1258
if (IS_UNIFIED_TELEMETRY) {
1259
await TelemetryController.removeAbortedSessionPing();
1260
}
1261
1262
reset();
1263
})();
1264
};
1265
1266
// We can be in one the following states here:
1267
// 1) delayedInit was never called
1268
// or it was called and
1269
// 2) _delayedInitTask is running now.
1270
// 3) _delayedInitTask finished running already.
1271
1272
// This handles 1).
1273
if (!this._initStarted) {
1274
return Promise.resolve();
1275
}
1276
1277
// This handles 3).
1278
if (!this._delayedInitTask) {
1279
// We already ran the delayed initialization.
1280
return cleanup();
1281
}
1282
1283
// This handles 2).
1284
return this._delayedInitTask.then(cleanup);
1285
},
1286
1287
/**
1288
* Gather and send a daily ping.
1289
* @return {Promise} Resolved when the ping is sent.
1290
*/
1291
_sendDailyPing() {
1292
this._log.trace("_sendDailyPing");
1293
let payload = this.getSessionPayload(REASON_DAILY, true);
1294
1295
let options = {
1296
addClientId: true,
1297
addEnvironment: true,
1298
};
1299
1300
let promise = TelemetryController.submitExternalPing(
1301
getPingType(payload),
1302
payload,
1303
options
1304
);
1305
1306
// Also save the payload as an aborted session. If we delay this, aborted-session can
1307
// lag behind for the profileSubsessionCounter and other state, complicating analysis.
1308
if (IS_UNIFIED_TELEMETRY) {
1309
this._saveAbortedSessionPing(payload).catch(e =>
1310
this._log.error(
1311
"_sendDailyPing - Failed to save the aborted session ping",
1312
e
1313
)
1314
);
1315
}
1316
1317
return promise;
1318
},
1319
1320
/** Loads session data from the session data file.
1321
* @return {Promise<object>} A promise which is resolved with an object when
1322
* loading has completed, with null otherwise.
1323
*/
1324
async _loadSessionData() {
1325
let data = await TelemetryStorage.loadSessionData();
1326
1327
if (!data) {
1328
return null;
1329
}
1330
1331
if (
1332
!("profileSubsessionCounter" in data) ||
1333
!(typeof data.profileSubsessionCounter == "number") ||
1334
!("subsessionId" in data) ||
1335
!("sessionId" in data)
1336
) {
1337
this._log.error("_loadSessionData - session data is invalid");
1338
Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").add(
1339
1
1340
);
1341
return null;
1342
}
1343
1344
this._previousSessionId = data.sessionId;
1345
this._previousSubsessionId = data.subsessionId;
1346
// Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
1347
// new subsession while loading still takes place. This will always be exactly
1348
// 1 - the current subsessions.
1349
this._profileSubsessionCounter =
1350
data.profileSubsessionCounter + this._subsessionCounter;
1351
// If we don't have this flag in the state file, it means that this is an old profile.
1352
// We don't want to send the "new-profile" ping on new profile, so se this to true.
1353
this._newProfilePingSent =
1354
"newProfilePingSent" in data ? data.newProfilePingSent : true;
1355
return data;
1356
},
1357
1358
/**
1359
* Get the session data object to serialise to disk.
1360
*/
1361
_getSessionDataObject() {
1362
return {
1363
sessionId: this._sessionId,
1364
subsessionId: this._subsessionId,
1365
profileSubsessionCounter: this._profileSubsessionCounter,
1366
newProfilePingSent: this._newProfilePingSent,
1367
};
1368
},
1369
1370
_onEnvironmentChange(reason, oldEnvironment) {
1371
this._log.trace("_onEnvironmentChange", reason);
1372
1373
let now = Policy.monotonicNow();
1374
let timeDelta = now - this._lastEnvironmentChangeDate;
1375
if (timeDelta <= MIN_SUBSESSION_LENGTH_MS) {
1376
this._log.trace(
1377
`_onEnvironmentChange - throttling; last change was ${Math.round(
1378
timeDelta / 1000
1379
)}s ago.`
1380
);
1381
return;
1382
}
1383
1384
this._lastEnvironmentChangeDate = now;
1385
let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
1386
TelemetryScheduler.rescheduleDailyPing(payload);
1387
1388
let options = {
1389
addClientId: true,
1390
addEnvironment: true,
1391
overrideEnvironment: oldEnvironment,
1392
};
1393
TelemetryController.submitExternalPing(
1394
getPingType(payload),
1395
payload,
1396
options
1397
);
1398
},
1399
1400
_isClassicReason(reason) {
1401
const classicReasons = [
1402
REASON_SAVED_SESSION,
1403
REASON_GATHER_PAYLOAD,
1404
REASON_TEST_PING,
1405
];
1406
return classicReasons.includes(reason);
1407
},
1408
1409
/**
1410
* Get an object describing the current state of this module for AsyncShutdown diagnostics.
1411
*/
1412
_getState() {
1413
return {
1414
initialized: this._initialized,
1415
initStarted: this._initStarted,
1416
haveDelayedInitTask: !!this._delayedInitTask,
1417
};
1418
},
1419
1420
/**
1421
* Saves the aborted session ping to disk.
1422
* @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
1423
* session ping. The reason of this payload is changed to aborted-session.
1424
* If not provided, a new payload is gathered.
1425
*/
1426
_saveAbortedSessionPing(aProvidedPayload = null) {
1427
this._log.trace("_saveAbortedSessionPing");
1428
1429
let payload = null;
1430
if (aProvidedPayload) {
1431
payload = Cu.cloneInto(aProvidedPayload, myScope);
1432
// Overwrite the original reason.
1433
payload.info.reason = REASON_ABORTED_SESSION;
1434
} else {
1435
payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
1436
}
1437
1438
return TelemetryController.saveAbortedSessionPing(payload);
1439
},
1440
1441
async markNewProfilePingSent() {
1442
this._log.trace("markNewProfilePingSent");
1443
this._newProfilePingSent = true;
1444
return TelemetryStorage.saveSessionData(this._getSessionDataObject());
1445
},
1446
};