servo-default-resources 0.1.0

A component of the servo web-engine.
Documentation
<!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.gcButton.onclick = async () => {
          window.gcButton.disabled = true;
          navigator.servo.garbageCollectAllContexts();
          window.gcButton.disabled = false;
        };

        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>
    <button id="gcButton">Force GC</button>
    <div id="reports" class="hidden"></div>
  </body>
</html>