recording-utils.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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. "use strict";
  5. const { Cc, Ci, Cu, Cr } = require("chrome");
  6. loader.lazyRequireGetter(this, "extend",
  7. "sdk/util/object", true);
  8. /**
  9. * Utility functions for managing recording models and their internal data,
  10. * such as filtering profile samples or offsetting timestamps.
  11. */
  12. function mapRecordingOptions(type, options) {
  13. if (type === "profiler") {
  14. return {
  15. entries: options.bufferSize,
  16. interval: options.sampleFrequency ? (1000 / (options.sampleFrequency * 1000)) : void 0
  17. };
  18. }
  19. if (type === "memory") {
  20. return {
  21. probability: options.allocationsSampleProbability,
  22. maxLogLength: options.allocationsMaxLogLength
  23. };
  24. }
  25. if (type === "timeline") {
  26. return {
  27. withMarkers: true,
  28. withTicks: options.withTicks,
  29. withMemory: options.withMemory,
  30. withFrames: true,
  31. withGCEvents: true,
  32. withDocLoadingEvents: false
  33. };
  34. }
  35. return options;
  36. }
  37. /**
  38. * Takes an options object for `startRecording`, and normalizes
  39. * it based off of server support. For example, if the user
  40. * requests to record memory `withMemory = true`, but the server does
  41. * not support that feature, then the `false` will overwrite user preference
  42. * in order to define the recording with what is actually available, not
  43. * what the user initially requested.
  44. *
  45. * @param {object} options
  46. * @param {boolean}
  47. */
  48. function normalizePerformanceFeatures(options, supportedFeatures) {
  49. return Object.keys(options).reduce((modifiedOptions, feature) => {
  50. if (supportedFeatures[feature] !== false) {
  51. modifiedOptions[feature] = options[feature];
  52. }
  53. return modifiedOptions;
  54. }, Object.create(null));
  55. }
  56. /**
  57. * Filters all the samples in the provided profiler data to be more recent
  58. * than the specified start time.
  59. *
  60. * @param object profile
  61. * The profiler data received from the backend.
  62. * @param number profilerStartTime
  63. * The earliest acceptable sample time (in milliseconds).
  64. */
  65. function filterSamples(profile, profilerStartTime) {
  66. let firstThread = profile.threads[0];
  67. const TIME_SLOT = firstThread.samples.schema.time;
  68. firstThread.samples.data = firstThread.samples.data.filter(e => {
  69. return e[TIME_SLOT] >= profilerStartTime;
  70. });
  71. }
  72. /**
  73. * Offsets all the samples in the provided profiler data by the specified time.
  74. *
  75. * @param object profile
  76. * The profiler data received from the backend.
  77. * @param number timeOffset
  78. * The amount of time to offset by (in milliseconds).
  79. */
  80. function offsetSampleTimes(profile, timeOffset) {
  81. let firstThread = profile.threads[0];
  82. const TIME_SLOT = firstThread.samples.schema.time;
  83. let samplesData = firstThread.samples.data;
  84. for (let i = 0; i < samplesData.length; i++) {
  85. samplesData[i][TIME_SLOT] -= timeOffset;
  86. }
  87. }
  88. /**
  89. * Offsets all the markers in the provided timeline data by the specified time.
  90. *
  91. * @param array markers
  92. * The markers array received from the backend.
  93. * @param number timeOffset
  94. * The amount of time to offset by (in milliseconds).
  95. */
  96. function offsetMarkerTimes(markers, timeOffset) {
  97. for (let marker of markers) {
  98. marker.start -= timeOffset;
  99. marker.end -= timeOffset;
  100. }
  101. }
  102. /**
  103. * Offsets and scales all the timestamps in the provided array by the
  104. * specified time and scale factor.
  105. *
  106. * @param array array
  107. * A list of timestamps received from the backend.
  108. * @param number timeOffset
  109. * The amount of time to offset by (in milliseconds).
  110. * @param number timeScale
  111. * The factor to scale by, after offsetting.
  112. */
  113. function offsetAndScaleTimestamps(timestamps, timeOffset, timeScale) {
  114. for (let i = 0, len = timestamps.length; i < len; i++) {
  115. timestamps[i] -= timeOffset;
  116. if (timeScale) {
  117. timestamps[i] /= timeScale;
  118. }
  119. }
  120. }
  121. /**
  122. * Push all elements of src array into dest array. Marker data will come in small chunks
  123. * and add up over time, whereas allocation arrays can be > 500000 elements (and
  124. * Function.prototype.apply throws if applying more than 500000 elements, which
  125. * is what spawned this separate function), so iterate one element at a time.
  126. * @see bug 1166823
  127. * @see http://jsperf.com/concat-large-arrays
  128. * @see http://jsperf.com/concat-large-arrays/2
  129. *
  130. * @param {Array} dest
  131. * @param {Array} src
  132. */
  133. function pushAll(dest, src) {
  134. let length = src.length;
  135. for (let i = 0; i < length; i++) {
  136. dest.push(src[i]);
  137. }
  138. }
  139. /**
  140. * Cache used in `RecordingUtils.getProfileThreadFromAllocations`.
  141. */
  142. var gProfileThreadFromAllocationCache = new WeakMap();
  143. /**
  144. * Converts allocation data from the memory actor to something that follows
  145. * the same structure as the samples data received from the profiler.
  146. *
  147. * @see MemoryActor.prototype.getAllocations for more information.
  148. *
  149. * @param object allocations
  150. * A list of { sites, timestamps, frames, sizes } arrays.
  151. * @return object
  152. * The "profile" describing the allocations log.
  153. */
  154. function getProfileThreadFromAllocations(allocations) {
  155. let cached = gProfileThreadFromAllocationCache.get(allocations);
  156. if (cached) {
  157. return cached;
  158. }
  159. let { sites, timestamps, frames, sizes } = allocations;
  160. let uniqueStrings = new UniqueStrings();
  161. // Convert allocation frames to the the stack and frame tables expected by
  162. // the profiler format.
  163. //
  164. // Since the allocations log is already presented as a tree, we would be
  165. // wasting time if we jumped through the same hoops as deflateProfile below
  166. // and instead use the existing structure of the allocations log to build up
  167. // the profile JSON.
  168. //
  169. // The allocations.frames array corresponds roughly to the profile stack
  170. // table: a trie of all stacks. We could work harder to further deduplicate
  171. // each individual frame as the profiler does, but it is not necessary for
  172. // correctness.
  173. let stackTable = new Array(frames.length);
  174. let frameTable = new Array(frames.length);
  175. // Array used to concat the location.
  176. let locationConcatArray = new Array(5);
  177. for (let i = 0; i < frames.length; i++) {
  178. let frame = frames[i];
  179. if (!frame) {
  180. stackTable[i] = frameTable[i] = null;
  181. continue;
  182. }
  183. let prefix = frame.parent;
  184. // Schema:
  185. // [prefix, frame]
  186. stackTable[i] = [frames[prefix] ? prefix : null, i];
  187. // Schema:
  188. // [location]
  189. //
  190. // The only field a frame will have in an allocations profile is location.
  191. //
  192. // If frame.functionDisplayName is present, the format is
  193. // "functionDisplayName (source:line:column)"
  194. // Otherwise, it is
  195. // "source:line:column"
  196. //
  197. // A static array is used to join to save memory on intermediate strings.
  198. locationConcatArray[0] = frame.source;
  199. locationConcatArray[1] = ":";
  200. locationConcatArray[2] = String(frame.line);
  201. locationConcatArray[3] = ":";
  202. locationConcatArray[4] = String(frame.column);
  203. locationConcatArray[5] = "";
  204. let location = locationConcatArray.join("");
  205. let funcName = frame.functionDisplayName;
  206. if (funcName) {
  207. locationConcatArray[0] = funcName;
  208. locationConcatArray[1] = " (";
  209. locationConcatArray[2] = location;
  210. locationConcatArray[3] = ")";
  211. locationConcatArray[4] = "";
  212. locationConcatArray[5] = "";
  213. location = locationConcatArray.join("");
  214. }
  215. frameTable[i] = [uniqueStrings.getOrAddStringIndex(location)];
  216. }
  217. let samples = new Array(sites.length);
  218. let writePos = 0;
  219. for (let i = 0; i < sites.length; i++) {
  220. // Schema:
  221. // [stack, time, size]
  222. //
  223. // Originally, sites[i] indexes into the frames array. Note that in the
  224. // loop above, stackTable[sites[i]] and frames[sites[i]] index the same
  225. // information.
  226. let stackIndex = sites[i];
  227. if (frames[stackIndex]) {
  228. samples[writePos++] = [stackIndex, timestamps[i], sizes[i]];
  229. }
  230. }
  231. samples.length = writePos;
  232. let thread = {
  233. name: "allocations",
  234. samples: allocationsWithSchema(samples),
  235. stackTable: stackTableWithSchema(stackTable),
  236. frameTable: frameTableWithSchema(frameTable),
  237. stringTable: uniqueStrings.stringTable
  238. };
  239. gProfileThreadFromAllocationCache.set(allocations, thread);
  240. return thread;
  241. }
  242. function allocationsWithSchema(data) {
  243. let slot = 0;
  244. return {
  245. schema: {
  246. stack: slot++,
  247. time: slot++,
  248. size: slot++,
  249. },
  250. data: data
  251. };
  252. }
  253. /**
  254. * Deduplicates a profile by deduplicating stacks, frames, and strings.
  255. *
  256. * This is used to adapt version 2 profiles from the backend to version 3, for
  257. * use with older Geckos (like B2G).
  258. *
  259. * Note that the schemas used by this must be kept in sync with schemas used
  260. * by the C++ UniqueStacks class in tools/profiler/ProfileEntry.cpp.
  261. *
  262. * @param object profile
  263. * A profile with version 2.
  264. */
  265. function deflateProfile(profile) {
  266. profile.threads = profile.threads.map((thread) => {
  267. let uniqueStacks = new UniqueStacks();
  268. return deflateThread(thread, uniqueStacks);
  269. });
  270. profile.meta.version = 3;
  271. return profile;
  272. }
  273. /**
  274. * Given an array of frame objects, deduplicates each frame as well as all
  275. * prefixes in the stack. Returns the index of the deduplicated stack.
  276. *
  277. * @param object frames
  278. * Array of frame objects.
  279. * @param UniqueStacks uniqueStacks
  280. * @return number index
  281. */
  282. function deflateStack(frames, uniqueStacks) {
  283. // Deduplicate every prefix in the stack by keeping track of the current
  284. // prefix hash.
  285. let prefixIndex = null;
  286. for (let i = 0; i < frames.length; i++) {
  287. let frameIndex = uniqueStacks.getOrAddFrameIndex(frames[i]);
  288. prefixIndex = uniqueStacks.getOrAddStackIndex(prefixIndex, frameIndex);
  289. }
  290. return prefixIndex;
  291. }
  292. /**
  293. * Given an array of sample objects, deduplicate each sample's stack and
  294. * convert the samples to a table with a schema. Returns the deflated samples.
  295. *
  296. * @param object samples
  297. * Array of samples
  298. * @param UniqueStacks uniqueStacks
  299. * @return object
  300. */
  301. function deflateSamples(samples, uniqueStacks) {
  302. // Schema:
  303. // [stack, time, responsiveness, rss, uss, frameNumber, power]
  304. let deflatedSamples = new Array(samples.length);
  305. for (let i = 0; i < samples.length; i++) {
  306. let sample = samples[i];
  307. deflatedSamples[i] = [
  308. deflateStack(sample.frames, uniqueStacks),
  309. sample.time,
  310. sample.responsiveness,
  311. sample.rss,
  312. sample.uss,
  313. sample.frameNumber,
  314. sample.power
  315. ];
  316. }
  317. return samplesWithSchema(deflatedSamples);
  318. }
  319. /**
  320. * Given an array of marker objects, convert the markers to a table with a
  321. * schema. Returns the deflated markers.
  322. *
  323. * If a marker contains a backtrace as its payload, the backtrace stack is
  324. * deduplicated in the context of the profile it's in.
  325. *
  326. * @param object markers
  327. * Array of markers
  328. * @param UniqueStacks uniqueStacks
  329. * @return object
  330. */
  331. function deflateMarkers(markers, uniqueStacks) {
  332. // Schema:
  333. // [name, time, data]
  334. let deflatedMarkers = new Array(markers.length);
  335. for (let i = 0; i < markers.length; i++) {
  336. let marker = markers[i];
  337. if (marker.data && marker.data.type === "tracing" && marker.data.stack) {
  338. marker.data.stack = deflateThread(marker.data.stack, uniqueStacks);
  339. }
  340. deflatedMarkers[i] = [
  341. uniqueStacks.getOrAddStringIndex(marker.name),
  342. marker.time,
  343. marker.data
  344. ];
  345. }
  346. let slot = 0;
  347. return {
  348. schema: {
  349. name: slot++,
  350. time: slot++,
  351. data: slot++
  352. },
  353. data: deflatedMarkers
  354. };
  355. }
  356. /**
  357. * Deflate a thread.
  358. *
  359. * @param object thread
  360. * The profile thread.
  361. * @param UniqueStacks uniqueStacks
  362. * @return object
  363. */
  364. function deflateThread(thread, uniqueStacks) {
  365. // Some extra threads in a profile come stringified as a full profile (so
  366. // it has nested threads itself) so the top level "thread" does not have markers
  367. // or samples. We don't use this anyway so just make this safe to deflate.
  368. // can be a string rather than an object on import. Bug 1173695
  369. if (typeof thread === "string") {
  370. thread = JSON.parse(thread);
  371. }
  372. if (!thread.samples) {
  373. thread.samples = [];
  374. }
  375. if (!thread.markers) {
  376. thread.markers = [];
  377. }
  378. return {
  379. name: thread.name,
  380. tid: thread.tid,
  381. samples: deflateSamples(thread.samples, uniqueStacks),
  382. markers: deflateMarkers(thread.markers, uniqueStacks),
  383. stackTable: uniqueStacks.getStackTableWithSchema(),
  384. frameTable: uniqueStacks.getFrameTableWithSchema(),
  385. stringTable: uniqueStacks.getStringTable()
  386. };
  387. }
  388. function stackTableWithSchema(data) {
  389. let slot = 0;
  390. return {
  391. schema: {
  392. prefix: slot++,
  393. frame: slot++
  394. },
  395. data: data
  396. };
  397. }
  398. function frameTableWithSchema(data) {
  399. let slot = 0;
  400. return {
  401. schema: {
  402. location: slot++,
  403. implementation: slot++,
  404. optimizations: slot++,
  405. line: slot++,
  406. category: slot++
  407. },
  408. data: data
  409. };
  410. }
  411. function samplesWithSchema(data) {
  412. let slot = 0;
  413. return {
  414. schema: {
  415. stack: slot++,
  416. time: slot++,
  417. responsiveness: slot++,
  418. rss: slot++,
  419. uss: slot++,
  420. frameNumber: slot++,
  421. power: slot++
  422. },
  423. data: data
  424. };
  425. }
  426. /**
  427. * A helper class to deduplicate strings.
  428. */
  429. function UniqueStrings() {
  430. this.stringTable = [];
  431. this._stringHash = Object.create(null);
  432. }
  433. UniqueStrings.prototype.getOrAddStringIndex = function (s) {
  434. if (!s) {
  435. return null;
  436. }
  437. let stringHash = this._stringHash;
  438. let stringTable = this.stringTable;
  439. let index = stringHash[s];
  440. if (index !== undefined) {
  441. return index;
  442. }
  443. index = stringTable.length;
  444. stringHash[s] = index;
  445. stringTable.push(s);
  446. return index;
  447. };
  448. /**
  449. * A helper class to deduplicate old-version profiles.
  450. *
  451. * The main functionality provided is deduplicating frames and stacks.
  452. *
  453. * For example, given 2 stacks
  454. * [A, B, C]
  455. * and
  456. * [A, B, D]
  457. *
  458. * There are 4 unique frames: A, B, C, and D.
  459. * There are 4 unique prefixes: [A], [A, B], [A, B, C], [A, B, D]
  460. *
  461. * For the example, the output of using UniqueStacks is:
  462. *
  463. * Frame table:
  464. * [A, B, C, D]
  465. *
  466. * That is, A has id 0, B has id 1, etc.
  467. *
  468. * Since stack prefixes are themselves deduplicated (shared), stacks are
  469. * represented as a tree, or more concretely, a pair of ids, the prefix and
  470. * the leaf.
  471. *
  472. * Stack table:
  473. * [
  474. * [null, 0],
  475. * [0, 1],
  476. * [1, 2],
  477. * [1, 3]
  478. * ]
  479. *
  480. * That is, [A] has id 0 and value [null, 0]. This means it has no prefix, and
  481. * has the leaf frame 0, which resolves to A in the frame table.
  482. *
  483. * [A, B] has id 1 and value [0, 1]. This means it has prefix 0, which is [A],
  484. * and leaf 1, thus [A, B].
  485. *
  486. * [A, B, C] has id 2 and value [1, 2]. This means it has prefix 1, which in
  487. * turn is [A, B], and leaf 2, thus [A, B, C].
  488. *
  489. * [A, B, D] has id 3 and value [1, 3]. Note how it shares the prefix 1 with
  490. * [A, B, C].
  491. */
  492. function UniqueStacks() {
  493. this._frameTable = [];
  494. this._stackTable = [];
  495. this._frameHash = Object.create(null);
  496. this._stackHash = Object.create(null);
  497. this._uniqueStrings = new UniqueStrings();
  498. }
  499. UniqueStacks.prototype.getStackTableWithSchema = function () {
  500. return stackTableWithSchema(this._stackTable);
  501. };
  502. UniqueStacks.prototype.getFrameTableWithSchema = function () {
  503. return frameTableWithSchema(this._frameTable);
  504. };
  505. UniqueStacks.prototype.getStringTable = function () {
  506. return this._uniqueStrings.stringTable;
  507. };
  508. UniqueStacks.prototype.getOrAddFrameIndex = function (frame) {
  509. // Schema:
  510. // [location, implementation, optimizations, line, category]
  511. let frameHash = this._frameHash;
  512. let frameTable = this._frameTable;
  513. let locationIndex = this.getOrAddStringIndex(frame.location);
  514. let implementationIndex = this.getOrAddStringIndex(frame.implementation);
  515. // Super dumb.
  516. let hash = `${locationIndex} ${implementationIndex || ""} ${frame.line || ""} ${frame.category || ""}`;
  517. let index = frameHash[hash];
  518. if (index !== undefined) {
  519. return index;
  520. }
  521. index = frameTable.length;
  522. frameHash[hash] = index;
  523. frameTable.push([
  524. this.getOrAddStringIndex(frame.location),
  525. this.getOrAddStringIndex(frame.implementation),
  526. // Don't bother with JIT optimization info for deflating old profile data
  527. // format to the new format.
  528. null,
  529. frame.line,
  530. frame.category
  531. ]);
  532. return index;
  533. };
  534. UniqueStacks.prototype.getOrAddStackIndex = function (prefixIndex, frameIndex) {
  535. // Schema:
  536. // [prefix, frame]
  537. let stackHash = this._stackHash;
  538. let stackTable = this._stackTable;
  539. // Also super dumb.
  540. let hash = prefixIndex + " " + frameIndex;
  541. let index = stackHash[hash];
  542. if (index !== undefined) {
  543. return index;
  544. }
  545. index = stackTable.length;
  546. stackHash[hash] = index;
  547. stackTable.push([prefixIndex, frameIndex]);
  548. return index;
  549. };
  550. UniqueStacks.prototype.getOrAddStringIndex = function (s) {
  551. return this._uniqueStrings.getOrAddStringIndex(s);
  552. };
  553. exports.pushAll = pushAll;
  554. exports.mapRecordingOptions = mapRecordingOptions;
  555. exports.normalizePerformanceFeatures = normalizePerformanceFeatures;
  556. exports.filterSamples = filterSamples;
  557. exports.offsetSampleTimes = offsetSampleTimes;
  558. exports.offsetMarkerTimes = offsetMarkerTimes;
  559. exports.offsetAndScaleTimestamps = offsetAndScaleTimestamps;
  560. exports.getProfileThreadFromAllocations = getProfileThreadFromAllocations;
  561. exports.deflateProfile = deflateProfile;
  562. exports.deflateThread = deflateThread;
  563. exports.UniqueStrings = UniqueStrings;
  564. exports.UniqueStacks = UniqueStacks;