<!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; } #stats table { display: inline-block; padding-right: 50px; } #stats .transitionTable { max-height: 200px; overflow-y: scroll; } #timeline { position: relative; height: 300px; overflow-y: hidden; overflow-x: scroll; user-select: none; } #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); } .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; } </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(text) { let node = document.createElement("td"); node.innerText = text; return node; } function tr() { let node = document.createElement("tr"); return node; } function define(prototype, name, fn) { Object.defineProperty(prototype, name, {value:fn, enumerable:false}); } define(Array.prototype, "max", function(fn) { if (this.length == 0) return undefined; if (fn == undefined) fn = (each) => each; let max = fn(this[0]); for (let i = 1; i < this.length; i++) { max = Math.max(max, fn(this[i])); } return max; }) 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; }); define(Array.prototype, "first", function() { return this[0] }); define(Array.prototype, "last", function() { return this[this.length - 1] }); // ========================================================================= // EventHandlers function handleBodyLoad() { let upload = $('uploadInput'); upload.onclick = (e) => { e.target.value = null }; upload.onchange = (e) => { handleLoadFile(e.target) }; 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.join("\n"); } }); } function handleLoadFile(upload) { let files = upload.files; let file = files[0]; let reader = new FileReader(); reader.onload = function(evt) { handleLoadText(this.result); } reader.readAsText(file); } 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 += "\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); }; 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)); this.setTimelineChunkBackground(chunk, node); chunksNode.appendChild(node); chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name)); } // 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.drawHistograms(); 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()); } 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/png"); 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/png"); $("timelineOverview").style.backgroundImage = "url(" + imageData + ")"; } drawHistograms() { $("mapsDepthHistogram").histogram = this.timeline.depthHistogram(); $("mapsFanOutHistogram").histogram = this.timeline.fanOutHistogram(); } drawMapsDepthHistogram() { let canvas = $("mapsDepthCanvas"); let histogram = this.timeline.depthHistogram(); this.drawHistogram(canvas, histogram, true); } drawMapsFanOutHistogram() { let canvas = $("mapsFanOutCanvas"); let histogram = this.timeline.fanOutHistogram(); this.drawHistogram(canvas, histogram, true, true); } drawHistogram(canvas, histogram, logScaleX=false, logScaleY=false) { let ctx = canvas.getContext("2d"); let yMax = histogram.max(each => each.length); if (logScaleY) yMax = Math.log(yMax); let xMax = histogram.length; if (logScaleX) xMax = Math.log(xMax); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.moveTo(0,canvas.height); for (let i = 0; i < histogram.length; i++) { let x = i; if (logScaleX) x = Math.log(x); x = x / xMax * canvas.width; let bucketLength = histogram[i].length; if (logScaleY) bucketLength = Math.log(bucketLength); let y = (1 - bucketLength / yMax) * canvas.height; ctx.lineTo(x, y); } ctx.lineTo(canvas.width, canvas.height); ctx.closePath; ctx.stroke(); ctx.fill(); } 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 = [ ["Maps", e => true], ["Transitions", e => e.edge && e.edge.isTransition()], ["Fast to Slow", e => e.edge && e.edge.isFastToSlow()], ["Slow to Fast", e => e.edge && e.edge.isSlowToFast()], ["Initial Map", e => e.edge && e.edge.isInitial()], ["Replace Descriptors", e => e.edge && e.edge.isReplaceDescriptors()], ["Copy as Prototype", e => e.edge && e.edge.isCopyAsPrototype()], ["Optimize as Prototype", e => e.edge && e.edge.isOptimizeAsPrototype()], ["Deprecated", e => e.isDeprecated()], ]; let text = ""; let tableNode = table(); let name, filter; let total = this.timeline.size(); pairs.forEach(([name, filter]) => { let row = tr(); row.maps = this.timeline.filterUniqueTransitions(filter); row.addEventListener("click", e => this.transitionView.showMaps(e.target.parentNode.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 + "%")); tableNode.appendChild(row); }); this.node.appendChild(tableNode); }; updateNamedTransitionsStats() { let tableNode = table("transitionTable"); let nameMapPairs = Array.from(this.timeline.transitions.entries()); 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 "map=SlowToFast": return "orange"; case "InitialMap": return "yellow"; case "Transition": return "black"; case "ReplaceDescriptors": return "red"; } return "black"; } // ShadowDom elements ========================================================= customElements.define('x-histogram', class extends HTMLElement { constructor() { super(); let shadowRoot = this.attachShadow({mode: 'open'}); const t = document.querySelector('#x-histogram-template'); const instance = t.content.cloneNode(true); shadowRoot.appendChild(instance); this._histogram = undefined; this.mouseX = 0; this.mouseY = 0; this.canvas.addEventListener('mousemove', event => this.handleCanvasMove(event)); } setBoolAttribute(name, value) { if (value) { this.setAttribute(name, ""); } else { this.deleteAttribute(name); } } static get observedAttributes() { return ['title', 'xlog', 'ylog', 'xlabel', 'ylabel']; } $(query) { return this.shadowRoot.querySelector(query) } get h1() { return this.$("h2") } get canvas() { return this.$("canvas") } get xLabelDiv() { return this.$("#xLabel") } get yLabelDiv() { return this.$("#yLabel") } get histogram() { return this._histogram; } set histogram(array) { this._histogram = array; if (this._histogram) { this.yMax = this._histogram.max(each => each.length); this.xMax = this._histogram.length; } this.draw(); } get title() { return this.getAttribute("title") } set title(string) { this.setAttribute("title", string) } get xLabel() { return this.getAttribute("xlabel") } set xLabel(string) { this.setAttribute("xlabel", string)} get yLabel() { return this.getAttribute("ylabel") } set yLabel(string) { this.setAttribute("ylabel", string)} get xLog() { return this.hasAttribute("xlog") } set xLog(value) { this.setBoolAttribute("xlog", value) } get yLog() { return this.hasAttribute("ylog") } set yLog(value) { this.setBoolAttribute("ylog", value) } attributeChangedCallback(name, oldValue, newValue) { if (name == "title") { this.h1.innerText = newValue; return; } if (name == "ylabel") { this.yLabelDiv.innerText = newValue; return; } if (name == "xlabel") { this.xLabelDiv.innerText = newValue; return; } this.draw(); } handleCanvasMove(event) { this.mouseX = event.offsetX; this.mouseY = event.offsetY; this.draw(); } xPosition(i) { let x = i; if (this.xLog) x = Math.log(x); return x / this.xMax * this.canvas.width; } yPosition(i) { let bucketLength = this.histogram[i].length; if (this.yLog) { return (1 - Math.log(bucketLength) / Math.log(this.yMax)) * this.drawHeight + 10; } else { return (1 - bucketLength / this.yMax) * this.drawHeight + 10; } } get drawHeight() { return this.canvas.height - 10 } draw() { if (!this.histogram) return; let width = this.canvas.width; let height = this.drawHeight; let ctx = this.canvas.getContext("2d"); if (this.xLog) yMax = Math.log(yMax); let xMax = this.histogram.length; if (this.yLog) xMax = Math.log(xMax); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.beginPath(); ctx.moveTo(0, height); for (let i = 0; i < this.histogram.length; i++) { ctx.lineTo(this.xPosition(i), this.yPosition(i)); } ctx.lineTo(width, height); ctx.closePath; ctx.stroke(); ctx.fill(); if (!this.mouseX) return; ctx.beginPath(); let index = Math.round(this.mouseX); let yBucket = this.histogram[index]; let y = this.yPosition(index); if (this.yLog) y = Math.log(y); ctx.moveTo(0, y); ctx.lineTo(width-40, y); ctx.moveTo(this.mouseX, 0); ctx.lineTo(this.mouseX, height); ctx.stroke(); ctx.textAlign = "left"; ctx.fillText(yBucket.length, width-30, y); } }); </script> </head> <template id="x-histogram-template"> <style> #yLabel { transform: rotate(90deg); } canvas, #yLabel, #info { float: left; } #xLabel { clear: both } </style> <h2></h2> <div id="yLabel"></div> <canvas height=50></canvas> <div id="info"> </div> <div id="xLabel"></div> </template> <body onload="handleBodyLoad(event)" onkeypress="handleKeyDown(event)"> <h2>Data</h2> <section> <form name="fileForm"> <p> <input id="uploadInput" type="file" name="files"> </p> </form> </section> <h2>Stats</h2> <section id="stats"></section> <h2>Timeline</h2> <div id="timeline"> <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>Selected Map</h2> <section id="mapDetails"></section> <x-histogram id="mapsDepthHistogram" title="Maps Depth" xlabel="depth" ylabel="nof"></x-histogram> <x-histogram id="mapsFanOutHistogram" xlabel="fan-out" title="Maps Fan-out" ylabel="nof"></x-histogram> <div id="tooltip"> <div id="tooltipContents"></div> </div> </body> </html>