utils.js (15145B)
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/. */ 4 5 "use strict"; 6 7 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 8 const STRINGS_URI = "devtools/client/locales/memory.properties"; 9 const L10N = (exports.L10N = new LocalizationHelper(STRINGS_URI)); 10 11 const { assert } = require("resource://devtools/shared/DevToolsUtils.js"); 12 const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays"; 13 const CUSTOM_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays"; 14 const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays"; 15 const BYTES = 1024; 16 const KILOBYTES = Math.pow(BYTES, 2); 17 const MEGABYTES = Math.pow(BYTES, 3); 18 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 19 const { 20 snapshotState: states, 21 diffingState, 22 censusState, 23 treeMapState, 24 dominatorTreeState, 25 individualsState, 26 } = require("resource://devtools/client/memory/constants.js"); 27 28 /** 29 * Takes a snapshot object and returns the localized form of its timestamp to be 30 * used as a title. 31 * 32 * @param {Snapshot} snapshot 33 * @return {string} 34 */ 35 exports.getSnapshotTitle = function (snapshot) { 36 if (!snapshot.creationTime) { 37 return L10N.getStr("snapshot-title.loading"); 38 } 39 40 if (snapshot.imported) { 41 // Strip out the extension if it's the expected ".fxsnapshot" 42 return PathUtils.filename(snapshot.path.replace(/\.fxsnapshot$/, "")); 43 } 44 45 const date = new Date(snapshot.creationTime / 1000); 46 return date.toLocaleTimeString(void 0, { 47 year: "2-digit", 48 month: "2-digit", 49 day: "2-digit", 50 hour12: false, 51 }); 52 }; 53 54 function getCustomDisplaysHelper(pref) { 55 let customDisplays = Object.create(null); 56 try { 57 customDisplays = 58 JSON.parse(Services.prefs.getStringPref(pref)) || Object.create(null); 59 } catch (e) { 60 DevToolsUtils.reportException( 61 `String stored in "${pref}" pref cannot be parsed by \`JSON.parse()\`.` 62 ); 63 } 64 return Object.freeze(customDisplays); 65 } 66 67 /** 68 * Returns custom displays defined in `devtools.memory.custom-census-displays` 69 * pref. 70 * 71 * @return {object} 72 */ 73 exports.getCustomCensusDisplays = function () { 74 return getCustomDisplaysHelper(CUSTOM_CENSUS_DISPLAY_PREF); 75 }; 76 77 /** 78 * Returns custom displays defined in 79 * `devtools.memory.custom-label-displays` pref. 80 * 81 * @return {object} 82 */ 83 exports.getCustomLabelDisplays = function () { 84 return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF); 85 }; 86 87 /** 88 * Returns custom displays defined in 89 * `devtools.memory.custom-tree-map-displays` pref. 90 * 91 * @return {object} 92 */ 93 exports.getCustomTreeMapDisplays = function () { 94 return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF); 95 }; 96 97 /** 98 * Returns a string representing a readable form of the snapshot's state. More 99 * concise than `getStatusTextFull`. 100 * 101 * @param {snapshotState | diffingState} state 102 * @return {string} 103 */ 104 // eslint-disable-next-line complexity 105 exports.getStatusText = function (state) { 106 assert(state, "Must have a state"); 107 108 switch (state) { 109 case diffingState.ERROR: 110 return L10N.getStr("diffing.state.error"); 111 112 case states.ERROR: 113 return L10N.getStr("snapshot.state.error"); 114 115 case states.SAVING: 116 return L10N.getStr("snapshot.state.saving"); 117 118 case states.IMPORTING: 119 return L10N.getStr("snapshot.state.importing"); 120 121 case states.SAVED: 122 case states.READING: 123 return L10N.getStr("snapshot.state.reading"); 124 125 case censusState.SAVING: 126 return L10N.getStr("snapshot.state.saving-census"); 127 128 case treeMapState.SAVING: 129 return L10N.getStr("snapshot.state.saving-tree-map"); 130 131 case diffingState.TAKING_DIFF: 132 return L10N.getStr("diffing.state.taking-diff"); 133 134 case diffingState.SELECTING: 135 return L10N.getStr("diffing.state.selecting"); 136 137 case dominatorTreeState.COMPUTING: 138 case individualsState.COMPUTING_DOMINATOR_TREE: 139 return L10N.getStr("dominatorTree.state.computing"); 140 141 case dominatorTreeState.COMPUTED: 142 case dominatorTreeState.FETCHING: 143 return L10N.getStr("dominatorTree.state.fetching"); 144 145 case dominatorTreeState.INCREMENTAL_FETCHING: 146 return L10N.getStr("dominatorTree.state.incrementalFetching"); 147 148 case dominatorTreeState.ERROR: 149 return L10N.getStr("dominatorTree.state.error"); 150 151 case individualsState.ERROR: 152 return L10N.getStr("individuals.state.error"); 153 154 case individualsState.FETCHING: 155 return L10N.getStr("individuals.state.fetching"); 156 157 // These states do not have any message to show as other content will be 158 // displayed. 159 case dominatorTreeState.LOADED: 160 case diffingState.TOOK_DIFF: 161 case states.READ: 162 case censusState.SAVED: 163 case treeMapState.SAVED: 164 case individualsState.FETCHED: 165 return ""; 166 167 default: 168 assert(false, `Unexpected state: ${state}`); 169 return ""; 170 } 171 }; 172 173 /** 174 * Returns a string representing a readable form of the snapshot's state; 175 * more verbose than `getStatusText`. 176 * 177 * @param {snapshotState | diffingState} state 178 * @return {string} 179 */ 180 // eslint-disable-next-line complexity 181 exports.getStatusTextFull = function (state) { 182 assert(!!state, "Must have a state"); 183 184 switch (state) { 185 case diffingState.ERROR: 186 return L10N.getStr("diffing.state.error.full"); 187 188 case states.ERROR: 189 return L10N.getStr("snapshot.state.error.full"); 190 191 case states.SAVING: 192 return L10N.getStr("snapshot.state.saving.full"); 193 194 case states.IMPORTING: 195 return L10N.getStr("snapshot.state.importing"); 196 197 case states.SAVED: 198 case states.READING: 199 return L10N.getStr("snapshot.state.reading.full"); 200 201 case censusState.SAVING: 202 return L10N.getStr("snapshot.state.saving-census.full"); 203 204 case treeMapState.SAVING: 205 return L10N.getStr("snapshot.state.saving-tree-map.full"); 206 207 case diffingState.TAKING_DIFF: 208 return L10N.getStr("diffing.state.taking-diff.full"); 209 210 case diffingState.SELECTING: 211 return L10N.getStr("diffing.state.selecting.full"); 212 213 case dominatorTreeState.COMPUTING: 214 case individualsState.COMPUTING_DOMINATOR_TREE: 215 return L10N.getStr("dominatorTree.state.computing.full"); 216 217 case dominatorTreeState.COMPUTED: 218 case dominatorTreeState.FETCHING: 219 return L10N.getStr("dominatorTree.state.fetching.full"); 220 221 case dominatorTreeState.INCREMENTAL_FETCHING: 222 return L10N.getStr("dominatorTree.state.incrementalFetching.full"); 223 224 case dominatorTreeState.ERROR: 225 return L10N.getStr("dominatorTree.state.error.full"); 226 227 case individualsState.ERROR: 228 return L10N.getStr("individuals.state.error.full"); 229 230 case individualsState.FETCHING: 231 return L10N.getStr("individuals.state.fetching.full"); 232 233 // These states do not have any full message to show as other content will 234 // be displayed. 235 case dominatorTreeState.LOADED: 236 case diffingState.TOOK_DIFF: 237 case states.READ: 238 case censusState.SAVED: 239 case treeMapState.SAVED: 240 case individualsState.FETCHED: 241 return ""; 242 243 default: 244 assert(false, `Unexpected state: ${state}`); 245 return ""; 246 } 247 }; 248 249 /** 250 * Return true if the snapshot is in a diffable state, false otherwise. 251 * 252 * @param {snapshotModel} snapshot 253 * @returns {boolean} 254 */ 255 exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) { 256 return ( 257 (snapshot.census && snapshot.census.state === censusState.SAVED) || 258 (snapshot.census && snapshot.census.state === censusState.SAVING) || 259 snapshot.state === states.SAVED || 260 snapshot.state === states.READ 261 ); 262 }; 263 264 /** 265 * Takes an array of snapshots and a snapshot and returns 266 * the snapshot instance in `snapshots` that matches 267 * the snapshot passed in. 268 * 269 * @param {appModel} state 270 * @param {snapshotId} id 271 * @return {snapshotModel|null} 272 */ 273 exports.getSnapshot = function getSnapshot(state, id) { 274 const found = state.snapshots.find(s => s.id === id); 275 assert(found, `No matching snapshot found with id = ${id}`); 276 return found; 277 }; 278 279 /** 280 * Get the ID of the selected snapshot, if one is selected, null otherwise. 281 * 282 * @returns {SnapshotId|null} 283 */ 284 exports.findSelectedSnapshot = function (state) { 285 const found = state.snapshots.find(s => s.selected); 286 return found ? found.id : null; 287 }; 288 289 /** 290 * Creates a new snapshot object. 291 * 292 * @param {appModel} state 293 * @return {Snapshot} 294 */ 295 let ID_COUNTER = 0; 296 exports.createSnapshot = function createSnapshot(state) { 297 let dominatorTree = null; 298 if (state.view.state === dominatorTreeState.DOMINATOR_TREE) { 299 dominatorTree = Object.freeze({ 300 dominatorTreeId: null, 301 root: null, 302 error: null, 303 state: dominatorTreeState.COMPUTING, 304 }); 305 } 306 307 return Object.freeze({ 308 id: ++ID_COUNTER, 309 state: states.SAVING, 310 dominatorTree, 311 census: null, 312 treeMap: null, 313 path: null, 314 imported: false, 315 selected: false, 316 error: null, 317 }); 318 }; 319 320 /** 321 * Return true if the census is up to date with regards to the current filtering 322 * and requested display, false otherwise. 323 * 324 * @param {string} filter 325 * @param {censusDisplayModel} display 326 * @param {censusModel} census 327 * 328 * @returns {boolean} 329 */ 330 exports.censusIsUpToDate = function (filter, display, census) { 331 return ( 332 census && 333 // Filter could be null == undefined so use loose equality. 334 filter == census.filter && 335 display === census.display 336 ); 337 }; 338 339 /** 340 * Check to see if the snapshot is in a state that it can take a census. 341 * 342 * @param {SnapshotModel} A snapshot to check. 343 * @param {boolean} Assert that the snapshot must be in a ready state. 344 * @returns {boolean} 345 */ 346 exports.canTakeCensus = function (snapshot) { 347 return ( 348 snapshot.state === states.READ && 349 (!snapshot.census || 350 snapshot.census.state === censusState.SAVED || 351 !snapshot.treeMap || 352 snapshot.treeMap.state === treeMapState.SAVED) 353 ); 354 }; 355 356 /** 357 * Returns true if the given snapshot's dominator tree has been computed, false 358 * otherwise. 359 * 360 * @param {SnapshotModel} snapshot 361 * @returns {boolean} 362 */ 363 exports.dominatorTreeIsComputed = function (snapshot) { 364 return ( 365 snapshot.dominatorTree && 366 (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED || 367 snapshot.dominatorTree.state === dominatorTreeState.LOADED || 368 snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING) 369 ); 370 }; 371 372 /** 373 * Find the first SAVED census, either from the tree map or the normal 374 * census. 375 * 376 * @param {SnapshotModel} snapshot 377 * @returns {object | null} Either the census, or null if one hasn't completed 378 */ 379 exports.getSavedCensus = function (snapshot) { 380 if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) { 381 return snapshot.treeMap; 382 } 383 if (snapshot.census && snapshot.census.state === censusState.SAVED) { 384 return snapshot.census; 385 } 386 return null; 387 }; 388 389 /** 390 * Takes a snapshot and returns the total bytes and total count that this 391 * snapshot represents. 392 * 393 * @param {CensusModel} census 394 * @return {object} 395 */ 396 exports.getSnapshotTotals = function (census) { 397 let bytes = 0; 398 let count = 0; 399 400 const report = census.report; 401 if (report) { 402 bytes = report.totalBytes; 403 count = report.totalCount; 404 } 405 406 return { bytes, count }; 407 }; 408 409 /** 410 * Takes some configurations and opens up a file picker and returns 411 * a promise to the chosen file if successful. 412 * 413 * @param {string} .title 414 * The title displayed in the file picker window. 415 * @param {Array<Array<string>>} .filters 416 * An array of filters to display in the file picker. Each filter in the array 417 * is a duple of two strings, one a name for the filter, and one the filter itself 418 * (like "*.json"). 419 * @param {string} .defaultName 420 * The default name chosen by the file picker window. 421 * @param {string} .mode 422 * The mode that this filepicker should open in. Can be "open" or "save". 423 * @return {Promise<?nsIFile>} 424 * The file selected by the user, or null, if cancelled. 425 */ 426 exports.openFilePicker = function ({ title, filters, defaultName, mode }) { 427 let fpMode; 428 if (mode === "save") { 429 fpMode = Ci.nsIFilePicker.modeSave; 430 } else if (mode === "open") { 431 fpMode = Ci.nsIFilePicker.modeOpen; 432 } else { 433 throw new Error("No valid mode specified for nsIFilePicker."); 434 } 435 436 const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 437 fp.init(window.browsingContext, title, fpMode); 438 439 for (const filter of filters || []) { 440 fp.appendFilter(filter[0], filter[1]); 441 } 442 fp.defaultString = defaultName; 443 444 return new Promise(resolve => { 445 fp.open({ 446 done: result => { 447 if (result === Ci.nsIFilePicker.returnCancel) { 448 resolve(null); 449 return; 450 } 451 resolve(fp.file); 452 }, 453 }); 454 }); 455 }; 456 457 /** 458 * Format the provided number with a space every 3 digits, and optionally 459 * prefixed by its sign. 460 * 461 * @param {number} number 462 * @param {boolean} showSign (defaults to false) 463 */ 464 exports.formatNumber = function (number, showSign = false) { 465 const rounded = Math.round(number); 466 // eslint-disable-next-line no-compare-neg-zero 467 if (rounded === 0 || rounded === -0) { 468 return "0"; 469 } 470 471 const abs = String(Math.abs(rounded)); 472 // replace every digit followed by (sets of 3 digits) by (itself and a space) 473 const formatted = abs.replace(/(\d)(?=(\d{3})+$)/g, "$1 "); 474 475 if (showSign) { 476 const sign = rounded < 0 ? "-" : "+"; 477 return sign + formatted; 478 } 479 return formatted; 480 }; 481 482 /** 483 * Format the provided percentage following the same logic as formatNumber and 484 * an additional % suffix. 485 * 486 * @param {number} percent 487 * @param {boolean} showSign (defaults to false) 488 */ 489 exports.formatPercent = function (percent, showSign = false) { 490 return exports.L10N.getFormatStr( 491 "tree-item.percent2", 492 exports.formatNumber(percent, showSign) 493 ); 494 }; 495 496 /** 497 * Change an HSL color array with values ranged 0-1 to a properly formatted 498 * ctx.fillStyle string. 499 * 500 * @param {number} h 501 * hue values ranged between [0 - 1] 502 * @param {number} s 503 * hue values ranged between [0 - 1] 504 * @param {number} l 505 * hue values ranged between [0 - 1] 506 * @return {type} 507 */ 508 exports.hslToStyle = function (h, s, l) { 509 h = parseInt(h * 360, 10); 510 s = parseInt(s * 100, 10); 511 l = parseInt(l * 100, 10); 512 513 return `hsl(${h},${s}%,${l}%)`; 514 }; 515 516 /** 517 * Linearly interpolate between 2 numbers. 518 * 519 * @param {number} a 520 * @param {number} b 521 * @param {number} t 522 * A value of 0 returns a, and 1 returns b 523 * @return {number} 524 */ 525 exports.lerp = function (a, b, t) { 526 return a * (1 - t) + b * t; 527 }; 528 529 /** 530 * Format a number of bytes as human readable, e.g. 13434 => '13KiB'. 531 * 532 * @param {number} n 533 * Number of bytes 534 * @return {string} 535 */ 536 exports.formatAbbreviatedBytes = function (n) { 537 if (n < BYTES) { 538 return n + "B"; 539 } else if (n < KILOBYTES) { 540 return Math.floor(n / BYTES) + "KiB"; 541 } else if (n < MEGABYTES) { 542 return Math.floor(n / KILOBYTES) + "MiB"; 543 } 544 return Math.floor(n / MEGABYTES) + "GiB"; 545 };