// **** graph renderers
// ** UOp graph
function intersectRect ( r1 , r2 ) {
const dx = r2 . x - r1 . x ;
const dy = r2 . y - r1 . y ;
if ( dx === 0 && dy === 0 ) throw new Error ( "Invalid node coordinates, rects must not overlap" ) ;
const scaleX = dx !== 0 ? ( r1 . width / 2 ) / Math . abs ( dx ) : Infinity ;
const scaleY = dy !== 0 ? ( r1 . height / 2 ) / Math . abs ( dy ) : Infinity ;
const scale = Math . min ( scaleX , scaleY ) ;
return { x : r1 . x + dx * scale , y : r1 . y + dy * scale } ;
}
const rect = ( s ) => document . querySelector ( s ) . getBoundingClientRect ( ) ;
let [ workerUrl , worker , timeout ] = [ null , null , null ] ;
async function renderDag ( graph , additions , recenter = false ) {
// start calculating the new layout (non-blocking)
if ( worker == null ) {
const resp = await Promise . all ( [ "/assets/dagrejs.github.io/project/dagre/latest/dagre.min.js" , "/js/worker.js" ] . map ( u => fetch ( u ) ) ) ;
workerUrl = URL . createObjectURL ( new Blob ( [ ( await Promise . all ( resp . map ( ( r ) => r . text ( ) ) ) ) . join ( "\n" ) ] , { type : "application/javascript" } ) ) ;
worker = new Worker ( workerUrl ) ;
} else {
worker . terminate ( ) ;
worker = new Worker ( workerUrl ) ;
}
if ( timeout != null ) clearTimeout ( timeout ) ;
const progressMessage = document . querySelector ( ".progress-message" ) ;
timeout = setTimeout ( ( ) => { progressMessage . style . display = "block" } , 2000 ) ;
worker . postMessage ( { graph , additions , ctxs } ) ;
worker . onmessage = ( e ) => {
progressMessage . style . display = "none" ;
clearTimeout ( timeout ) ;
d3 . select ( "#bars" ) . html ( "" ) ;
const g = dagre . graphlib . json . read ( e . data ) ;
// draw nodes
const STROKE _WIDTH = 1.4 ;
const nodes = d3 . select ( "#nodes" ) . selectAll ( "g" ) . data ( g . nodes ( ) . map ( id => g . node ( id ) ) , d => d ) . join ( "g" )
. attr ( "transform" , d => ` translate( ${ d . x } , ${ d . y } ) ` ) . classed ( "clickable" , d => d . ref != null )
. on ( "click" , ( _ , d ) => setCtxWithHistory ( d . ref ) ) ;
nodes . selectAll ( "rect" ) . data ( d => [ d ] ) . join ( "rect" ) . attr ( "width" , d => d . width ) . attr ( "height" , d => d . height ) . attr ( "fill" , d => d . color )
. attr ( "x" , d => - d . width / 2 ) . attr ( "y" , d => - d . height / 2 ) . attr ( "style" , d => d . style ? ? ` stroke:#4a4b57; stroke-width: ${ STROKE _WIDTH } px; ` ) ;
nodes . selectAll ( "g.label" ) . data ( d => [ d ] ) . join ( "g" ) . attr ( "class" , "label" ) . attr ( "transform" , d => {
const x = ( d . width - d . padding * 2 ) / 2 ;
const y = ( d . height - d . padding * 2 ) / 2 + STROKE _WIDTH ;
return ` translate(- ${ x } , - ${ y } ) ` ;
} ) . selectAll ( "text" ) . data ( d => [ d . label . split ( "\n" ) ] ) . join ( "text" ) . selectAll ( "tspan" ) . data ( d => d ) . join ( "tspan" ) . text ( d => d ) . attr ( "x" , "0" )
. attr ( "dy" , 14 ) . attr ( "xml:space" , "preserve" ) ;
const tags = nodes . selectAll ( "g.tag" ) . data ( d => d . tag != null ? [ d ] : [ ] ) . join ( "g" ) . attr ( "class" , "tag" )
. attr ( "transform" , d => ` translate( ${ - d . width / 2 + 8 } , ${ - d . height / 2 + 8 } ) ` ) ;
tags . selectAll ( "circle" ) . data ( d => [ d ] ) . join ( "circle" ) ;
tags . selectAll ( "text" ) . data ( d => [ d . tag ] ) . join ( "text" ) . text ( d => d ) . attr ( "dy" , "0.35em" ) ;
// draw edges
const line = d3 . line ( ) . x ( d => d . x ) . y ( d => d . y ) . curve ( d3 . curveBasis ) ;
d3 . select ( "#edges" ) . selectAll ( "path.edgePath" ) . data ( g . edges ( ) ) . join ( "path" ) . attr ( "class" , "edgePath" ) . attr ( "d" , ( e ) => {
const edge = g . edge ( e ) ;
const points = edge . points . slice ( 1 , edge . points . length - 1 ) ;
points . unshift ( intersectRect ( g . node ( e . v ) , points [ 0 ] ) ) ;
points . push ( intersectRect ( g . node ( e . w ) , points [ points . length - 1 ] ) ) ;
return line ( points ) ;
} ) . attr ( "marker-end" , "url(#arrowhead)" ) ;
const edgeLabels = d3 . select ( "#edge-labels" ) . selectAll ( "g" ) . data ( g . edges ( ) . filter ( e => g . edge ( e ) . label != null ) ) . join ( "g" ) . attr ( "transform" , ( e ) => {
// get a point near the end
const [ p1 , p2 ] = g . edge ( e ) . points . slice ( - 2 ) ;
const dx = p2 . x - p1 . x ;
const dy = p2 . y - p1 . y ;
// normalize to the unit vector
const len = Math . sqrt ( dx * dx + dy * dy ) ;
const ux = dx / len ;
const uy = dy / len ;
// avoid overlap with the arrowhead
const offset = 17 ;
const x = p2 . x - ux * offset ;
const y = p2 . y - uy * offset ;
return ` translate( ${ x } , ${ y } ) `
} ) . attr ( "class" , "tag" ) ;
edgeLabels . selectAll ( "circle" ) . data ( e => [ g . edge ( e ) . label ] ) . join ( "circle" ) ;
edgeLabels . selectAll ( "text" ) . data ( e => [ g . edge ( e ) . label ] ) . join ( "text" ) . text ( d => d ) . attr ( "dy" , "0.35em" ) ;
if ( recenter ) document . getElementById ( "zoom-to-fit-btn" ) . click ( ) ;
} ;
}
// ** Memory graph (WIP)
DTYPE _SIZE = { "bool" : 1 , "char" : 1 , "uchar" : 1 , "short" : 2 , "ushort" : 2 , "int" : 4 , "uint" : 4 ,
"long" : 8 , "ulong" : 8 , "half" : 2 , "bfloat" : 2 , "float" : 4 , "double" : 8 }
function getBuffer ( e ) {
const [ _ , size , dtype , num , device ] = e . label . split ( "\n" ) ;
return { nbytes : size * DTYPE _SIZE [ dtype . split ( "dtypes." ) [ 1 ] ] , dtype , device : device . split ( " " ) [ 1 ] , num : parseInt ( num . split ( " " ) [ 1 ] ) } ;
}
function pluralize ( num , name , alt = null ) {
return num === 1 ? ` ${ num } ${ name } ` : ` ${ num } ${ alt ? ? name + 's' } `
}
function renderMemoryGraph ( graph ) {
// ** construct alloc/free traces
// we can map reads/writes from the kernel graph
const actions = [ ] ;
const children = new Map ( ) ; // {buffer: [...assign]}
for ( const [ k , v ] of Object . entries ( graph ) ) {
if ( ! v . label . startsWith ( "ASSIGN" ) ) continue ;
actions . push ( { op : "write" , buffer : v . src [ 0 ] } ) ;
for ( const ks of graph [ v . src [ 1 ] ] . src ) {
const node = graph [ ks ] ;
const s = node . label . startsWith ( "ASSIGN" ) ? node . src [ 0 ] : ks ;
if ( ! children . has ( s ) ) children . set ( s , [ ] ) ;
children . get ( s ) . push ( v ) ;
if ( s !== v . src [ 0 ] ) actions . push ( { op : "read" , buffer : s } ) ;
}
}
const prealloc = new Set ( ) ;
const traces = [ ] ;
for ( const a of actions ) {
// a buffer is allocated immediately before the first write
// TODO: we don't know the buffer is preallocated if there's only an assign in the graph
if ( a . op === "write" ) {
traces . push ( { type : "alloc" , buffer : a . buffer } ) ;
}
else {
if ( traces . find ( t => t . buffer === a . buffer && t . type === "alloc" ) == null ) {
prealloc . add ( a . buffer ) ;
}
else if ( a === actions . findLast ( ( { buffer } ) => buffer === a . buffer ) ) {
traces . push ( { type : "free" , buffer : a . buffer } ) ;
}
}
}
// ** get coordinates and layout for each buffer
const ret = { } ;
let timestep = 0 ; // x
let memUsed = 0 ; // y
for ( const id of prealloc ) {
const buf = getBuffer ( graph [ id ] ) ;
ret [ id ] = { x : [ timestep ] , y : [ memUsed ] , buf , id } ;
memUsed += buf . nbytes ;
}
let peak = memUsed ;
const liveBufs = [ ... prealloc ] ;
for ( const t of traces ) {
const buf = getBuffer ( graph [ t . buffer ] ) ;
const idx = liveBufs . findLastIndex ( b => t . buffer === b ) ;
// alloc
if ( idx === - 1 ) {
liveBufs . push ( t . buffer ) ;
ret [ t . buffer ] = { x : [ timestep ] , y : [ memUsed ] , buf , id : t . buffer } ;
memUsed += buf . nbytes ;
peak = Math . max ( memUsed , peak ) ;
timestep += 1 ;
} // free
else {
memUsed -= buf . nbytes ;
timestep += 1 ;
const removed = ret [ liveBufs . splice ( idx , 1 ) [ 0 ] ] ;
removed . x . push ( timestep ) ;
removed . y . push ( removed . y . at ( - 1 ) ) ;
if ( idx < liveBufs . length ) {
for ( let j = idx ; j < liveBufs . length ; j ++ ) {
const b = ret [ liveBufs [ j ] ] ;
b . x . push ( timestep , timestep ) ;
b . y . push ( b . y . at ( - 1 ) , b . y . at ( - 1 ) - buf . nbytes ) ;
}
}
}
}
for ( const id of liveBufs ) {
const b = ret [ id ] ;
b . x . push ( timestep ) ;
b . y . push ( b . y . at ( - 1 ) ) ;
}
// ** render traces
// clear existing groups
document . querySelector ( ".progress-message" ) . style . display = "none" ;
for ( c of document . getElementById ( "render" ) . children ) c . innerHTML = "" ;
const render = d3 . select ( "#bars" ) ;
const yscale = d3 . scaleLinear ( ) . domain ( [ 0 , peak ] ) . range ( [ 576 , 0 ] ) ;
const xscale = d3 . scaleLinear ( ) . domain ( [ 0 , timestep ] ) . range ( [ 0 , 1024 ] ) ;
const axesGroup = render . append ( "g" ) . attr ( "id" , "axes" ) ;
const nbytes _format = ( d ) => d3 . format ( ".3~s" ) ( d ) + "B" ;
axesGroup . append ( "g" ) . call ( d3 . axisLeft ( yscale ) . tickFormat ( nbytes _format ) ) ;
axesGroup . append ( "g" ) . attr ( "transform" , ` translate(0, ${ yscale . range ( ) [ 0 ] } ) ` ) . call ( d3 . axisBottom ( xscale ) . tickFormat ( ( ) => "" ) ) ;
const polygonGroup = render . append ( "g" ) . attr ( "id" , "polygons" ) ;
const colors = [ "7aa2f7" , "ff9e64" , "f7768e" , "2ac3de" , "7dcfff" , "1abc9c" , "9ece6a" , "e0af68" , "bb9af7" , "9d7cd8" , "ff007c" ] ;
const polygons = polygonGroup . selectAll ( "polygon" ) . data ( Object . values ( ret ) ) . join ( "polygon" ) . attr ( "points" , ( d ) => {
const xs = d . x . map ( t => xscale ( t ) ) ;
const y1 = d . y . map ( t => yscale ( t ) ) ;
const y2 = d . y . map ( t => yscale ( t + d . buf . nbytes ) ) ;
const p0 = xs . map ( ( x , i ) => ` ${ x } , ${ y1 [ i ] } ` ) ;
const p1 = xs . map ( ( x , i ) => ` ${ x } , ${ y2 [ i ] } ` ) . reverse ( ) ;
return ` ${ p0 . join ( ' ' ) } ${ p1 . join ( ' ' ) } ` ;
} ) . attr ( "fill" , d => ` # ${ colors [ d . buf . num % colors . length ] } ` ) . on ( "mouseover" , ( e , { id , buf , x } ) => {
d3 . select ( e . currentTarget ) . attr ( "stroke" , "rgba(26, 27, 38, 0.8)" ) . attr ( "stroke-width" , 0.8 ) ;
const metadata = document . querySelector ( ".metadata" ) ;
document . getElementById ( "current-buf" ) ? . remove ( ) ;
const { num , dtype , nbytes , ... rest } = buf ;
let label = ` <BUFFER n ${ num } ${ dtype } ${ nbytes _format ( nbytes ) } > \n alive for ${ pluralize ( x [ x . length - 1 ] - x [ 0 ] , 'timestep' ) } ` ;
label += '\n' + Object . entries ( rest ) . map ( ( [ k , v ] ) => ` ${ k } = ${ v } ` ) . join ( '\n' ) ;
const buf _children = children . get ( id ) ;
if ( buf _children ) {
label += ` \n ${ pluralize ( buf _children . length , 'child' , 'children' ) } \n ` ;
label += buf _children . map ( ( c , i ) => ` [ ${ i + 1 } ] ` + graph [ c . src [ 1 ] ] . label . split ( "\n" ) [ 1 ] ) . join ( "\n" ) ;
}
metadata . appendChild ( Object . assign ( document . createElement ( "pre" ) , { innerText : label , id : "current-buf" , className : "wrap" } ) ) ;
} ) . on ( "mouseout" , ( e , _ ) => {
d3 . select ( e . currentTarget ) . attr ( "stroke" , null ) . attr ( "stroke-width" , null ) ;
document . getElementById ( "current-buf" ) ? . remove ( )
} ) ;
// TODO: add the kernel line here
document . getElementById ( "zoom-to-fit-btn" ) . click ( ) ;
}
// ** zoom and recentering
const svgZoom = d3 . zoom ( ) . on ( "zoom" , ( e ) => d3 . select ( "#render" ) . attr ( "transform" , e . transform ) ) ;
d3 . select ( "#graph-svg" ) . call ( svgZoom ) ;
// zoom to fit into view
document . getElementById ( "zoom-to-fit-btn" ) . addEventListener ( "click" , ( ) => {
const svg = d3 . select ( "#graph-svg" ) ;
svg . call ( svgZoom . transform , d3 . zoomIdentity ) ;
const mainRect = rect ( ".main-container" ) ;
const x0 = rect ( ".ctx-list-parent" ) . right ;
const x1 = rect ( ".metadata-parent" ) . left ;
const pad = 16 ;
const R = { x : x0 + pad , y : mainRect . top + pad , width : ( x1 > 0 ? x1 - x0 : mainRect . width ) - 2 * pad , height : mainRect . height - 2 * pad } ;
const r = rect ( "#render" ) ;
if ( r . width === 0 ) return ;
const scale = Math . min ( R . width / r . width , R . height / r . height ) ;
const [ tx , ty ] = [ R . x + ( R . width - r . width * scale ) / 2 - r . left * scale , R . y + ( R . height - r . height * scale ) / 2 ] ;
svg . call ( svgZoom . transform , d3 . zoomIdentity . translate ( tx , ty ) . scale ( scale ) ) ;
} ) ;
// **** main VIZ interfacae
function codeBlock ( st , language , { loc , wrap } ) {
const code = document . createElement ( "code" ) ;
code . innerHTML = hljs . highlight ( st , { language } ) . value ;
code . className = "hljs" ;
const ret = document . createElement ( "pre" ) ;
if ( wrap ) ret . className = "wrap" ;
if ( loc != null ) {
const link = ret . appendChild ( document . createElement ( "a" ) ) ;
link . href = "vscode://file" + loc . join ( ":" ) ;
link . textContent = ` ${ loc [ 0 ] . split ( "/" ) . at ( - 1 ) } : ${ loc [ 1 ] } ` + "\n\n" ;
}
ret . appendChild ( code ) ;
return ret ;
}
function setActive ( e ) {
e . classList . add ( "active" ) ;
requestAnimationFrame ( ( ) => e . scrollIntoView ( { behavior : "auto" , block : "nearest" } ) ) ;
}
// ** hljs extra definitions for UOps and float4
hljs . registerLanguage ( "python" , ( hljs ) => ( {
... hljs . getLanguage ( "python" ) ,
case _insensitive : false ,
contains : [
{ begin : 'dtypes\\.[a-zA-Z_][a-zA-Z0-9_-]*(\\.[a-zA-Z_][a-zA-Z0-9_-]*)*' + '(?=[.\\s\\n[:,(])' , className : "type" } ,
{ begin : 'dtypes\\.[a-zA-Z_][a-zA-Z0-9_-].vec*' + '(?=[.\\s\\n[:,(])' , className : "type" } ,
{ begin : '[a-zA-Z_][a-zA-Z0-9_-]*\\.[a-zA-Z_][a-zA-Z0-9_-]*' + '(?=[.\\s\\n[:,()])' , className : "operator" } ,
{ begin : '[A-Z][a-zA-Z0-9_]*(?=\\()' , className : "section" , ignoreEnd : true } ,
... hljs . getLanguage ( "python" ) . contains ,
]
} ) ) ;
hljs . registerLanguage ( "cpp" , ( hljs ) => ( {
... hljs . getLanguage ( 'cpp' ) ,
contains : [ { begin : '\\b(?:float|half)[0-9]+\\b' , className : 'type' } , ... hljs . getLanguage ( 'cpp' ) . contains ]
} ) ) ;
var ret = [ ] ;
var cache = { } ;
var ctxs = null ;
const evtSources = [ ] ;
// VIZ displays graph rewrites in 3 levels, from bottom-up:
// rewrite: a single UOp transformation
// step: collection of rewrites
// context: collection of steps
const state = { currentCtx : - 1 , currentStep : 0 , currentRewrite : 0 , expandSteps : false } ;
function setState ( ns ) {
const { currentCtx : prevCtx , currentStep : prevStep } = state ;
Object . assign ( state , ns ) ;
// update element styles if needed
document . getElementById ( ` ctx- ${ state . currentCtx } ` ) ? . classList . toggle ( "expanded" , state . expandSteps ) ;
if ( state . currentCtx !== prevCtx ) {
document . getElementById ( ` ctx- ${ prevCtx } ` ) ? . classList . remove ( "active" , "expanded" ) ;
setActive ( document . getElementById ( ` ctx- ${ state . currentCtx } ` ) ) ;
}
if ( state . currentCtx !== prevCtx || state . currentStep !== prevStep ) {
document . getElementById ( ` step- ${ prevCtx } - ${ prevStep } ` ) ? . classList . remove ( "active" ) ;
setActive ( document . getElementById ( ` step- ${ state . currentCtx } - ${ state . currentStep } ` ) ) ;
}
// re-render
main ( ) ;
}
// set a new context and keep the old one in browser history
function setCtxWithHistory ( newCtx ) {
if ( newCtx == null ) return ;
// NOTE: browser does a structured clone, passing a mutable object is safe.
history . replaceState ( state , "" ) ;
history . pushState ( state , "" ) ;
setState ( { expandSteps : true , currentCtx : newCtx , currentStep : 0 , currentRewrite : 0 } ) ;
}
window . addEventListener ( "popstate" , ( e ) => {
if ( e . state != null ) setState ( e . state ) ;
} ) ;
async function main ( ) {
// ** left sidebar context list
if ( ctxs == null ) {
ctxs = await ( await fetch ( "/ctxs" ) ) . json ( ) ;
const ctxList = document . querySelector ( ".ctx-list" ) ;
for ( const [ i , { name , steps } ] of ctxs . entries ( ) ) {
const ul = ctxList . appendChild ( document . createElement ( "ul" ) ) ;
ul . id = ` ctx- ${ i } ` ;
const p = ul . appendChild ( document . createElement ( "p" ) ) ;
p . innerHTML = name . replace ( /\u001b\[(\d+)m(.*?)\u001b\[0m/g , ( _ , code , st ) => {
const colors = [ 'gray' , 'red' , 'green' , 'yellow' , 'blue' , 'magenta' , 'cyan' , 'white' ] ;
return ` <span style=" ${ ` color: color-mix(in srgb, ${ colors [ ( parseInt ( code ) - 30 + 60 ) % 60 ] } 60%, white) ` } "> ${ st } </span> ` ;
} ) ;
p . onclick = ( ) => {
setState ( i === state . currentCtx ? { expandSteps : ! state . expandSteps } : { expandSteps : true , currentCtx : i , currentStep : 0 , currentRewrite : 0 } ) ;
}
for ( const [ j , u ] of steps . entries ( ) ) {
const inner = ul . appendChild ( document . createElement ( "ul" ) ) ;
inner . id = ` step- ${ i } - ${ j } ` ;
inner . innerText = ` ${ u . name ? ? u . loc [ 0 ] . replaceAll ( "\\" , "/" ) . split ( "/" ) . pop ( ) + ':' + u . loc [ 1 ] } - ${ u . match _count } ` ;
inner . style . marginLeft = ` ${ 8 * u . depth } px ` ;
inner . onclick = ( e ) => {
e . stopPropagation ( ) ;
setState ( { currentStep : j , currentCtx : i , currentRewrite : 0 } ) ;
}
}
}
return setState ( { currentCtx : - 1 } ) ;
}
// ** center graph
const { currentCtx , currentStep , currentRewrite , expandSteps } = state ;
if ( currentCtx == - 1 ) return ;
const ctx = ctxs [ currentCtx ] ;
const step = ctx . steps [ currentStep ] ;
const ckey = ` ctx= ${ currentCtx } &idx= ${ currentStep } ` ;
// close any pending event sources
let activeSrc = null ;
for ( const e of evtSources ) {
if ( e . url . split ( "?" ) [ 1 ] !== ckey ) e . close ( ) ;
else if ( e . readyState === EventSource . OPEN ) activeSrc = e ;
}
if ( ckey in cache ) {
ret = cache [ ckey ] ;
}
// if we don't have a complete cache yet we start streaming rewrites in this step
if ( ! ( ckey in cache ) || ( cache [ ckey ] . length !== step . match _count + 1 && activeSrc == null ) ) {
ret = [ ] ;
cache [ ckey ] = ret ;
const eventSource = new EventSource ( ` /ctxs? ${ ckey } ` ) ;
evtSources . push ( eventSource ) ;
eventSource . onmessage = ( e ) => {
if ( e . data === "END" ) return eventSource . close ( ) ;
const chunk = JSON . parse ( e . data ) ;
ret . push ( chunk ) ;
// if it's the first one render this new rgaph
if ( ret . length === 1 ) return main ( ) ;
// otherwise just enable the graph selector
const ul = document . getElementById ( ` rewrite- ${ ret . length - 1 } ` ) ;
if ( ul != null ) ul . classList . remove ( "disabled" ) ;
} ;
}
if ( ret . length === 0 ) return ;
if ( step . name == "View Memory Graph" ) {
renderMemoryGraph ( ret [ currentRewrite ] . graph ) ;
} else {
renderDag ( ret [ currentRewrite ] . graph , ret [ currentRewrite ] . changed _nodes || [ ] , recenter = currentRewrite === 0 ) ;
}
// ** right sidebar code blocks
const metadata = document . querySelector ( ".metadata" ) ;
const [ code , lang ] = ctx . kernel _code != null ? [ ctx . kernel _code , "cpp" ] : [ ret [ currentRewrite ] . uop , "python" ] ;
metadata . replaceChildren ( codeBlock ( step . code _line , "python" , { loc : step . loc , wrap : true } ) , codeBlock ( code , lang , { wrap : false } ) ) ;
// ** rewrite steps
if ( step . match _count >= 1 ) {
const rewriteList = metadata . appendChild ( document . createElement ( "div" ) ) ;
rewriteList . className = "rewrite-list" ;
for ( let s = 0 ; s <= step . match _count ; s ++ ) {
const ul = rewriteList . appendChild ( document . createElement ( "ul" ) ) ;
ul . innerText = s ;
ul . id = ` rewrite- ${ s } ` ;
ul . onclick = ( ) => setState ( { currentRewrite : s } ) ;
ul . className = s > ret . length - 1 ? "disabled" : s === currentRewrite ? "active" : "" ;
if ( s > 0 && s === currentRewrite ) {
const { upat , diff } = ret [ s ] ;
metadata . appendChild ( codeBlock ( upat [ 1 ] , "python" , { loc : upat [ 0 ] , wrap : true } ) ) ;
const diffCode = metadata . appendChild ( document . createElement ( "pre" ) ) . appendChild ( document . createElement ( "code" ) ) ;
for ( const line of diff ) {
const span = diffCode . appendChild ( document . createElement ( "span" ) ) ;
span . style . color = line . startsWith ( "+" ) ? "#3aa56d" : line . startsWith ( "-" ) ? "#d14b4b" : "#f0f0f5" ;
span . innerText = line ;
diffCode . appendChild ( document . createElement ( "br" ) ) ;
}
diffCode . className = "wrap" ;
}
}
}
}
// **** collapse/expand
let isCollapsed = false ;
document . querySelector ( ".collapse-btn" ) . addEventListener ( "click" , ( e ) => {
isCollapsed = ! isCollapsed ;
document . querySelector ( ".main-container" ) . classList . toggle ( "collapsed" , isCollapsed ) ;
e . currentTarget . blur ( ) ;
e . currentTarget . style . transform = isCollapsed ? "rotate(180deg)" : "rotate(0deg)" ;
} ) ;
// **** resizer
function appendResizer ( element , { minWidth , maxWidth } , left = false ) {
const handle = Object . assign ( document . createElement ( "div" ) , { className : "resize-handle" , style : left ? "right: 0" : "left: 0; margin-top: 0" } ) ;
element . appendChild ( handle ) ;
const resize = ( e ) => {
const change = e . clientX - element . dataset . startX ;
let newWidth = ( ( Number ( element . dataset . startWidth ) + ( left ? change : - change ) ) / Number ( element . dataset . containerWidth ) ) * 100 ;
element . style . width = ` ${ Math . max ( minWidth , Math . min ( maxWidth , newWidth ) ) } % ` ;
} ;
handle . addEventListener ( "mousedown" , ( e ) => {
e . preventDefault ( ) ;
element . dataset . startX = e . clientX ;
element . dataset . containerWidth = rect ( ".main-container" ) . width ;
element . dataset . startWidth = element . getBoundingClientRect ( ) . width ;
document . documentElement . addEventListener ( "mousemove" , resize , false ) ;
document . documentElement . addEventListener ( "mouseup" , ( ) => {
document . documentElement . removeEventListener ( "mousemove" , resize , false ) ;
element . style . userSelect = "initial" ;
} , { once : true } ) ;
} ) ;
}
appendResizer ( document . querySelector ( ".ctx-list-parent" ) , { minWidth : 15 , maxWidth : 50 } , left = true ) ;
appendResizer ( document . querySelector ( ".metadata-parent" ) , { minWidth : 20 , maxWidth : 50 } ) ;
// **** keyboard shortcuts
document . addEventListener ( "keydown" , async function ( event ) {
const { currentCtx , currentStep , currentRewrite , expandSteps } = state ;
// up and down change the step or context from the list
const changeStep = expandSteps && ctxs [ currentCtx ] . steps ? . length ;
if ( event . key == "ArrowUp" ) {
event . preventDefault ( ) ;
if ( changeStep ) {
return setState ( { currentRewrite : 0 , currentStep : Math . max ( 0 , currentStep - 1 ) } ) ;
}
return setState ( { currentStep : 0 , currentRewrite : 0 , currentCtx : Math . max ( 0 , currentCtx - 1 ) , expandSteps : false } ) ;
}
if ( event . key == "ArrowDown" ) {
event . preventDefault ( ) ;
if ( changeStep ) {
const totalUOps = ctxs [ currentCtx ] . steps . length - 1 ;
return setState ( { currentRewrite : 0 , currentStep : Math . min ( totalUOps , currentStep + 1 ) } ) ;
}
return setState ( { currentStep : 0 , currentRewrite : 0 , currentCtx : Math . min ( ctxs . length - 1 , currentCtx + 1 ) , expandSteps : false } ) ;
}
// enter toggles focus on a single rewrite stage
if ( event . key == "Enter" ) {
event . preventDefault ( )
if ( currentCtx === - 1 ) {
return setState ( { currentCtx : 0 , expandSteps : true } ) ;
}
return setState ( { expandSteps : ! expandSteps } ) ;
}
// left and right go through rewrites in a single UOp
if ( event . key == "ArrowLeft" ) {
event . preventDefault ( )
return setState ( { currentRewrite : Math . max ( 0 , currentRewrite - 1 ) } ) ;
}
if ( event . key == "ArrowRight" ) {
event . preventDefault ( )
const totalRewrites = ret . length - 1 ;
return setState ( { currentRewrite : Math . min ( totalRewrites , currentRewrite + 1 ) } ) ;
}
// space recenters the graph
if ( event . key == " " ) {
event . preventDefault ( )
document . getElementById ( "zoom-to-fit-btn" ) . click ( ) ;
}
} ) ;
main ( )