├── README.md ├── build.sh ├── dist ├── taskmonitor_part1.uc.js ├── taskmonitor_part2.uc.js └── taskmonitor_part3_clearMemoryPeriodically.uc.js ├── include.py └── src ├── aboutProcesses.js ├── taskmonitor_part1.uc.js ├── taskmonitor_part2.uc.js └── taskmonitor_part3_clearMemoryPeriodically.uc.js /README.md: -------------------------------------------------------------------------------- 1 | # Firefox TaskMonitor 2 | 3 | Show CPU & memory bars and usage. 4 | 5 | ![screenshot](https://s1.ax1x.com/2020/07/18/UcsPLF.jpg) 6 | 7 | Firefox userChrome script. Tested on Firefox 128, with xiaoxiaoflood's uc loader. 8 | 9 | ## Usage 10 | 11 | Download `.uc.js` files from `dist/` folder. 12 | 13 | ## Features 14 | 15 | - Show every tab's CPU and memory usage (bars on every tab button) 16 | - Show browser CPU and memory bars on right side of tab bar (removable widget) 17 | - Clicking widget shows processes info dynamically in popup 18 | - Mouse hover shows more details (CPU, memory, PID, host ...) 19 | - Optional periodically clean Firefox memory (experimental) 20 | 21 | --------------------- 22 | 23 | This repo only contains the specific function, doesn't contain the code to enable userchrome scripts. 24 | 25 | **For how to enable, see: (also more of my scripts)** 26 | 27 | https://garywill.github.io/#Firefox-userChrome-CSS-or-JS 28 | 29 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p dist 4 | rm dist/* 5 | ./include.py > dist/taskmonitor_part1.uc.js 6 | cp src/taskmonitor_part2.uc.js dist/ 7 | cp src/taskmonitor_part3_clearMemoryPeriodically.uc.js dist/taskmonitor_part3_clearMemoryPeriodically.uc.js 8 | -------------------------------------------------------------------------------- /dist/taskmonitor_part1.uc.js: -------------------------------------------------------------------------------- 1 | /* Firefox userChrome script 2 | * Show tab cpu and memory bars on every tab button 3 | * Show all-process cpu and memory bars on a slender widget at the right of tab bar 4 | * Dynamically show processes on popup menu of the widget 5 | * 6 | * Tested on Firefox 128, with xiaoxiaoflood's uc loader 7 | * 8 | * Author: garywill (https://garywill.github.io) 9 | * https://github.com/garywill/firefoxtaskmonitor 10 | * 11 | * Notice 12 | * Some code is from Mozilla Firefox, which licensed under MPL 13 | * 14 | */ 15 | 16 | // ==UserScript== 17 | // @include main 18 | // ==/UserScript== 19 | 20 | console.log("taskmonitor_part1.js"); 21 | 22 | 23 | "use strict"; 24 | 25 | let taskMonitorTimerID = null; 26 | 27 | (() => { 28 | //===================== 29 | // User customization 30 | 31 | const tabCpuColor = "#fd9191"; // red 32 | //const tabMemColor = "rgb(242, 242, 0)"; //yellow 33 | const tabCpuMax = 100; 34 | const tabMemColor = "rgb(100, 160, 255)"; //blue 35 | const tabMemMax = 900*1000*1000; 36 | //const tabBarsTransp 37 | const allCpuColor = tabCpuColor; 38 | const allCpuMax = 200; 39 | const allMemColor = tabMemColor; 40 | const allMemMax = 1500*1000*1000; 41 | //const allBarsTransp 42 | 43 | //======================= 44 | 45 | const barWidth = 3; 46 | const barGap = 1; 47 | 48 | /* This Source Code Form is subject to the terms of the Mozilla Public 49 | * License, v. 2.0. If a copy of the MPL was not distributed with this 50 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 51 | 52 | 53 | // Time in ms before we start changing the sort order again after receiving a 54 | // mousemove event. 55 | const TIME_BEFORE_SORTING_AGAIN = 5000; 56 | 57 | // How long we should wait between samples. 58 | const MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS = 1000; 59 | 60 | // How often we should update 61 | const UPDATE_INTERVAL_MS = 2000; 62 | 63 | const NS_PER_US = 1000; 64 | const NS_PER_MS = 1000 * 1000; 65 | const NS_PER_S = 1000 * 1000 * 1000; 66 | const NS_PER_MIN = NS_PER_S * 60; 67 | const NS_PER_HOUR = NS_PER_MIN * 60; 68 | const NS_PER_DAY = NS_PER_HOUR * 24; 69 | 70 | const ONE_GIGA = 1024 * 1024 * 1024; 71 | const ONE_MEGA = 1024 * 1024; 72 | const ONE_KILO = 1024; 73 | 74 | // const { XPCOMUtils } = ChromeUtils.importESModule( 75 | // "resource://gre/modules/XPCOMUtils.sys.mjs" 76 | // ); 77 | // const { AppConstants } = ChromeUtils.importESModule( 78 | // "resource://gre/modules/AppConstants.sys.mjs" 79 | // ); 80 | 81 | ChromeUtils.defineESModuleGetters(this, { 82 | ContextualIdentityService: 83 | "resource://gre/modules/ContextualIdentityService.sys.mjs", 84 | }); 85 | 86 | ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () { 87 | return ChromeUtils.importESModule( 88 | "resource://devtools/client/performance-new/shared/background.sys.mjs" 89 | ); 90 | }); 91 | 92 | const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); 93 | 94 | 95 | const gLocalizedUnits = 96 | { 97 | "duration": { "ns": "ns", "us": "µs", "ms": "ms", "s": "s", "m": "m", "h": "h", "d": "d" }, 98 | "memory": { "B": "B", "KB": "KB", "MB": "MB", "GB": "GB", "TB": "TB", "PB": "PB", "EB": "EB" } 99 | }; 100 | 101 | 102 | let tabFinder = { 103 | update() { 104 | this._map = new Map(); 105 | for (let win of Services.wm.getEnumerator("navigator:browser")) { 106 | let tabbrowser = win.gBrowser; 107 | for (let browser of tabbrowser.browsers) { 108 | let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet 109 | if (id != null) { 110 | this._map.set(id, browser); 111 | } 112 | } 113 | if (tabbrowser.preloadedBrowser) { 114 | let browser = tabbrowser.preloadedBrowser; 115 | if (browser.outerWindowID) { 116 | this._map.set(browser.outerWindowID, browser); 117 | } 118 | } 119 | } 120 | }, 121 | 122 | /** 123 | * Find the for a window id. 124 | * 125 | * This is useful e.g. for reloading or closing tabs. 126 | * 127 | * @return null If the xul:tab could not be found, e.g. if the 128 | * windowId is that of a chrome window. 129 | * @return {{tabbrowser: , tab: }} The 130 | * tabbrowser and tab if the latter could be found. 131 | */ 132 | get(id) { 133 | let browser = this._map.get(id); 134 | if (!browser) { 135 | return null; 136 | } 137 | let tabbrowser = browser.getTabBrowser(); 138 | if (!tabbrowser) { 139 | return { 140 | tabbrowser: null, 141 | tab: { 142 | getAttribute() { 143 | return ""; 144 | }, 145 | linkedBrowser: browser, 146 | }, 147 | }; 148 | } 149 | return { tabbrowser, tab: tabbrowser.getTabForBrowser(browser) }; 150 | }, 151 | }; 152 | 153 | 154 | /** 155 | * Utilities for dealing with state 156 | */ 157 | let State = { 158 | // Store the previous and current samples so they can be compared. 159 | _previous: null, 160 | _latest: null, 161 | 162 | async _promiseSnapshot() { 163 | let date = Cu.now(); 164 | let main = await ChromeUtils.requestProcInfo(); 165 | main.date = date; 166 | 167 | let processes = new Map(); 168 | processes.set(main.pid, main); 169 | for (let child of main.children) { 170 | child.date = date; 171 | processes.set(child.pid, child); 172 | } 173 | 174 | return { processes, date }; 175 | }, 176 | 177 | /** 178 | * Update the internal state. 179 | * 180 | * @return {Promise} 181 | */ 182 | async update(force = false) { 183 | if ( 184 | force || 185 | !this._latest || 186 | Cu.now() - this._latest.date > MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS 187 | ) { 188 | // Replacing this._previous before we are done awaiting 189 | // this._promiseSnapshot can cause this._previous and this._latest to be 190 | // equal for a short amount of time, which can cause test failures when 191 | // a forced update of the display is triggered in the meantime. 192 | let newSnapshot = await this._promiseSnapshot(); 193 | this._previous = this._latest; 194 | this._latest = newSnapshot; 195 | } 196 | }, 197 | 198 | _getThreadDelta(cur, prev, deltaT) { 199 | let result = { 200 | tid: cur.tid, 201 | name: cur.name || `(${cur.tid})`, 202 | // Total amount of CPU used, in ns. 203 | totalCpu: cur.cpuTime, 204 | slopeCpu: null, 205 | active: null, 206 | }; 207 | if (!deltaT) { 208 | return result; 209 | } 210 | result.slopeCpu = (result.totalCpu - (prev ? prev.cpuTime : 0)) / deltaT; 211 | result.active = 212 | !!result.slopeCpu || cur.cpuCycleCount > (prev ? prev.cpuCycleCount : 0); 213 | return result; 214 | }, 215 | 216 | _getDOMWindows(process) { 217 | if (!process.windows) { 218 | return []; 219 | } 220 | if (!process.type == "extensions") { 221 | return []; 222 | } 223 | let windows = process.windows.map(win => { 224 | let tab = tabFinder.get(win.outerWindowId); 225 | let addon = 226 | process.type == "extension" 227 | ? WebExtensionPolicy.getByURI(win.documentURI) 228 | : null; 229 | let displayRank; 230 | if (tab) { 231 | displayRank = 1; 232 | } else if (win.isProcessRoot) { 233 | displayRank = 2; 234 | } else if (win.documentTitle) { 235 | displayRank = 3; 236 | } else { 237 | displayRank = 4; 238 | } 239 | return { 240 | outerWindowId: win.outerWindowId, 241 | documentURI: win.documentURI, 242 | documentTitle: win.documentTitle, 243 | isProcessRoot: win.isProcessRoot, 244 | isInProcess: win.isInProcess, 245 | tab, 246 | addon, 247 | // The number of instances we have collapsed. 248 | count: 1, 249 | // A rank used to quickly sort windows. 250 | displayRank, 251 | }; 252 | }); 253 | 254 | // We keep all tabs and addons but we collapse subframes that have the same host. 255 | 256 | // A map from host -> subframe. 257 | let collapsible = new Map(); 258 | let result = []; 259 | for (let win of windows) { 260 | if (win.tab || win.addon) { 261 | result.push(win); 262 | continue; 263 | } 264 | let prev = collapsible.get(win.documentURI.prePath); 265 | if (prev) { 266 | prev.count += 1; 267 | } else { 268 | collapsible.set(win.documentURI.prePath, win); 269 | result.push(win); 270 | } 271 | } 272 | return result; 273 | }, 274 | 275 | /** 276 | * Compute the delta between two process snapshots. 277 | * 278 | * @param {ProcessSnapshot} cur 279 | * @param {ProcessSnapshot?} prev 280 | */ 281 | _getProcessDelta(cur, prev) { 282 | let windows = this._getDOMWindows(cur); 283 | let result = { 284 | pid: cur.pid, 285 | childID: cur.childID, 286 | totalRamSize: cur.memory, 287 | deltaRamSize: null, 288 | totalCpu: cur.cpuTime, 289 | slopeCpu: null, 290 | active: null, 291 | type: cur.type, 292 | origin: cur.origin || "", 293 | threads: null, 294 | displayRank: Control._getDisplayGroupRank(cur, windows), 295 | windows, 296 | utilityActors: cur.utilityActors, 297 | // If this process has an unambiguous title, store it here. 298 | title: null, 299 | }; 300 | // Attempt to determine a title for this process. 301 | let titles = [ 302 | ...new Set( 303 | result.windows 304 | .filter(win => win.documentTitle) 305 | .map(win => win.documentTitle) 306 | ), 307 | ]; 308 | if (titles.length == 1) { 309 | result.title = titles[0]; 310 | } 311 | if (!prev) { 312 | return result; 313 | } 314 | if (prev.pid != cur.pid) { 315 | throw new Error("Assertion failed: A process cannot change pid."); 316 | } 317 | let deltaT = (cur.date - prev.date) * NS_PER_MS; 318 | let threads = null; 319 | 320 | result.deltaRamSize = cur.memory - prev.memory; 321 | result.slopeCpu = (cur.cpuTime - prev.cpuTime) / deltaT; 322 | result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount; 323 | result.threads = threads; 324 | return result; 325 | }, 326 | 327 | getCounters() { 328 | tabFinder.update(); 329 | 330 | let counters = []; 331 | 332 | for (let cur of this._latest.processes.values()) { 333 | let prev = this._previous?.processes.get(cur.pid); 334 | counters.push(this._getProcessDelta(cur, prev)); 335 | } 336 | 337 | return counters; 338 | } 339 | }; 340 | 341 | 342 | let View = { 343 | commit() { 344 | let tbody = document.createElement("table").appendChild(document.createElement("tbody")); 345 | let insertPoint = tbody.firstChild; 346 | let nextRow; 347 | while ((nextRow = this._orderedRows.shift())) { 348 | if (insertPoint && insertPoint === nextRow) { 349 | insertPoint = insertPoint.nextSibling; 350 | } else { 351 | tbody.insertBefore(nextRow, insertPoint); 352 | } 353 | } 354 | 355 | if (insertPoint) { 356 | while ((nextRow = insertPoint.nextSibling)) { 357 | this._removeRow(nextRow); 358 | } 359 | this._removeRow(insertPoint); 360 | } 361 | return tbody; 362 | }, 363 | _rowsById: new Map(), 364 | _removeRow(row) { 365 | this._rowsById.delete(row.rowId); 366 | 367 | row.remove(); 368 | }, 369 | _getOrCreateRow(rowId, cellCount) { 370 | let row = this._rowsById.get(rowId); 371 | if (!row) { 372 | row = document.createElement("tr"); 373 | while (cellCount--) { 374 | row.appendChild(document.createElement("td")); 375 | } 376 | row.rowId = rowId; 377 | this._rowsById.set(rowId, row); 378 | } 379 | this._orderedRows.push(row); 380 | return row; 381 | }, 382 | 383 | displayCpu(data, cpuCell, maxSlopeCpu) { 384 | // Put a value < 0% when we really don't want to see a bar as 385 | // otherwise it sometimes appears due to rounding errors when we 386 | // don't have an integer number of pixels. 387 | let barWidth = -0.5; 388 | if (data.slopeCpu == null) { 389 | this._fillCell(cpuCell, { 390 | fluentName: "about-processes-cpu-user-and-kernel-not-ready", 391 | classes: ["cpu"], 392 | }); 393 | } else { 394 | let { duration, unit } = this._getDuration(data.totalCpu); 395 | if (data.totalCpu == 0) { 396 | // A thread having used exactly 0ns of CPU time is not possible. 397 | // When we get 0 it means the thread used less than the precision of 398 | // the measurement, and it makes more sense to show '0ms' than '0ns'. 399 | // This is useful on Linux where the minimum non-zero CPU time value 400 | // for threads of child processes is 10ms, and on Windows ARM64 where 401 | // the minimum non-zero value is 16ms. 402 | unit = "ms"; 403 | } 404 | let localizedUnit = gLocalizedUnits.duration[unit]; 405 | if (data.slopeCpu == 0) { 406 | let fluentName = data.active 407 | ? "about-processes-cpu-almost-idle" 408 | : "about-processes-cpu-fully-idle"; 409 | this._fillCell(cpuCell, { 410 | fluentName, 411 | fluentArgs: { 412 | total: duration, 413 | unit: localizedUnit, 414 | }, 415 | classes: ["cpu"], 416 | }); 417 | } else { 418 | this._fillCell(cpuCell, { 419 | fluentName: "about-processes-cpu", 420 | fluentArgs: { 421 | percent: data.slopeCpu, 422 | total: duration, 423 | unit: localizedUnit, 424 | }, 425 | classes: ["cpu"], 426 | }); 427 | 428 | let cpuPercent = data.slopeCpu * 100; 429 | if (maxSlopeCpu > 1) { 430 | cpuPercent /= maxSlopeCpu; 431 | } 432 | // Ensure we always have a visible bar for non-0 values. 433 | barWidth = Math.max(0.5, cpuPercent); 434 | } 435 | } 436 | cpuCell.style.setProperty("--bar-width", barWidth); 437 | }, 438 | 439 | /** 440 | * Display a row showing a single process (without its threads). 441 | * 442 | * @param {ProcessDelta} data The data to display. 443 | * @param {Number} maxSlopeCpu The largest slopeCpu value. 444 | * @return {DOMElement} The row displaying the process. 445 | */ 446 | displayProcessRow(data, maxSlopeCpu) { 447 | const cellCount = 4; 448 | let rowId = "p:" + data.pid; 449 | let row = this._getOrCreateRow(rowId, cellCount); 450 | row.process = data; 451 | { 452 | let classNames = "process"; 453 | if (data.isHung) { 454 | classNames += " hung"; 455 | } 456 | row.className = classNames; 457 | } 458 | 459 | // Column: Name 460 | let nameCell = row.firstChild; 461 | { 462 | let classNames = []; 463 | let fluentName; 464 | let fluentArgs = { 465 | pid: "" + data.pid, // Make sure that this number is not localized 466 | }; 467 | switch (data.type) { 468 | case "web": 469 | fluentName = "about-processes-web-process"; 470 | break; 471 | case "webIsolated": 472 | fluentName = "about-processes-web-isolated-process"; 473 | fluentArgs.origin = data.origin; 474 | break; 475 | case "webServiceWorker": 476 | fluentName = "about-processes-web-serviceworker"; 477 | fluentArgs.origin = data.origin; 478 | break; 479 | case "file": 480 | fluentName = "about-processes-file-process"; 481 | break; 482 | case "extension": 483 | fluentName = "about-processes-extension-process"; 484 | classNames = ["extensions"]; 485 | break; 486 | case "privilegedabout": 487 | fluentName = "about-processes-privilegedabout-process"; 488 | break; 489 | case "privilegedmozilla": 490 | fluentName = "about-processes-privilegedmozilla-process"; 491 | break; 492 | case "withCoopCoep": 493 | fluentName = "about-processes-with-coop-coep-process"; 494 | fluentArgs.origin = data.origin; 495 | break; 496 | case "browser": 497 | fluentName = "about-processes-browser-process"; 498 | break; 499 | case "plugin": 500 | fluentName = "about-processes-plugin-process"; 501 | break; 502 | case "gmpPlugin": 503 | fluentName = "about-processes-gmp-plugin-process"; 504 | break; 505 | case "gpu": 506 | fluentName = "about-processes-gpu-process"; 507 | break; 508 | case "vr": 509 | fluentName = "about-processes-vr-process"; 510 | break; 511 | case "rdd": 512 | fluentName = "about-processes-rdd-process"; 513 | break; 514 | case "socket": 515 | fluentName = "about-processes-socket-process"; 516 | break; 517 | case "remoteSandboxBroker": 518 | fluentName = "about-processes-remote-sandbox-broker-process"; 519 | break; 520 | case "forkServer": 521 | fluentName = "about-processes-fork-server-process"; 522 | break; 523 | case "preallocated": 524 | fluentName = "about-processes-preallocated-process"; 525 | break; 526 | case "utility": 527 | fluentName = "about-processes-utility-process"; 528 | break; 529 | // The following are probably not going to show up for users 530 | // but let's handle the case anyway to avoid heisenoranges 531 | // during tests in case of a leftover process from a previous 532 | // test. 533 | default: 534 | fluentName = "about-processes-unknown-process"; 535 | fluentArgs.type = data.type; 536 | break; 537 | } 538 | 539 | // Show container names instead of raw origin attribute suffixes. 540 | if (fluentArgs.origin?.includes("^")) { 541 | let origin = fluentArgs.origin; 542 | let privateBrowsingId, userContextId; 543 | try { 544 | ({ privateBrowsingId, userContextId } = 545 | ChromeUtils.createOriginAttributesFromOrigin(origin)); 546 | fluentArgs.origin = origin.slice(0, origin.indexOf("^")); 547 | } catch (e) { 548 | // createOriginAttributesFromOrigin can throw NS_ERROR_FAILURE for incorrect origin strings. 549 | } 550 | if (userContextId) { 551 | let identityLabel = 552 | ContextualIdentityService.getUserContextLabel(userContextId); 553 | if (identityLabel) { 554 | fluentArgs.origin += ` — ${identityLabel}`; 555 | } 556 | } 557 | if (privateBrowsingId) { 558 | fluentName += "-private"; 559 | } 560 | } 561 | 562 | let processNameElement = nameCell; 563 | document.l10n.setAttributes(processNameElement, fluentName, fluentArgs); 564 | nameCell.className = ["type", "favicon", ...classNames].join(" "); 565 | nameCell.setAttribute("id", data.pid + "-label"); 566 | 567 | let image; 568 | switch (data.type) { 569 | case "browser": 570 | case "privilegedabout": 571 | image = "chrome://branding/content/icon32.png"; 572 | break; 573 | case "extension": 574 | image = "chrome://mozapps/skin/extensions/extension.svg"; 575 | break; 576 | default: 577 | // If all favicons match, pick the shared favicon. 578 | // Otherwise, pick a default icon. 579 | // If some tabs have no favicon, we ignore them. 580 | for (let win of data.windows || []) { 581 | if (!win.tab) { 582 | continue; 583 | } 584 | let favicon = win.tab.tab.getAttribute("image"); 585 | if (!favicon) { 586 | // No favicon here, let's ignore the tab. 587 | } else if (!image) { 588 | // Let's pick a first favicon. 589 | // We'll remove it later if we find conflicting favicons. 590 | image = favicon; 591 | } else if (image == favicon) { 592 | // So far, no conflict, keep the favicon. 593 | } else { 594 | // Conflicting favicons, fallback to default. 595 | image = null; 596 | break; 597 | } 598 | } 599 | if (!image) { 600 | image = "chrome://global/skin/icons/link.svg"; 601 | } 602 | } 603 | nameCell.style.backgroundImage = `url('${image}')`; 604 | } 605 | 606 | // Column: Memory 607 | let memoryCell = nameCell.nextSibling; 608 | { 609 | let formattedTotal = this._formatMemory(data.totalRamSize); 610 | if (data.deltaRamSize) { 611 | let formattedDelta = this._formatMemory(data.deltaRamSize); 612 | this._fillCell(memoryCell, { 613 | fluentName: "about-processes-total-memory-size-changed", 614 | fluentArgs: { 615 | total: formattedTotal.amount, 616 | totalUnit: gLocalizedUnits.memory[formattedTotal.unit], 617 | delta: Math.abs(formattedDelta.amount), 618 | deltaUnit: gLocalizedUnits.memory[formattedDelta.unit], 619 | deltaSign: data.deltaRamSize > 0 ? "+" : "-", 620 | }, 621 | classes: ["memory"], 622 | }); 623 | } else { 624 | this._fillCell(memoryCell, { 625 | fluentName: "about-processes-total-memory-size-no-change", 626 | fluentArgs: { 627 | total: formattedTotal.amount, 628 | totalUnit: gLocalizedUnits.memory[formattedTotal.unit], 629 | }, 630 | classes: ["memory"], 631 | }); 632 | } 633 | } 634 | 635 | // Column: CPU 636 | let cpuCell = memoryCell.nextSibling; 637 | this.displayCpu(data, cpuCell, maxSlopeCpu); 638 | 639 | 640 | return row; 641 | }, 642 | 643 | 644 | 645 | displayDOMWindowRow(data) { 646 | const cellCount = 2; 647 | let rowId = "w:" + data.outerWindowId; 648 | let row = this._getOrCreateRow(rowId, cellCount); 649 | row.win = data; 650 | row.className = "window"; 651 | 652 | // Column: name 653 | let nameCell = row.firstChild; 654 | let tab = tabFinder.get(data.outerWindowId); 655 | let fluentName; 656 | let fluentArgs = {}; 657 | let className; 658 | if (tab && tab.tabbrowser) { 659 | fluentName = "about-processes-tab-name"; 660 | fluentArgs.name = tab.tab.label; 661 | fluentArgs.tabWindowId = data.outerWindowId; // this tabWindowId can be used by tabFinder.get() 662 | className = "tab"; 663 | } else if (tab) { 664 | fluentName = "about-processes-preloaded-tab"; 665 | className = "preloaded-tab"; 666 | } else if (data.count == 1) { 667 | fluentName = "about-processes-frame-name-one"; 668 | fluentArgs.url = data.documentURI.spec; 669 | className = "frame-one"; 670 | } else { 671 | fluentName = "about-processes-frame-name-many"; 672 | fluentArgs.number = data.count; 673 | fluentArgs.shortUrl = 674 | data.documentURI.scheme == "about" 675 | ? data.documentURI.spec 676 | : data.documentURI.prePath; 677 | className = "frame-many"; 678 | } 679 | this._fillCell(nameCell, { 680 | fluentName, 681 | fluentArgs, 682 | classes: ["name", "indent", "favicon", className], 683 | }); 684 | let image = tab?.tab.getAttribute("image"); 685 | if (image) { 686 | nameCell.style.backgroundImage = `url('${image}')`; 687 | } 688 | }, 689 | 690 | utilityActorNameToFluentName(actorName) { 691 | let fluentName; 692 | switch (actorName) { 693 | case "audioDecoder_Generic": 694 | fluentName = "about-processes-utility-actor-audio-decoder-generic"; 695 | break; 696 | 697 | case "audioDecoder_AppleMedia": 698 | fluentName = "about-processes-utility-actor-audio-decoder-applemedia"; 699 | break; 700 | 701 | case "audioDecoder_WMF": 702 | fluentName = "about-processes-utility-actor-audio-decoder-wmf"; 703 | break; 704 | 705 | case "mfMediaEngineCDM": 706 | fluentName = "about-processes-utility-actor-mf-media-engine"; 707 | break; 708 | 709 | case "jSOracle": 710 | fluentName = "about-processes-utility-actor-js-oracle"; 711 | break; 712 | 713 | case "windowsUtils": 714 | fluentName = "about-processes-utility-actor-windows-utils"; 715 | break; 716 | 717 | case "windowsFileDialog": 718 | fluentName = "about-processes-utility-actor-windows-file-dialog"; 719 | break; 720 | 721 | default: 722 | fluentName = "about-processes-utility-actor-unknown"; 723 | break; 724 | } 725 | return fluentName; 726 | }, 727 | 728 | displayUtilityActorRow(data, parent) { 729 | const cellCount = 2; 730 | // The actor name is expected to be unique within a given utility process. 731 | let rowId = "u:" + parent.pid + data.actorName; 732 | let row = this._getOrCreateRow(rowId, cellCount); 733 | row.actor = data; 734 | row.className = "actor"; 735 | 736 | // Column: name 737 | let nameCell = row.firstChild; 738 | let fluentName = this.utilityActorNameToFluentName(data.actorName); 739 | let fluentArgs = {}; 740 | this._fillCell(nameCell, { 741 | fluentName, 742 | fluentArgs, 743 | classes: ["name", "indent", "favicon"], 744 | }); 745 | }, 746 | 747 | /** 748 | * Display a row showing a single thread. 749 | * 750 | * @param {ThreadDelta} data The data to display. 751 | * @param {Number} maxSlopeCpu The largest slopeCpu value. 752 | */ 753 | displayThreadRow(data, maxSlopeCpu) { 754 | const cellCount = 3; 755 | let rowId = "t:" + data.tid; 756 | let row = this._getOrCreateRow(rowId, cellCount); 757 | row.thread = data; 758 | row.className = "thread"; 759 | 760 | // Column: name 761 | let nameCell = row.firstChild; 762 | this._fillCell(nameCell, { 763 | fluentName: "about-processes-thread-name-and-id", 764 | fluentArgs: { 765 | name: data.name, 766 | tid: "" + data.tid /* Make sure that this number is not localized */, 767 | }, 768 | classes: ["name", "double_indent"], 769 | }); 770 | 771 | // Column: CPU 772 | this.displayCpu(data, nameCell.nextSibling, maxSlopeCpu); 773 | 774 | // Third column (Buttons) is empty, nothing to do. 775 | }, 776 | 777 | _orderedRows: [], 778 | _fillCell(elt, { classes, fluentName, fluentArgs }) { 779 | document.l10n.setAttributes(elt, fluentName, fluentArgs); 780 | elt.className = classes.join(" "); 781 | }, 782 | 783 | _getDuration(rawDurationNS) { 784 | if (rawDurationNS <= NS_PER_US) { 785 | return { duration: rawDurationNS, unit: "ns" }; 786 | } 787 | if (rawDurationNS <= NS_PER_MS) { 788 | return { duration: rawDurationNS / NS_PER_US, unit: "us" }; 789 | } 790 | if (rawDurationNS <= NS_PER_S) { 791 | return { duration: rawDurationNS / NS_PER_MS, unit: "ms" }; 792 | } 793 | if (rawDurationNS <= NS_PER_MIN) { 794 | return { duration: rawDurationNS / NS_PER_S, unit: "s" }; 795 | } 796 | if (rawDurationNS <= NS_PER_HOUR) { 797 | return { duration: rawDurationNS / NS_PER_MIN, unit: "m" }; 798 | } 799 | if (rawDurationNS <= NS_PER_DAY) { 800 | return { duration: rawDurationNS / NS_PER_HOUR, unit: "h" }; 801 | } 802 | return { duration: rawDurationNS / NS_PER_DAY, unit: "d" }; 803 | }, 804 | 805 | /** 806 | * Format a value representing an amount of memory. 807 | * 808 | * As a special case, we also handle `null`, which represents the case in which we do 809 | * not have sufficient information to compute an amount of memory. 810 | * 811 | * @param {Number?} value The value to format. Must be either `null` or a non-negative number. 812 | * @return { {unit: "GB" | "MB" | "KB" | B" | "?"}, amount: Number } The formated amount and its 813 | * unit, which may be used for e.g. additional CSS formating. 814 | */ 815 | _formatMemory(value) { 816 | if (value == null) { 817 | return { unit: "?", amount: 0 }; 818 | } 819 | if (typeof value != "number") { 820 | throw new Error(`Invalid memory value ${value}`); 821 | } 822 | let abs = Math.abs(value); 823 | if (abs >= ONE_GIGA) { 824 | return { 825 | unit: "GB", 826 | amount: value / ONE_GIGA, 827 | }; 828 | } 829 | if (abs >= ONE_MEGA) { 830 | return { 831 | unit: "MB", 832 | amount: value / ONE_MEGA, 833 | }; 834 | } 835 | if (abs >= ONE_KILO) { 836 | return { 837 | unit: "KB", 838 | amount: value / ONE_KILO, 839 | }; 840 | } 841 | return { 842 | unit: "B", 843 | amount: value, 844 | }; 845 | } 846 | }; 847 | 848 | 849 | 850 | 851 | let Control = { 852 | // The set of all processes reported as "hung" by the process hang monitor. 853 | // 854 | // type: Set 855 | _hungItems: new Set(), 856 | _sortColumn: null, 857 | _sortAscendent: true, 858 | 859 | init() { 860 | this._initHangReports(); 861 | }, 862 | 863 | _initHangReports() { 864 | const PROCESS_HANG_REPORT_NOTIFICATION = "process-hang-report"; 865 | 866 | // Receiving report of a hung child. 867 | // Let's store if for our next update. 868 | let hangReporter = report => { 869 | report.QueryInterface(Ci.nsIHangReport); 870 | this._hungItems.add(report.childID); 871 | }; 872 | 873 | 874 | }, 875 | async update(force = false) { 876 | await State.update(force); 877 | 878 | return await this._updateDisplay(force); 879 | }, 880 | 881 | // The force parameter can force a full update even when the mouse has been 882 | // moved recently. 883 | async _updateDisplay(force = false) { 884 | let counters = State.getCounters(); 885 | 886 | // We reset `_hungItems`, based on the assumption that the process hang 887 | // monitor will inform us again before the next update. Since the process hang monitor 888 | // pings its clients about once per second and we update about once per 2 seconds 889 | // (or more if the mouse moves), we should be ok. 890 | let hungItems = this._hungItems; 891 | this._hungItems = new Set(); 892 | 893 | counters = this._sortProcesses(counters); 894 | 895 | // Stored because it is used when opening the list of threads. 896 | this._maxSlopeCpu = Math.max(...counters.map(process => process.slopeCpu)); 897 | 898 | let previousProcess = null; 899 | for (let process of counters) { 900 | this._sortDOMWindows(process.windows); 901 | 902 | process.isHung = process.childID && hungItems.has(process.childID); 903 | 904 | let processRow = View.displayProcessRow(process, this._maxSlopeCpu); 905 | 906 | if (process.type != "extension") { 907 | // We do not want to display extensions. 908 | for (let win of process.windows) { 909 | if (win.tab || win.isProcessRoot) { 910 | View.displayDOMWindowRow(win, process); 911 | } 912 | } 913 | } 914 | 915 | if (process.type === "utility") { 916 | for (let actor of process.utilityActors) { 917 | View.displayUtilityActorRow(actor, process); 918 | } 919 | } 920 | 921 | 922 | if ( 923 | this._sortColumn == null && 924 | previousProcess && 925 | previousProcess.displayRank != process.displayRank 926 | ) { 927 | // Add a separation between successive categories of processes. 928 | processRow.classList.add("separate-from-previous-process-group"); 929 | } 930 | previousProcess = process; 931 | } 932 | 933 | 934 | 935 | return View.commit(); 936 | 937 | 938 | 939 | }, 940 | _compareCpu(a, b) { 941 | return ( 942 | b.slopeCpu - a.slopeCpu || b.active - a.active || b.totalCpu - a.totalCpu 943 | ); 944 | }, 945 | _showThreads(row, maxSlopeCpu) { 946 | let process = row.process; 947 | this._sortThreads(process.threads); 948 | for (let thread of process.threads) { 949 | View.displayThreadRow(thread, maxSlopeCpu); 950 | } 951 | }, 952 | _sortThreads(threads) { 953 | return threads.sort((a, b) => { 954 | let order; 955 | switch (this._sortColumn) { 956 | case "column-name": 957 | order = a.name.localeCompare(b.name) || a.tid - b.tid; 958 | break; 959 | case "column-cpu-total": 960 | order = this._compareCpu(a, b); 961 | break; 962 | case "column-memory-resident": 963 | case null: 964 | order = a.tid - b.tid; 965 | break; 966 | default: 967 | throw new Error("Unsupported order: " + this._sortColumn); 968 | } 969 | if (!this._sortAscendent) { 970 | order = -order; 971 | } 972 | return order; 973 | }); 974 | }, 975 | _sortProcesses(counters) { 976 | return counters.sort((a, b) => { 977 | let order; 978 | switch (this._sortColumn) { 979 | case "column-name": 980 | order = 981 | String(a.origin).localeCompare(b.origin) || 982 | String(a.type).localeCompare(b.type) || 983 | a.pid - b.pid; 984 | break; 985 | case "column-cpu-total": 986 | order = this._compareCpu(a, b); 987 | break; 988 | case "column-memory-resident": 989 | order = b.totalRamSize - a.totalRamSize; 990 | break; 991 | case null: 992 | // Default order: classify processes by group. 993 | order = 994 | a.displayRank - b.displayRank || 995 | // Other processes are ordered by origin. 996 | String(a.origin).localeCompare(b.origin); 997 | break; 998 | default: 999 | throw new Error("Unsupported order: " + this._sortColumn); 1000 | } 1001 | if (!this._sortAscendent) { 1002 | order = -order; 1003 | } 1004 | return order; 1005 | }); 1006 | }, 1007 | _sortDOMWindows(windows) { 1008 | return windows.sort((a, b) => { 1009 | let order = 1010 | a.displayRank - b.displayRank || 1011 | a.documentTitle.localeCompare(b.documentTitle) || 1012 | a.documentURI.spec.localeCompare(b.documentURI.spec); 1013 | if (!this._sortAscendent) { 1014 | order = -order; 1015 | } 1016 | return order; 1017 | }); 1018 | }, 1019 | 1020 | // Assign a display rank to a process. 1021 | // 1022 | // The `browser` process comes first (rank 0). 1023 | // Then come web tabs (rank 1). 1024 | // Then come web frames (rank 2). 1025 | // Then come special processes (minus preallocated) (rank 3). 1026 | // Then come preallocated processes (rank 4). 1027 | _getDisplayGroupRank(data, windows) { 1028 | const RANK_BROWSER = 0; 1029 | const RANK_WEB_TABS = 1; 1030 | const RANK_WEB_FRAMES = 2; 1031 | const RANK_UTILITY = 3; 1032 | const RANK_PREALLOCATED = 4; 1033 | let type = data.type; 1034 | switch (type) { 1035 | // Browser comes first. 1036 | case "browser": 1037 | return RANK_BROWSER; 1038 | // Web content comes next. 1039 | case "webIsolated": 1040 | case "webServiceWorker": 1041 | case "withCoopCoep": { 1042 | if (windows.some(w => w.tab)) { 1043 | return RANK_WEB_TABS; 1044 | } 1045 | return RANK_WEB_FRAMES; 1046 | } 1047 | // Preallocated processes come last. 1048 | case "preallocated": 1049 | return RANK_PREALLOCATED; 1050 | // "web" is special, as it could be one of: 1051 | // - web content currently loading/unloading/... 1052 | // - a preallocated process. 1053 | case "web": 1054 | if (windows.some(w => w.tab)) { 1055 | return RANK_WEB_TABS; 1056 | } 1057 | if (windows.length >= 1) { 1058 | return RANK_WEB_FRAMES; 1059 | } 1060 | // For the time being, we do not display DOM workers 1061 | // (and there's no API to get information on them). 1062 | // Once the blockers for bug 1663737 have landed, we'll be able 1063 | // to find out whether this process has DOM workers. If so, we'll 1064 | // count this process as a content process. 1065 | return RANK_PREALLOCATED; 1066 | // Other special processes before preallocated. 1067 | default: 1068 | return RANK_UTILITY; 1069 | } 1070 | } 1071 | 1072 | }; 1073 | 1074 | 1075 | 1076 | function parseTbody(tbody) 1077 | { 1078 | let ps = []; 1079 | 1080 | for (var iRow = 0; iRow c_maxwidth) 1274 | widthToSet = c_maxwidth; 1275 | if (widthToSet < c_minwidth) 1276 | widthToSet = c_minwidth; 1277 | */ 1278 | var widthToSet = parseInt( getComputedStyle(insertNode).width ) / 2 ; 1279 | tabAllBarsCont.style.width = widthToSet + "px"; 1280 | 1281 | 1282 | addBarsToNode(tabAllBarsCont, taskInfo.cpu, taskInfo.mem, {cpuColor: tabCpuColor, memColor: tabMemColor, cpuMax: tabCpuMax, memMax: tabMemMax, rightBlank: 2}, taskInfo); 1283 | tabAllBarsCont.title = tabAllBarsCont.tooltipText = taskInfo.pmtext; 1284 | 1285 | //var ttp = `CPU ${taskInfo.cpu}\nMEM ${taskInfo.mem_united}\nPID ${taskInfo.pid}`; 1286 | //tabNode.getElementsByClassName("tab-icon-image")[0].tooltipText = ttp; 1287 | } 1288 | 1289 | 1290 | function addCpuMem2whole(cpu, mem, tooltip) 1291 | { 1292 | 1293 | var arr_tooltip_split = tooltip.split('\n'); 1294 | 1295 | wins.forEach( function(win, win_i) { 1296 | 1297 | //var fftm_widget = document.getElementById("fftm_widget"); 1298 | var fftm_widget = win.document.body.getElementsByClassName("fftm_widget_class")[0]; 1299 | if ( fftm_widget ) 1300 | { 1301 | var allBarsCont = null; 1302 | allBarsCont = createVertRightEdgeCont(fftm_widget, 'fftm_widget_p', 1303 | { 1304 | position: "relative", 1305 | display: "inline-block", 1306 | zIndex: "999", 1307 | height: "100%", 1308 | //minWidth: (barWidth*2 + barGap) + "px", 1309 | //width: (barWidth*2 + barGap) + "px", 1310 | }, 1311 | { 1312 | display: "block", 1313 | position: "absolute", 1314 | height: "100%", 1315 | zIndex: "99999", 1316 | minWidth: (barWidth*2 + barGap) + "px", 1317 | marginLeft: -(barWidth*2 + barGap) + "px", 1318 | } 1319 | ); 1320 | addBarsToNode(allBarsCont, cpu, mem, {cpuColor: allCpuColor, memColor: allMemColor, cpuMax: allCpuMax, memMax: allMemMax} ); 1321 | fftm_widget.title = fftm_widget.tooltipText = tooltip; 1322 | 1323 | 1324 | for (var i=0; i< 100; i++) 1325 | { 1326 | //var menu_task_obj = document.getElementById( "fftm_widget_task_"+i ); 1327 | var menu_task_obj = win.document.body.getElementsByClassName( "fftm_widget_task" )[i]; 1328 | var text = arr_tooltip_split[i]; 1329 | if ( menu_task_obj && text ) { 1330 | menu_task_obj.label = text.replaceAll("\t", " "); 1331 | menu_task_obj.tooltipText = text; 1332 | menu_task_obj.hidden = false; 1333 | } 1334 | if ( menu_task_obj && !text ) { 1335 | menu_task_obj.hidden = true; 1336 | } 1337 | if ( !menu_task_obj && !text ) { 1338 | break; 1339 | } 1340 | } 1341 | 1342 | 1343 | } 1344 | }); 1345 | } 1346 | 1347 | 1348 | function createVertRightEdgeCont(BrowserNode, pname, PStyle, CStyle, hide=false) 1349 | { 1350 | var contParent = BrowserNode.getElementsByClassName(pname)[0]; 1351 | 1352 | if (hide && contParent) 1353 | { 1354 | contParent.style.visibility="hidden"; 1355 | return; 1356 | }else if (!hide && contParent) 1357 | { 1358 | contParent.style.visibility="visible"; 1359 | } 1360 | 1361 | var cont = null; 1362 | if (!contParent) { 1363 | contParent = document.createXULElement("div"); 1364 | contParent.className = pname; 1365 | for (var key in PStyle) 1366 | { 1367 | contParent.style[key] = PStyle[key] 1368 | } 1369 | //contParent.tooltipText = "PAPA"; 1370 | 1371 | cont = document.createXULElement("div"); 1372 | cont.className = "taskMonitorBarsCont"; 1373 | for (var key in CStyle) 1374 | { 1375 | cont.style[key] = CStyle[key] 1376 | } 1377 | //cont.tooltipText = "haha"; 1378 | 1379 | contParent.appendChild(cont); 1380 | BrowserNode.appendChild(contParent); 1381 | }else{ 1382 | cont = contParent.getElementsByClassName("taskMonitorBarsCont")[0]; 1383 | } 1384 | 1385 | return cont; 1386 | } 1387 | 1388 | function addBarsToNode(node, cpu, memory, ui, taskInfo) 1389 | { 1390 | if (ui.rightBlank === undefined) ui.rightBlank = 0; 1391 | 1392 | var cpubar; 1393 | cpubar = node.getElementsByClassName("cpuBar")[0]; 1394 | if (!cpubar) { 1395 | cpubar = document.createElement("div"); 1396 | cpubar.className = "cpuBar"; 1397 | cpubar.style.backgroundColor = ui.cpuColor; 1398 | cpubar.style.width = barWidth + "px"; 1399 | cpubar.style.position = "absolute"; 1400 | cpubar.style.right = (ui.rightBlank + barWidth + barGap ) + "px"; 1401 | cpubar.style.bottom = 0; 1402 | node.appendChild(cpubar); 1403 | } 1404 | cpubar.style.height = Math.min((cpu > 0) ? cpu * (100/ui.cpuMax) : 0, 100) + "%"; 1405 | 1406 | var membar; 1407 | membar = node.getElementsByClassName("memBar")[0]; 1408 | if (!membar) { 1409 | membar = document.createElement("div"); 1410 | membar.className = "memBar"; 1411 | membar.style.backgroundColor = ui.memColor; 1412 | membar.style.width = barWidth + "px"; 1413 | membar.style.position = "absolute"; 1414 | membar.style.right = ui.rightBlank + "px"; 1415 | membar.style.bottom = 0; 1416 | node.appendChild(membar); 1417 | } 1418 | membar.style.height = Math.min(memory / ui.memMax * 100, 100) + "%"; 1419 | 1420 | //node.style.width = (barWidth*2 + barGap + ui.rightBlank) + "px"; 1421 | node.style.minWidth = (barWidth*2 + barGap + ui.rightBlank) + "px"; 1422 | if (taskInfo){ 1423 | node.tooltipText = `CPU ${taskInfo.cpu}\nMEM ${taskInfo.mem_united}\nPID ${taskInfo.pid}` 1424 | } 1425 | } 1426 | 1427 | 1428 | //================================ 1429 | 1430 | 1431 | let wins = []; 1432 | 1433 | async function TaskMonitorUpdate() { 1434 | 1435 | wins = getAllWindows(); // wins is needed by updating bars 1436 | 1437 | if (isThisTheFirstWindowInOpeningWindowsList() ){ 1438 | //console.log("TaskMonitor refreshing"); 1439 | 1440 | var tbody = await Control.update(true); 1441 | var ps = parseTbody(tbody); 1442 | 1443 | var mtext_arr = psToMTextArr(ps) ; 1444 | var mtext_tooltip = mtext_arr.join('\n'); 1445 | var totalCpuMem = calcPsTotalCpuMem(ps); 1446 | addCpuMem2whole(totalCpuMem.cpu, totalCpuMem.mem, mtext_tooltip); 1447 | 1448 | for (var p of ps) { 1449 | for (var web of p.webs) { 1450 | if (web.tabWindowId !== undefined) { 1451 | const r_tabfinder = tabFinder.get(web.tabWindowId); 1452 | // { 1453 | // tab: the tab button DOM node (.tabbrowser-tab) , 1454 | // tabbrowser: seems to be a bigger object 1455 | // } 1456 | 1457 | const tabNode = r_tabfinder.tab; 1458 | addCpuMem2Tabbtn(tabNode, { 1459 | cpu: p.cpu, 1460 | mem: p.mem, 1461 | mem_united: p.mem_united, 1462 | pid: p.pid, 1463 | pmtext: pToPMText(p) 1464 | }); 1465 | } 1466 | } 1467 | } 1468 | wins.forEach( function(win, win_i) { 1469 | win.document.body.querySelectorAll("tab[pending=true]").forEach( function(tabnode) { 1470 | addCpuMem2Tabbtn(tabnode, {cpu:0, mem:0, mem_united:"", pid:0, pmtext: null}, true); 1471 | }); 1472 | }); 1473 | 1474 | 1475 | }else{ 1476 | //console.log("TaskMonitor staling for not first window"); 1477 | } 1478 | 1479 | } 1480 | 1481 | 1482 | function isThisTheFirstWindowInOpeningWindowsList() { 1483 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 1484 | .getService(Components.interfaces.nsIWindowMediator); 1485 | var enumerator = wm.getEnumerator("navigator:browser"); 1486 | var win = enumerator.getNext(); 1487 | if (gBrowser === win.gBrowser){ //gBrowser is available only when no @onlyonce 1488 | return true; 1489 | } 1490 | } 1491 | function getAllWindows() { 1492 | var windows = []; 1493 | 1494 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 1495 | .getService(Components.interfaces.nsIWindowMediator); 1496 | var enumerator = wm.getEnumerator("navigator:browser"); 1497 | while(enumerator.hasMoreElements()) { 1498 | var win = enumerator.getNext(); 1499 | // win is [Object ChromeWindow] (just like window), do something with it 1500 | windows.push(win); 1501 | } 1502 | return windows; 1503 | } 1504 | 1505 | 1506 | async function startTaskMonitor() { 1507 | if (taskMonitorTimerID) { 1508 | console.log("TaskMonitor already running"); 1509 | return; 1510 | } 1511 | await Control.init(); 1512 | await Control.update(); 1513 | 1514 | taskMonitorTimerID = window.setInterval(() => TaskMonitorUpdate(), UPDATE_INTERVAL_MS); 1515 | //console.log("taskMonitorTimerID: ", taskMonitorTimerID); 1516 | 1517 | }; 1518 | startTaskMonitor(); 1519 | 1520 | 1521 | 1522 | })(); 1523 | 1524 | function stopTaskMonitor() { 1525 | window.clearInterval(taskMonitorTimerID); 1526 | taskMonitorTimerID = null; 1527 | if (memoryCleanerTimerID) 1528 | { 1529 | window.clearInterval(memoryCleanerTimerID); 1530 | memoryCleanerTimerID = null; 1531 | } 1532 | } 1533 | 1534 | 1535 | 1536 | -------------------------------------------------------------------------------- /dist/taskmonitor_part2.uc.js: -------------------------------------------------------------------------------- 1 | /* Firefox userChrome script 2 | * Show tab cpu and memory bars on every tab button 3 | * Show all-process cpu and memory bars on a slender widget at the right of tab bar 4 | * Dynamically show processes on popup menu of the widget 5 | * 6 | * Tested on Firefox 128, with xiaoxiaoflood's uc loader 7 | * 8 | * Author: garywill (https://garywill.github.io) 9 | * https://github.com/garywill/firefoxtaskmonitor 10 | */ 11 | 12 | // ==UserScript== 13 | // @include main 14 | // @onlyonce 15 | // ==/UserScript== 16 | 17 | console.log("taskmonitor_part2.js"); 18 | 19 | "use strict"; 20 | 21 | (() => { 22 | const menu_show_tasks_num = 12; 23 | 24 | 25 | const barGap = 1; 26 | const barWidth = 3; 27 | 28 | const sss = Components.classes["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService); 29 | 30 | widget_init(); 31 | 32 | function widget_init() { 33 | const fftm_widget_label = "TaskManager Widget"; 34 | const fftm_widget_id = "fftm_widget"; 35 | 36 | Components.utils.import("resource:///modules/CustomizableUI.jsm"); 37 | 38 | // if ( ! CustomizableUI.getWidget(fftm_widget_id) ) { 39 | CustomizableUI.createWidget({ 40 | id: fftm_widget_id, 41 | type: "custom", 42 | defaultArea: CustomizableUI.AREA_TABSTRIP, 43 | removable: true, 44 | onBuild: function (doc) { 45 | let btn = doc.createXULElement('toolbarbutton'); 46 | btn.id = fftm_widget_id; 47 | btn.label = fftm_widget_label; 48 | btn.tooltipText = fftm_widget_label; 49 | btn.type = 'menu'; 50 | btn.className = 'toolbarbutton-1 chromeclass-toolbar-additional fftm_widget_class'; 51 | btn.style.MozBoxAlign="unset"; 52 | 53 | let mp = doc.createXULElement("menupopup"); 54 | mp.id = 'fftm_widget_menupopup'; 55 | // mp.onclick = function(event) { event.preventDefault() ;} ; 56 | 57 | 58 | for (var i=0; i { 23 | // User customization 24 | const cleanMemory_period = 20*60*1000; // milisecond 25 | 26 | memoryCleanerTimerID = window.setInterval(() => cleanMemory(), cleanMemory_period); 27 | 28 | function cleanMemory() { 29 | 30 | if (isThisTheFirstWindowInOpeningWindowsList() ){ 31 | const gMgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService( 32 | Ci.nsIMemoryReporterManager 33 | ); 34 | 35 | Services.obs.notifyObservers(null, "child-mmu-request"); 36 | gMgr.minimizeMemoryUsage( function() {console.log("minimizeMemoryUsage");} ); 37 | } 38 | } 39 | 40 | function isThisTheFirstWindowInOpeningWindowsList() { 41 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 42 | .getService(Components.interfaces.nsIWindowMediator); 43 | var enumerator = wm.getEnumerator("navigator:browser"); 44 | var win = enumerator.getNext(); 45 | if (gBrowser === win.gBrowser){ //gBrowser is available only when no @onlyonce 46 | return true; 47 | } 48 | } 49 | })(); 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /include.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | def include_files(input_file, output_file): 4 | with open(input_file, 'r') as f: 5 | lines = f.readlines() 6 | 7 | with open(output_file, 'w') as out: 8 | for line in lines: 9 | if line.strip().startswith('#include "'): 10 | include_file = line.split('"')[1] 11 | with open('src/'+include_file, 'r') as inc: 12 | out.write(inc.read()) 13 | else: 14 | out.write(line) 15 | 16 | # 使用示例 17 | include_files('src/taskmonitor_part1.uc.js', '/dev/stdout') 18 | -------------------------------------------------------------------------------- /src/aboutProcesses.js: -------------------------------------------------------------------------------- 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 | 6 | // Time in ms before we start changing the sort order again after receiving a 7 | // mousemove event. 8 | const TIME_BEFORE_SORTING_AGAIN = 5000; 9 | 10 | // How long we should wait between samples. 11 | const MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS = 1000; 12 | 13 | // How often we should update 14 | const UPDATE_INTERVAL_MS = 2000; 15 | 16 | const NS_PER_US = 1000; 17 | const NS_PER_MS = 1000 * 1000; 18 | const NS_PER_S = 1000 * 1000 * 1000; 19 | const NS_PER_MIN = NS_PER_S * 60; 20 | const NS_PER_HOUR = NS_PER_MIN * 60; 21 | const NS_PER_DAY = NS_PER_HOUR * 24; 22 | 23 | const ONE_GIGA = 1024 * 1024 * 1024; 24 | const ONE_MEGA = 1024 * 1024; 25 | const ONE_KILO = 1024; 26 | 27 | // const { XPCOMUtils } = ChromeUtils.importESModule( 28 | // "resource://gre/modules/XPCOMUtils.sys.mjs" 29 | // ); 30 | // const { AppConstants } = ChromeUtils.importESModule( 31 | // "resource://gre/modules/AppConstants.sys.mjs" 32 | // ); 33 | 34 | ChromeUtils.defineESModuleGetters(this, { 35 | ContextualIdentityService: 36 | "resource://gre/modules/ContextualIdentityService.sys.mjs", 37 | }); 38 | 39 | ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () { 40 | return ChromeUtils.importESModule( 41 | "resource://devtools/client/performance-new/shared/background.sys.mjs" 42 | ); 43 | }); 44 | 45 | const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); 46 | 47 | 48 | const gLocalizedUnits = 49 | { 50 | "duration": { "ns": "ns", "us": "µs", "ms": "ms", "s": "s", "m": "m", "h": "h", "d": "d" }, 51 | "memory": { "B": "B", "KB": "KB", "MB": "MB", "GB": "GB", "TB": "TB", "PB": "PB", "EB": "EB" } 52 | }; 53 | 54 | 55 | let tabFinder = { 56 | update() { 57 | this._map = new Map(); 58 | for (let win of Services.wm.getEnumerator("navigator:browser")) { 59 | let tabbrowser = win.gBrowser; 60 | for (let browser of tabbrowser.browsers) { 61 | let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet 62 | if (id != null) { 63 | this._map.set(id, browser); 64 | } 65 | } 66 | if (tabbrowser.preloadedBrowser) { 67 | let browser = tabbrowser.preloadedBrowser; 68 | if (browser.outerWindowID) { 69 | this._map.set(browser.outerWindowID, browser); 70 | } 71 | } 72 | } 73 | }, 74 | 75 | /** 76 | * Find the for a window id. 77 | * 78 | * This is useful e.g. for reloading or closing tabs. 79 | * 80 | * @return null If the xul:tab could not be found, e.g. if the 81 | * windowId is that of a chrome window. 82 | * @return {{tabbrowser: , tab: }} The 83 | * tabbrowser and tab if the latter could be found. 84 | */ 85 | get(id) { 86 | let browser = this._map.get(id); 87 | if (!browser) { 88 | return null; 89 | } 90 | let tabbrowser = browser.getTabBrowser(); 91 | if (!tabbrowser) { 92 | return { 93 | tabbrowser: null, 94 | tab: { 95 | getAttribute() { 96 | return ""; 97 | }, 98 | linkedBrowser: browser, 99 | }, 100 | }; 101 | } 102 | return { tabbrowser, tab: tabbrowser.getTabForBrowser(browser) }; 103 | }, 104 | }; 105 | 106 | 107 | /** 108 | * Utilities for dealing with state 109 | */ 110 | let State = { 111 | // Store the previous and current samples so they can be compared. 112 | _previous: null, 113 | _latest: null, 114 | 115 | async _promiseSnapshot() { 116 | let date = Cu.now(); 117 | let main = await ChromeUtils.requestProcInfo(); 118 | main.date = date; 119 | 120 | let processes = new Map(); 121 | processes.set(main.pid, main); 122 | for (let child of main.children) { 123 | child.date = date; 124 | processes.set(child.pid, child); 125 | } 126 | 127 | return { processes, date }; 128 | }, 129 | 130 | /** 131 | * Update the internal state. 132 | * 133 | * @return {Promise} 134 | */ 135 | async update(force = false) { 136 | if ( 137 | force || 138 | !this._latest || 139 | Cu.now() - this._latest.date > MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS 140 | ) { 141 | // Replacing this._previous before we are done awaiting 142 | // this._promiseSnapshot can cause this._previous and this._latest to be 143 | // equal for a short amount of time, which can cause test failures when 144 | // a forced update of the display is triggered in the meantime. 145 | let newSnapshot = await this._promiseSnapshot(); 146 | this._previous = this._latest; 147 | this._latest = newSnapshot; 148 | } 149 | }, 150 | 151 | _getThreadDelta(cur, prev, deltaT) { 152 | let result = { 153 | tid: cur.tid, 154 | name: cur.name || `(${cur.tid})`, 155 | // Total amount of CPU used, in ns. 156 | totalCpu: cur.cpuTime, 157 | slopeCpu: null, 158 | active: null, 159 | }; 160 | if (!deltaT) { 161 | return result; 162 | } 163 | result.slopeCpu = (result.totalCpu - (prev ? prev.cpuTime : 0)) / deltaT; 164 | result.active = 165 | !!result.slopeCpu || cur.cpuCycleCount > (prev ? prev.cpuCycleCount : 0); 166 | return result; 167 | }, 168 | 169 | _getDOMWindows(process) { 170 | if (!process.windows) { 171 | return []; 172 | } 173 | if (!process.type == "extensions") { 174 | return []; 175 | } 176 | let windows = process.windows.map(win => { 177 | let tab = tabFinder.get(win.outerWindowId); 178 | let addon = 179 | process.type == "extension" 180 | ? WebExtensionPolicy.getByURI(win.documentURI) 181 | : null; 182 | let displayRank; 183 | if (tab) { 184 | displayRank = 1; 185 | } else if (win.isProcessRoot) { 186 | displayRank = 2; 187 | } else if (win.documentTitle) { 188 | displayRank = 3; 189 | } else { 190 | displayRank = 4; 191 | } 192 | return { 193 | outerWindowId: win.outerWindowId, 194 | documentURI: win.documentURI, 195 | documentTitle: win.documentTitle, 196 | isProcessRoot: win.isProcessRoot, 197 | isInProcess: win.isInProcess, 198 | tab, 199 | addon, 200 | // The number of instances we have collapsed. 201 | count: 1, 202 | // A rank used to quickly sort windows. 203 | displayRank, 204 | }; 205 | }); 206 | 207 | // We keep all tabs and addons but we collapse subframes that have the same host. 208 | 209 | // A map from host -> subframe. 210 | let collapsible = new Map(); 211 | let result = []; 212 | for (let win of windows) { 213 | if (win.tab || win.addon) { 214 | result.push(win); 215 | continue; 216 | } 217 | let prev = collapsible.get(win.documentURI.prePath); 218 | if (prev) { 219 | prev.count += 1; 220 | } else { 221 | collapsible.set(win.documentURI.prePath, win); 222 | result.push(win); 223 | } 224 | } 225 | return result; 226 | }, 227 | 228 | /** 229 | * Compute the delta between two process snapshots. 230 | * 231 | * @param {ProcessSnapshot} cur 232 | * @param {ProcessSnapshot?} prev 233 | */ 234 | _getProcessDelta(cur, prev) { 235 | let windows = this._getDOMWindows(cur); 236 | let result = { 237 | pid: cur.pid, 238 | childID: cur.childID, 239 | totalRamSize: cur.memory, 240 | deltaRamSize: null, 241 | totalCpu: cur.cpuTime, 242 | slopeCpu: null, 243 | active: null, 244 | type: cur.type, 245 | origin: cur.origin || "", 246 | threads: null, 247 | displayRank: Control._getDisplayGroupRank(cur, windows), 248 | windows, 249 | utilityActors: cur.utilityActors, 250 | // If this process has an unambiguous title, store it here. 251 | title: null, 252 | }; 253 | // Attempt to determine a title for this process. 254 | let titles = [ 255 | ...new Set( 256 | result.windows 257 | .filter(win => win.documentTitle) 258 | .map(win => win.documentTitle) 259 | ), 260 | ]; 261 | if (titles.length == 1) { 262 | result.title = titles[0]; 263 | } 264 | if (!prev) { 265 | return result; 266 | } 267 | if (prev.pid != cur.pid) { 268 | throw new Error("Assertion failed: A process cannot change pid."); 269 | } 270 | let deltaT = (cur.date - prev.date) * NS_PER_MS; 271 | let threads = null; 272 | 273 | result.deltaRamSize = cur.memory - prev.memory; 274 | result.slopeCpu = (cur.cpuTime - prev.cpuTime) / deltaT; 275 | result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount; 276 | result.threads = threads; 277 | return result; 278 | }, 279 | 280 | getCounters() { 281 | tabFinder.update(); 282 | 283 | let counters = []; 284 | 285 | for (let cur of this._latest.processes.values()) { 286 | let prev = this._previous?.processes.get(cur.pid); 287 | counters.push(this._getProcessDelta(cur, prev)); 288 | } 289 | 290 | return counters; 291 | } 292 | }; 293 | 294 | 295 | let View = { 296 | commit() { 297 | let tbody = document.createElement("table").appendChild(document.createElement("tbody")); 298 | let insertPoint = tbody.firstChild; 299 | let nextRow; 300 | while ((nextRow = this._orderedRows.shift())) { 301 | if (insertPoint && insertPoint === nextRow) { 302 | insertPoint = insertPoint.nextSibling; 303 | } else { 304 | tbody.insertBefore(nextRow, insertPoint); 305 | } 306 | } 307 | 308 | if (insertPoint) { 309 | while ((nextRow = insertPoint.nextSibling)) { 310 | this._removeRow(nextRow); 311 | } 312 | this._removeRow(insertPoint); 313 | } 314 | return tbody; 315 | }, 316 | _rowsById: new Map(), 317 | _removeRow(row) { 318 | this._rowsById.delete(row.rowId); 319 | 320 | row.remove(); 321 | }, 322 | _getOrCreateRow(rowId, cellCount) { 323 | let row = this._rowsById.get(rowId); 324 | if (!row) { 325 | row = document.createElement("tr"); 326 | while (cellCount--) { 327 | row.appendChild(document.createElement("td")); 328 | } 329 | row.rowId = rowId; 330 | this._rowsById.set(rowId, row); 331 | } 332 | this._orderedRows.push(row); 333 | return row; 334 | }, 335 | 336 | displayCpu(data, cpuCell, maxSlopeCpu) { 337 | // Put a value < 0% when we really don't want to see a bar as 338 | // otherwise it sometimes appears due to rounding errors when we 339 | // don't have an integer number of pixels. 340 | let barWidth = -0.5; 341 | if (data.slopeCpu == null) { 342 | this._fillCell(cpuCell, { 343 | fluentName: "about-processes-cpu-user-and-kernel-not-ready", 344 | classes: ["cpu"], 345 | }); 346 | } else { 347 | let { duration, unit } = this._getDuration(data.totalCpu); 348 | if (data.totalCpu == 0) { 349 | // A thread having used exactly 0ns of CPU time is not possible. 350 | // When we get 0 it means the thread used less than the precision of 351 | // the measurement, and it makes more sense to show '0ms' than '0ns'. 352 | // This is useful on Linux where the minimum non-zero CPU time value 353 | // for threads of child processes is 10ms, and on Windows ARM64 where 354 | // the minimum non-zero value is 16ms. 355 | unit = "ms"; 356 | } 357 | let localizedUnit = gLocalizedUnits.duration[unit]; 358 | if (data.slopeCpu == 0) { 359 | let fluentName = data.active 360 | ? "about-processes-cpu-almost-idle" 361 | : "about-processes-cpu-fully-idle"; 362 | this._fillCell(cpuCell, { 363 | fluentName, 364 | fluentArgs: { 365 | total: duration, 366 | unit: localizedUnit, 367 | }, 368 | classes: ["cpu"], 369 | }); 370 | } else { 371 | this._fillCell(cpuCell, { 372 | fluentName: "about-processes-cpu", 373 | fluentArgs: { 374 | percent: data.slopeCpu, 375 | total: duration, 376 | unit: localizedUnit, 377 | }, 378 | classes: ["cpu"], 379 | }); 380 | 381 | let cpuPercent = data.slopeCpu * 100; 382 | if (maxSlopeCpu > 1) { 383 | cpuPercent /= maxSlopeCpu; 384 | } 385 | // Ensure we always have a visible bar for non-0 values. 386 | barWidth = Math.max(0.5, cpuPercent); 387 | } 388 | } 389 | cpuCell.style.setProperty("--bar-width", barWidth); 390 | }, 391 | 392 | /** 393 | * Display a row showing a single process (without its threads). 394 | * 395 | * @param {ProcessDelta} data The data to display. 396 | * @param {Number} maxSlopeCpu The largest slopeCpu value. 397 | * @return {DOMElement} The row displaying the process. 398 | */ 399 | displayProcessRow(data, maxSlopeCpu) { 400 | const cellCount = 4; 401 | let rowId = "p:" + data.pid; 402 | let row = this._getOrCreateRow(rowId, cellCount); 403 | row.process = data; 404 | { 405 | let classNames = "process"; 406 | if (data.isHung) { 407 | classNames += " hung"; 408 | } 409 | row.className = classNames; 410 | } 411 | 412 | // Column: Name 413 | let nameCell = row.firstChild; 414 | { 415 | let classNames = []; 416 | let fluentName; 417 | let fluentArgs = { 418 | pid: "" + data.pid, // Make sure that this number is not localized 419 | }; 420 | switch (data.type) { 421 | case "web": 422 | fluentName = "about-processes-web-process"; 423 | break; 424 | case "webIsolated": 425 | fluentName = "about-processes-web-isolated-process"; 426 | fluentArgs.origin = data.origin; 427 | break; 428 | case "webServiceWorker": 429 | fluentName = "about-processes-web-serviceworker"; 430 | fluentArgs.origin = data.origin; 431 | break; 432 | case "file": 433 | fluentName = "about-processes-file-process"; 434 | break; 435 | case "extension": 436 | fluentName = "about-processes-extension-process"; 437 | classNames = ["extensions"]; 438 | break; 439 | case "privilegedabout": 440 | fluentName = "about-processes-privilegedabout-process"; 441 | break; 442 | case "privilegedmozilla": 443 | fluentName = "about-processes-privilegedmozilla-process"; 444 | break; 445 | case "withCoopCoep": 446 | fluentName = "about-processes-with-coop-coep-process"; 447 | fluentArgs.origin = data.origin; 448 | break; 449 | case "browser": 450 | fluentName = "about-processes-browser-process"; 451 | break; 452 | case "plugin": 453 | fluentName = "about-processes-plugin-process"; 454 | break; 455 | case "gmpPlugin": 456 | fluentName = "about-processes-gmp-plugin-process"; 457 | break; 458 | case "gpu": 459 | fluentName = "about-processes-gpu-process"; 460 | break; 461 | case "vr": 462 | fluentName = "about-processes-vr-process"; 463 | break; 464 | case "rdd": 465 | fluentName = "about-processes-rdd-process"; 466 | break; 467 | case "socket": 468 | fluentName = "about-processes-socket-process"; 469 | break; 470 | case "remoteSandboxBroker": 471 | fluentName = "about-processes-remote-sandbox-broker-process"; 472 | break; 473 | case "forkServer": 474 | fluentName = "about-processes-fork-server-process"; 475 | break; 476 | case "preallocated": 477 | fluentName = "about-processes-preallocated-process"; 478 | break; 479 | case "utility": 480 | fluentName = "about-processes-utility-process"; 481 | break; 482 | // The following are probably not going to show up for users 483 | // but let's handle the case anyway to avoid heisenoranges 484 | // during tests in case of a leftover process from a previous 485 | // test. 486 | default: 487 | fluentName = "about-processes-unknown-process"; 488 | fluentArgs.type = data.type; 489 | break; 490 | } 491 | 492 | // Show container names instead of raw origin attribute suffixes. 493 | if (fluentArgs.origin?.includes("^")) { 494 | let origin = fluentArgs.origin; 495 | let privateBrowsingId, userContextId; 496 | try { 497 | ({ privateBrowsingId, userContextId } = 498 | ChromeUtils.createOriginAttributesFromOrigin(origin)); 499 | fluentArgs.origin = origin.slice(0, origin.indexOf("^")); 500 | } catch (e) { 501 | // createOriginAttributesFromOrigin can throw NS_ERROR_FAILURE for incorrect origin strings. 502 | } 503 | if (userContextId) { 504 | let identityLabel = 505 | ContextualIdentityService.getUserContextLabel(userContextId); 506 | if (identityLabel) { 507 | fluentArgs.origin += ` — ${identityLabel}`; 508 | } 509 | } 510 | if (privateBrowsingId) { 511 | fluentName += "-private"; 512 | } 513 | } 514 | 515 | let processNameElement = nameCell; 516 | document.l10n.setAttributes(processNameElement, fluentName, fluentArgs); 517 | nameCell.className = ["type", "favicon", ...classNames].join(" "); 518 | nameCell.setAttribute("id", data.pid + "-label"); 519 | 520 | let image; 521 | switch (data.type) { 522 | case "browser": 523 | case "privilegedabout": 524 | image = "chrome://branding/content/icon32.png"; 525 | break; 526 | case "extension": 527 | image = "chrome://mozapps/skin/extensions/extension.svg"; 528 | break; 529 | default: 530 | // If all favicons match, pick the shared favicon. 531 | // Otherwise, pick a default icon. 532 | // If some tabs have no favicon, we ignore them. 533 | for (let win of data.windows || []) { 534 | if (!win.tab) { 535 | continue; 536 | } 537 | let favicon = win.tab.tab.getAttribute("image"); 538 | if (!favicon) { 539 | // No favicon here, let's ignore the tab. 540 | } else if (!image) { 541 | // Let's pick a first favicon. 542 | // We'll remove it later if we find conflicting favicons. 543 | image = favicon; 544 | } else if (image == favicon) { 545 | // So far, no conflict, keep the favicon. 546 | } else { 547 | // Conflicting favicons, fallback to default. 548 | image = null; 549 | break; 550 | } 551 | } 552 | if (!image) { 553 | image = "chrome://global/skin/icons/link.svg"; 554 | } 555 | } 556 | nameCell.style.backgroundImage = `url('${image}')`; 557 | } 558 | 559 | // Column: Memory 560 | let memoryCell = nameCell.nextSibling; 561 | { 562 | let formattedTotal = this._formatMemory(data.totalRamSize); 563 | if (data.deltaRamSize) { 564 | let formattedDelta = this._formatMemory(data.deltaRamSize); 565 | this._fillCell(memoryCell, { 566 | fluentName: "about-processes-total-memory-size-changed", 567 | fluentArgs: { 568 | total: formattedTotal.amount, 569 | totalUnit: gLocalizedUnits.memory[formattedTotal.unit], 570 | delta: Math.abs(formattedDelta.amount), 571 | deltaUnit: gLocalizedUnits.memory[formattedDelta.unit], 572 | deltaSign: data.deltaRamSize > 0 ? "+" : "-", 573 | }, 574 | classes: ["memory"], 575 | }); 576 | } else { 577 | this._fillCell(memoryCell, { 578 | fluentName: "about-processes-total-memory-size-no-change", 579 | fluentArgs: { 580 | total: formattedTotal.amount, 581 | totalUnit: gLocalizedUnits.memory[formattedTotal.unit], 582 | }, 583 | classes: ["memory"], 584 | }); 585 | } 586 | } 587 | 588 | // Column: CPU 589 | let cpuCell = memoryCell.nextSibling; 590 | this.displayCpu(data, cpuCell, maxSlopeCpu); 591 | 592 | 593 | return row; 594 | }, 595 | 596 | 597 | 598 | displayDOMWindowRow(data) { 599 | const cellCount = 2; 600 | let rowId = "w:" + data.outerWindowId; 601 | let row = this._getOrCreateRow(rowId, cellCount); 602 | row.win = data; 603 | row.className = "window"; 604 | 605 | // Column: name 606 | let nameCell = row.firstChild; 607 | let tab = tabFinder.get(data.outerWindowId); 608 | let fluentName; 609 | let fluentArgs = {}; 610 | let className; 611 | if (tab && tab.tabbrowser) { 612 | fluentName = "about-processes-tab-name"; 613 | fluentArgs.name = tab.tab.label; 614 | fluentArgs.tabWindowId = data.outerWindowId; // this tabWindowId can be used by tabFinder.get() 615 | className = "tab"; 616 | } else if (tab) { 617 | fluentName = "about-processes-preloaded-tab"; 618 | className = "preloaded-tab"; 619 | } else if (data.count == 1) { 620 | fluentName = "about-processes-frame-name-one"; 621 | fluentArgs.url = data.documentURI.spec; 622 | className = "frame-one"; 623 | } else { 624 | fluentName = "about-processes-frame-name-many"; 625 | fluentArgs.number = data.count; 626 | fluentArgs.shortUrl = 627 | data.documentURI.scheme == "about" 628 | ? data.documentURI.spec 629 | : data.documentURI.prePath; 630 | className = "frame-many"; 631 | } 632 | this._fillCell(nameCell, { 633 | fluentName, 634 | fluentArgs, 635 | classes: ["name", "indent", "favicon", className], 636 | }); 637 | let image = tab?.tab.getAttribute("image"); 638 | if (image) { 639 | nameCell.style.backgroundImage = `url('${image}')`; 640 | } 641 | }, 642 | 643 | utilityActorNameToFluentName(actorName) { 644 | let fluentName; 645 | switch (actorName) { 646 | case "audioDecoder_Generic": 647 | fluentName = "about-processes-utility-actor-audio-decoder-generic"; 648 | break; 649 | 650 | case "audioDecoder_AppleMedia": 651 | fluentName = "about-processes-utility-actor-audio-decoder-applemedia"; 652 | break; 653 | 654 | case "audioDecoder_WMF": 655 | fluentName = "about-processes-utility-actor-audio-decoder-wmf"; 656 | break; 657 | 658 | case "mfMediaEngineCDM": 659 | fluentName = "about-processes-utility-actor-mf-media-engine"; 660 | break; 661 | 662 | case "jSOracle": 663 | fluentName = "about-processes-utility-actor-js-oracle"; 664 | break; 665 | 666 | case "windowsUtils": 667 | fluentName = "about-processes-utility-actor-windows-utils"; 668 | break; 669 | 670 | case "windowsFileDialog": 671 | fluentName = "about-processes-utility-actor-windows-file-dialog"; 672 | break; 673 | 674 | default: 675 | fluentName = "about-processes-utility-actor-unknown"; 676 | break; 677 | } 678 | return fluentName; 679 | }, 680 | 681 | displayUtilityActorRow(data, parent) { 682 | const cellCount = 2; 683 | // The actor name is expected to be unique within a given utility process. 684 | let rowId = "u:" + parent.pid + data.actorName; 685 | let row = this._getOrCreateRow(rowId, cellCount); 686 | row.actor = data; 687 | row.className = "actor"; 688 | 689 | // Column: name 690 | let nameCell = row.firstChild; 691 | let fluentName = this.utilityActorNameToFluentName(data.actorName); 692 | let fluentArgs = {}; 693 | this._fillCell(nameCell, { 694 | fluentName, 695 | fluentArgs, 696 | classes: ["name", "indent", "favicon"], 697 | }); 698 | }, 699 | 700 | /** 701 | * Display a row showing a single thread. 702 | * 703 | * @param {ThreadDelta} data The data to display. 704 | * @param {Number} maxSlopeCpu The largest slopeCpu value. 705 | */ 706 | displayThreadRow(data, maxSlopeCpu) { 707 | const cellCount = 3; 708 | let rowId = "t:" + data.tid; 709 | let row = this._getOrCreateRow(rowId, cellCount); 710 | row.thread = data; 711 | row.className = "thread"; 712 | 713 | // Column: name 714 | let nameCell = row.firstChild; 715 | this._fillCell(nameCell, { 716 | fluentName: "about-processes-thread-name-and-id", 717 | fluentArgs: { 718 | name: data.name, 719 | tid: "" + data.tid /* Make sure that this number is not localized */, 720 | }, 721 | classes: ["name", "double_indent"], 722 | }); 723 | 724 | // Column: CPU 725 | this.displayCpu(data, nameCell.nextSibling, maxSlopeCpu); 726 | 727 | // Third column (Buttons) is empty, nothing to do. 728 | }, 729 | 730 | _orderedRows: [], 731 | _fillCell(elt, { classes, fluentName, fluentArgs }) { 732 | document.l10n.setAttributes(elt, fluentName, fluentArgs); 733 | elt.className = classes.join(" "); 734 | }, 735 | 736 | _getDuration(rawDurationNS) { 737 | if (rawDurationNS <= NS_PER_US) { 738 | return { duration: rawDurationNS, unit: "ns" }; 739 | } 740 | if (rawDurationNS <= NS_PER_MS) { 741 | return { duration: rawDurationNS / NS_PER_US, unit: "us" }; 742 | } 743 | if (rawDurationNS <= NS_PER_S) { 744 | return { duration: rawDurationNS / NS_PER_MS, unit: "ms" }; 745 | } 746 | if (rawDurationNS <= NS_PER_MIN) { 747 | return { duration: rawDurationNS / NS_PER_S, unit: "s" }; 748 | } 749 | if (rawDurationNS <= NS_PER_HOUR) { 750 | return { duration: rawDurationNS / NS_PER_MIN, unit: "m" }; 751 | } 752 | if (rawDurationNS <= NS_PER_DAY) { 753 | return { duration: rawDurationNS / NS_PER_HOUR, unit: "h" }; 754 | } 755 | return { duration: rawDurationNS / NS_PER_DAY, unit: "d" }; 756 | }, 757 | 758 | /** 759 | * Format a value representing an amount of memory. 760 | * 761 | * As a special case, we also handle `null`, which represents the case in which we do 762 | * not have sufficient information to compute an amount of memory. 763 | * 764 | * @param {Number?} value The value to format. Must be either `null` or a non-negative number. 765 | * @return { {unit: "GB" | "MB" | "KB" | B" | "?"}, amount: Number } The formated amount and its 766 | * unit, which may be used for e.g. additional CSS formating. 767 | */ 768 | _formatMemory(value) { 769 | if (value == null) { 770 | return { unit: "?", amount: 0 }; 771 | } 772 | if (typeof value != "number") { 773 | throw new Error(`Invalid memory value ${value}`); 774 | } 775 | let abs = Math.abs(value); 776 | if (abs >= ONE_GIGA) { 777 | return { 778 | unit: "GB", 779 | amount: value / ONE_GIGA, 780 | }; 781 | } 782 | if (abs >= ONE_MEGA) { 783 | return { 784 | unit: "MB", 785 | amount: value / ONE_MEGA, 786 | }; 787 | } 788 | if (abs >= ONE_KILO) { 789 | return { 790 | unit: "KB", 791 | amount: value / ONE_KILO, 792 | }; 793 | } 794 | return { 795 | unit: "B", 796 | amount: value, 797 | }; 798 | } 799 | }; 800 | 801 | 802 | 803 | 804 | let Control = { 805 | // The set of all processes reported as "hung" by the process hang monitor. 806 | // 807 | // type: Set 808 | _hungItems: new Set(), 809 | _sortColumn: null, 810 | _sortAscendent: true, 811 | 812 | init() { 813 | this._initHangReports(); 814 | }, 815 | 816 | _initHangReports() { 817 | const PROCESS_HANG_REPORT_NOTIFICATION = "process-hang-report"; 818 | 819 | // Receiving report of a hung child. 820 | // Let's store if for our next update. 821 | let hangReporter = report => { 822 | report.QueryInterface(Ci.nsIHangReport); 823 | this._hungItems.add(report.childID); 824 | }; 825 | 826 | 827 | }, 828 | async update(force = false) { 829 | await State.update(force); 830 | 831 | return await this._updateDisplay(force); 832 | }, 833 | 834 | // The force parameter can force a full update even when the mouse has been 835 | // moved recently. 836 | async _updateDisplay(force = false) { 837 | let counters = State.getCounters(); 838 | 839 | // We reset `_hungItems`, based on the assumption that the process hang 840 | // monitor will inform us again before the next update. Since the process hang monitor 841 | // pings its clients about once per second and we update about once per 2 seconds 842 | // (or more if the mouse moves), we should be ok. 843 | let hungItems = this._hungItems; 844 | this._hungItems = new Set(); 845 | 846 | counters = this._sortProcesses(counters); 847 | 848 | // Stored because it is used when opening the list of threads. 849 | this._maxSlopeCpu = Math.max(...counters.map(process => process.slopeCpu)); 850 | 851 | let previousProcess = null; 852 | for (let process of counters) { 853 | this._sortDOMWindows(process.windows); 854 | 855 | process.isHung = process.childID && hungItems.has(process.childID); 856 | 857 | let processRow = View.displayProcessRow(process, this._maxSlopeCpu); 858 | 859 | if (process.type != "extension") { 860 | // We do not want to display extensions. 861 | for (let win of process.windows) { 862 | if (win.tab || win.isProcessRoot) { 863 | View.displayDOMWindowRow(win, process); 864 | } 865 | } 866 | } 867 | 868 | if (process.type === "utility") { 869 | for (let actor of process.utilityActors) { 870 | View.displayUtilityActorRow(actor, process); 871 | } 872 | } 873 | 874 | 875 | if ( 876 | this._sortColumn == null && 877 | previousProcess && 878 | previousProcess.displayRank != process.displayRank 879 | ) { 880 | // Add a separation between successive categories of processes. 881 | processRow.classList.add("separate-from-previous-process-group"); 882 | } 883 | previousProcess = process; 884 | } 885 | 886 | 887 | 888 | return View.commit(); 889 | 890 | 891 | 892 | }, 893 | _compareCpu(a, b) { 894 | return ( 895 | b.slopeCpu - a.slopeCpu || b.active - a.active || b.totalCpu - a.totalCpu 896 | ); 897 | }, 898 | _showThreads(row, maxSlopeCpu) { 899 | let process = row.process; 900 | this._sortThreads(process.threads); 901 | for (let thread of process.threads) { 902 | View.displayThreadRow(thread, maxSlopeCpu); 903 | } 904 | }, 905 | _sortThreads(threads) { 906 | return threads.sort((a, b) => { 907 | let order; 908 | switch (this._sortColumn) { 909 | case "column-name": 910 | order = a.name.localeCompare(b.name) || a.tid - b.tid; 911 | break; 912 | case "column-cpu-total": 913 | order = this._compareCpu(a, b); 914 | break; 915 | case "column-memory-resident": 916 | case null: 917 | order = a.tid - b.tid; 918 | break; 919 | default: 920 | throw new Error("Unsupported order: " + this._sortColumn); 921 | } 922 | if (!this._sortAscendent) { 923 | order = -order; 924 | } 925 | return order; 926 | }); 927 | }, 928 | _sortProcesses(counters) { 929 | return counters.sort((a, b) => { 930 | let order; 931 | switch (this._sortColumn) { 932 | case "column-name": 933 | order = 934 | String(a.origin).localeCompare(b.origin) || 935 | String(a.type).localeCompare(b.type) || 936 | a.pid - b.pid; 937 | break; 938 | case "column-cpu-total": 939 | order = this._compareCpu(a, b); 940 | break; 941 | case "column-memory-resident": 942 | order = b.totalRamSize - a.totalRamSize; 943 | break; 944 | case null: 945 | // Default order: classify processes by group. 946 | order = 947 | a.displayRank - b.displayRank || 948 | // Other processes are ordered by origin. 949 | String(a.origin).localeCompare(b.origin); 950 | break; 951 | default: 952 | throw new Error("Unsupported order: " + this._sortColumn); 953 | } 954 | if (!this._sortAscendent) { 955 | order = -order; 956 | } 957 | return order; 958 | }); 959 | }, 960 | _sortDOMWindows(windows) { 961 | return windows.sort((a, b) => { 962 | let order = 963 | a.displayRank - b.displayRank || 964 | a.documentTitle.localeCompare(b.documentTitle) || 965 | a.documentURI.spec.localeCompare(b.documentURI.spec); 966 | if (!this._sortAscendent) { 967 | order = -order; 968 | } 969 | return order; 970 | }); 971 | }, 972 | 973 | // Assign a display rank to a process. 974 | // 975 | // The `browser` process comes first (rank 0). 976 | // Then come web tabs (rank 1). 977 | // Then come web frames (rank 2). 978 | // Then come special processes (minus preallocated) (rank 3). 979 | // Then come preallocated processes (rank 4). 980 | _getDisplayGroupRank(data, windows) { 981 | const RANK_BROWSER = 0; 982 | const RANK_WEB_TABS = 1; 983 | const RANK_WEB_FRAMES = 2; 984 | const RANK_UTILITY = 3; 985 | const RANK_PREALLOCATED = 4; 986 | let type = data.type; 987 | switch (type) { 988 | // Browser comes first. 989 | case "browser": 990 | return RANK_BROWSER; 991 | // Web content comes next. 992 | case "webIsolated": 993 | case "webServiceWorker": 994 | case "withCoopCoep": { 995 | if (windows.some(w => w.tab)) { 996 | return RANK_WEB_TABS; 997 | } 998 | return RANK_WEB_FRAMES; 999 | } 1000 | // Preallocated processes come last. 1001 | case "preallocated": 1002 | return RANK_PREALLOCATED; 1003 | // "web" is special, as it could be one of: 1004 | // - web content currently loading/unloading/... 1005 | // - a preallocated process. 1006 | case "web": 1007 | if (windows.some(w => w.tab)) { 1008 | return RANK_WEB_TABS; 1009 | } 1010 | if (windows.length >= 1) { 1011 | return RANK_WEB_FRAMES; 1012 | } 1013 | // For the time being, we do not display DOM workers 1014 | // (and there's no API to get information on them). 1015 | // Once the blockers for bug 1663737 have landed, we'll be able 1016 | // to find out whether this process has DOM workers. If so, we'll 1017 | // count this process as a content process. 1018 | return RANK_PREALLOCATED; 1019 | // Other special processes before preallocated. 1020 | default: 1021 | return RANK_UTILITY; 1022 | } 1023 | } 1024 | 1025 | }; 1026 | 1027 | 1028 | -------------------------------------------------------------------------------- /src/taskmonitor_part1.uc.js: -------------------------------------------------------------------------------- 1 | /* Firefox userChrome script 2 | * Show tab cpu and memory bars on every tab button 3 | * Show all-process cpu and memory bars on a slender widget at the right of tab bar 4 | * Dynamically show processes on popup menu of the widget 5 | * 6 | * Tested on Firefox 128, with xiaoxiaoflood's uc loader 7 | * 8 | * Author: garywill (https://garywill.github.io) 9 | * https://github.com/garywill/firefoxtaskmonitor 10 | * 11 | * Notice 12 | * Some code is from Mozilla Firefox, which licensed under MPL 13 | * 14 | */ 15 | 16 | // ==UserScript== 17 | // @include main 18 | // ==/UserScript== 19 | 20 | console.log("taskmonitor_part1.js"); 21 | 22 | 23 | "use strict"; 24 | 25 | let taskMonitorTimerID = null; 26 | 27 | (() => { 28 | //===================== 29 | // User customization 30 | 31 | const tabCpuColor = "#fd9191"; // red 32 | //const tabMemColor = "rgb(242, 242, 0)"; //yellow 33 | const tabCpuMax = 100; 34 | const tabMemColor = "rgb(100, 160, 255)"; //blue 35 | const tabMemMax = 900*1000*1000; 36 | //const tabBarsTransp 37 | const allCpuColor = tabCpuColor; 38 | const allCpuMax = 200; 39 | const allMemColor = tabMemColor; 40 | const allMemMax = 1500*1000*1000; 41 | //const allBarsTransp 42 | 43 | //======================= 44 | 45 | const barWidth = 3; 46 | const barGap = 1; 47 | 48 | #include "aboutProcesses.js" 49 | 50 | function parseTbody(tbody) 51 | { 52 | let ps = []; 53 | 54 | for (var iRow = 0; iRow c_maxwidth) 248 | widthToSet = c_maxwidth; 249 | if (widthToSet < c_minwidth) 250 | widthToSet = c_minwidth; 251 | */ 252 | var widthToSet = parseInt( getComputedStyle(insertNode).width ) / 2 ; 253 | tabAllBarsCont.style.width = widthToSet + "px"; 254 | 255 | 256 | addBarsToNode(tabAllBarsCont, taskInfo.cpu, taskInfo.mem, {cpuColor: tabCpuColor, memColor: tabMemColor, cpuMax: tabCpuMax, memMax: tabMemMax, rightBlank: 2}, taskInfo); 257 | tabAllBarsCont.title = tabAllBarsCont.tooltipText = taskInfo.pmtext; 258 | 259 | //var ttp = `CPU ${taskInfo.cpu}\nMEM ${taskInfo.mem_united}\nPID ${taskInfo.pid}`; 260 | //tabNode.getElementsByClassName("tab-icon-image")[0].tooltipText = ttp; 261 | } 262 | 263 | 264 | function addCpuMem2whole(cpu, mem, tooltip) 265 | { 266 | 267 | var arr_tooltip_split = tooltip.split('\n'); 268 | 269 | wins.forEach( function(win, win_i) { 270 | 271 | //var fftm_widget = document.getElementById("fftm_widget"); 272 | var fftm_widget = win.document.body.getElementsByClassName("fftm_widget_class")[0]; 273 | if ( fftm_widget ) 274 | { 275 | var allBarsCont = null; 276 | allBarsCont = createVertRightEdgeCont(fftm_widget, 'fftm_widget_p', 277 | { 278 | position: "relative", 279 | display: "inline-block", 280 | zIndex: "999", 281 | height: "100%", 282 | //minWidth: (barWidth*2 + barGap) + "px", 283 | //width: (barWidth*2 + barGap) + "px", 284 | }, 285 | { 286 | display: "block", 287 | position: "absolute", 288 | height: "100%", 289 | zIndex: "99999", 290 | minWidth: (barWidth*2 + barGap) + "px", 291 | marginLeft: -(barWidth*2 + barGap) + "px", 292 | } 293 | ); 294 | addBarsToNode(allBarsCont, cpu, mem, {cpuColor: allCpuColor, memColor: allMemColor, cpuMax: allCpuMax, memMax: allMemMax} ); 295 | fftm_widget.title = fftm_widget.tooltipText = tooltip; 296 | 297 | 298 | for (var i=0; i< 100; i++) 299 | { 300 | //var menu_task_obj = document.getElementById( "fftm_widget_task_"+i ); 301 | var menu_task_obj = win.document.body.getElementsByClassName( "fftm_widget_task" )[i]; 302 | var text = arr_tooltip_split[i]; 303 | if ( menu_task_obj && text ) { 304 | menu_task_obj.label = text.replaceAll("\t", " "); 305 | menu_task_obj.tooltipText = text; 306 | menu_task_obj.hidden = false; 307 | } 308 | if ( menu_task_obj && !text ) { 309 | menu_task_obj.hidden = true; 310 | } 311 | if ( !menu_task_obj && !text ) { 312 | break; 313 | } 314 | } 315 | 316 | 317 | } 318 | }); 319 | } 320 | 321 | 322 | function createVertRightEdgeCont(BrowserNode, pname, PStyle, CStyle, hide=false) 323 | { 324 | var contParent = BrowserNode.getElementsByClassName(pname)[0]; 325 | 326 | if (hide && contParent) 327 | { 328 | contParent.style.visibility="hidden"; 329 | return; 330 | }else if (!hide && contParent) 331 | { 332 | contParent.style.visibility="visible"; 333 | } 334 | 335 | var cont = null; 336 | if (!contParent) { 337 | contParent = document.createXULElement("div"); 338 | contParent.className = pname; 339 | for (var key in PStyle) 340 | { 341 | contParent.style[key] = PStyle[key] 342 | } 343 | //contParent.tooltipText = "PAPA"; 344 | 345 | cont = document.createXULElement("div"); 346 | cont.className = "taskMonitorBarsCont"; 347 | for (var key in CStyle) 348 | { 349 | cont.style[key] = CStyle[key] 350 | } 351 | //cont.tooltipText = "haha"; 352 | 353 | contParent.appendChild(cont); 354 | BrowserNode.appendChild(contParent); 355 | }else{ 356 | cont = contParent.getElementsByClassName("taskMonitorBarsCont")[0]; 357 | } 358 | 359 | return cont; 360 | } 361 | 362 | function addBarsToNode(node, cpu, memory, ui, taskInfo) 363 | { 364 | if (ui.rightBlank === undefined) ui.rightBlank = 0; 365 | 366 | var cpubar; 367 | cpubar = node.getElementsByClassName("cpuBar")[0]; 368 | if (!cpubar) { 369 | cpubar = document.createElement("div"); 370 | cpubar.className = "cpuBar"; 371 | cpubar.style.backgroundColor = ui.cpuColor; 372 | cpubar.style.width = barWidth + "px"; 373 | cpubar.style.position = "absolute"; 374 | cpubar.style.right = (ui.rightBlank + barWidth + barGap ) + "px"; 375 | cpubar.style.bottom = 0; 376 | node.appendChild(cpubar); 377 | } 378 | cpubar.style.height = Math.min((cpu > 0) ? cpu * (100/ui.cpuMax) : 0, 100) + "%"; 379 | 380 | var membar; 381 | membar = node.getElementsByClassName("memBar")[0]; 382 | if (!membar) { 383 | membar = document.createElement("div"); 384 | membar.className = "memBar"; 385 | membar.style.backgroundColor = ui.memColor; 386 | membar.style.width = barWidth + "px"; 387 | membar.style.position = "absolute"; 388 | membar.style.right = ui.rightBlank + "px"; 389 | membar.style.bottom = 0; 390 | node.appendChild(membar); 391 | } 392 | membar.style.height = Math.min(memory / ui.memMax * 100, 100) + "%"; 393 | 394 | //node.style.width = (barWidth*2 + barGap + ui.rightBlank) + "px"; 395 | node.style.minWidth = (barWidth*2 + barGap + ui.rightBlank) + "px"; 396 | if (taskInfo){ 397 | node.tooltipText = `CPU ${taskInfo.cpu}\nMEM ${taskInfo.mem_united}\nPID ${taskInfo.pid}` 398 | } 399 | } 400 | 401 | 402 | //================================ 403 | 404 | 405 | let wins = []; 406 | 407 | async function TaskMonitorUpdate() { 408 | 409 | wins = getAllWindows(); // wins is needed by updating bars 410 | 411 | if (isThisTheFirstWindowInOpeningWindowsList() ){ 412 | //console.log("TaskMonitor refreshing"); 413 | 414 | var tbody = await Control.update(true); 415 | var ps = parseTbody(tbody); 416 | 417 | var mtext_arr = psToMTextArr(ps) ; 418 | var mtext_tooltip = mtext_arr.join('\n'); 419 | var totalCpuMem = calcPsTotalCpuMem(ps); 420 | addCpuMem2whole(totalCpuMem.cpu, totalCpuMem.mem, mtext_tooltip); 421 | 422 | for (var p of ps) { 423 | for (var web of p.webs) { 424 | if (web.tabWindowId !== undefined) { 425 | const r_tabfinder = tabFinder.get(web.tabWindowId); 426 | // { 427 | // tab: the tab button DOM node (.tabbrowser-tab) , 428 | // tabbrowser: seems to be a bigger object 429 | // } 430 | 431 | const tabNode = r_tabfinder.tab; 432 | addCpuMem2Tabbtn(tabNode, { 433 | cpu: p.cpu, 434 | mem: p.mem, 435 | mem_united: p.mem_united, 436 | pid: p.pid, 437 | pmtext: pToPMText(p) 438 | }); 439 | } 440 | } 441 | } 442 | wins.forEach( function(win, win_i) { 443 | win.document.body.querySelectorAll("tab[pending=true]").forEach( function(tabnode) { 444 | addCpuMem2Tabbtn(tabnode, {cpu:0, mem:0, mem_united:"", pid:0, pmtext: null}, true); 445 | }); 446 | }); 447 | 448 | 449 | }else{ 450 | //console.log("TaskMonitor staling for not first window"); 451 | } 452 | 453 | } 454 | 455 | 456 | function isThisTheFirstWindowInOpeningWindowsList() { 457 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 458 | .getService(Components.interfaces.nsIWindowMediator); 459 | var enumerator = wm.getEnumerator("navigator:browser"); 460 | var win = enumerator.getNext(); 461 | if (gBrowser === win.gBrowser){ //gBrowser is available only when no @onlyonce 462 | return true; 463 | } 464 | } 465 | function getAllWindows() { 466 | var windows = []; 467 | 468 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 469 | .getService(Components.interfaces.nsIWindowMediator); 470 | var enumerator = wm.getEnumerator("navigator:browser"); 471 | while(enumerator.hasMoreElements()) { 472 | var win = enumerator.getNext(); 473 | // win is [Object ChromeWindow] (just like window), do something with it 474 | windows.push(win); 475 | } 476 | return windows; 477 | } 478 | 479 | 480 | async function startTaskMonitor() { 481 | if (taskMonitorTimerID) { 482 | console.log("TaskMonitor already running"); 483 | return; 484 | } 485 | await Control.init(); 486 | await Control.update(); 487 | 488 | taskMonitorTimerID = window.setInterval(() => TaskMonitorUpdate(), UPDATE_INTERVAL_MS); 489 | //console.log("taskMonitorTimerID: ", taskMonitorTimerID); 490 | 491 | }; 492 | startTaskMonitor(); 493 | 494 | 495 | 496 | })(); 497 | 498 | function stopTaskMonitor() { 499 | window.clearInterval(taskMonitorTimerID); 500 | taskMonitorTimerID = null; 501 | if (memoryCleanerTimerID) 502 | { 503 | window.clearInterval(memoryCleanerTimerID); 504 | memoryCleanerTimerID = null; 505 | } 506 | } 507 | 508 | 509 | 510 | -------------------------------------------------------------------------------- /src/taskmonitor_part2.uc.js: -------------------------------------------------------------------------------- 1 | /* Firefox userChrome script 2 | * Show tab cpu and memory bars on every tab button 3 | * Show all-process cpu and memory bars on a slender widget at the right of tab bar 4 | * Dynamically show processes on popup menu of the widget 5 | * 6 | * Tested on Firefox 128, with xiaoxiaoflood's uc loader 7 | * 8 | * Author: garywill (https://garywill.github.io) 9 | * https://github.com/garywill/firefoxtaskmonitor 10 | */ 11 | 12 | // ==UserScript== 13 | // @include main 14 | // @onlyonce 15 | // ==/UserScript== 16 | 17 | console.log("taskmonitor_part2.js"); 18 | 19 | "use strict"; 20 | 21 | (() => { 22 | const menu_show_tasks_num = 12; 23 | 24 | 25 | const barGap = 1; 26 | const barWidth = 3; 27 | 28 | const sss = Components.classes["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService); 29 | 30 | widget_init(); 31 | 32 | function widget_init() { 33 | const fftm_widget_label = "TaskManager Widget"; 34 | const fftm_widget_id = "fftm_widget"; 35 | 36 | Components.utils.import("resource:///modules/CustomizableUI.jsm"); 37 | 38 | // if ( ! CustomizableUI.getWidget(fftm_widget_id) ) { 39 | CustomizableUI.createWidget({ 40 | id: fftm_widget_id, 41 | type: "custom", 42 | defaultArea: CustomizableUI.AREA_TABSTRIP, 43 | removable: true, 44 | onBuild: function (doc) { 45 | let btn = doc.createXULElement('toolbarbutton'); 46 | btn.id = fftm_widget_id; 47 | btn.label = fftm_widget_label; 48 | btn.tooltipText = fftm_widget_label; 49 | btn.type = 'menu'; 50 | btn.className = 'toolbarbutton-1 chromeclass-toolbar-additional fftm_widget_class'; 51 | btn.style.MozBoxAlign="unset"; 52 | 53 | let mp = doc.createXULElement("menupopup"); 54 | mp.id = 'fftm_widget_menupopup'; 55 | mp.onclick = function(event) { event.preventDefault() ;} ; 56 | 57 | 58 | for (var i=0; i { 23 | // User customization 24 | const cleanMemory_period = 20*60*1000; // milisecond 25 | 26 | memoryCleanerTimerID = window.setInterval(() => cleanMemory(), cleanMemory_period); 27 | 28 | function cleanMemory() { 29 | 30 | if (isThisTheFirstWindowInOpeningWindowsList() ){ 31 | const gMgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService( 32 | Ci.nsIMemoryReporterManager 33 | ); 34 | 35 | Services.obs.notifyObservers(null, "child-mmu-request"); 36 | gMgr.minimizeMemoryUsage( function() {console.log("minimizeMemoryUsage");} ); 37 | } 38 | } 39 | 40 | function isThisTheFirstWindowInOpeningWindowsList() { 41 | var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 42 | .getService(Components.interfaces.nsIWindowMediator); 43 | var enumerator = wm.getEnumerator("navigator:browser"); 44 | var win = enumerator.getNext(); 45 | if (gBrowser === win.gBrowser){ //gBrowser is available only when no @onlyonce 46 | return true; 47 | } 48 | } 49 | })(); 50 | 51 | 52 | 53 | 54 | --------------------------------------------------------------------------------