#!/usr/bin/env python
# Gant-chart style build time visualization tool for bam event logs. Usefull to find things preventing
# good parallelization. Not very done, not very nice.
#
# build:
# bam --debug-eventlog evlog.txt
# Run:
# vistime.py evlog.txt
#
# Written by Markus Alind (markus.alind at gmail.com)
import sys
import os
import random
import copy
import math
try:
# for Python2
import Tkinter as tkinter
except ImportError:
# for Python3
import tkinter
def TimeToStr( val ):
if val >= 1.0:
return "% 3.2fs " % val
elif val >= 0.001:
return "% 3.2fms" % ( val * 1000 )
else:
return "% 3.2fus" % ( val * 1000**2 )
class Job:
def __init__( self, name, start, thread ):
self.name = name
self.start = start
self.end = None
self.thread = thread
def SetEnd( self, end ) :
self.end = end
def GetRunTime( self ):
if self.end :
return self.end - self.start
class JobThread:
def __init__ ( self ):
self.cur = None
self.all = []
class Jobs:
def __init__ ( self ):
self.map = {}
self.allJobs = []
self.threads = {}
def ParseLine( self, line ):
line = line.strip().split( None , 3 )
thread = int( line[ 0 ] )
time = float( line[ 1 ] )
action = line[ 2 ]
name = line[ 3 ]
if name == "build:":
return
if not thread in self.threads:
self.threads[ thread ] = JobThread()
th = self.threads[ thread ]
if ( action == "begin" ):
assert ( not th.cur )
th.cur = Job( name, time, thread )
th.all.append( th.cur )
self.allJobs.append( th.cur )
elif ( action == "end" ) :
assert( th.cur )
th.cur.SetEnd( time )
th.cur = None
else:
assert( False )
def Parse( self, lines ):
for line in lines:
if line.strip():
self.ParseLine( line )
def GetThreads( self ):
res = list( self.threads.keys() )
res.sort()
return res
class TkGui:
def __init__( self, jobs = None ):
self.axisGraphics = []
self.CreateWindow( ( 800, 800 ) )
self.canvasSize = ( 800, 800 )
self.CalcSizes( 32 )
self.InitCanvas( jobs )
def WindowLoop( self ):
self.win.mainloop()
def CalcSizes( self, xScale ):
#xSize = 2000
self.yRectSize = 20
self.ySpacing = self.yRectSize * 1.25
self.xScale = xScale
self.xMargin = 30
self.xStart = self.xMargin
self.yMargin = self.ySpacing * 2
self.timeOffset = 0
self.yStart = self.yMargin
#rects = []
#minTime = 0
#maxTime = 0
#~ for t in jobs.GetThreads():
#~ th = jobs.threads[ t ]
#~ minTime = min( minTime, th.all[ 0 ].start )
#~ maxTime = max( maxTime, th.all[ -1 ].end )
#~ numThreads = len( jobs.threads )
#~ ySize = ( numThreads ) * ySpacing + yMargin*2
#~ yStart = yMargin
#~ totTime = maxTime - minTime
#~ xScale = ( xSize - xMargin*2 ) / totTime
#~ xStart = xMargin
def CreateWindow( self, canvasSize ):
self.win = tkinter.Tk( )
self.statusbar = tkinter.Label( self.win, text="", bd=1, relief=tkinter.SUNKEN, anchor=tkinter.W )
self.statusbar.pack( side=tkinter.BOTTOM, fill=tkinter.X )
self.scrollbarx = tkinter.Scrollbar( self.win, orient=tkinter.HORIZONTAL, width=24 )
#self.scrollbarx.grid(column=0, row=1, sticky=(tkinter.W,tkinter.E))
self.scrollbarx.pack( side = tkinter.BOTTOM, fill=tkinter.X )
self.scrollbary = tkinter.Scrollbar( self.win, orient=tkinter.VERTICAL, width=24 )
self.scrollbary.pack( side = tkinter.RIGHT, fill=tkinter.Y )
self.canvas = tkinter.Canvas( self.win, bg="lightgray",
width = canvasSize[ 0 ], height = canvasSize[ 1 ],
scrollregion=( 0, 0, canvasSize[ 0 ], canvasSize[ 1 ] ),
xscrollcommand=self.scrollbarx.set, yscrollcommand=self.scrollbary.set
)
#self.canvas.grid( column=0, row=0, sticky=( tkinter.N, tkinter.W, tkinter.E, tkinter.S ) )
self.scrollbarx.config( command = self.canvas.xview )
self.scrollbary.config( command = self.canvas.yview )
#self.canvas.config(scrollregion=canvas.bbox(tkinter.ALL))
self.canvas.pack( side = tkinter.TOP, fill=tkinter.BOTH )
self.canvas.tag_bind( "jobrect", "<Enter>", self.Ev_RectEnter )
self.canvas.tag_bind( "jobrect", "<Leave>", self.Ev_RectLeave )
self.canvas.bind( '<Button-5>', lambda e: self.Ev_Zoom( e, -1 ) )
self.canvas.bind( '<Button-4>', lambda e: self.Ev_Zoom( e, 1 ) )
self.canvas.bind( '<Button-3>', self.Ev_MButtonRightDown )
self.canvas.bind( '<B3-Motion>', self.Ev_MButtonRightMove )
self.win.bind( '<MouseWheel>', self.Ev_ZoomWheel )
def UpdateCanvasSize( self, newSize ):
maxX = newSize[ 0 ] + self.xMargin
maxY = newSize[ 1 ] + self.yMargin
if not maxX == self.canvasSize[ 0 ] or not maxY == self.canvasSize[ 1 ]:
self.canvasSize = ( maxX, maxY )
self.canvas.config( scrollregion=( 0, 0, maxX, maxY ), height = maxY )
#self.canvas.config( width=maxX, height=maxY, scrollregion=( 0, 0, maxX, maxY ), )
def UpdateAxis( self ):
# we just wipe everything
for obj in self.axisGraphics:
self.canvas.delete( obj )
self.axisGraphics = []
tbY = self.yMargin - self.ySpacing
tTickY = tbY - 2
tMajorY = 8
tMinorY = 5
tMinorPerMajor = 4
tMajorXTarget = 100
tbStartX = self.xMargin
tbStopX = self.canvasSize[ 0 ] - self.xMargin
baseLine = self.canvas.create_line( tbStartX, tbY, tbStopX, tbY )
self.axisGraphics.append( baseLine )
#print ( "self.timeMax,", self.timeMax, "tbStopX - tbStartX,", tbStopX - tbStartX )
secPerMajor = self.timeMax / ( ( tbStopX - tbStartX ) / tMajorXTarget )
#print ( "secPerMajor", secPerMajor )
#if secPerMajor < 1 :
# secPerMajor = 1
if secPerMajor < 30 :
if self.timeMax / secPerMajor > 1000 :
secPerMajor = self.timeMax / 1000
tenPot = math.floor( math.log10( secPerMajor ) )
norm = secPerMajor / ( 10**tenPot )
#print norm
for i in [ 5, 2, 1 ]:
if norm > i :
norm = i
break
secPerMajor = norm * ( 10**tenPot )
elif secPerMajor < 60 :
secPerMajor = 30
else:
minPerMajor = secPerMajor / 60
tenPot = math.floor( math.log10( minPerMajor ) )
norm = minPerMajor / ( 10**tenPot )
for i in [ 5, 2, 1 ]:
if norm > i :
norm = i
break
minPerMajor = norm * ( 10**tenPot )
secPerMajor = minPerMajor * 60
#print ( "secPerMajor", secPerMajor )
numMajor = int( math.floor( self.timeMax / secPerMajor ) )
#print ( "numMajor", numMajor )
for m in range( numMajor + 1 ):
majorPos = tbStartX + m * secPerMajor * self.xScale
self.axisGraphics.append( self.canvas.create_line( majorPos, tTickY, majorPos, tTickY + tMajorY ) )
time = m*secPerMajor
self.axisGraphics.append( self.canvas.create_text( majorPos, tTickY, anchor=tkinter.S, text=TimeToStr(time) ) )
for minor in range(1, tMinorPerMajor ) :
minorPos = majorPos + minor * ( secPerMajor / tMinorPerMajor ) * self.xScale
if ( minorPos <= tbStopX ):
self.axisGraphics.append( self.canvas.create_line( minorPos, tTickY, minorPos, tTickY + tMinorY ) )
#print len( self.axisGraphics )
class JobRect:
def __init__( self, job ):
self.job = job
self.rect = None
def CalcRect(self, job ):
x0 = ( job.start - self.timeOffset ) * self.xScale +self.xStart
x1 = ( job.end - self.timeOffset ) * self.xScale + self.xStart
y0 = ( job.thread*self.ySpacing - self.yRectSize*0.5) + self.yStart
y1 = ( job.thread*self.ySpacing + self.yRectSize*0.5) + self.yStart
return ( x0, y0, x1, y1 )
def InitCanvas( self, jobs ):
self.guiIdToJob = {}
self.jobToGuiId = {}
self.jobRects = []
maxX = 0
maxY = 0
timeMax = 0
for t in jobs.GetThreads():
th = jobs.threads[ t ]
for j in th.all:
jr = self.JobRect( j )
( x0, y0, x1, y1 ) = self.CalcRect( jr.job )
maxX = max( maxX, x1 )
maxY = max( maxY, y1 )
timeMax = max( timeMax, jr.job.end )
#~ jr.rect = self.canvas.create_polygon(
#~ x0, y0,
#~ x0, y1,
#~ x1, y1,
#~ x1, y0,
#~ fill="lightgreen", outline="black", activefill="green", activeoutline="black", tag="jobrect" )
# https://stackoverflow.com/questions/4969543/colour-chart-for-tkinter-and-tix-using-python
col = ( "lightgreen", "green" )
if not "job:" in j.name and ( "cache load" in j.name or "script parse" in j.name or "prepare" in j.name ):
col = ( "lightcyan", "cyan" )
elif not ("c " in j.name and "c++" in j.name ) and "link" in j.name:
col = ( "lightblue", "blue" )
elif "job:" in j.name and "/batchcpp/" in j.name :
col = ( "salmon", "lightsalmon" )
elif "job:" in j.name and "precomp" in j.name :
col = ( "lightyellow", "yellow" )
elif "job:" in j.name and ".dll" in j.name :
col = ( "lightblue", "blue" )
jr.rect = self.canvas.create_rectangle(
( x0, y0, x1, y1 ),
fill=col[0], outline="black", activefill=col[1], activeoutline="black", tag="jobrect" )
self.guiIdToJob[ jr.rect ] = jr
self.jobToGuiId[ j.name ] = jr
self.jobRects.append( jr )
#canvas.tag_bind( rect, "<Enter>", test )
self.timeMax = timeMax
self.UpdateCanvasSize( ( maxX, maxY ) )
self.UpdateAxis()
def UpdateCanvas( self ):
maxX = 0
maxY = 0
for jr in self.jobRects:
( x0, y0, x1, y1 ) = self.CalcRect( jr.job )
maxX = max( maxX, x1 )
maxY = max( maxY, y1 )
self.canvas.coords( jr.rect, ( x0, y0, x1, y1 ) )
self.UpdateCanvasSize( ( maxX, maxY ) )
self.UpdateAxis()
def SetScale( self, xScale ):
self.CalcSizes( xScale )
self.UpdateCanvas()
def CenterAtTime( self, time ):
winWidth = self.canvas.winfo_width()
xNewCenter = ( time*self.xScale ) / ( self.canvasSize[ 0 ] - self.xMargin*2 ) - ( winWidth / self.canvasSize[ 0 ] ) / 2
self.canvas.xview_moveto( xNewCenter )
def Ev_RectEnter( self, event ):
curId = self.canvas.find_withtag( tkinter.CURRENT )
jr = self.guiIdToJob[ curId[ 0 ] ]
text = "%s %s" % ( TimeToStr( jr.job.GetRunTime() ), jr.job.name )
self.statusbar.config( text=text )
#print guiIdToJob[ tkinter.CURRENT ]
#print event
#print dir( event )
#print event.num
#canvas.itemconfigure( tkinter.CURRENT, fill="red" )
def Ev_RectLeave( self, event ):
self.statusbar.config( text="" )
#print event
#print dir( event )
#print event.num
#canvas.itemconfigure( tkinter.CURRENT, fill="lightgreen" )
def Ev_Zoom( self, event, val ):
# zoom but keep the spot under the cursor under the cursor
xCurWindow = event.x +0.5
xCurCanvas = self.canvas.canvasx( xCurWindow )
xCenterTime = ( xCurCanvas - self.xStart ) / self.xScale
#print "time:", xCenterTime
scale = self.xScale*0.75**(-val)
self.SetScale( scale )
#print self.canvasSize
#print "width", self.canvas.winfo_width()
winWidth = self.canvas.winfo_width()
xCenterPixel = xCenterTime*self.xScale + self.xStart - (xCurWindow - winWidth/2 )
xCenterFrac = xCenterPixel / ( self.canvasSize[ 0 ] ) - ( winWidth / self.canvasSize[ 0 ] ) / 2
#print xCenterPixel
#print "res:", xCenterFrac
self.canvas.xview_moveto( xCenterFrac )
def Ev_ZoomWheel( self, event ) :
self.Ev_Zoom( event, event.delta/120 )
def Ev_MButtonRightDown( self, event ):
self.canvas.scan_mark( event.x, 0 )
def Ev_MButtonRightMove( self, event ):
self.canvas.scan_dragto( event.x, 0, 1 )
def callback(event):
canvas = event.widget
x = canvas.canvasx(event.x)
y = canvas.canvasy(event.y)
print( canvas.find_closest(x, y) )
def main( argv ):
#bam --debug-eventlog log.txt
#com = argv[ 1 ]
logPath = argv[ 1 ]
jobs = Jobs( )
jobs.Parse( open( logPath ).readlines() )
# allSorted = sorted( jobs.allJobs, key = lambda j : j.GetRunTime(), reverse = True )
# for j in allSorted:
# print( "%s, % 2d, %s" % ( TimeToStr( j.GetRunTime() ), j.thread, j.name ) )
w = TkGui( jobs )
w.WindowLoop()
if __name__ == "__main__":
main( sys.argv )