| <!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> |