GCTelemetry.jsm 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. "use strict";
  6. /**
  7. * This module records detailed timing information about selected
  8. * GCs. The data is sent back in the telemetry session ping. To avoid
  9. * bloating the ping, only a few GCs are included. There are two
  10. * selection strategies. We always save the five GCs with the worst
  11. * max_pause time. Additionally, five collections are selected at
  12. * random. If a GC runs for C milliseconds and the total time for all
  13. * GCs since the session began is T milliseconds, then the GC has a
  14. * 5*C/T probablility of being selected (the factor of 5 is because we
  15. * save 5 of them).
  16. *
  17. * GCs from both the main process and all content processes are
  18. * recorded. The data is cleared for each new subsession.
  19. */
  20. const Cu = Components.utils;
  21. Cu.import("resource://gre/modules/Services.jsm", this);
  22. this.EXPORTED_SYMBOLS = ["GCTelemetry"];
  23. // Names of processes where we record GCs.
  24. const PROCESS_NAMES = ["main", "content"];
  25. // Should be the time we started up in milliseconds since the epoch.
  26. const BASE_TIME = Date.now() - Services.telemetry.msSinceProcessStart();
  27. // Records selected GCs. There is one instance per process type.
  28. class GCData {
  29. constructor(kind) {
  30. let numRandom = {main: 0, content: 2};
  31. let numWorst = {main: 2, content: 2};
  32. this.totalGCTime = 0;
  33. this.randomlySelected = Array(numRandom[kind]).fill(null);
  34. this.worst = Array(numWorst[kind]).fill(null);
  35. }
  36. // Turn absolute timestamps (in microseconds since the epoch) into
  37. // milliseconds since startup.
  38. rebaseTimes(data) {
  39. function fixup(t) {
  40. return t / 1000.0 - BASE_TIME;
  41. }
  42. data.timestamp = fixup(data.timestamp);
  43. for (let i = 0; i < data.slices.length; i++) {
  44. let slice = data.slices[i];
  45. slice.start_timestamp = fixup(slice.start_timestamp);
  46. slice.end_timestamp = fixup(slice.end_timestamp);
  47. }
  48. }
  49. // Records a GC (represented by |data|) in the randomlySelected or
  50. // worst batches depending on the criteria above.
  51. record(data) {
  52. this.rebaseTimes(data);
  53. let time = data.total_time;
  54. this.totalGCTime += time;
  55. // Probability that we will replace any one of our
  56. // current randomlySelected GCs with |data|.
  57. let prob = time / this.totalGCTime;
  58. // Note that we may replace multiple GCs in
  59. // randomlySelected. It's easier to reason about the
  60. // probabilities this way, and it's unlikely to have any effect in
  61. // practice.
  62. for (let i = 0; i < this.randomlySelected.length; i++) {
  63. let r = Math.random();
  64. if (r <= prob) {
  65. this.randomlySelected[i] = data;
  66. }
  67. }
  68. // Save the 5 worst GCs based on max_pause. A GC may appear in
  69. // both worst and randomlySelected.
  70. for (let i = 0; i < this.worst.length; i++) {
  71. if (!this.worst[i]) {
  72. this.worst[i] = data;
  73. break;
  74. }
  75. if (this.worst[i].max_pause < data.max_pause) {
  76. this.worst.splice(i, 0, data);
  77. this.worst.length--;
  78. break;
  79. }
  80. }
  81. }
  82. entries() {
  83. return {
  84. random: this.randomlySelected.filter(e => e !== null),
  85. worst: this.worst.filter(e => e !== null),
  86. };
  87. }
  88. }
  89. // If you adjust any of the constants here (slice limit, number of keys, etc.)
  90. // make sure to update the JSON schema at:
  91. // https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/telemetry/main.schema.json
  92. // You should also adjust browser_TelemetryGC.js.
  93. const MAX_GC_KEYS = 25;
  94. const MAX_SLICES = 4;
  95. const MAX_SLICE_KEYS = 15;
  96. const MAX_PHASES = 65;
  97. function limitProperties(obj, count) {
  98. // If there are too many properties, just delete them all. We don't
  99. // expect this ever to happen.
  100. if (Object.keys(obj).length > count) {
  101. for (let key of Object.keys(obj)) {
  102. delete obj[key];
  103. }
  104. }
  105. }
  106. function limitSize(data) {
  107. // Store the number of slices so we know if we lost any at the end.
  108. data.num_slices = data.slices.length;
  109. data.slices.sort((a, b) => b.pause - a.pause);
  110. if (data.slices.length > MAX_SLICES) {
  111. // Make sure we always keep the first slice since it has the
  112. // reason the GC was started.
  113. let firstSliceIndex = data.slices.findIndex(s => s.slice == 0);
  114. if (firstSliceIndex >= MAX_SLICES) {
  115. data.slices[MAX_SLICES - 1] = data.slices[firstSliceIndex];
  116. }
  117. data.slices.length = MAX_SLICES;
  118. }
  119. data.slices.sort((a, b) => a.slice - b.slice);
  120. limitProperties(data, MAX_GC_KEYS);
  121. for (let slice of data.slices) {
  122. limitProperties(slice, MAX_SLICE_KEYS);
  123. limitProperties(slice.times, MAX_PHASES);
  124. }
  125. limitProperties(data.totals, MAX_PHASES);
  126. }
  127. let processData = new Map();
  128. for (let name of PROCESS_NAMES) {
  129. processData.set(name, new GCData(name));
  130. }
  131. var GCTelemetry = {
  132. initialized: false,
  133. init() {
  134. if (this.initialized) {
  135. return false;
  136. }
  137. this.initialized = true;
  138. Services.obs.addObserver(this, "garbage-collection-statistics", false);
  139. if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
  140. Services.ppmm.addMessageListener("Telemetry:GCStatistics", this);
  141. }
  142. return true;
  143. },
  144. shutdown() {
  145. if (!this.initialized) {
  146. return;
  147. }
  148. Services.obs.removeObserver(this, "garbage-collection-statistics");
  149. if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
  150. Services.ppmm.removeMessageListener("Telemetry:GCStatistics", this);
  151. }
  152. this.initialized = false;
  153. },
  154. observe(subject, topic, arg) {
  155. let data = JSON.parse(arg);
  156. limitSize(data);
  157. if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
  158. processData.get("main").record(data);
  159. } else {
  160. Services.cpmm.sendAsyncMessage("Telemetry:GCStatistics", data);
  161. }
  162. },
  163. receiveMessage(msg) {
  164. processData.get("content").record(msg.data);
  165. },
  166. entries(kind, clear) {
  167. let result = processData.get(kind).entries();
  168. if (clear) {
  169. processData.set(kind, new GCData(kind));
  170. }
  171. return result;
  172. },
  173. };