| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>about:memory</title> |
| <script> |
| document.addEventListener("DOMContentLoaded", start); |
| |
| function insertNode(root, report) { |
| let currentNode = root; |
| for (let path of report.path) { |
| if (!currentNode[path]) { |
| currentNode[path] = { total: 0, container: true }; |
| } |
| currentNode = currentNode[path]; |
| currentNode.total += report.size; |
| } |
| currentNode.size = report.size; |
| currentNode.container = false; |
| } |
| |
| function formatBytes(bytes) { |
| if (bytes < 1024) { |
| return bytes + " B"; |
| } else if (bytes < 1024 * 1024) { |
| return (bytes / 1024).toFixed(2) + " KiB"; |
| } else if (bytes < 1024 * 1024 * 1024) { |
| return (bytes / (1024 * 1024)).toFixed(2) + " MiB"; |
| } else { |
| return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GiB"; |
| } |
| } |
| |
| function formattedSize(size) { |
| // Use enough padding to take into account the "MiB" part. |
| return formatBytes(size).padStart(10); |
| } |
| |
| function convertNodeToDOM(node, name = null) { |
| let result = document.createDocumentFragment(); |
| |
| if (node.container) { |
| let details = document.createElement("details"); |
| let summary = document.createElement("summary"); |
| summary.textContent = `${formattedSize(node.total)} -- ${name}`; |
| details.append(summary); |
| result.append(details); |
| |
| // Add the children in descending order of total size. |
| let entries = Object.entries(node) |
| .filter((item) => { |
| return !["total", "size", "container"].includes(item[0]); |
| }) |
| .sort((a, b) => b[1].total - a[1].total) |
| .forEach((item) => |
| details.append(convertNodeToDOM(item[1], item[0])) |
| ); |
| } else { |
| let inner = document.createElement("div"); |
| inner.textContent = `${formattedSize(node.size)} -- ${name}`; |
| result.append(inner); |
| } |
| |
| return result; |
| } |
| |
| function reportsForProcess(processReport) { |
| let explicitRoot = {}; |
| let nonExplicitRoot = {}; |
| |
| let jemallocHeapReportedSize = 0; |
| let systemHeapReportedSize = 0; |
| |
| let jemallocHeapAllocatedSize = NaN; |
| let systemHeapAllocatedSize = NaN; |
| |
| // In content processes, get the list of urls. |
| let urls = new Set(); |
| |
| processReport.reports.forEach((report) => { |
| if (report.path[0].startsWith("url(")) { |
| // This can be a list of urls. |
| let path_urls = report.path[0].slice(4, -1).split(", "); |
| path_urls.forEach((url) => urls.add(url)); |
| } |
| |
| // Add "explicit" to the start of the path, when appropriate. |
| if (report.kind.startsWith("Explicit")) { |
| report.path.unshift("explicit"); |
| } |
| |
| // Update the reported fractions of the heaps, when appropriate. |
| if (report.kind == "ExplicitJemallocHeapSize") { |
| jemallocHeapReportedSize += report.size; |
| } else if (report.kind == "ExplicitSystemHeapSize") { |
| systemHeapReportedSize += report.size; |
| } |
| |
| // Record total size of the heaps, when we see them. |
| if (report.path.length == 1) { |
| if (report.path[0] == "jemalloc-heap-allocated") { |
| jemallocHeapAllocatedSize = report.size; |
| } else if (report.path[0] == "system-heap-allocated") { |
| systemHeapAllocatedSize = report.size; |
| } |
| } |
| |
| // Insert this report at the proper position. |
| insertNode( |
| report.kind.startsWith("Explicit") ? explicitRoot : nonExplicitRoot, |
| report |
| ); |
| }); |
| |
| // Compute and insert the heap-unclassified values. |
| if (!isNaN(jemallocHeapAllocatedSize)) { |
| insertNode(explicitRoot, { |
| path: ["explicit", "jemalloc-heap-unclassified"], |
| size: jemallocHeapAllocatedSize - jemallocHeapReportedSize, |
| }); |
| } |
| if (!isNaN(systemHeapAllocatedSize)) { |
| insertNode(explicitRoot, { |
| path: ["explicit", "system-heap-unclassified"], |
| size: systemHeapAllocatedSize - systemHeapReportedSize, |
| }); |
| } |
| |
| // Create the DOM structure for each process report: |
| // <div class=process> <h4>...<h4> <pre> ...</pre> </div> |
| let container = document.createElement("div"); |
| container.classList.add("process"); |
| let reportTitle = document.createElement("h4"); |
| reportTitle.textContent = `${ |
| processReport.isMainProcess ? "Main Process" : "Content Process" |
| } (pid ${processReport.pid}) ${[...urls.values()].join(", ")}`; |
| |
| container.append(reportTitle); |
| let reportNode = document.createElement("pre"); |
| reportNode.classList.add("report"); |
| container.append(reportNode); |
| |
| reportNode.append(convertNodeToDOM(explicitRoot.explicit, "explicit")); |
| |
| for (let prop in nonExplicitRoot) { |
| reportNode.append(convertNodeToDOM(nonExplicitRoot[prop], prop)); |
| } |
| |
| // Make sure we always put the main process first. |
| if (processReport.isMainProcess) { |
| window.reports.prepend(container); |
| } else { |
| window.reports.append(container); |
| } |
| } |
| |
| function start() { |
| window.startButton.onclick = async () => { |
| let content = await navigator.servo.reportMemory(); |
| let reports = JSON.parse(content); |
| if (reports.error) { |
| console.error(reports.error); |
| return; |
| } |
| window.reports.innerHTML = ""; |
| window.reports.classList.remove("hidden"); |
| |
| if (!Array.isArray(reports)) { |
| console.error("Unexpected memory report format!"); |
| return; |
| } |
| |
| reports.forEach(reportsForProcess); |
| }; |
| } |
| </script> |
| <style> |
| html { |
| font-family: sans-serif; |
| } |
| |
| details, |
| details div { |
| margin-left: 1em; |
| } |
| |
| summary:hover { |
| cursor: pointer; |
| } |
| |
| div.process { |
| margin: 0.5em; |
| border: 2px solid gray; |
| border-radius: 10px; |
| padding: 5px; |
| background-color: lightgray; |
| } |
| |
| .report { |
| line-height: 1.5em; |
| } |
| |
| .report > details { |
| margin-bottom: 1em; |
| } |
| |
| .hidden { |
| display: none; |
| } |
| </style> |
| </head> |
| <body> |
| <h2>Memory Reports</h2> |
| <button id="startButton">Measure</button> |
| <div id="reports" class="hidden"></div> |
| </body> |
| </html> |