/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function toFixed(num, fixed) { fixed = fixed || 0; fixed = Math.pow(10, fixed); return Math.floor(num * fixed) / fixed; } function createElement(name, props) { var el = document.createElement(name); for (var key in props) { if (key === "style") { for (var styleName in props.style) { el.style[styleName] = props.style[styleName]; } } else { el[key] = props[key]; } } return el; } function parseDisplayList(lines) { var root = { line: "DisplayListRoot 0", name: "DisplayListRoot", address: "0x0", frame: "Root", children: [], }; var objectAtIndentation = { "-1": root, }; for (var i = 0; i < lines.length; i++) { var line = lines[i]; var layerObject = { line, children: [], }; if (!root) { root = layerObject; } var matches = line.match( "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$" ); if (!matches) { dump("Failed to match: " + line + "\n"); continue; } var indentation = Math.floor(matches[1].length / 2); objectAtIndentation[indentation] = layerObject; var parent = objectAtIndentation[indentation - 1]; if (parent) { parent.children.push(layerObject); } layerObject.name = matches[2]; layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump layerObject.frame = matches[4]; layerObject.contentDescriptor = matches[5]; layerObject.z = matches[7]; var rest = matches[8]; if (matches[10]) { // WrapList don't provide a layer layerObject.layer = matches[10]; } layerObject.rest = rest; // the content node name doesn't have a prefix, this makes the parsing easier rest = "content" + rest; var nesting = 0; var startIndex; var lastSpace = -1; for (var j = 0; j < rest.length; j++) { if (rest.charAt(j) == "(") { nesting++; if (nesting == 1) { startIndex = j; } } else if (rest.charAt(j) == ")") { nesting--; if (nesting == 0) { var name = rest.substring(lastSpace + 1, startIndex); var value = rest.substring(startIndex + 1, j); var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$"); if (rectMatches) { layerObject[name] = [ parseFloat(rectMatches[1]), parseFloat(rectMatches[2]), parseFloat(rectMatches[3]), parseFloat(rectMatches[4]), ]; } else { layerObject[name] = value; } } } else if (nesting == 0 && rest.charAt(j) == " ") { lastSpace = j; } } // dump("FIELDS: " + JSON.stringify(fields) + "\n"); } return root; } function trim(s) { return (s || "").replace(/^\s+|\s+$/g, ""); } function getDataURI(str) { if (str.indexOf("data:image/png;base64,") == 0) { return str; } var matches = str.match( "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)" ); if (!matches) { return null; } var canvas = document.createElement("canvas"); var w = parseInt(matches[1]); var stride = parseInt(matches[2]); var h = parseInt(matches[3]); canvas.width = w; canvas.height = h; // TODO handle stride var binary_string = window.atob(matches[4]); var len = binary_string.length; var bytes = new Uint8Array(len); var decoded = new Uint8Array(stride * h); for (var i = 0; i < len; i++) { var ascii = binary_string.charCodeAt(i); bytes[i] = ascii; } var ctxt = canvas.getContext("2d"); var out = ctxt.createImageData(w, h); // This is actually undefined throughout the tree and it isn't clear what it // should be. Since this is only development code, leave it alone for now. // eslint-disable-next-line no-undef LZ4_uncompressChunk(bytes, decoded); for (var x = 0; x < w; x++) { for (var y = 0; y < h; y++) { out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2]; out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1]; out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0]; out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3]; } } ctxt.putImageData(out, 0, 0); return canvas.toDataURL(); } function parseLayers(layersDumpLines) { function parseMatrix2x3(str) { str = trim(str); // Something like '[ 1 0; 0 1; 0 158; ]' var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$"); if (!matches) { return null; } var matrix = [ [parseFloat(matches[1]), parseFloat(matches[2])], [parseFloat(matches[3]), parseFloat(matches[4])], [parseFloat(matches[5]), parseFloat(matches[6])], ]; return matrix; } function parseColor(str) { str = trim(str); // Something like 'rgba(0, 0, 0, 0)' var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$"); if (!colorMatches) { return null; } var color = { r: colorMatches[1], g: colorMatches[2], b: colorMatches[3], a: colorMatches[4], }; return color; } function parseFloat_cleo(str) { str = trim(str); // Something like 2.000 if (parseFloat(str) == str) { return parseFloat(str); } return null; } function parseRect2D(str) { str = trim(str); // Something like '(x=0, y=0, w=2842, h=158)' var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$"); if (!rectMatches) { return null; } var rect = [ parseFloat(rectMatches[1]), parseFloat(rectMatches[2]), parseFloat(rectMatches[3]), parseFloat(rectMatches[4]), ]; return rect; } function parseRegion(str) { str = trim(str); // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >' if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") { return null; } var region = []; str = trim(str.substring(1, str.length - 1)); while (str != "") { var rectMatches = str.match( "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$" ); if (!rectMatches) { return null; } var rect = [ parseFloat(rectMatches[1]), parseFloat(rectMatches[2]), parseFloat(rectMatches[3]), parseFloat(rectMatches[4]), ]; str = trim(rectMatches[5]); region.push(rect); } return region; } var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)"; var root; var objectAtIndentation = []; for (var i = 0; i < layersDumpLines.length; i++) { // Something like 'ThebesLayerComposite (0x12104cc00) [shadow-visible=< (x=0, y=0, w=1920, h=158); >] [visible=< (x=0, y=0, w=1920, h=158); >] [opaqueContent] [valid=< (x=0, y=0, w=1920, h=2218); >]' var line = layersDumpLines[i].name || layersDumpLines[i]; var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)"); if (tileMatches) { let indentation = Math.floor(matches[1].length / 2); var x = tileMatches[2]; var y = tileMatches[3]; var dataUri = tileMatches[4]; let parent = objectAtIndentation[indentation - 1]; var tiles = parent.tiles || {}; tiles[x] = tiles[x] || {}; tiles[x][y] = dataUri; parent.tiles = tiles; continue; } var surfaceMatches = line.match("(\\s*)Surface: (.*)"); if (surfaceMatches) { let indentation = Math.floor(matches[1].length / 2); let parent = objectAtIndentation[indentation - 1] || objectAtIndentation[indentation - 2]; var surfaceURI = surfaceMatches[2]; if (parent.surfaceURI != null) { console.log( "error: surfaceURI already set for this layer " + parent.line ); } parent.surfaceURI = surfaceURI; // Look for the buffer-rect offset var contentHostLine = layersDumpLines[i - 2].name || layersDumpLines[i - 2]; let matches = contentHostLine.match(LAYERS_LINE_REGEX); if (matches) { var contentHostRest = matches[4]; parent.contentHostProp = {}; parseProperties(contentHostRest, parent.contentHostProp); } continue; } var layerObject = { line, children: [], }; if (!root) { root = layerObject; } let matches = line.match(LAYERS_LINE_REGEX); if (!matches) { continue; // Something like a texturehost dump. Safe to ignore } if ( matches[2].includes("TiledContentHost") || matches[2].includes("ContentHost") || matches[2].includes("ContentClient") || matches[2].includes("MemoryTextureHost") || matches[2].includes("ImageHost") ) { continue; // We're already pretty good at visualizing these } var indentation = Math.floor(matches[1].length / 2); objectAtIndentation[indentation] = layerObject; for (var c = indentation + 1; c < objectAtIndentation.length; c++) { objectAtIndentation[c] = null; } if (indentation > 0) { var parent = objectAtIndentation[indentation - 1]; while (!parent) { indentation--; parent = objectAtIndentation[indentation - 1]; } parent.children.push(layerObject); } layerObject.name = matches[2]; layerObject.address = matches[3]; var rest = matches[4]; function parseProperties(rest, layerObject) { var fields = []; var nesting = 0; var startIndex; for (let j = 0; j < rest.length; j++) { if (rest.charAt(j) == "[") { nesting++; if (nesting == 1) { startIndex = j; } } else if (rest.charAt(j) == "]") { nesting--; if (nesting == 0) { fields.push(rest.substring(startIndex + 1, j)); } } } for (let j = 0; j < fields.length; j++) { // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent' var field = fields[j]; // dump("FIELD: " + field + "\n"); var parts = field.split("=", 2); var fieldName = parts[0]; rest = field.substring(fieldName.length + 1); if (parts.length == 1) { layerObject[fieldName] = "true"; layerObject[fieldName].type = "bool"; continue; } var float = parseFloat_cleo(rest); if (float) { layerObject[fieldName] = float; layerObject[fieldName].type = "float"; continue; } var region = parseRegion(rest); if (region) { layerObject[fieldName] = region; layerObject[fieldName].type = "region"; continue; } var rect = parseRect2D(rest); if (rect) { layerObject[fieldName] = rect; layerObject[fieldName].type = "rect2d"; continue; } var matrix = parseMatrix2x3(rest); if (matrix) { layerObject[fieldName] = matrix; layerObject[fieldName].type = "matrix2x3"; continue; } var color = parseColor(rest); if (color) { layerObject[fieldName] = color; layerObject[fieldName].type = "color"; continue; } if (rest[0] == "{" && rest[rest.length - 1] == "}") { var object = {}; parseProperties(rest.substring(1, rest.length - 2).trim(), object); layerObject[fieldName] = object; layerObject[fieldName].type = "object"; continue; } fieldName = fieldName.split(" ")[0]; layerObject[fieldName] = rest[0]; layerObject[fieldName].type = "string"; } } parseProperties(rest, layerObject); if (!layerObject["shadow-transform"]) { // No shadow transform = identify layerObject["shadow-transform"] = [ [1, 0], [0, 1], [0, 0], ]; } // Compute screenTransformX/screenTransformY // TODO Fully support transforms if (layerObject["shadow-transform"] && layerObject.transform) { layerObject["screen-transform"] = [ layerObject["shadow-transform"][2][0], layerObject["shadow-transform"][2][1], ]; var currIndentation = indentation - 1; while (currIndentation >= 0) { var transform = objectAtIndentation[currIndentation]["shadow-transform"] || objectAtIndentation[currIndentation].transform; if (transform) { layerObject["screen-transform"][0] += transform[2][0]; layerObject["screen-transform"][1] += transform[2][1]; } currIndentation--; } } // dump("Fields: " + JSON.stringify(fields) + "\n"); } root.compositeTime = layersDumpLines.compositeTime; // dump("OBJECTS: " + JSON.stringify(root) + "\n"); return root; } function populateLayers( root, displayList, pane, previewParent, hasSeenRoot, contentScale, rootPreviewParent ) { contentScale = contentScale || 1; rootPreviewParent = rootPreviewParent || previewParent; function getDisplayItemForLayer(displayList) { var items = []; if (!displayList) { return items; } if (displayList.layer == root.address) { items.push(displayList); } for (var i = 0; i < displayList.children.length; i++) { var subDisplayItems = getDisplayItemForLayer(displayList.children[i]); for (let j = 0; j < subDisplayItems.length; j++) { items.push(subDisplayItems[j]); } } return items; } var elem = createElement("div", { className: "layerObjectDescription", textContent: root.line, style: { whiteSpace: "pre", }, onmouseover() { if (this.layerViewport) { this.layerViewport.classList.add("layerHover"); } }, onmouseout() { if (this.layerViewport) { this.layerViewport.classList.remove("layerHover"); } }, }); var icon = createElement("img", { src: "show.png", style: { width: "12px", height: "12px", marginLeft: "4px", marginRight: "4px", cursor: "pointer", }, onclick() { if (this.layerViewport) { if (this.layerViewport.style.visibility == "hidden") { this.layerViewport.style.visibility = ""; this.src = "show.png"; } else { this.layerViewport.style.visibility = "hidden"; this.src = "hide.png"; } } }, }); elem.insertBefore(icon, elem.firstChild); pane.appendChild(elem); if (root["shadow-visible"] || root.visible) { var visibleRegion = root["shadow-visible"] || root.visible; var layerViewport = createElement("div", { id: root.address + "_viewport", style: { position: "absolute", pointerEvents: "none", }, }); elem.layerViewport = layerViewport; icon.layerViewport = layerViewport; var layerViewportMatrix = [1, 0, 0, 1, 0, 0]; if (root["shadow-clip"] || root.clip) { var clip = root["shadow-clip"] || root.clip; var clipElem = createElement("div", { id: root.address + "_clip", style: { left: clip[0] + "px", top: clip[1] + "px", width: clip[2] + "px", height: clip[3] + "px", position: "absolute", overflow: "hidden", pointerEvents: "none", }, }); layerViewportMatrix[4] += -clip[0]; layerViewportMatrix[5] += -clip[1]; layerViewport.style.transform = "translate(-" + clip[0] + "px, -" + clip[1] + "px)"; } if (root["shadow-transform"] || root.transform) { var matrix = root["shadow-transform"] || root.transform; layerViewportMatrix[0] = matrix[0][0]; layerViewportMatrix[1] = matrix[0][1]; layerViewportMatrix[2] = matrix[1][0]; layerViewportMatrix[3] = matrix[1][1]; layerViewportMatrix[4] += matrix[2][0]; layerViewportMatrix[5] += matrix[2][1]; } layerViewport.style.transform = "matrix(" + layerViewportMatrix[0] + "," + layerViewportMatrix[1] + "," + layerViewportMatrix[2] + "," + layerViewportMatrix[3] + "," + layerViewportMatrix[4] + "," + layerViewportMatrix[5] + ")"; if (!hasSeenRoot) { hasSeenRoot = true; layerViewport.style.transform = "scale(" + 1 / contentScale + "," + 1 / contentScale + ")"; } if (clipElem) { previewParent.appendChild(clipElem); clipElem.appendChild(layerViewport); } else { previewParent.appendChild(layerViewport); } previewParent = layerViewport; for (let i = 0; i < visibleRegion.length; i++) { let rect2d = visibleRegion[i]; var layerPreview = createElement("div", { id: root.address + "_visible_part" + i + "-" + visibleRegion.length, className: "layerPreview", style: { position: "absolute", left: rect2d[0] + "px", top: rect2d[1] + "px", width: rect2d[2] + "px", height: rect2d[3] + "px", overflow: "hidden", border: "solid 1px black", background: 'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))', }, }); layerViewport.appendChild(layerPreview); function isInside(rect1, rect2) { if ( rect1[0] + rect1[2] < rect2[0] && rect2[0] + rect2[2] < rect1[0] && rect1[1] + rect1[3] < rect2[1] && rect2[1] + rect2[3] < rect1[1] ) { return true; } return true; } var hasImg = false; // Add tile img objects for this part var previewOffset = rect2d; if (root.tiles) { hasImg = true; for (var x in root.tiles) { for (var y in root.tiles[x]) { if (isInside(rect2d, [x, y, 512, 512])) { var tileImgElem = createElement("img", { src: getDataURI(root.tiles[x][y]), style: { position: "absolute", left: x - previewOffset[0] + "px", top: y - previewOffset[1] + "px", pointerEvents: "auto", }, }); layerPreview.appendChild(tileImgElem); } } } layerPreview.style.background = ""; } else if (root.surfaceURI) { hasImg = true; var offsetX = 0; var offsetY = 0; if (root.contentHostProp && root.contentHostProp["buffer-rect"]) { offsetX = root.contentHostProp["buffer-rect"][0]; offsetY = root.contentHostProp["buffer-rect"][1]; } var surfaceImgElem = createElement("img", { src: getDataURI(root.surfaceURI), style: { position: "absolute", left: offsetX - previewOffset[0] + "px", top: offsetY - previewOffset[1] + "px", pointerEvents: "auto", }, }); layerPreview.appendChild(surfaceImgElem); layerPreview.style.background = ""; } else if (root.color) { hasImg = true; layerPreview.style.background = "rgba(" + root.color.r + ", " + root.color.g + ", " + root.color.b + ", " + root.color.a + ")"; } if (hasImg || true) { layerPreview.mouseoverElem = elem; layerPreview.onmouseenter = function () { this.mouseoverElem.onmouseover(); }; layerPreview.onmouseout = function () { this.mouseoverElem.onmouseout(); }; } } var layerDisplayItems = getDisplayItemForLayer(displayList); for (let i = 0; i < layerDisplayItems.length; i++) { var displayItem = layerDisplayItems[i]; var displayElem = createElement("div", { className: "layerObjectDescription", textContent: " " + trim(displayItem.line), style: { whiteSpace: "pre", }, displayItem, layerViewport, onmouseover() { if (this.diPreview) { this.diPreview.classList.add("displayHover"); var description = ""; if (this.displayItem.contentDescriptor) { description += "Content: " + this.displayItem.contentDescriptor; } else { description += "Content: Unknown"; } description += "
Item: " + this.displayItem.name + " (" + this.displayItem.address + ")"; description += "
Layer: " + root.name + " (" + root.address + ")"; if (this.displayItem.frame) { description += "
Frame: " + this.displayItem.frame; } if (this.displayItem.layerBounds) { description += "
Bounds: [" + toFixed(this.displayItem.layerBounds[0] / 60, 2) + ", " + toFixed(this.displayItem.layerBounds[1] / 60, 2) + ", " + toFixed(this.displayItem.layerBounds[2] / 60, 2) + ", " + toFixed(this.displayItem.layerBounds[3] / 60, 2) + "] (CSS Pixels)"; } if (this.displayItem.z) { description += "
Z: " + this.displayItem.z; } // At the end if (this.displayItem.rest) { description += "
" + this.displayItem.rest; } var box = this.diPreview.getBoundingClientRect(); this.diPreview.tooltip = createElement("div", { className: "csstooltip", innerHTML: description, style: { top: Math.min( box.bottom, document.documentElement.clientHeight - 150 ) + "px", left: box.left + "px", }, }); document.body.appendChild(this.diPreview.tooltip); } }, onmouseout() { if (this.diPreview) { this.diPreview.classList.remove("displayHover"); document.body.removeChild(this.diPreview.tooltip); } }, }); icon = createElement("img", { style: { width: "12px", height: "12px", marginLeft: "4px", marginRight: "4px", }, }); displayElem.insertBefore(icon, displayElem.firstChild); pane.appendChild(displayElem); // bounds doesn't adjust for within the layer. It's not a bad fallback but // will have the wrong offset let rect2d = displayItem.layerBounds || displayItem.bounds; if (rect2d) { // This doesn't place them corectly var appUnitsToPixels = 60 / contentScale; let diPreview = createElement("div", { id: "displayitem_" + displayItem.content + "_" + displayItem.address, className: "layerPreview", style: { position: "absolute", left: rect2d[0] / appUnitsToPixels + "px", top: rect2d[1] / appUnitsToPixels + "px", width: rect2d[2] / appUnitsToPixels + "px", height: rect2d[3] / appUnitsToPixels + "px", border: "solid 1px gray", pointerEvents: "auto", }, displayElem, onmouseover() { this.displayElem.onmouseover(); }, onmouseout() { this.displayElem.onmouseout(); }, }); layerViewport.appendChild(diPreview); displayElem.diPreview = diPreview; } } } for (var i = 0; i < root.children.length; i++) { populateLayers( root.children[i], displayList, pane, previewParent, hasSeenRoot, contentScale, rootPreviewParent ); } } // This function takes a stdout snippet and finds the frames function parseMultiLineDump(log) { var container = createElement("div", { style: { height: "100%", position: "relative", }, }); var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n"; var nextLineStartWithSpace = "([ \\t].*$\n)*"; var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")"; var startLine = "Painting --- after optimization:\n"; var endLine = "Painting --- layer tree:"; var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")"; var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm"); var matches = log.match(regex); console.log(matches); window.matches = matches; var matchList = createElement("span", { style: { height: "95%", width: "10%", position: "relative", border: "solid black 2px", display: "inline-block", float: "left", overflow: "auto", }, }); container.appendChild(matchList); var contents = createElement("span", { style: { height: "95%", width: "88%", display: "inline-block", }, textContent: "Click on a frame on the left to view the layer tree", }); container.appendChild(contents); var lastDisplayList = null; var frameID = 1; for (let i = 0; i < matches.length; i++) { var currMatch = matches[i]; if (currMatch.indexOf(startLine) == 0) { // Display list match var matchLines = matches[i].split("\n"); lastDisplayList = parseDisplayList(matchLines); } else { // Layer tree match: let displayList = lastDisplayList; lastDisplayList = null; var currFrameDiv = createElement("a", { style: { padding: "3px", display: "block", }, href: "#", textContent: "LayerTree " + frameID++, onclick() { contents.innerHTML = ""; var matchLines = matches[i].split("\n"); var dumpDiv = parseDump(matchLines, displayList); contents.appendChild(dumpDiv); }, }); matchList.appendChild(currFrameDiv); } } return container; } function parseDump(log, displayList, compositeTitle, compositeTime) { compositeTitle |= ""; compositeTime |= 0; var container = createElement("div", { style: { background: "white", height: "100%", position: "relative", }, }); if (compositeTitle == null && compositeTime == null) { var titleDiv = createElement("div", { className: "treeColumnHeader", style: { width: "100%", }, textContent: compositeTitle + (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""), }); container.appendChild(titleDiv); } var mainDiv = createElement("div", { style: { position: "absolute", top: "16px", left: "0px", right: "0px", bottom: "0px", }, }); container.appendChild(mainDiv); var layerListPane = createElement("div", { style: { cssFloat: "left", height: "100%", width: "300px", overflowY: "scroll", }, }); mainDiv.appendChild(layerListPane); var previewDiv = createElement("div", { style: { position: "absolute", left: "300px", right: "0px", top: "0px", bottom: "0px", overflow: "auto", }, }); mainDiv.appendChild(previewDiv); var root = parseLayers(log); populateLayers(root, displayList, layerListPane, previewDiv); return container; }