| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Fetches all the urls, and calls {@link createGraph} with the JSON contents. |
| */ |
| function fetchAllAndCreateGraph(urls) { |
| Promise.all( |
| urls.map(url => fetch(url) |
| .then(response => response.ok ? response.json() : undefined))) |
| .then(jsonContents => createGraph.apply(this, jsonContents)); |
| } |
| |
| const CODE_PAGE = "code_page"; |
| const REACHED_PAGE = "reached_page"; |
| const RESIDENCY = "residency"; |
| |
| /** |
| * @return {string} the fill color for a given page slice. |
| */ |
| function getFillColor(d) { |
| switch (d.type) { |
| case CODE_PAGE: |
| return pageFillColor(d.data); |
| case REACHED_PAGE: |
| return reachedPageFillColor(d.data); |
| case RESIDENCY: |
| return residencyPageFillColor(d.data); |
| } |
| } |
| |
| // Prefixes, color, description. |
| const colors = d3.scale.category20(); |
| let colorIndex = 1; |
| const COLOR_MAPPING = [ |
| [["third_party/blink"], colors(colorIndex++), "Blink"], |
| [["v8"], colors(colorIndex++), "V8"], |
| [["base"], colors(colorIndex++), "//base"], |
| [["content"], colors(colorIndex++), "//content"], |
| [["components"], colors(colorIndex++), "//components"], |
| [["chrome/android"], colors(colorIndex++), "//chrome/android"], |
| [["chrome"], colors(colorIndex++), "//chrome"], |
| [["net", "third_party/boringssl"], colors(colorIndex++), "Net"], |
| [["third_party/webrtc", "third_party/opus", "third_party/usrsctp"], |
| colors(colorIndex++), "WebRTC"], |
| [["third_party/ffmpeg", "third_party/libvpx"], colors(colorIndex++), "Media"], |
| [["third_party/icu"], colors(colorIndex++), "ICU"], |
| [["skia"], colors(colorIndex++), "Skia"], |
| [["ui"], colors(colorIndex++), "UI"], |
| [["cc"], colors(colorIndex++), "CC"]]; |
| |
| /** |
| * @return the fill color for an item representing a code page. |
| */ |
| function pageFillColor(page) { |
| const sizeAndFilename = page.size_and_filenames[0]; |
| if (!sizeAndFilename) return "lightgrey"; |
| |
| let dominantFilename = sizeAndFilename[1]; |
| if (dominantFilename.startsWith("obj/")) { |
| dominantFilename = dominantFilename.substring(4); |
| } |
| for (let i = 0; i < COLOR_MAPPING.length; i++) { |
| const prefixes = COLOR_MAPPING[i][0]; |
| const color = COLOR_MAPPING[i][1]; |
| for (let j = 0; j < prefixes.length; j++) { |
| if (dominantFilename.startsWith(prefixes[j])) return color; |
| } |
| } |
| return "darkgrey"; |
| } |
| |
| const reachedColorScale = d3.scale.linear() |
| .domain([0, 100]) |
| .range(["black", "green"]); |
| /** |
| * @return the fill color for an item reprensting reached data. |
| */ |
| function reachedPageFillColor(page) { |
| if (page.total == 0) return "white"; |
| if (page.reached == 0) return "red"; |
| const percentage = 100 * page.reached / page.total; |
| return reachedColorScale(percentage); |
| } |
| |
| function residencyPageFillColor(page) { |
| return page.resident ? "green" : "red"; |
| } |
| |
| /** |
| * Adds the color legend to the document. |
| */ |
| function buildColorLegend() { |
| let table = document.getElementById("colors-legend-table"); |
| for (let i = 0; i < COLOR_MAPPING.length; i++) { |
| let color = COLOR_MAPPING[i][1]; |
| let name = COLOR_MAPPING[i][2]; |
| let row = document.createElement("tr"); |
| row.setAttribute("align", "left"); |
| let colorRectangle = document.createElement("td"); |
| colorRectangle.setAttribute("class", "legend-rectangle"); |
| colorRectangle.setAttribute("style", `background-color: ${color}`); |
| let label = document.createElement("td"); |
| label.innerHTML = name; |
| row.appendChild(colorRectangle); |
| row.appendChild(label); |
| table.appendChild(row); |
| } |
| } |
| |
| /** |
| * Updates the legend. |
| * |
| * @param offset Offset of the item to label in the legend. |
| * @param offsetToData {offset: data} map. |
| */ |
| function updateLegend(offset, offsetToData) { |
| let data = offsetToData[offset]; |
| if (!data) return; |
| |
| let page = data[CODE_PAGE]; |
| let reached = data[REACHED_PAGE]; |
| |
| let legend = document.getElementById("legend"); |
| legend.style.display = "block"; |
| |
| let title = document.getElementById("legend-title"); |
| let sizeAndFilename = page.size_and_filenames[0]; |
| let filename = ""; |
| if (sizeAndFilename) filename = sizeAndFilename[1]; |
| |
| let reachedSize = reached ? reached.reached : 0; |
| let reachedPercentage = reachedSize ? 100 * reachedSize / reached.total : 0; |
| title.innerHTML = ` |
| <b>Page offset:</b> ${page.offset} |
| <br/> |
| <b>Accounted for:</b> ${page.accounted_for} |
| <br/> |
| <b>Reached: </b> ${reachedSize} (${reachedPercentage.toFixed(2)}%) |
| <br/> |
| <b>Dominant filename:</b> ${filename}`; |
| |
| let table = document.getElementById("legend-table"); |
| table.innerHTML = ""; |
| let header = document.createElement("tr"); |
| header.setAttribute("align", "left"); |
| header.innerHTML = "<th>Size</th><th>Filename</th>"; |
| table.appendChild(header); |
| for (let i = 0; i < page.size_and_filenames.length; i++) { |
| let row = document.createElement("tr"); |
| row.setAttribute("align", "left"); |
| row.innerHTML = `<td>${page.size_and_filenames[i][0]}</td> |
| <td>${page.size_and_filenames[i][1]}</td>`; |
| table.appendChild(row); |
| } |
| } |
| |
| |
| /** |
| * @return {int} the lane index for a given data item. |
| */ |
| function typeToLane(d) { |
| switch (d.type) { |
| case CODE_PAGE: |
| return 0; |
| case REACHED_PAGE: |
| return 1; |
| case RESIDENCY: |
| return 2; |
| } |
| } |
| |
| /** |
| * Returns: offset -> {"code_page": codeData, "reached": reachedData} |
| */ |
| function getOffsetToData(flatData) { |
| let result = []; |
| for (let i = 0; i < flatData.length; i++) { |
| let data = flatData[i].data; |
| let type = flatData[i].type; |
| let offset = data["offset"]; |
| if (!result[offset]) result[offset] = {}; |
| result[offset][type] = data; |
| } |
| return result; |
| } |
| |
| /** |
| * Returns: [startOfOrderedText, EndOfOrderedText]. |
| */ |
| function getStartAndEndOfOrderedText(codePages) { |
| let startEnd = []; |
| |
| for (const page of codePages) { |
| let hasAnchorFunctions = false; |
| for (const sizeAndFilename of page.size_and_filenames) { |
| // anchor_functions.o contains two dummy anchor functions which are placed |
| // at the beginning and end of the ordered part of text by the |
| // orderfile. These anchor functions are ~10 bytes long on ARM. All other |
| // functions in anchor_functions.o are at least 100 bytes long. So, to |
| // find the ordered text section all code pages are scanned to find these |
| // two small functions. |
| if (sizeAndFilename[0] < 20 |
| && sizeAndFilename[1].endsWith("anchor_functions.o")) { |
| startEnd.push(page.offset); |
| break; |
| } |
| } |
| } |
| console.assert(startEnd.length == 2); |
| return startEnd; |
| } |
| |
| /** |
| * Creates the graph, and adds it to the DOM. |
| * |
| * reachedData can be undefined. In this case, only the code page data is shown. |
| * |
| * @param codePages data relative to code pages and their content. |
| * @param reachedData data relative to which fraction of code pages is reached. |
| */ |
| function createGraph(codePages, reachedPerPage, residency) { |
| const PAGE_SIZE = 4096; |
| |
| let offsets = codePages.map((x) => x.offset).sort((a, b) => a - b); |
| let minOffset = +offsets[0]; |
| let maxOffset = +offsets[offsets.length - 1] + PAGE_SIZE; |
| let startEndOfOrderedText = getStartAndEndOfOrderedText(codePages); |
| |
| const labels = ["Component", "Reached", "Residency"]; |
| const lanes = labels.length; |
| // [{type: REACHED_PAGE | CODE_PAGE | RESIDENCY, data: pageData}] |
| let flatData = codePages.map((page) => ({"type": CODE_PAGE, "data": page})); |
| if (reachedPerPage) { |
| let typedReachedPerPage = reachedPerPage.map( |
| (page) => ({"type": REACHED_PAGE, "data": page})); |
| flatData = flatData.concat(typedReachedPerPage); |
| } |
| |
| if (residency) { |
| let timestamps = Object.keys( |
| residency).map((x) => +x).sort((a, b) => a - b); |
| let lastTimestamp = +timestamps[timestamps.length - 1]; |
| let residencyData = residency[lastTimestamp]; |
| // Other offsets are relative to start of the native library. |
| let typedResidencyData = residencyData.map( |
| (page) => ( |
| {"type": RESIDENCY, "data": {"offset": page.offset + minOffset, |
| "resident": page.resident}})); |
| flatData = flatData.concat(typedResidencyData); |
| } |
| |
| const offsetToData = getOffsetToData(flatData); |
| |
| let margins = [20, 15, 15, 120] // top right bottom left |
| let width = window.innerWidth - margins[1] - margins[3] |
| let height = 500 - margins[0] - margins[2] |
| let miniHeight = lanes * 12 + 50 |
| let mainHeight = height - miniHeight - 50 |
| |
| let globalXScale = d3.scale.linear().domain([minOffset, maxOffset]) |
| .range([0, width]); |
| const globalScalePageWidth = globalXScale(PAGE_SIZE) - globalXScale(0); |
| |
| let zoomedXScale = d3.scale.linear().domain([minOffset, maxOffset]) |
| .range([0, width]); |
| |
| let mainYScale = d3.scale.linear().domain([0, lanes]).range([0, mainHeight]); |
| let miniYScale = d3.scale.linear().domain([0, lanes]).range([0, miniHeight]); |
| |
| let drag = d3.behavior.drag().on("drag", dragmove); |
| |
| let chart = d3.select("body") |
| .append("svg") |
| .attr("width", width + margins[1] + margins[3]) |
| .attr("height", height + margins[0] + margins[2]) |
| .attr("class", "chart"); |
| |
| chart.append("defs").append("clipPath") |
| .attr("id", "clip") |
| .append("rect") |
| .attr("width", width) |
| .attr("height", mainHeight); |
| |
| let main = chart.append("g") |
| .attr("transform", `translate(${margins[3]}, ${margins[0]})`) |
| .attr("width", width) |
| .attr("height", mainHeight) |
| .attr("class", "main"); |
| |
| let mini = chart.append("g") |
| .attr("transform", `translate(${margins[3]}, ${mainHeight + margins[0]})`) |
| .attr("width", width) |
| .attr("height", miniHeight) |
| .attr("class", "mini"); |
| |
| let miniAxisScale = d3.scale.linear() |
| .domain([0, (maxOffset - minOffset) / 1e6]) |
| .range([0, width]); |
| let miniAxis = d3.svg.axis().scale(miniAxisScale).orient("bottom"); |
| let miniXAxis = mini.append("g") |
| .attr("transform", `translate(0, ${miniHeight})`) |
| .call(miniAxis); |
| |
| let mainAxisScale = d3.scale.linear() |
| .domain([0, (maxOffset - minOffset) / 1e6]) |
| .range([0, width]); |
| let mainAxis = d3.svg.axis().scale(mainAxisScale).orient("bottom"); |
| let mainXAxis = main.append("g") |
| .attr("class", "x axis") |
| .attr("transform", `translate(0, -10)`) |
| .call(mainAxis); |
| |
| main.append("g").selectAll(".laneLines") |
| .data(labels) |
| .enter().append("line") |
| .attr("x1", margins[1]) |
| .attr("y1", (d, i) => mainYScale(i)) |
| .attr("x2", width) |
| .attr("y2", (d, i) => mainYScale(i)) |
| .attr("stroke", "lightgray"); |
| |
| main.append("g").selectAll(".laneText") |
| .data(labels) |
| .enter().append("text") |
| .text((d) => d) |
| .attr("x", -margins[1]) |
| .attr("y", (d, i) => mainYScale(i + .5)) |
| .attr("dy", ".5ex") |
| .attr("text-anchor", "end") |
| .attr("class", "laneText"); |
| |
| let itemRects = main.append("g") |
| .attr("clip-path", "url(#clip)"); |
| |
| mini.append("g").selectAll(".laneLines") |
| .data(labels) |
| .enter().append("line") |
| .attr("x1", margins[1]) |
| .attr("y1", (d, i) => miniYScale(i)) |
| .attr("x2", width) |
| .attr("y2", (d, i) => miniYScale(i)) |
| .attr("stroke", "lightgray"); |
| |
| mini.append("g").selectAll(".laneText") |
| .data(labels) |
| .enter().append("text") |
| .text((d) => d) |
| .attr("x", -margins[1]) |
| .attr("y", (d, i) => miniYScale(i + .5)) |
| .attr("dy", ".5ex") |
| .attr("text-anchor", "end"); |
| |
| mini.append("g").selectAll("miniItems") |
| .data(flatData) |
| .enter().append("rect") |
| .attr("class", (d) => "miniItem") |
| .attr("x", (d) => globalXScale(d.data.offset)) |
| .attr("y", (d) => miniYScale(typeToLane(d) + .5) - 5) |
| .attr("width", (d) => globalScalePageWidth) |
| .attr("height", 10) |
| .style("fill", getFillColor); |
| |
| mini.append("g").selectAll(".orderedTextMarkers") |
| .data(startEndOfOrderedText) |
| .enter().append("rect") |
| .attr("x", globalXScale(startEndOfOrderedText[0])) |
| .attr("y", miniYScale(0)) |
| .attr("width", (globalXScale(startEndOfOrderedText[1]) |
| - globalXScale(startEndOfOrderedText[0]))) |
| .attr("height", miniYScale(3)) |
| .attr("fill", "red") |
| .attr("opacity", .2); |
| |
| let brush = d3.svg.brush().x(globalXScale).on("brush", display); |
| mini.append("g") |
| .attr("class", "x brush") |
| .call(brush) |
| .selectAll("rect") |
| .attr("y", 1) |
| .attr("height", miniHeight - 1); |
| |
| display(); |
| |
| function display() { |
| let rects, labels, |
| minExtent = brush.extent()[0]; |
| maxExtent = brush.extent()[1]; |
| visibleItems = flatData.filter((d) => d.data.offset < maxExtent && |
| d.data.offset + PAGE_SIZE > minExtent); |
| |
| mini.select(".brush").call(brush.extent([minExtent, maxExtent])); |
| zoomedXScale.domain([minExtent, maxExtent]); |
| |
| mainAxisScale.domain([(minExtent - minOffset) / 1e6, |
| (maxExtent - minOffset) / 1e6]); |
| chart.selectAll(".axis").call(mainAxis); |
| |
| // Update the main rects. |
| const zoomedXScalePageWidth = zoomedXScale(PAGE_SIZE) - zoomedXScale(0); |
| rects = itemRects.selectAll("rect") |
| .data(visibleItems, (d) => d.data.offset + d.type) |
| .attr("x", (d) => zoomedXScale(d.data.offset)) |
| .attr("width", (d) => zoomedXScalePageWidth); |
| |
| rects.enter().append("rect") |
| .attr("class", (d) => "mainItem") |
| .attr("x", (d) => zoomedXScale(d.data.offset)) |
| .attr("y", (d) => mainYScale(typeToLane(d)) + 10) |
| .attr("width", (d) => zoomedXScalePageWidth) |
| .attr("height", (d) => .8 * mainYScale(1)) |
| .style("fill", getFillColor) |
| .on("mouseover", (d) => updateLegend(d.data.offset, offsetToData)); |
| |
| rects.exit().remove(); |
| } |
| |
| function dragmove(d) { |
| d3.select(this).attr("x", d.x = Math.max(0, d3.event.x)); |
| } |
| } |