<!DOCTYPE html> <html> <!-- Copyright 2017 the V8 project authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. --> <head> <meta charset="utf-8"> <style> html, body { font-family: sans-serif; padding: 0px; margin: 0px; } h1, h2, h3, section { padding-left: 15px; } kbd { background-color: #eee; border-radius: 3px; border: 1px solid black; display: inline-block; font-size: .9em; font-weight: bold; padding: 0px 4px 2px 4px; white-space: nowrap; } dl { display: grid; grid-template-columns: min-content auto; grid-gap: 10px; } dt { text-align: right; white-space: nowrap; } dd { margin: 0; } #content { opacity: 0.0; height: 0px; transition: all 0.5s ease-in-out; } .success #content { height: auto; opacity: 1.0; } #fileReader { width: 100%; height: 100px; line-height: 100px; text-align: center; border: solid 1px #000000; border-radius: 5px; cursor: pointer; transition: all 0.5s ease-in-out; } .failure #fileReader { background-color: #FFAAAA; } .success #fileReader { height: 20px; line-height: 20px; } #fileReader:hover { background-color: #e0edfe; } .loading #fileReader { cursor: wait; } #fileReader > input { display: none; } #loader { display: none; } .loading #loader { display: block; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.5); } #spinner { position: absolute; width: 100px; height: 100px; top: 40%; left: 50%; margin-left: -50px; border: 30px solid #000; border-top: 30px solid #36E; border-radius: 50%; animation: spin 1s ease-in-out infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .colorbox { width: 10px; height: 10px; border: 1px black solid; } #stats { display: flex; height: 250px; } #stats table { flex: 1; padding-right: 50px; max-height: 250px; display: inline-block; } #stats table td { cursor: pointer; } #stats .transitionTable { overflow-y: scroll; } #stats .transitionTable tr { max-width: 200px; } #stats .transitionType { text-align: right; max-width: 380px; } #stats .transitionType tr td:nth-child(2) { text-align: left; } #stats table thead td { border-bottom: 1px black dotted; } #timeline { position: relative; height: 300px; overflow-y: hidden; overflow-x: scroll; user-select: none; } #timelineLabel { transform: rotate(90deg); transform-origin: left bottom 0; position: absolute; left: 0; width: 250px; text-align: center; font-size: 10px; opacity: 0.5; } #timelineChunks { height: 250px; position: absolute; margin-right: 100px; } #timelineCanvas { height: 250px; position: relative; overflow: visible; pointer-events: none; } .chunk { width: 6px; border: 0px white solid; border-width: 0 2px 0 2px; position: absolute; background-size: 100% 100%; image-rendering: pixelated; bottom: 0px; } .timestamp { height: 250px; width: 100px; border-left: 1px black dashed; padding-left: 4px; position: absolute; pointer-events: none; font-size: 10px; opacity: 0.5; } #timelineOverview { width: 100%; height: 50px; position: relative; margin-top: -50px; margin-bottom: 10px; background-size: 100% 100%; border: 1px black solid; border-width: 1px 0 1px 0; overflow: hidden; } #timelineOverviewIndicator { height: 100%; position: absolute; box-shadow: 0px 2px 20px -5px black inset; top: 0px; cursor: ew-resize; } #timelineOverviewIndicator .leftMask, #timelineOverviewIndicator .rightMask { background-color: rgba(200, 200, 200, 0.5); width: 10000px; height: 100%; position: absolute; top: 0px; } #timelineOverviewIndicator .leftMask { right: 100%; } #timelineOverviewIndicator .rightMask { left: 100%; } #mapDetails { font-family: monospace; white-space: pre; } #transitionView { overflow-x: scroll; white-space: nowrap; min-height: 50px; max-height: 200px; padding: 50px 0 0 0; margin-top: -25px; width: 100%; } .map { width: 20px; height: 20px; display: inline-block; border-radius: 50%; background-color: black; border: 4px solid white; font-size: 10px; text-align: center; line-height: 18px; color: white; vertical-align: top; margin-top: -13px; /* raise z-index */ position: relative; z-index: 2; cursor: pointer; } .map.selected { border-color: black; } .transitions { display: inline-block; margin-left: -15px; } .transition { min-height: 55px; margin: 0 0 -2px 2px; } /* gray out deprecated transitions */ .deprecated > .transitionEdge, .deprecated > .map { opacity: 0.5; } .deprecated > .transition { border-color: rgba(0, 0, 0, 0.5); } /* Show a border for all but the first transition */ .transition:nth-of-type(2), .transition:nth-last-of-type(n+2) { border-left: 2px solid; margin-left: 0px; } /* special case for 2 transitions */ .transition:nth-last-of-type(1) { border-left: none; } /* topmost transitions are not related */ #transitionView > .transition { border-left: none; } /* topmost transition edge needs initial offset to be aligned */ #transitionView > .transition > .transitionEdge { margin-left: 13px; } .transitionEdge { height: 2px; width: 80px; display: inline-block; margin: 0 0 2px 0; background-color: black; vertical-align: top; padding-left: 15px; } .transitionLabel { color: black; transform: rotate(-15deg); transform-origin: top left; margin-top: -10px; font-size: 10px; white-space: normal; word-break: break-all; background-color: rgba(255,255,255,0.5); } .black{ background-color: black; } .red { background-color: red; } .green { background-color: green; } .yellow { background-color: yellow; color: black; } .blue { background-color: blue; } .orange { background-color: orange; } .violet { background-color: violet; color: black; } .showSubtransitions { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 10px solid black; cursor: zoom-in; margin: 4px 0 0 4px; } .showSubtransitions.opened { border-top: none; border-bottom: 10px solid black; cursor: zoom-out; } #tooltip { position: absolute; width: 10px; height: 10px; background-color: red; pointer-events: none; z-index: 100; display: none; } #searchBarInput { width: 200px; } </style> <script src="./splaytree.js"></script> <script src="./codemap.js"></script> <script src="./csvparser.js"></script> <script src="./consarray.js"></script> <script src="./profile.js"></script> <script src="./profile_view.js"></script> <script src="./logreader.js"></script> <script src="./SourceMap.js"></script> <script src="./arguments.js"></script> <script src="./map-processor.js"></script> <script> "use strict" // ========================================================================= const kChunkHeight = 250; const kChunkWidth = 10; class State { constructor() { this._nofChunks = 400; this._map = undefined; this._timeline = undefined; this._chunks = undefined; this._view = new View(this); this._navigation = new Navigation(this, this.view); } get timeline() { return this._timeline } set timeline(value) { this._timeline = value; this.updateChunks(); this.view.updateTimeline(); this.view.updateStats(); } get chunks() { return this._chunks } get nofChunks() { return this._nofChunks } set nofChunks(count) { this._nofChunks = count; this.updateChunks(); this.view.updateTimeline(); } get view() { return this._view } get navigation() { return this._navigation } get map() { return this._map } set map(value) { this._map = value; this._navigation.updateUrl(); this.view.updateMapDetails(); this.view.redraw(); } updateChunks() { this._chunks = this._timeline.chunks(this._nofChunks); } get entries() { if (!this.map) return {}; return { map: this.map.id, time: this.map.time } } } // ========================================================================= // DOM Helper function $(id) { return document.getElementById(id) } function removeAllChildren(node) { while (node.lastChild) { node.removeChild(node.lastChild); } } function selectOption(select, match) { let options = select.options; for (let i = 0; i < options.length; i++) { if (match(i, options[i])) { select.selectedIndex = i; return; } } } function div(classes) { let node = document.createElement('div'); if (classes !== void 0) { if (typeof classes === "string") { node.classList.add(classes); } else { classes.forEach(cls => node.classList.add(cls)); } } return node; } function table(className) { let node = document.createElement("table") if (className) node.classList.add(className) return node; } function td(textOrNode) { let node = document.createElement("td"); if (typeof textOrNode === "object") { node.appendChild(textOrNode); } else { node.innerText = textOrNode; } return node; } function tr() { return document.createElement("tr"); } define(Array.prototype, "histogram", function(mapFn) { let histogram = []; for (let i = 0; i < this.length; i++) { let value = this[i]; let index = Math.round(mapFn(value)) let bucket = histogram[index]; if (bucket !== undefined) { bucket.push(value); } else { histogram[index] = [value]; } } for (let i = 0; i < histogram.length; i++) { histogram[i] = histogram[i] || []; } return histogram; }); // ========================================================================= // EventHandlers function handleSearchBar(){ let searchBar = $('searchBarInput'); let searchBarInput = searchBar.value; let selectedMap = V8Map.get(searchBarInput); //removeAllChildren($('mapIdList')); if(selectedMap){ let map = selectedMap; document.state.map = map; searchBar.className = "green"; } else { searchBar.className = "red"; } } function handleBodyLoad() { let upload = $('fileReader'); upload.onclick = (e) => $("file").click(); upload.ondragover = (e) => e.preventDefault(); upload.ondrop = (e) => handleLoadFile(e); $('file').onchange = (e) => handleLoadFile(e); upload.onkeydown = (e) => { if (event.key == "Enter") $("file").click(); }; upload.focus(); document.state = new State(); $("transitionView").addEventListener("mousemove", e => { let tooltip = $("tooltip"); tooltip.style.left = e.pageX + "px"; tooltip.style.top = e.pageY + "px"; let map = e.target.map; if (map) { $("tooltipContents").innerText = map.description; } }); function handleLoadFile(event) { // Used for drop and file change. event.preventDefault(); let host = event.dataTransfer ? event.dataTransfer : event.target; let file = host.files[0]; let reader = new FileReader(); document.body.className = 'loading'; reader.onload = function(evt) { try { handleLoadText(this.result); document.body.className = 'success'; } catch(e) { document.body.className = 'failure'; console.error(e); } } // Defer the reading to allow spinner CSS animation. setTimeout(() => reader.readAsText(file), 0); } } function handleLoadText(text) { let mapProcessor = new MapProcessor(); document.state.timeline = mapProcessor.processString(text); } function handleKeyDown(event) { let nav = document.state.navigation; switch(event.key) { case "ArrowUp": event.preventDefault(); if (event.shiftKey) { nav.selectPrevEdge(); } else { nav.moveInChunk(-1); } return false; case "ArrowDown": event.preventDefault(); if (event.shiftKey) { nav.selectNextEdge(); } else { nav.moveInChunk(1); } return false; case "ArrowLeft": nav.moveInChunks(false); break; case "ArrowRight": nav.moveInChunks(true); break; case "+": nav.increaseTimelineResolution(); break; case "-": nav.decreaseTimelineResolution(); break; } }; document.onkeydown = handleKeyDown; function handleTimelineIndicatorMove(event) { if (event.buttons == 0) return; let timelineTotalWidth = $("timelineCanvas").offsetWidth; let factor = $("timelineOverview").offsetWidth / timelineTotalWidth; $("timeline").scrollLeft += event.movementX / factor; } // ========================================================================= Object.defineProperty(Edge.prototype, 'getColor', { value:function() { return transitionTypeToColor(this.type); }}); class Navigation { constructor(state, view) { this.state = state; this.view = view; } get map() { return this.state.map } set map(value) { this.state.map = value } get chunks() { return this.state.chunks } increaseTimelineResolution() { this.state.nofChunks *= 1.5; } decreaseTimelineResolution() { this.state.nofChunks /= 1.5; } selectNextEdge() { if (!this.map) return; if (this.map.children.length != 1) return; this.map = this.map.children[0].to; } selectPrevEdge() { if (!this.map) return; if (!this.map.parent()) return; this.map = this.map.parent(); } selectDefaultMap() { this.map = this.chunks[0].at(0); } moveInChunks(next) { if (!this.map) return this.selectDefaultMap(); let chunkIndex = this.map.chunkIndex(this.chunks); let chunk = this.chunks[chunkIndex]; let index = chunk.indexOf(this.map); if (next) { chunk = chunk.next(this.chunks); } else { chunk = chunk.prev(this.chunks); } if (!chunk) return; index = Math.min(index, chunk.size()-1); this.map = chunk.at(index); } moveInChunk(delta) { if (!this.map) return this.selectDefaultMap(); let chunkIndex = this.map.chunkIndex(this.chunks) let chunk = this.chunks[chunkIndex]; let index = chunk.indexOf(this.map) + delta; let map; if (index < 0) { map = chunk.prev(this.chunks).last(); } else if (index >= chunk.size()) { map = chunk.next(this.chunks).first() } else { map = chunk.at(index); } this.map = map; } updateUrl() { let entries = this.state.entries; let params = new URLSearchParams(entries); window.history.pushState(entries, "", "?" + params.toString()); } } class View { constructor(state) { this.state = state; setInterval(this.updateOverviewWindow, 50); this.backgroundCanvas = document.createElement("canvas"); this.transitionView = new TransitionView(state, $("transitionView")); this.statsView = new StatsView(state, $("stats")); this.isLocked = false; } get chunks() { return this.state.chunks } get timeline() { return this.state.timeline } get map() { return this.state.map } updateStats() { this.statsView.update(); } updateMapDetails() { let details = ""; if (this.map) { details += "ID: " + this.map.id; details += "\nSource location: " + this.map.filePosition; details += "\n" + this.map.description; } $("mapDetails").innerText = details; this.transitionView.showMap(this.map); } updateTimeline() { let chunksNode = $("timelineChunks"); removeAllChildren(chunksNode); let chunks = this.chunks; let max = chunks.max(each => each.size()); let start = this.timeline.startTime; let end = this.timeline.endTime; let duration = end - start; const timeToPixel = chunks.length * kChunkWidth / duration; let addTimestamp = (time, name) => { let timeNode = div("timestamp"); timeNode.innerText = name; timeNode.style.left = ((time-start) * timeToPixel) + "px"; chunksNode.appendChild(timeNode); }; let backgroundTodo = []; for (let i = 0; i < chunks.length; i++) { let chunk = chunks[i]; let height = (chunk.size() / max * kChunkHeight); chunk.height = height; if (chunk.isEmpty()) continue; let node = div(); node.className = "chunk"; node.style.left = (i * kChunkWidth) + "px"; node.style.height = height + "px"; node.chunk = chunk; node.addEventListener("mousemove", e => this.handleChunkMouseMove(e)); node.addEventListener("click", e => this.handleChunkClick(e)); node.addEventListener("dblclick", e => this.handleChunkDoubleClick(e)); backgroundTodo.push([chunk, node]) chunksNode.appendChild(node); chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name)); } this.asyncSetTimelineChunkBackground(backgroundTodo) // Put a time marker roughly every 20 chunks. let expected = duration / chunks.length * 20; let interval = (10 ** Math.floor(Math.log10(expected))); let correction = Math.log10(expected / interval); correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5; interval *= correction; let time = start; while (time < end) { addTimestamp(time, ((time-start) / 1000) + " ms"); time += interval; } this.drawOverview(); this.redraw(); } handleChunkMouseMove(event) { if (this.isLocked) return false; let chunk = event.target.chunk; if (!chunk) return; // topmost map (at chunk.height) == map #0. let relativeIndex = Math.round(event.layerY / event.target.offsetHeight * chunk.size()); let map = chunk.at(relativeIndex); this.state.map = map; } handleChunkClick(event) { this.isLocked = !this.isLocked; } handleChunkDoubleClick(event) { this.isLocked = true; let chunk = event.target.chunk; if (!chunk) return; this.transitionView.showMaps(chunk.getUniqueTransitions()); } asyncSetTimelineChunkBackground(backgroundTodo) { const kIncrement = 100; let start = 0; let delay = 1; while (start < backgroundTodo.length) { let end = Math.min(start+kIncrement, backgroundTodo.length); setTimeout((from, to) => { for (let i = from; i < to; i++) { let [chunk, node] = backgroundTodo[i]; this.setTimelineChunkBackground(chunk, node); } }, delay++, start, end); start = end; } } setTimelineChunkBackground(chunk, node) { // Render the types of transitions as bar charts const kHeight = chunk.height; const kWidth = 1; this.backgroundCanvas.width = kWidth; this.backgroundCanvas.height = kHeight; let ctx = this.backgroundCanvas.getContext("2d"); ctx.clearRect(0, 0, kWidth, kHeight); let y = 0; let total = chunk.size(); let type, count; if (true) { chunk.getTransitionBreakdown().forEach(([type, count]) => { ctx.fillStyle = transitionTypeToColor(type); let height = count / total * kHeight; ctx.fillRect(0, y, kWidth, y + height); y += height; }); } else { chunk.items.forEach(map => { ctx.fillStyle = transitionTypeToColor(map.getType()); let y = chunk.yOffset(map); ctx.fillRect(0, y, kWidth, y + 1); }); } let imageData = this.backgroundCanvas.toDataURL("image/webp", 0.2); node.style.backgroundImage = "url(" + imageData + ")"; } updateOverviewWindow() { let indicator = $("timelineOverviewIndicator"); let totalIndicatorWidth = $("timelineOverview").offsetWidth; let div = $("timeline"); let timelineTotalWidth = $("timelineCanvas").offsetWidth; let factor = $("timelineOverview").offsetWidth / timelineTotalWidth; let width = div.offsetWidth * factor; let left = div.scrollLeft * factor; indicator.style.width = width + "px"; indicator.style.left = left + "px"; } drawOverview() { const height = 50; const kFactor = 2; let canvas = this.backgroundCanvas; canvas.height = height; canvas.width = window.innerWidth; let ctx = canvas.getContext("2d"); let chunks = this.state.timeline.chunkSizes(canvas.width * kFactor); let max = chunks.max(); ctx.clearRect(0, 0, canvas.width, height); ctx.strokeStyle = "black"; ctx.fillStyle = "black"; ctx.beginPath(); ctx.moveTo(0,height); for (let i = 0; i < chunks.length; i++) { ctx.lineTo(i/kFactor, height - chunks[i]/max * height); } ctx.lineTo(chunks.length, height); ctx.stroke(); ctx.closePath(); ctx.fill(); let imageData = canvas.toDataURL("image/webp", 0.2); $("timelineOverview").style.backgroundImage = "url(" + imageData + ")"; } redraw() { let canvas= $("timelineCanvas"); canvas.width = (this.chunks.length+1) * kChunkWidth; canvas.height = kChunkHeight; let ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, kChunkHeight); if (!this.state.map) return; this.drawEdges(ctx); } setMapStyle(map, ctx) { ctx.fillStyle = map.edge && map.edge.from ? "black" : "green"; } setEdgeStyle(edge, ctx) { let color = edge.getColor(); ctx.strokeStyle = color; ctx.fillStyle = color; } markMap(ctx, map) { let [x, y] = map.position(this.state.chunks); ctx.beginPath(); this.setMapStyle(map, ctx); ctx.arc(x, y, 3, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); ctx.fillStyle = "white"; ctx.arc(x, y, 2, 0, 2 * Math.PI); ctx.fill(); } markSelectedMap(ctx, map) { let [x, y] = map.position(this.state.chunks); ctx.beginPath(); this.setMapStyle(map, ctx); ctx.arc(x, y, 6, 0, 2 * Math.PI); ctx.stroke(); } drawEdges(ctx) { // Draw the trace of maps in reverse order to make sure the outgoing // transitions of previous maps aren't drawn over. const kMaxOutgoingEdges = 100; let nofEdges = 0; let stack = []; let current = this.state.map; while (current && nofEdges < kMaxOutgoingEdges) { nofEdges += current.children.length; stack.push(current); current = current.parent(); } ctx.save(); this.drawOutgoingEdges(ctx, this.state.map, 3); ctx.restore(); let labelOffset = 15; let xPrev = 0; while (current = stack.pop()) { if (current.edge) { this.setEdgeStyle(current.edge, ctx); let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset); if (xTo == xPrev) { labelOffset += 8; } else { labelOffset = 15 } xPrev = xTo; } this.markMap(ctx, current); current = current.parent(); ctx.save(); // this.drawOutgoingEdges(ctx, current, 1); ctx.restore(); } // Mark selected map this.markSelectedMap(ctx, this.state.map); } drawEdge(ctx, edge, showLabel=true, labelOffset=20) { if (!edge.from || !edge.to) return [-1, -1]; let [xFrom, yFrom] = edge.from.position(this.chunks); let [xTo, yTo] = edge.to.position(this.chunks); let sameChunk = xTo == xFrom; if (sameChunk) labelOffset += 8; ctx.beginPath(); ctx.moveTo(xFrom, yFrom); let offsetX = 20; let offsetY = 20; let midX = xFrom + (xTo- xFrom) / 2; let midY = (yFrom + yTo) / 2 - 100; if (!sameChunk) { ctx.quadraticCurveTo(midX, midY, xTo, yTo); } else { ctx.lineTo(xTo, yTo); } if (!showLabel) { ctx.stroke(); } else { let centerX, centerY; if (!sameChunk) { centerX = (xFrom/2 + midX + xTo/2)/2; centerY = (yFrom/2 + midY + yTo/2)/2; } else { centerX = xTo; centerY = yTo; } ctx.moveTo(centerX, centerY); ctx.lineTo(centerX + offsetX, centerY - labelOffset); ctx.stroke(); ctx.textAlign = "left"; ctx.fillText(edge.toString(), centerX + offsetX + 2, centerY - labelOffset) } return [xTo, yTo]; } drawOutgoingEdges(ctx, map, max=10, depth=0) { if (!map) return; if (depth >= max) return; ctx.globalAlpha = 0.5 - depth * (0.3/max); ctx.strokeStyle = "#666"; const limit = Math.min(map.children.length, 100) for (let i = 0; i < limit; i++) { let edge = map.children[i]; this.drawEdge(ctx, edge, true); this.drawOutgoingEdges(ctx, edge.to, max, depth+1); } } } class TransitionView { constructor(state, node) { this.state = state; this.container = node; this.currentNode = node; this.currentMap = undefined; } selectMap(map) { this.currentMap = map; this.state.map = map; } showMap(map) { if (this.currentMap === map) return; this.currentMap = map; this._showMaps([map]); } showMaps(list, name) { this.state.view.isLocked = true; this._showMaps(list); } _showMaps(list, name) { // Hide the container to avoid any layouts. this.container.style.display = "none"; removeAllChildren(this.container); list.forEach(map => this.addMapAndParentTransitions(map)); this.container.style.display = "" } addMapAndParentTransitions(map) { if (map === void 0) return; this.currentNode = this.container; let parents = map.getParents(); if (parents.length > 0) { this.addTransitionTo(parents.pop()); parents.reverse().forEach(each => this.addTransitionTo(each)); } let mapNode = this.addSubtransitions(map); // Mark and show the selected map. mapNode.classList.add("selected"); if (this.selectedMap == map) { setTimeout(() => mapNode.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }), 1); } } addMapNode(map) { let node = div("map"); if (map.edge) node.classList.add(map.edge.getColor()); node.map = map; node.addEventListener("click", () => this.selectMap(map)); if (map.children.length > 1) { node.innerText = map.children.length; let showSubtree = div("showSubtransitions"); showSubtree.addEventListener("click", (e) => this.toggleSubtree(e, node)); node.appendChild(showSubtree); } else if (map.children.length == 0) { node.innerHTML = "●" } this.currentNode.appendChild(node); return node; } addSubtransitions(map) { let mapNode = this.addTransitionTo(map); // Draw outgoing linear transition line. let current = map; while (current.children.length == 1) { current = current.children[0].to; this.addTransitionTo(current); } return mapNode; } addTransitionEdge(map) { let classes = ["transitionEdge", map.edge.getColor()]; let edge = div(classes); let labelNode = div("transitionLabel"); labelNode.innerText = map.edge.toString(); edge.appendChild(labelNode); return edge; } addTransitionTo(map) { // transition[ transitions[ transition[...], transition[...], ...]]; let transition = div("transition"); if (map.isDeprecated()) transition.classList.add("deprecated"); if (map.edge) { transition.appendChild(this.addTransitionEdge(map)); } let mapNode = this.addMapNode(map); transition.appendChild(mapNode); let subtree = div("transitions"); transition.appendChild(subtree); this.currentNode.appendChild(transition); this.currentNode = subtree; return mapNode; } toggleSubtree(event, node) { let map = node.map; event.target.classList.toggle("opened"); let transitionsNode = node.parentElement.querySelector(".transitions"); let subtransitionNodes = transitionsNode.children; if (subtransitionNodes.length <= 1) { // Add subtransitions excepth the one that's already shown. let visibleTransitionMap = subtransitionNodes.length == 1 ? transitionsNode.querySelector(".map").map : void 0; map.children.forEach(edge => { if (edge.to != visibleTransitionMap) { this.currentNode = transitionsNode; this.addSubtransitions(edge.to); } }); } else { // remove all but the first (currently selected) subtransition for (let i = subtransitionNodes.length-1; i > 0; i--) { transitionsNode.removeChild(subtransitionNodes[i]); } } } } class StatsView { constructor(state, node) { this.state = state; this.node = node; } get timeline() { return this.state.timeline } get transitionView() { return this.state.view.transitionView; } update() { removeAllChildren(this.node); this.updateGeneralStats(); this.updateNamedTransitionsStats(); } updateGeneralStats() { let pairs = [ ["Total", null, e => true], ["Transitions", 'black', e => e.edge && e.edge.isTransition()], ["Fast to Slow", 'violet', e => e.edge && e.edge.isFastToSlow()], ["Slow to Fast", 'orange', e => e.edge && e.edge.isSlowToFast()], ["Initial Map", 'yellow', e => e.edge && e.edge.isInitial()], ["Replace Descriptors", 'red', e => e.edge && e.edge.isReplaceDescriptors()], ["Copy as Prototype", 'red', e => e.edge && e.edge.isCopyAsPrototype()], ["Optimize as Prototype", null, e => e.edge && e.edge.isOptimizeAsPrototype()], ["Deprecated", null, e => e.isDeprecated()], ["Bootstrapped", 'green', e => e.isBootstrapped()], ]; let text = ""; let tableNode = table("transitionType"); tableNode.innerHTML = "<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>"; let name, filter; let total = this.timeline.size(); pairs.forEach(([name, color, filter]) => { let row = tr(); if (color !== null) { row.appendChild(td(div(['colorbox', color]))); } else { row.appendChild(td("")); } row.onclick = (e) => { // lazily compute the stats let node = e.target.parentNode; if (node.maps == undefined) { node.maps = this.timeline.filterUniqueTransitions(filter); } this.transitionView.showMaps(node.maps); } row.appendChild(td(name)); let count = this.timeline.count(filter); row.appendChild(td(count)); let percent = Math.round(count / total * 1000) / 10; row.appendChild(td(percent.toFixed(1) + "%")); tableNode.appendChild(row); }); this.node.appendChild(tableNode); }; updateNamedTransitionsStats() { let tableNode = table("transitionTable"); let nameMapPairs = Array.from(this.timeline.transitions.entries()); tableNode.innerHTML = "<thead><tr><td>Propery Name</td><td>#</td></tr></thead>"; nameMapPairs .sort((a,b) => b[1].length - a[1].length) .forEach(([name, maps]) => { let row = tr(); row.maps = maps; row.addEventListener("click", e => this.transitionView.showMaps( e.target.parentNode.maps.map(map => map.to))); row.appendChild(td(name)); row.appendChild(td(maps.length)); tableNode.appendChild(row); }); this.node.appendChild(tableNode); } } // ========================================================================= function transitionTypeToColor(type) { switch(type) { case "new": return "green"; case "Normalize": return "violet"; case "SlowToFast": return "orange"; case "InitialMap": return "yellow"; case "Transition": return "black"; case "ReplaceDescriptors": return "red"; } return "black"; } // ShadowDom elements ========================================================= </script> </head> <body onload="handleBodyLoad(event)" onkeypress="handleKeyDown(event)"> <h1>V8 Map Explorer</h1> <section> <div id="fileReader" tabindex=1 > <span id="label"> Drag and drop a v8.log file into this area, or click to choose from disk. </span> <input id="file" type="file" name="files"> </div> <div id="loader"> <div id="spinner"></div> </div> </section> <div id="content"> <h2>Stats</h2> <section id="stats"></section> <h2>Timeline</h2> <div id="timeline"> <div id="timelineLabel">Frequency</div> <div id="timelineChunks"></div> <canvas id="timelineCanvas"></canvas> </div> <div id="timelineOverview" onmousemove="handleTimelineIndicatorMove(event)" > <div id="timelineOverviewIndicator"> <div class="leftMask"></div> <div class="rightMask"></div> </div> </div> <h2>Transitions</h2> <section id="transitionView"></section> <br/> <h2>Search Map by Address</h2> <section id="searchBar"></section> <input type="search" id="searchBarInput" placeholder="Search maps by address.."> <button onclick="handleSearchBar()">Search</button> <ul id="mapIdList" title="Map Id List"> </ul> <h2>Selected Map</h2> <section id="mapDetails"></section> </div> <section> <h2>Instructions</h2> <p>Visualize Map trees that have been gathered using <code>path/to/d8 $FILE --trace-maps</code>.</p> <p>You can inspect the transition tree in DevTools by looking at <code>document.state.timeline.values</code>. <h3>Keyboard Shortcuts</h3> <dl> <dt><kbd>SHIFT</kbd> + <kbd>Arrow Up</kbd></dt> <dd>Follow Map transition forward (first child)</dd> <dt><kbd>SHIFT</kbd> + <kbd>Arrow Down</kbd></dt> <dd>Follow Map transition backwards</dd> <dt><kbd>Arrow Up</kbd></dt> <dd>Go to previous Map chunk</dd> <dt><kbd>Arrow Down</kbd></dt> <dd>Go to next Map in chunk</dd> <dt><kbd>Arrow Left</kbd></dt> <dd>Go to previous chunk</dd> <dt><kbd>Arrow Right</kbd></dt> <dd>Go to next chunk</dd> <dt><kbd>+</kbd></dt> <dd>Timeline zoom in</dd> <dt><kbd>-</kbd></dt> <dd>Timeline zoom out</dd> </dl> </section> <div id="tooltip"> <div id="tooltipContents"></div> </div> </body> </html>