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/. */
5 function toFixed(num, fixed) {
7 fixed = Math.pow(10, fixed);
8 return Math.floor(num * fixed) / fixed;
10 function createElement(name, props) {
11 var el = document.createElement(name);
13 for (var key in props) {
14 if (key === "style") {
15 for (var styleName in props.style) {
16 el.style[styleName] = props.style[styleName];
26 function parseDisplayList(lines) {
28 line: "DisplayListRoot 0",
29 name: "DisplayListRoot",
35 var objectAtIndentation = {
39 for (var i = 0; i < lines.length; i++) {
50 var matches = line.match(
51 "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$"
54 dump("Failed to match: " + line + "\n");
58 var indentation = Math.floor(matches[1].length / 2);
59 objectAtIndentation[indentation] = layerObject;
60 var parent = objectAtIndentation[indentation - 1];
62 parent.children.push(layerObject);
65 layerObject.name = matches[2];
66 layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump
67 layerObject.frame = matches[4];
68 layerObject.contentDescriptor = matches[5];
69 layerObject.z = matches[7];
70 var rest = matches[8];
72 // WrapList don't provide a layer
73 layerObject.layer = matches[10];
75 layerObject.rest = rest;
77 // the content node name doesn't have a prefix, this makes the parsing easier
78 rest = "content" + rest;
83 for (var j = 0; j < rest.length; j++) {
84 if (rest.charAt(j) == "(") {
89 } else if (rest.charAt(j) == ")") {
92 var name = rest.substring(lastSpace + 1, startIndex);
93 var value = rest.substring(startIndex + 1, j);
95 var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$");
98 parseFloat(rectMatches[1]),
99 parseFloat(rectMatches[2]),
100 parseFloat(rectMatches[3]),
101 parseFloat(rectMatches[4]),
104 layerObject[name] = value;
107 } else if (nesting == 0 && rest.charAt(j) == " ") {
111 // dump("FIELDS: " + JSON.stringify(fields) + "\n");
117 return (s || "").replace(/^\s+|\s+$/g, "");
120 function getDataURI(str) {
121 if (str.indexOf("data:image/png;base64,") == 0) {
125 var matches = str.match(
126 "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)"
132 var canvas = document.createElement("canvas");
133 var w = parseInt(matches[1]);
134 var stride = parseInt(matches[2]);
135 var h = parseInt(matches[3]);
139 // TODO handle stride
141 var binary_string = window.atob(matches[4]);
142 var len = binary_string.length;
143 var bytes = new Uint8Array(len);
144 var decoded = new Uint8Array(stride * h);
145 for (var i = 0; i < len; i++) {
146 var ascii = binary_string.charCodeAt(i);
150 var ctxt = canvas.getContext("2d");
151 var out = ctxt.createImageData(w, h);
152 // This is actually undefined throughout the tree and it isn't clear what it
153 // should be. Since this is only development code, leave it alone for now.
154 // eslint-disable-next-line no-undef
155 LZ4_uncompressChunk(bytes, decoded);
157 for (var x = 0; x < w; x++) {
158 for (var y = 0; y < h; y++) {
159 out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2];
160 out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1];
161 out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0];
162 out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3];
166 ctxt.putImageData(out, 0, 0);
167 return canvas.toDataURL();
170 function parseLayers(layersDumpLines) {
171 function parseMatrix2x3(str) {
174 // Something like '[ 1 0; 0 1; 0 158; ]'
175 var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$");
181 [parseFloat(matches[1]), parseFloat(matches[2])],
182 [parseFloat(matches[3]), parseFloat(matches[4])],
183 [parseFloat(matches[5]), parseFloat(matches[6])],
188 function parseColor(str) {
191 // Something like 'rgba(0, 0, 0, 0)'
192 var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$");
205 function parseFloat_cleo(str) {
208 // Something like 2.000
209 if (parseFloat(str) == str) {
210 return parseFloat(str);
215 function parseRect2D(str) {
218 // Something like '(x=0, y=0, w=2842, h=158)'
219 var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$");
225 parseFloat(rectMatches[1]),
226 parseFloat(rectMatches[2]),
227 parseFloat(rectMatches[3]),
228 parseFloat(rectMatches[4]),
232 function parseRegion(str) {
235 // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >'
236 if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") {
241 str = trim(str.substring(1, str.length - 1));
243 var rectMatches = str.match(
244 "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$"
251 parseFloat(rectMatches[1]),
252 parseFloat(rectMatches[2]),
253 parseFloat(rectMatches[3]),
254 parseFloat(rectMatches[4]),
256 str = trim(rectMatches[5]);
262 var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)";
265 var objectAtIndentation = [];
266 for (var i = 0; i < layersDumpLines.length; i++) {
267 // 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); >]'
268 var line = layersDumpLines[i].name || layersDumpLines[i];
270 var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)");
272 let indentation = Math.floor(matches[1].length / 2);
273 var x = tileMatches[2];
274 var y = tileMatches[3];
275 var dataUri = tileMatches[4];
276 let parent = objectAtIndentation[indentation - 1];
277 var tiles = parent.tiles || {};
279 tiles[x] = tiles[x] || {};
280 tiles[x][y] = dataUri;
282 parent.tiles = tiles;
287 var surfaceMatches = line.match("(\\s*)Surface: (.*)");
288 if (surfaceMatches) {
289 let indentation = Math.floor(matches[1].length / 2);
291 objectAtIndentation[indentation - 1] ||
292 objectAtIndentation[indentation - 2];
294 var surfaceURI = surfaceMatches[2];
295 if (parent.surfaceURI != null) {
297 "error: surfaceURI already set for this layer " + parent.line
300 parent.surfaceURI = surfaceURI;
302 // Look for the buffer-rect offset
303 var contentHostLine =
304 layersDumpLines[i - 2].name || layersDumpLines[i - 2];
305 let matches = contentHostLine.match(LAYERS_LINE_REGEX);
307 var contentHostRest = matches[4];
308 parent.contentHostProp = {};
309 parseProperties(contentHostRest, parent.contentHostProp);
323 let matches = line.match(LAYERS_LINE_REGEX);
325 continue; // Something like a texturehost dump. Safe to ignore
329 matches[2].includes("TiledContentHost") ||
330 matches[2].includes("ContentHost") ||
331 matches[2].includes("ContentClient") ||
332 matches[2].includes("MemoryTextureHost") ||
333 matches[2].includes("ImageHost")
335 continue; // We're already pretty good at visualizing these
338 var indentation = Math.floor(matches[1].length / 2);
339 objectAtIndentation[indentation] = layerObject;
340 for (var c = indentation + 1; c < objectAtIndentation.length; c++) {
341 objectAtIndentation[c] = null;
343 if (indentation > 0) {
344 var parent = objectAtIndentation[indentation - 1];
347 parent = objectAtIndentation[indentation - 1];
350 parent.children.push(layerObject);
353 layerObject.name = matches[2];
354 layerObject.address = matches[3];
356 var rest = matches[4];
358 function parseProperties(rest, layerObject) {
362 for (let j = 0; j < rest.length; j++) {
363 if (rest.charAt(j) == "[") {
368 } else if (rest.charAt(j) == "]") {
371 fields.push(rest.substring(startIndex + 1, j));
376 for (let j = 0; j < fields.length; j++) {
377 // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent'
378 var field = fields[j];
379 // dump("FIELD: " + field + "\n");
380 var parts = field.split("=", 2);
381 var fieldName = parts[0];
382 rest = field.substring(fieldName.length + 1);
383 if (parts.length == 1) {
384 layerObject[fieldName] = "true";
385 layerObject[fieldName].type = "bool";
388 var float = parseFloat_cleo(rest);
390 layerObject[fieldName] = float;
391 layerObject[fieldName].type = "float";
394 var region = parseRegion(rest);
396 layerObject[fieldName] = region;
397 layerObject[fieldName].type = "region";
400 var rect = parseRect2D(rest);
402 layerObject[fieldName] = rect;
403 layerObject[fieldName].type = "rect2d";
406 var matrix = parseMatrix2x3(rest);
408 layerObject[fieldName] = matrix;
409 layerObject[fieldName].type = "matrix2x3";
412 var color = parseColor(rest);
414 layerObject[fieldName] = color;
415 layerObject[fieldName].type = "color";
418 if (rest[0] == "{" && rest[rest.length - 1] == "}") {
420 parseProperties(rest.substring(1, rest.length - 2).trim(), object);
421 layerObject[fieldName] = object;
422 layerObject[fieldName].type = "object";
425 fieldName = fieldName.split(" ")[0];
426 layerObject[fieldName] = rest[0];
427 layerObject[fieldName].type = "string";
430 parseProperties(rest, layerObject);
432 if (!layerObject["shadow-transform"]) {
433 // No shadow transform = identify
434 layerObject["shadow-transform"] = [
441 // Compute screenTransformX/screenTransformY
442 // TODO Fully support transforms
443 if (layerObject["shadow-transform"] && layerObject.transform) {
444 layerObject["screen-transform"] = [
445 layerObject["shadow-transform"][2][0],
446 layerObject["shadow-transform"][2][1],
448 var currIndentation = indentation - 1;
449 while (currIndentation >= 0) {
451 objectAtIndentation[currIndentation]["shadow-transform"] ||
452 objectAtIndentation[currIndentation].transform;
454 layerObject["screen-transform"][0] += transform[2][0];
455 layerObject["screen-transform"][1] += transform[2][1];
461 // dump("Fields: " + JSON.stringify(fields) + "\n");
463 root.compositeTime = layersDumpLines.compositeTime;
464 // dump("OBJECTS: " + JSON.stringify(root) + "\n");
467 function populateLayers(
476 contentScale = contentScale || 1;
477 rootPreviewParent = rootPreviewParent || previewParent;
479 function getDisplayItemForLayer(displayList) {
484 if (displayList.layer == root.address) {
485 items.push(displayList);
487 for (var i = 0; i < displayList.children.length; i++) {
488 var subDisplayItems = getDisplayItemForLayer(displayList.children[i]);
489 for (let j = 0; j < subDisplayItems.length; j++) {
490 items.push(subDisplayItems[j]);
495 var elem = createElement("div", {
496 className: "layerObjectDescription",
497 textContent: root.line,
502 if (this.layerViewport) {
503 this.layerViewport.classList.add("layerHover");
507 if (this.layerViewport) {
508 this.layerViewport.classList.remove("layerHover");
512 var icon = createElement("img", {
522 if (this.layerViewport) {
523 if (this.layerViewport.style.visibility == "hidden") {
524 this.layerViewport.style.visibility = "";
525 this.src = "show.png";
527 this.layerViewport.style.visibility = "hidden";
528 this.src = "hide.png";
533 elem.insertBefore(icon, elem.firstChild);
534 pane.appendChild(elem);
536 if (root["shadow-visible"] || root.visible) {
537 var visibleRegion = root["shadow-visible"] || root.visible;
538 var layerViewport = createElement("div", {
539 id: root.address + "_viewport",
541 position: "absolute",
542 pointerEvents: "none",
545 elem.layerViewport = layerViewport;
546 icon.layerViewport = layerViewport;
547 var layerViewportMatrix = [1, 0, 0, 1, 0, 0];
548 if (root["shadow-clip"] || root.clip) {
549 var clip = root["shadow-clip"] || root.clip;
550 var clipElem = createElement("div", {
551 id: root.address + "_clip",
553 left: clip[0] + "px",
555 width: clip[2] + "px",
556 height: clip[3] + "px",
557 position: "absolute",
559 pointerEvents: "none",
562 layerViewportMatrix[4] += -clip[0];
563 layerViewportMatrix[5] += -clip[1];
564 layerViewport.style.transform =
565 "translate(-" + clip[0] + "px, -" + clip[1] + "px)";
567 if (root["shadow-transform"] || root.transform) {
568 var matrix = root["shadow-transform"] || root.transform;
569 layerViewportMatrix[0] = matrix[0][0];
570 layerViewportMatrix[1] = matrix[0][1];
571 layerViewportMatrix[2] = matrix[1][0];
572 layerViewportMatrix[3] = matrix[1][1];
573 layerViewportMatrix[4] += matrix[2][0];
574 layerViewportMatrix[5] += matrix[2][1];
576 layerViewport.style.transform =
578 layerViewportMatrix[0] +
580 layerViewportMatrix[1] +
582 layerViewportMatrix[2] +
584 layerViewportMatrix[3] +
586 layerViewportMatrix[4] +
588 layerViewportMatrix[5] +
592 layerViewport.style.transform =
593 "scale(" + 1 / contentScale + "," + 1 / contentScale + ")";
596 previewParent.appendChild(clipElem);
597 clipElem.appendChild(layerViewport);
599 previewParent.appendChild(layerViewport);
601 previewParent = layerViewport;
602 for (let i = 0; i < visibleRegion.length; i++) {
603 let rect2d = visibleRegion[i];
604 var layerPreview = createElement("div", {
605 id: root.address + "_visible_part" + i + "-" + visibleRegion.length,
606 className: "layerPreview",
608 position: "absolute",
609 left: rect2d[0] + "px",
610 top: rect2d[1] + "px",
611 width: rect2d[2] + "px",
612 height: rect2d[3] + "px",
614 border: "solid 1px black",
616 'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))',
619 layerViewport.appendChild(layerPreview);
621 function isInside(rect1, rect2) {
623 rect1[0] + rect1[2] < rect2[0] &&
624 rect2[0] + rect2[2] < rect1[0] &&
625 rect1[1] + rect1[3] < rect2[1] &&
626 rect2[1] + rect2[3] < rect1[1]
634 // Add tile img objects for this part
635 var previewOffset = rect2d;
639 for (var x in root.tiles) {
640 for (var y in root.tiles[x]) {
641 if (isInside(rect2d, [x, y, 512, 512])) {
642 var tileImgElem = createElement("img", {
643 src: getDataURI(root.tiles[x][y]),
645 position: "absolute",
646 left: x - previewOffset[0] + "px",
647 top: y - previewOffset[1] + "px",
648 pointerEvents: "auto",
651 layerPreview.appendChild(tileImgElem);
655 layerPreview.style.background = "";
656 } else if (root.surfaceURI) {
660 if (root.contentHostProp && root.contentHostProp["buffer-rect"]) {
661 offsetX = root.contentHostProp["buffer-rect"][0];
662 offsetY = root.contentHostProp["buffer-rect"][1];
664 var surfaceImgElem = createElement("img", {
665 src: getDataURI(root.surfaceURI),
667 position: "absolute",
668 left: offsetX - previewOffset[0] + "px",
669 top: offsetY - previewOffset[1] + "px",
670 pointerEvents: "auto",
673 layerPreview.appendChild(surfaceImgElem);
674 layerPreview.style.background = "";
675 } else if (root.color) {
677 layerPreview.style.background =
689 if (hasImg || true) {
690 layerPreview.mouseoverElem = elem;
691 layerPreview.onmouseenter = function () {
692 this.mouseoverElem.onmouseover();
694 layerPreview.onmouseout = function () {
695 this.mouseoverElem.onmouseout();
700 var layerDisplayItems = getDisplayItemForLayer(displayList);
701 for (let i = 0; i < layerDisplayItems.length; i++) {
702 var displayItem = layerDisplayItems[i];
703 var displayElem = createElement("div", {
704 className: "layerObjectDescription",
705 textContent: " " + trim(displayItem.line),
712 if (this.diPreview) {
713 this.diPreview.classList.add("displayHover");
715 var description = "";
716 if (this.displayItem.contentDescriptor) {
717 description += "Content: " + this.displayItem.contentDescriptor;
719 description += "Content: Unknown";
723 this.displayItem.name +
725 this.displayItem.address +
728 "<br>Layer: " + root.name + " (" + root.address + ")";
729 if (this.displayItem.frame) {
730 description += "<br>Frame: " + this.displayItem.frame;
732 if (this.displayItem.layerBounds) {
735 toFixed(this.displayItem.layerBounds[0] / 60, 2) +
737 toFixed(this.displayItem.layerBounds[1] / 60, 2) +
739 toFixed(this.displayItem.layerBounds[2] / 60, 2) +
741 toFixed(this.displayItem.layerBounds[3] / 60, 2) +
744 if (this.displayItem.z) {
745 description += "<br>Z: " + this.displayItem.z;
748 if (this.displayItem.rest) {
749 description += "<br>" + this.displayItem.rest;
752 var box = this.diPreview.getBoundingClientRect();
753 this.diPreview.tooltip = createElement("div", {
754 className: "csstooltip",
755 innerHTML: description,
760 document.documentElement.clientHeight - 150
762 left: box.left + "px",
766 document.body.appendChild(this.diPreview.tooltip);
770 if (this.diPreview) {
771 this.diPreview.classList.remove("displayHover");
772 document.body.removeChild(this.diPreview.tooltip);
777 icon = createElement("img", {
785 displayElem.insertBefore(icon, displayElem.firstChild);
786 pane.appendChild(displayElem);
787 // bounds doesn't adjust for within the layer. It's not a bad fallback but
788 // will have the wrong offset
789 let rect2d = displayItem.layerBounds || displayItem.bounds;
791 // This doesn't place them corectly
792 var appUnitsToPixels = 60 / contentScale;
793 let diPreview = createElement("div", {
794 id: "displayitem_" + displayItem.content + "_" + displayItem.address,
795 className: "layerPreview",
797 position: "absolute",
798 left: rect2d[0] / appUnitsToPixels + "px",
799 top: rect2d[1] / appUnitsToPixels + "px",
800 width: rect2d[2] / appUnitsToPixels + "px",
801 height: rect2d[3] / appUnitsToPixels + "px",
802 border: "solid 1px gray",
803 pointerEvents: "auto",
807 this.displayElem.onmouseover();
810 this.displayElem.onmouseout();
814 layerViewport.appendChild(diPreview);
815 displayElem.diPreview = diPreview;
820 for (var i = 0; i < root.children.length; i++) {
833 // This function takes a stdout snippet and finds the frames
834 function parseMultiLineDump(log) {
835 var container = createElement("div", {
838 position: "relative",
842 var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n";
843 var nextLineStartWithSpace = "([ \\t].*$\n)*";
844 var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")";
846 var startLine = "Painting --- after optimization:\n";
847 var endLine = "Painting --- layer tree:";
848 var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")";
850 var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm");
851 var matches = log.match(regex);
852 console.log(matches);
853 window.matches = matches;
855 var matchList = createElement("span", {
859 position: "relative",
860 border: "solid black 2px",
861 display: "inline-block",
866 container.appendChild(matchList);
867 var contents = createElement("span", {
871 display: "inline-block",
873 textContent: "Click on a frame on the left to view the layer tree",
875 container.appendChild(contents);
877 var lastDisplayList = null;
879 for (let i = 0; i < matches.length; i++) {
880 var currMatch = matches[i];
882 if (currMatch.indexOf(startLine) == 0) {
883 // Display list match
884 var matchLines = matches[i].split("\n");
885 lastDisplayList = parseDisplayList(matchLines);
888 let displayList = lastDisplayList;
889 lastDisplayList = null;
890 var currFrameDiv = createElement("a", {
896 textContent: "LayerTree " + frameID++,
898 contents.innerHTML = "";
899 var matchLines = matches[i].split("\n");
900 var dumpDiv = parseDump(matchLines, displayList);
901 contents.appendChild(dumpDiv);
904 matchList.appendChild(currFrameDiv);
911 function parseDump(log, displayList, compositeTitle, compositeTime) {
912 compositeTitle |= "";
915 var container = createElement("div", {
919 position: "relative",
923 if (compositeTitle == null && compositeTime == null) {
924 var titleDiv = createElement("div", {
925 className: "treeColumnHeader",
931 (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""),
933 container.appendChild(titleDiv);
936 var mainDiv = createElement("div", {
938 position: "absolute",
945 container.appendChild(mainDiv);
947 var layerListPane = createElement("div", {
955 mainDiv.appendChild(layerListPane);
957 var previewDiv = createElement("div", {
959 position: "absolute",
967 mainDiv.appendChild(previewDiv);
969 var root = parseLayers(log);
970 populateLayers(root, displayList, layerListPane, previewDiv);