898 lines
30 KiB
Go
898 lines
30 KiB
Go
package porcupine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
)
|
|
|
|
type historyElement struct {
|
|
ClientId int
|
|
Start int64
|
|
End int64
|
|
Description string
|
|
}
|
|
|
|
type linearizationStep struct {
|
|
Index int
|
|
StateDescription string
|
|
}
|
|
|
|
type partialLinearization = []linearizationStep
|
|
|
|
type partitionVisualizationData struct {
|
|
History []historyElement
|
|
PartialLinearizations []partialLinearization
|
|
Largest map[int]int
|
|
}
|
|
|
|
type visualizationData = []partitionVisualizationData
|
|
|
|
func computeVisualizationData(model Model, info linearizationInfo) visualizationData {
|
|
model = fillDefault(model)
|
|
data := make(visualizationData, len(info.history))
|
|
for partition := 0; partition < len(info.history); partition++ {
|
|
// history
|
|
n := len(info.history[partition]) / 2
|
|
history := make([]historyElement, n)
|
|
callValue := make(map[int]interface{})
|
|
returnValue := make(map[int]interface{})
|
|
for _, elem := range info.history[partition] {
|
|
switch elem.kind {
|
|
case callEntry:
|
|
history[elem.id].ClientId = elem.clientId
|
|
history[elem.id].Start = elem.time
|
|
callValue[elem.id] = elem.value
|
|
case returnEntry:
|
|
history[elem.id].End = elem.time
|
|
history[elem.id].Description = model.DescribeOperation(callValue[elem.id], elem.value)
|
|
returnValue[elem.id] = elem.value
|
|
}
|
|
}
|
|
// partial linearizations
|
|
largestIndex := make(map[int]int)
|
|
largestSize := make(map[int]int)
|
|
linearizations := make([]partialLinearization, len(info.partialLinearizations[partition]))
|
|
partials := info.partialLinearizations[partition]
|
|
sort.Slice(partials, func(i, j int) bool {
|
|
return len(partials[i]) > len(partials[j])
|
|
})
|
|
for i, partial := range partials {
|
|
linearization := make(partialLinearization, len(partial))
|
|
state := model.Init()
|
|
for j, histId := range partial {
|
|
var ok bool
|
|
ok, state = model.Step(state, callValue[histId], returnValue[histId])
|
|
if ok != true {
|
|
panic("valid partial linearization returned non-ok result from model step")
|
|
}
|
|
stateDesc := model.DescribeState(state)
|
|
linearization[j] = linearizationStep{histId, stateDesc}
|
|
if largestSize[histId] < len(partial) {
|
|
largestSize[histId] = len(partial)
|
|
largestIndex[histId] = i
|
|
}
|
|
}
|
|
linearizations[i] = linearization
|
|
}
|
|
data[partition] = partitionVisualizationData{
|
|
History: history,
|
|
PartialLinearizations: linearizations,
|
|
Largest: largestIndex,
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
func Visualize(model Model, info linearizationInfo, output io.Writer) error {
|
|
data := computeVisualizationData(model, info)
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintf(output, html, jsonData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func VisualizePath(model Model, info linearizationInfo, path string) error {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
return Visualize(model, info, f)
|
|
}
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Porcupine</title>
|
|
<style>
|
|
html {
|
|
font-family: Helvetica, Arial, sans-serif;
|
|
font-size: 16px;
|
|
}
|
|
|
|
text {
|
|
dominant-baseline: middle;
|
|
}
|
|
|
|
#legend {
|
|
position: fixed;
|
|
left: 10px;
|
|
top: 10px;
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
backdrop-filter: blur(3px);
|
|
padding: 5px 2px 1px 2px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
#canvas {
|
|
margin-top: 45px;
|
|
}
|
|
|
|
#calc {
|
|
width: 0;
|
|
height: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.bg {
|
|
fill: transparent;
|
|
}
|
|
|
|
.divider {
|
|
stroke: #ccc;
|
|
stroke-width: 1;
|
|
}
|
|
|
|
.history-rect {
|
|
stroke: #888;
|
|
stroke-width: 1;
|
|
fill: #42d1f5;
|
|
}
|
|
|
|
.link {
|
|
fill: #206475;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.selected {
|
|
stroke-width: 5;
|
|
}
|
|
|
|
.target-rect {
|
|
opacity: 0;
|
|
}
|
|
|
|
.history-text {
|
|
font-size: 0.9rem;
|
|
font-family: Menlo, Courier New, monospace;
|
|
}
|
|
|
|
.hidden {
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.hidden line {
|
|
opacity: 0.5; /* note: this is multiplicative */
|
|
}
|
|
|
|
.linearization {
|
|
stroke: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.linearization-invalid {
|
|
stroke: rgba(255, 0, 0, 0.5);
|
|
}
|
|
|
|
.linearization-point {
|
|
stroke-width: 5;
|
|
}
|
|
|
|
.linearization-line {
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
opacity: 0;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
border-radius: 4px;
|
|
padding: 5px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.inactive {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="legend">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="660" height="20">
|
|
<text x="0" y="10">Clients</text>
|
|
<line x1="50" y1="0" x2="70" y2="20" stroke="#000" stroke-width="1"></line>
|
|
<text x="70" y="10">Time</text>
|
|
<line x1="110" y1="10" x2="200" y2="10" stroke="#000" stroke-width="2"></line>
|
|
<polygon points="200,5 200,15, 210,10" fill="#000"></polygon>
|
|
<rect x="300" y="5" width="10" height="10" fill="rgba(0, 0, 0, 0.5)"></rect>
|
|
<text x="315" y="10">Valid LP</text>
|
|
<rect x="400" y="5" width="10" height="10" fill="rgba(255, 0, 0, 0.5)"></rect>
|
|
<text x="415" y="10">Invalid LP</text>
|
|
<text x="520" y="10" id="jump-link" class="link">[ jump to first error ]</text>
|
|
</svg>
|
|
</div>
|
|
<div id="canvas">
|
|
</div>
|
|
<div id="calc">
|
|
</div>
|
|
<script>
|
|
'use strict'
|
|
|
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
|
|
function svgnew(tag, attrs) {
|
|
const el = document.createElementNS(SVG_NS, tag)
|
|
svgattr(el, attrs)
|
|
return el
|
|
}
|
|
|
|
function svgattr(el, attrs) {
|
|
if (attrs != null) {
|
|
for (var k in attrs) {
|
|
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
|
|
el.setAttributeNS(null, k, attrs[k])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function svgattach(parent, child) {
|
|
return parent.appendChild(child)
|
|
}
|
|
|
|
function svgadd(el, tag, attrs) {
|
|
return svgattach(el, svgnew(tag, attrs))
|
|
}
|
|
|
|
function newArray(n, fn) {
|
|
const arr = new Array(n)
|
|
for (let i = 0; i < n; i++) {
|
|
arr[i] = fn(i)
|
|
}
|
|
return arr
|
|
}
|
|
|
|
function arrayEq(a, b) {
|
|
if (a === b) {
|
|
return true
|
|
}
|
|
if (a == null || b == null) {
|
|
return false
|
|
}
|
|
if (a.length != b.length) {
|
|
return false
|
|
}
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
function render(data) {
|
|
const PADDING = 10
|
|
const BOX_HEIGHT = 30
|
|
const BOX_SPACE = 15
|
|
const XOFF = 20
|
|
const EPSILON = 20
|
|
const LINE_BLEED = 5
|
|
const BOX_GAP = 20
|
|
const BOX_TEXT_PADDING = 10
|
|
const HISTORY_RECT_RADIUS = 4
|
|
|
|
let maxClient = -1
|
|
data.forEach(partition => {
|
|
partition['History'].forEach(el => {
|
|
maxClient = Math.max(maxClient, el['ClientId'])
|
|
})
|
|
})
|
|
const nClient = maxClient + 1
|
|
|
|
// Prepare some useful data to be used later:
|
|
// - Add a GID to each event
|
|
// - Create a mapping from GIDs back to events
|
|
// - Create a set of all timestamps
|
|
// - Create a set of all start timestamps
|
|
const allTimestamps = new Set()
|
|
const startTimestamps = new Set()
|
|
let gid = 0
|
|
const byGid = {}
|
|
data.forEach(partition => {
|
|
partition['History'].forEach(el => {
|
|
allTimestamps.add(el['Start'])
|
|
startTimestamps.add(el['Start'])
|
|
allTimestamps.add(el['End'])
|
|
// give elements GIDs
|
|
el['Gid'] = gid
|
|
byGid[gid] = el
|
|
gid++
|
|
})
|
|
})
|
|
let sortedTimestamps = Array.from(allTimestamps).sort((a, b) => a - b)
|
|
|
|
// This should not happen with "real" histories, but for certain edge
|
|
// cases, we need to deal with having multiple events share a start/end
|
|
// time. We solve this by tweaking the events that share the end time,
|
|
// updating the time to end+epsilon. In practice, rather than having to
|
|
// choose an epsilon, we choose to average the value with the next largest
|
|
// timestamp.
|
|
const nextTs = {}
|
|
for (let i = 0; i < sortedTimestamps.length-1; i++) {
|
|
nextTs[sortedTimestamps[i]] = sortedTimestamps[i+1]
|
|
}
|
|
data.forEach(partition => {
|
|
partition['History'].forEach(el => {
|
|
let end = el['End']
|
|
el['OriginalEnd'] = end // for display purposes
|
|
if (startTimestamps.has(end)) {
|
|
if (Object.prototype.hasOwnProperty.call(nextTs, end)) {
|
|
const tweaked = (end + nextTs[end])/2
|
|
el['End'] = tweaked
|
|
allTimestamps.add(tweaked)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
// Update sortedTimestamps, because we created some new timestamps.
|
|
sortedTimestamps = Array.from(allTimestamps).sort((a, b) => a - b)
|
|
|
|
// Compute layout.
|
|
//
|
|
// We warp time to make it easier to see what's going on. We can think
|
|
// of there being a monotonically increasing mapping from timestamps to
|
|
// x-positions. This mapping should satisfy some criteria to make the
|
|
// visualization interpretable:
|
|
//
|
|
// - distinguishability: there should be some minimum distance between
|
|
// unequal timestamps
|
|
// - visible text: history boxes should be wide enough to fit the text
|
|
// they contain
|
|
// - enough space for LPs: history boxes should be wide enough to fit
|
|
// all linearization points that go through them, while maintaining
|
|
// readability of linearizations (where each LP in a sequence is spaced
|
|
// some minimum distance away from the previous one)
|
|
//
|
|
// Originally, I thought about this as a linear program:
|
|
//
|
|
// - variables for every unique timestamp, x_i = warp(timestamp_i)
|
|
// - objective: minimize sum x_i
|
|
// - constraint: non-negative
|
|
// - constraint: ordering + distinguishability, timestamp_i < timestamp_j -> x_i + EPS < x_j
|
|
// - constraint: visible text, size_text_j < x_{timestamp_j_end} - x_{timestamp_j_start}
|
|
// - constraint: linearization lines have points that fit within box, ...
|
|
//
|
|
// This used to actually be implemented using an LP solver (without the
|
|
// linearization point part, though that should be doable too), but
|
|
// then I realized it's possible to solve optimally using a greedy
|
|
// left-to-right scan in linear time.
|
|
//
|
|
// So that is what we do here. We optimally solve the above, and while
|
|
// doing so, also compute some useful information (e.g. x-positions of
|
|
// linearization points) that is useful later.
|
|
const xPos = {}
|
|
// Compute some information about history elements, sorted by end time;
|
|
// the most important information here is box width.
|
|
const byEnd = data.flatMap(partition =>
|
|
partition['History'].map(el => {
|
|
// compute width of the text inside the history element by actually
|
|
// drawing it (in a hidden div)
|
|
const scratch = document.getElementById('calc')
|
|
scratch.innerHTML = ''
|
|
const svg = svgadd(scratch, 'svg')
|
|
const text = svgadd(svg, 'text', {
|
|
'text-anchor': 'middle',
|
|
'class': 'history-text',
|
|
})
|
|
text.textContent = el['Description']
|
|
const bbox = text.getBBox()
|
|
const width = bbox.width + 2*BOX_TEXT_PADDING
|
|
return {
|
|
'start': el['Start'],
|
|
'end': el['End'],
|
|
'width': width,
|
|
'gid': el['Gid']
|
|
}
|
|
})
|
|
).sort((a, b) => a.end - b.end)
|
|
// Some preprocessing for linearization points and illegal next
|
|
// linearizations. We need to figure out where exactly LPs end up
|
|
// as we go, so we can make sure event boxes are wide enough.
|
|
const eventToLinearizations = newArray(gid, () => []) // event -> [{index, position}]
|
|
const eventIllegalLast = newArray(gid, () => []) // event -> [index]
|
|
const allLinearizations = []
|
|
let lgid = 0
|
|
data.forEach(partition => {
|
|
partition['PartialLinearizations'].forEach(lin => {
|
|
const globalized = [] // linearization with global indexes instead of partition-local ones
|
|
const included = new Set() // for figuring out illegal next LPs
|
|
lin.forEach((id, position) => {
|
|
included.add(id['Index'])
|
|
const gid = partition['History'][id['Index']]['Gid']
|
|
globalized.push(gid)
|
|
eventToLinearizations[gid].push({'index': lgid, 'position': position})
|
|
})
|
|
allLinearizations.push(globalized)
|
|
let minEnd = Infinity
|
|
partition['History'].forEach((el, index) => {
|
|
if (!included.has(index)) {
|
|
minEnd = Math.min(minEnd, el['End'])
|
|
}
|
|
})
|
|
partition['History'].forEach((el, index) => {
|
|
if (!included.has(index) && el['Start'] < minEnd) {
|
|
eventIllegalLast[el['Gid']].push(lgid)
|
|
}
|
|
})
|
|
lgid++
|
|
})
|
|
})
|
|
const linearizationPositions = newArray(lgid, () => []) // [[xpos]]
|
|
// Okay, now we're ready to do the left-to-right scan.
|
|
// Solve timestamp -> xPos.
|
|
let eventIndex = 0
|
|
xPos[sortedTimestamps[0]] = 0 // positions start at 0
|
|
for (let i = 1; i < sortedTimestamps.length; i++) {
|
|
// left-to-right scan, finding minimum time we can use
|
|
const ts = sortedTimestamps[i]
|
|
// ensure some gap from last timestamp
|
|
let pos = xPos[sortedTimestamps[i-1]] + BOX_GAP
|
|
// ensure that text fits in boxes
|
|
while (eventIndex < byEnd.length && byEnd[eventIndex].end <= ts) {
|
|
// push our position as far as necessary to accommodate text in box
|
|
const event = byEnd[eventIndex]
|
|
const textEndPos = xPos[event.start] + event.width
|
|
pos = Math.max(pos, textEndPos)
|
|
// Ensure that LPs fit in box.
|
|
//
|
|
// When placing the end of an event, for all partial linearizations
|
|
// that include that event, for the prefix that comes before that event,
|
|
// all their start points must have been placed already, so we can figure
|
|
// out the minimum width that the box needs to be to accommodate the LP.
|
|
eventToLinearizations[event.gid]
|
|
.concat(eventIllegalLast[event.gid].map(index => {
|
|
return {
|
|
'index': index,
|
|
'position': allLinearizations[index].length-1,
|
|
}
|
|
}))
|
|
.forEach(li => {
|
|
const {index, position} = li
|
|
for (let i = linearizationPositions[index].length; i <= position; i++) {
|
|
// determine past points
|
|
let prev = null
|
|
if (linearizationPositions[index].length != 0) {
|
|
prev = linearizationPositions[index][i-1]
|
|
}
|
|
const nextGid = allLinearizations[index][i]
|
|
let nextPos
|
|
if (prev === null) {
|
|
nextPos = xPos[byGid[nextGid]['Start']]
|
|
} else {
|
|
nextPos = Math.max(xPos[byGid[nextGid]['Start']], prev + EPSILON)
|
|
}
|
|
linearizationPositions[index].push(nextPos)
|
|
}
|
|
// this next line only really makes sense for the ones in
|
|
// eventToLinearizations, not the ones from eventIllegalLast,
|
|
// but it's safe to do it for all points, so we don't bother to
|
|
// distinguish.
|
|
pos = Math.max(pos, linearizationPositions[index][position])
|
|
})
|
|
// ensure that illegal next LPs fit in box too
|
|
eventIllegalLast[event.gid].forEach(li => {
|
|
const lin = linearizationPositions[li]
|
|
const prev = lin[lin.length-1]
|
|
pos = Math.max(pos, prev + EPSILON)
|
|
})
|
|
|
|
eventIndex++
|
|
}
|
|
xPos[ts] = pos
|
|
}
|
|
|
|
// Solved, now draw UI.
|
|
|
|
let selected = false
|
|
let selectedIndex = [-1, -1]
|
|
|
|
const height = 2*PADDING + BOX_HEIGHT * nClient + BOX_SPACE * (nClient - 1)
|
|
const width = 2*PADDING + XOFF + xPos[sortedTimestamps[sortedTimestamps.length-1]]
|
|
const svg = svgadd(document.getElementById('canvas'), 'svg', {
|
|
'width': width,
|
|
'height': height,
|
|
})
|
|
|
|
// draw background, etc.
|
|
const bg = svgadd(svg, 'g')
|
|
const bgRect = svgadd(bg, 'rect', {
|
|
'height': height,
|
|
'width': width,
|
|
'x': 0,
|
|
'y': 0,
|
|
'class': 'bg',
|
|
})
|
|
bgRect.onclick = handleBgClick
|
|
for (let i = 0; i < nClient; i++) {
|
|
const text = svgadd(bg, 'text', {
|
|
'x': XOFF/2,
|
|
'y': PADDING + BOX_HEIGHT/2 + i * (BOX_HEIGHT + BOX_SPACE),
|
|
'text-anchor': 'middle',
|
|
})
|
|
text.textContent = i
|
|
}
|
|
svgadd(bg, 'line', {
|
|
'x1': PADDING + XOFF,
|
|
'y1': PADDING,
|
|
'x2': PADDING + XOFF,
|
|
'y2': height - PADDING,
|
|
'class': 'divider'
|
|
})
|
|
|
|
// draw history
|
|
const historyLayers = []
|
|
const historyRects = []
|
|
const targetRects = svgnew('g')
|
|
data.forEach((partition, partitionIndex) => {
|
|
const l = svgadd(svg, 'g')
|
|
historyLayers.push(l)
|
|
const rects = []
|
|
partition['History'].forEach((el, elIndex) => {
|
|
const g = svgadd(l, 'g')
|
|
const rx = xPos[el['Start']]
|
|
const width = xPos[el['End']] - rx
|
|
const x = rx + XOFF + PADDING
|
|
const y = PADDING + el['ClientId'] * (BOX_HEIGHT + BOX_SPACE)
|
|
rects.push(svgadd(g, 'rect', {
|
|
'height': BOX_HEIGHT,
|
|
'width': width,
|
|
'x': x,
|
|
'y': y,
|
|
'rx': HISTORY_RECT_RADIUS,
|
|
'ry': HISTORY_RECT_RADIUS,
|
|
'class': 'history-rect'
|
|
}))
|
|
const text = svgadd(g, 'text', {
|
|
'x': x + width/2,
|
|
'y': y + BOX_HEIGHT/2,
|
|
'text-anchor': 'middle',
|
|
'class': 'history-text',
|
|
})
|
|
text.textContent = el['Description']
|
|
// we don't add mouseTarget to g, but to targetRects, because we
|
|
// want to layer this on top of everything at the end; otherwise, the
|
|
// LPs and lines will be over the target, which will create holes
|
|
// where hover etc. won't work
|
|
const mouseTarget = svgadd(targetRects, 'rect', {
|
|
'height': BOX_HEIGHT,
|
|
'width': width,
|
|
'x': x,
|
|
'y': y,
|
|
'class': 'target-rect',
|
|
'data-partition': partitionIndex,
|
|
'data-index': elIndex,
|
|
})
|
|
mouseTarget.onmouseover = handleMouseOver
|
|
mouseTarget.onmousemove = handleMouseMove
|
|
mouseTarget.onmouseout = handleMouseOut
|
|
mouseTarget.onclick = handleClick
|
|
})
|
|
historyRects.push(rects)
|
|
})
|
|
|
|
// draw partial linearizations
|
|
const illegalLast = data.map(partition => {
|
|
return partition['PartialLinearizations'].map(() => new Set())
|
|
})
|
|
const largestIllegal = data.map(() => {return {}})
|
|
const largestIllegalLength = data.map(() => {return {}})
|
|
const partialLayers = []
|
|
const errorPoints = []
|
|
data.forEach((partition, partitionIndex) => {
|
|
const l = []
|
|
partialLayers.push(l)
|
|
partition['PartialLinearizations'].forEach((lin, linIndex) => {
|
|
const g = svgadd(svg, 'g')
|
|
l.push(g)
|
|
let prevX = null
|
|
let prevY = null
|
|
let prevEl = null
|
|
const included = new Set()
|
|
lin.forEach(id => {
|
|
const el = partition['History'][id['Index']]
|
|
const hereX = PADDING + XOFF + xPos[el['Start']]
|
|
const x = prevX !== null ? Math.max(hereX, prevX + EPSILON) : hereX
|
|
const y = PADDING + el['ClientId'] * (BOX_HEIGHT + BOX_SPACE) - LINE_BLEED
|
|
// line from previous
|
|
if (prevEl !== null) {
|
|
svgadd(g, 'line', {
|
|
'x1': prevX,
|
|
'x2': x,
|
|
'y1': prevEl['ClientId'] >= el['ClientId'] ? prevY : prevY + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'y2': prevEl['ClientId'] <= el['ClientId'] ? y : y + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'class': 'linearization linearization-line',
|
|
})
|
|
}
|
|
// current line
|
|
svgadd(g, 'line', {
|
|
'x1': x,
|
|
'x2': x,
|
|
'y1': y,
|
|
'y2': y + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'class': 'linearization linearization-point'
|
|
})
|
|
prevX = x
|
|
prevY = y
|
|
prevEl = el
|
|
included.add(id['Index'])
|
|
})
|
|
// show possible but illegal next linearizations
|
|
// a history element is a possible next try
|
|
// if no other history element must be linearized earlier
|
|
// i.e. forall others, this.start < other.end
|
|
let minEnd = Infinity
|
|
partition['History'].forEach((el, index) => {
|
|
if (!included.has(index)) {
|
|
minEnd = Math.min(minEnd, el['End'])
|
|
}
|
|
})
|
|
partition['History'].forEach((el, index) => {
|
|
if (!included.has(index) && el['Start'] < minEnd) {
|
|
const hereX = PADDING + XOFF + xPos[el['Start']]
|
|
const x = prevX !== null ? Math.max(hereX, prevX + EPSILON) : hereX
|
|
const y = PADDING + el['ClientId'] * (BOX_HEIGHT + BOX_SPACE) - LINE_BLEED
|
|
// line from previous
|
|
svgadd(g, 'line', {
|
|
'x1': prevX,
|
|
'x2': x,
|
|
'y1': prevEl['ClientId'] >= el['ClientId'] ? prevY : prevY + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'y2': prevEl['ClientId'] <= el['ClientId'] ? y : y + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'class': 'linearization-invalid linearization-line',
|
|
})
|
|
// current line
|
|
const point = svgadd(g, 'line', {
|
|
'x1': x,
|
|
'x2': x,
|
|
'y1': y,
|
|
'y2': y + BOX_HEIGHT + 2*LINE_BLEED,
|
|
'class': 'linearization-invalid linearization-point',
|
|
})
|
|
errorPoints.push({
|
|
x: x,
|
|
partition: partitionIndex,
|
|
index: lin[lin.length-1]['Index'], // NOTE not index
|
|
element: point
|
|
})
|
|
illegalLast[partitionIndex][linIndex].add(index)
|
|
if (!Object.prototype.hasOwnProperty.call(largestIllegalLength[partitionIndex], index) || largestIllegalLength[partitionIndex][index] < lin.length) {
|
|
largestIllegalLength[partitionIndex][index] = lin.length
|
|
largestIllegal[partitionIndex][index] = linIndex
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
errorPoints.sort((a, b) => a.x - b.x)
|
|
|
|
// attach targetRects
|
|
svgattach(svg, targetRects)
|
|
|
|
// tooltip
|
|
const tooltip = document.getElementById('canvas').appendChild(document.createElement('div'))
|
|
tooltip.setAttribute('class', 'tooltip')
|
|
|
|
function handleMouseOver() {
|
|
if (!selected) {
|
|
const partition = parseInt(this.dataset['partition'])
|
|
const index = parseInt(this.dataset['index'])
|
|
highlight(partition, index)
|
|
}
|
|
tooltip.style.opacity = 1
|
|
}
|
|
|
|
function linearizationIndex(partition, index) {
|
|
// show this linearization
|
|
if (Object.prototype.hasOwnProperty.call(data[partition]['Largest'], index)) {
|
|
return data[partition]['Largest'][index]
|
|
} else if (Object.prototype.hasOwnProperty.call(largestIllegal[partition], index)) {
|
|
return largestIllegal[partition][index]
|
|
}
|
|
return null
|
|
}
|
|
|
|
function highlight(partition, index) {
|
|
// hide all but this partition
|
|
historyLayers.forEach((layer, i) => {
|
|
if (i === partition) {
|
|
layer.classList.remove('hidden')
|
|
} else {
|
|
layer.classList.add('hidden')
|
|
}
|
|
})
|
|
// hide all but the relevant linearization
|
|
partialLayers.forEach(layer => {
|
|
layer.forEach(g => {
|
|
g.classList.add('hidden')
|
|
})
|
|
})
|
|
// show this linearization
|
|
const maxIndex = linearizationIndex(partition, index)
|
|
if (maxIndex !== null) {
|
|
partialLayers[partition][maxIndex].classList.remove('hidden')
|
|
}
|
|
updateJump()
|
|
}
|
|
|
|
let lastTooltip = [null, null, null, null, null]
|
|
function handleMouseMove() {
|
|
const partition = parseInt(this.dataset['partition'])
|
|
const index = parseInt(this.dataset['index'])
|
|
const [sPartition, sIndex] = selectedIndex
|
|
const thisTooltip = [partition, index, selected, sPartition, sIndex]
|
|
|
|
if (!arrayEq(lastTooltip, thisTooltip)) {
|
|
let maxIndex
|
|
if (!selected) {
|
|
maxIndex = linearizationIndex(partition, index)
|
|
} else {
|
|
// if selected, show info relevant to the selected linearization
|
|
maxIndex = linearizationIndex(sPartition, sIndex)
|
|
}
|
|
if (selected && sPartition !== partition) {
|
|
tooltip.innerHTML = 'Not part of selected partition.'
|
|
} else if (maxIndex === null) {
|
|
if (!selected) {
|
|
tooltip.innerHTML = 'Not part of any partial linearization.'
|
|
} else {
|
|
tooltip.innerHTML = 'Selected element is not part of any partial linearization.'
|
|
}
|
|
} else {
|
|
const lin = data[partition]['PartialLinearizations'][maxIndex]
|
|
let prev = null, curr = null
|
|
let found = false
|
|
for (let i = 0; i < lin.length; i++) {
|
|
prev = curr
|
|
curr = lin[i]
|
|
if (curr['Index'] === index) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
let call = data[partition]['History'][index]['Start']
|
|
let ret = data[partition]['History'][index]['OriginalEnd']
|
|
let msg = ''
|
|
if (found) {
|
|
// part of linearization
|
|
if (prev !== null) {
|
|
msg = '<strong>Previous state:</strong><br>' + prev['StateDescription'] + '<br><br>'
|
|
}
|
|
msg += '<strong>New state:</strong><br>' + curr['StateDescription'] +
|
|
'<br><br>Call: ' + call +
|
|
'<br><br>Return: ' + ret
|
|
} else if (illegalLast[partition][maxIndex].has(index)) {
|
|
// illegal next one
|
|
msg = '<strong>Previous state:</strong><br>' + lin[lin.length-1]['StateDescription'] +
|
|
'<br><br><strong>New state:</strong><br>⟨invalid op⟩' +
|
|
'<br><br>Call: ' + call +
|
|
'<br><br>Return: ' + ret
|
|
} else {
|
|
// not part of this one
|
|
msg = 'Not part of selected element\'s partial linearization.'
|
|
}
|
|
tooltip.innerHTML = msg
|
|
}
|
|
lastTooltip = thisTooltip
|
|
}
|
|
tooltip.style.left = (event.pageX+20) + 'px'
|
|
tooltip.style.top = (event.pageY+20) + 'px'
|
|
}
|
|
|
|
function handleMouseOut() {
|
|
if (!selected) {
|
|
resetHighlight()
|
|
}
|
|
tooltip.style.opacity = 0
|
|
lastTooltip = [null, null, null, null, null]
|
|
}
|
|
|
|
function resetHighlight() {
|
|
// show all layers
|
|
historyLayers.forEach(layer => {
|
|
layer.classList.remove('hidden')
|
|
})
|
|
// show longest linearizations, which are first
|
|
partialLayers.forEach(layers => {
|
|
layers.forEach((l, i) => {
|
|
if (i === 0) {
|
|
l.classList.remove('hidden')
|
|
} else {
|
|
l.classList.add('hidden')
|
|
}
|
|
})
|
|
})
|
|
updateJump()
|
|
}
|
|
|
|
function updateJump() {
|
|
const jump = document.getElementById('jump-link')
|
|
// find first non-hidden point
|
|
// feels a little hacky, but it works
|
|
const point = errorPoints.find(pt => !pt.element.parentElement.classList.contains('hidden'))
|
|
if (point) {
|
|
jump.classList.remove('inactive')
|
|
jump.onclick = () => {
|
|
point.element.scrollIntoView({behavior: 'smooth', inline: 'center', block: 'center'})
|
|
if (!selected) {
|
|
select(point.partition, point.index)
|
|
}
|
|
}
|
|
} else {
|
|
jump.classList.add('inactive')
|
|
}
|
|
}
|
|
|
|
function handleClick() {
|
|
const partition = parseInt(this.dataset['partition'])
|
|
const index = parseInt(this.dataset['index'])
|
|
if (selected) {
|
|
const [sPartition, sIndex] = selectedIndex
|
|
if (partition === sPartition && index === sIndex) {
|
|
deselect()
|
|
return
|
|
} else {
|
|
historyRects[sPartition][sIndex].classList.remove('selected')
|
|
}
|
|
}
|
|
select(partition, index)
|
|
}
|
|
|
|
function handleBgClick() {
|
|
deselect()
|
|
}
|
|
|
|
function select(partition, index) {
|
|
selected = true
|
|
selectedIndex = [partition, index]
|
|
highlight(partition, index)
|
|
historyRects[partition][index].classList.add('selected')
|
|
}
|
|
|
|
function deselect() {
|
|
if (!selected) {
|
|
return
|
|
}
|
|
selected = false
|
|
resetHighlight()
|
|
const [partition, index] = selectedIndex
|
|
historyRects[partition][index].classList.remove('selected')
|
|
}
|
|
|
|
handleMouseOut() // initialize, same as mouse out
|
|
}
|
|
|
|
const data = %s
|
|
|
|
render(data)
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`
|