p4clog.py #1

  • //
  • psla/
  • dev/
  • p4clog.py
  • View
  • Commits
  • Open Download .zip Download (14 KB)
#!/usr/bin/python

# p4clog.py [ options ] < p4d-log-file > tables.csv
#
# $Id: //psla/dev/p4clog.py#1 $
#
# -s - submit only
# -c - show locking by command
# -d - show delay analysis
# -h - show lock held histogram
# -i - interval for histogram instead of 600
# -w - show wait vs held analysis
# -v - log page I/Os, wait/hold more than 100ms, delays
#

import sys, re, time, getopt, math

# logParse - parse a line of p4d log tracking

class logParse:

	def __init__( p ):
		# what we're parsing:
		# 2013/01/17 22:01:11 pid 28322 u@c 10.0.50.39/10.0.0.40 [p4tter/v72 (brokered)] 'command args'
		# --- db.rev
		# ---   pages in+out+cached 222520+0+96
		# ---   locks read/write 1/0 rows get+pos+scan put+del 0+1+7372595 0+0
		# ---   total lock wait+held read/write 0ms+9578ms/0ms+0ms
		# --- db.change
		# ---   total lock wait+held read/write 0ms+9583ms/0ms+0ms
	
		p.pa = re.compile( "^\t(..../../.. ..:..:..) pid [^']*'([^ ']*)([^']*)" )
		p.pb = re.compile( "^--- (db.*)$" )
		p.pc = re.compile( "^---   pages in\+out\+cached ([^+]*)\+([^+]*)\+(.*)$" )
		p.pd = re.compile( "^---   locks .* put\+del ([^+]*)\+([^+]*)\+([^ ]*) ([^+]*)\+(.*)$" )
		p.pe = re.compile( "^---   total .* read/write ([^m]*)ms\+([^m]*)ms/([^m]*)ms\+([^m]*)ms$" )
		p.pf = re.compile( "^$" )

		p.reset()

	def reset( p ):

		p.date = p.command = p.args = p.table = ""

	# parse a single line of log

	def parse( p, line ):
		p.lineType = "none"
		ll = len( line )
	
		if( line[0] == "\n" ):
			p.lineType = "end"
			return
	
		if( ll > 5 and line[5] == "/" ):
			m = p.pa.search( line )
			if( m ): 
				p.date,p.command,p.args = m.group(1,2,3)
				p.lineType = "command"
			return
	
		if( ll > 4 and line[4] == "d" ):
			m = p.pb.search( line )
			if( m ): p.table = m.group(1)
			return
	
		if( ll > 6 and line[6] == "t" ):
			m = p.pe.search( line )
			if( m ): 
				p.lrw,p.lrh,p.lww,p.lwh = map(int,m.group(1,2,3,4))
				p.lineType = "locks"
			return
	
		if( ll > 6 and line[6] == "p" ):
			m = p.pc.search( line )
			if( m ): 
				p.pgi,p.pgo,p.pgc = map(int,m.group(1,2,3))
				p.lineType = "pages"
				p.isPages = True
			return
	
		return
	
		# don't use these yet
	
		if( ll > 6 and line[6] == "l" ):
			m = p.pd.search( line )
			if( m ): rgt,rpo,rsc,rpt,rdl = map(int,m.group(1,2,3,4,5))
			return

# tableCounts - read/write wait/held time counter

class tableCounts:

	def __init__( s, lrw, lrh, lww, lwh ):
		s.readWaitCount = lrw > 0; s.readWaitTime = lrw
		s.readHeldCount = lrh > 0; s.readHeldTime = lrh
		s.writeWaitCount = lww > 0; s.writeWaitTime = lww
		s.writeHeldCount = lwh > 0; s.writeHeldTime = lwh

	def __iadd__( s, o ):
	
		s.readHeldCount += o.readHeldCount
		s.readHeldTime += o.readHeldTime
		s.readWaitCount += o.readWaitCount
		s.readWaitTime += o.readWaitTime
	
		s.writeHeldCount += o.writeHeldCount
		s.writeHeldTime += o.writeHeldTime
		s.writeWaitCount += o.writeWaitCount
		s.writeWaitTime += o.writeWaitTime
	
		return s

	def __ior__( s, o ):

		# add waits, but max of held
	
		s.readWaitTime += o.readWaitTime
		s.writeWaitTime += o.writeWaitTime
	
		if( o.readHeldTime > s.readHeldTime ):
			s.readHeldTime = o.readHeldTime
		if( o.writeHeldTime > s.writeHeldTime ):
			s.writeHeldTime = o.writeHeldTime
	
		return s

# Spanner - track how long locks are held for one db file

class Spanner:

	# per interval result

	class Result:

		def __init__( r, time, secsHeld, secsFree, longestHeld ):
			r.time = time
			r.secsHeld = secsHeld
			r.secsFree = secsFree
			r.longestHeld = longestHeld

	# when the log file begins

	epoch = 0

	def __init__( s ):

		# number of locks held in each second 
		# since log file begins (gets long!)
	
		s.byTheSeconds = []
		s.byTheInterval = []

	# translate log file date into seconds since epoch

	def secs( s, date ):

		try:
			t = int( time.mktime( time.strptime( date, "%Y/%m/%d %H:%M:%S" ) ) )
			if( Spanner.epoch == 0 ): Spanner.epoch = t
			return t - Spanner.epoch
	
		except ValueError:
			print ("bad date %s" % date, file=sys.stderr)
			return 0

	# process a single lock hold

	def span( s, start, held ):

		# translate start/held to start/end in second since epoch
	
		heldStart = s.secs( start )
		heldEnd = int(heldStart + ( held + 500 ) / 1000)
	
		# sometime the log file is out of order
	
		if( heldStart < 0 or heldStart == heldEnd ):
			return
	
		# dynamically extend array
	
		if( len( s.byTheSeconds ) <= heldEnd ):
			s.byTheSeconds.extend( [ 0 ] * ( heldEnd + 1 - len( s.byTheSeconds ) ) )
	
		# count this as an instance this lock was held this second
	
		for x in range( heldStart, heldEnd ):
			s.byTheSeconds[ x ] += 1

	# condense into intervals

	def condense( s, interval ):

		lastFree = -1
		longestHeld = 0
	
		# for each 'interval' seconds
		# find the time held/free/longest
		# and dump out a single line
	
		for x in range( 0, len( s.byTheSeconds ), interval ):
	
			secsHeld = 0
			secsFree = interval
	
			for y in range( x, min( x + interval, len( s.byTheSeconds ) ) ):
	
				sex = s.byTheSeconds[ y ]
		
				secsHeld += sex
				secsFree -= sex == 0
		
				if( sex == 0 ):
					lastFree = y
				elif( y - lastFree > longestHeld ):
					longestHeld = y - lastFree
	
			s.byTheInterval.append( s.Result( Spanner.epoch + x, secsHeld, secsFree, longestHeld ) )
	
			longestHeld = 0

	# dump one table's locks

	def dump( s, t, interval ):

		s.condense( interval )
	
		if( len( s.byTheInterval ) == 0 ): return
	
		print(">>> %s saturation each %d seconds" % ( t, interval ))
		print("%s,%s,%s,%s" % ( "time", "not free", "held", "longest held" ))
	
		# for each 'interval' seconds
		# find the time held/free/longest
		# and dump out a single line
	
		for x in s.byTheInterval:
	
			print("%s,%d,%d,%d" % ( 
				time.ctime( x.time ),
				x.secsFree,
				x.secsHeld,
				x.longestHeld ))

# LocksByTime: one spanner for each table

class LocksByTime:

	def __init__( s ):

		s.tableSpanner = {}

	def tally( s, commandTables, date ):

		for t in commandTables:
	
			if( not t in s.tableSpanner ):
				s.tableSpanner[ t ] = Spanner()
	
			s.tableSpanner[ t ].span( date,
				commandTables[ t ].writeHeldTime +
				commandTables[ t ].readHeldTime )

	def dump( s, spannerInterval ):

		for t in s.tableSpanner:
			s.tableSpanner[ t ].dump( t, spannerInterval )

#
# WaitVsHeld -- calculate how long locks are held waiting for others
#
#	.tally() - given lock+held times for all tables used by a command's
#		processing, calculate which tables are holding up others
#
#	.dump() - dump out all wait vs held times for tables held more than
#		1 seconds total
#

class WaitVsHeld:

	def __init__( s ):

		s.table = {}

	def tally( s, commandTables, showWaitVsHeld, count, command ):

		for t in commandTables:
	
			# wait vs held
	
			held = commandTables[ t ].writeHeldTime
	
			if( held == 0 ): continue
	
			for t2 in commandTables:
	
				wait = commandTables[ t2 ].writeWaitTime
		
				if( t == t2 or wait > held or wait <= held / 2 ):
					continue
		
				if( not t in s.table ):
					s.table[ t ] = {}
				if( not t2 in s.table[ t ] ):
					s.table[ t ][ t2 ] = tableCounts( 0, 0, 0, 0 )
		
				s.table[ t ][ t2 ] += commandTables[ t2 ]
		
				# for interest sake
		
				if( showWaitVsHeld ):
					print(count, "held", t, held, "wait", t2, wait, command)

	def dump( s, totalTables ):

		print(">>> Wait times causing hold times")

		print("%s,%s,%s,%s,%s,%s" % (
			"table", 
			"write-wait-Time", 
			"read-wait-Time", 
			"table",
			"write-held-Time",
			"read-held-Time" ))

		for t in s.table:
			tc = totalTables[ t ]
			for t2 in s.table[ t ]:
				tg = s.table[ t ][ t2 ]
		
				if( tg.writeWaitTime > 1000 and tc.writeHeldTime > 1000 or
					tg.readWaitTime > 1000 and tc.readHeldTime > 1000 ):
					print("%s,%d,%d,%s,%d,%d" % (
						t2, 
						tg.writeWaitTime / 1000,
						tg.readWaitTime / 1000,
						t, 
						tc.writeHeldTime / 1000,
						tc.readHeldTime / 1000 ))

# 
# TimesByCommand -- calculate read+write hold time for each command
#
#	.tally() - add a single read+write hold to a command's total
#	.dump() - dump out each command's total
#

class TimesByCommand:

	def __init__( s ):

		s.byCommand = {}

	def tally( s, table, command, tc ):

		# by table then command 
	
		if( not table in s.byCommand ):
			s.byCommand[ table ] = {}
		if( not command in s.byCommand[ table ] ):
			s.byCommand[ table ][ command ] = tableCounts( 0, 0, 0, 0 )
	
		s.byCommand[ table ][ command ] += tc

	def dump( s ):

		print(">>> Table hold times by command")
		print("%s,%s,%s,%s" % (
			"table", 
			"command",
			"read-held-Time", 
			"write-held-Time" ))
	
		for t in s.byCommand:
			for c in s.byCommand[ t ]:
				tt = s.byCommand[ t ][ c ]
				if( tt.readHeldTime >= 1000 or tt.writeHeldTime >= 1000 ):
					print("%s,%s,%d,%d" % (
						t, c, 
						tt.readHeldTime/1000,
						tt.writeHeldTime/1000 ))

#
# TimesByTable -- calculate read+write/wait+hold time for each table
#
#	.tally() - add a single read+write/wait+hold to a table's total
#	.dump() - dump out each table's tally, using a grandTotal for %age
#

class TimesByTable:

	def __init__( s ):

		s.byTable = {}

	def tally( s, table, tc ):

		if( not table in s.byTable ):
			s.byTable[ table ] = tableCounts( 0, 0, 0, 0 )

		s.byTable[ table ] += tc

	def dump( s, grandTotal ):

		grandTotal += tableCounts( 1, 1, 1, 1 ) # no divide by zero errors
	
		print(">>> Hold and wait times per table")
	
		print("%s,%s,%s,%s,%s,%s,%s" % (
			"table", 
			"read-wait-Time", 
			"%-of-Total",
			"read-held-Time", 
			"write-wait-Time", 
			"%-of-Total",
			"write-held-Time" ))
	
		# sort descending by readWaitTime
	
		#for t,tc in s.byTable.items():
		for t,tc in sorted( s.byTable.items(), key=lambda v: 0-(v[1].readWaitTime + v[1].writeWaitTime) ):
	
			print("%s,%d,%d%%,%d,%d,%d%%,%d" % (
				t, 
				tc.readWaitTime/1000, 
				tc.readWaitTime * 100 / grandTotal.readWaitTime,
				tc.readHeldTime/1000,
				tc.writeWaitTime/1000,
				tc.writeWaitTime * 100 / grandTotal.writeWaitTime,
				tc.writeHeldTime/1000 ))

#
# WaitDelays - how much commands are delayed by wait times
#

class WaitDelays:

	def __init__( s ):

		s.maxSlots = 10
		s.power = 2
		s.delaySlots = [ 0 ] * s.maxSlots

	def tally( s, commandTotal, logDelays, count, command ):

		wait = commandTotal.readWaitTime + commandTotal.writeWaitTime 
		held = commandTotal.readHeldTime + commandTotal.writeHeldTime
	
		if( wait < 1000 or wait < held / 10 ):
			return
	
		if( logDelays ):
			print(count, "commandWait", wait, "commandHeld", held, command)
	
		slot = int( math.log( wait / 1000, s.power ) )
	
		if( slot >= s.maxSlots ):
			slot = s.maxSlots-1
	
		s.delaySlots[ slot ] += 1

	def dump( s ):

		print(">>> Commands delayed")
		print("%s,%s" % ( "Seconds of delay", "Commands affected" ))
	
		for x in range( 0, s.maxSlots ):
			print("%d,%d" % ( s.power**x, s.delaySlots[ x ] ))

def main():

	# process args

	showByCommand = False
	showDelays = False
	showWaitVsHeld = False
	showHisto = False
	showTally = False
	spannerInterval = 600
	submitOnly = False
	verboseLogging = False

	try:
		opts, args = getopt.getopt( sys.argv[1:], "acdhi:stwv" )

	except getopt.error as msg:
		print(msg)
		print("Usage: p4clog.py [ -v ] [ -acdhswt ] [ -i interval ] < log-file")
		sys.exit(2)

	for o, a in opts:
		if o == '-h': showHisto = True
		elif o == '-s': submitOnly = True
		elif o == '-i': spannerInterval = int(a)
		elif o == '-c': showByCommand = True
		elif o == '-d': showDelays = True
		elif o == '-t': showTally = True
		elif o == '-a': showHisto = showByCommand = showDelays = showTally = showWaitVsHeld = True
		elif o == '-w': showWaitVsHeld = True
		elif o == '-v': verboseLogging = True

	# per table counts of held and wait times

	lineCount = 0
	commandCount = 0
	pagesIn = 0
	pagesOut = 0
	firstDate = 0
	lastDate = 0

	p = logParse()

	commandTables = {}

	timesByTable = TimesByTable()
	timesByCommand = TimesByCommand()
	waitVsHeld = WaitVsHeld()
	locksByTime = LocksByTime()
	waitDelays = WaitDelays()

	commandTotal = tableCounts( 0, 0, 0, 0 )
	grandTotal = tableCounts( 0, 0, 0, 0 )

	for line in sys.stdin:

		lineCount += 1

		if( lineCount % 1000000 == 0 ):
			print("%s" % lineCount, file=sys.stderr)

		# match one of the lines

		p.parse( line )

		# per table tallying

		if( submitOnly and p.command != "dm-CommitSubmit" ):
			continue

		# command count

		if( p.lineType == "command" ):
			commandCount += 1
			if( firstDate == 0 ): firstDate = p.date
			lastDate = p.date

		# pages

		if( p.lineType == "pages" ):

			pagesIn += p.pgi
			pagesOut += p.pgo

			if( verboseLogging ):
				if( p.pgi + p.pgo > 0 ):
					print(lineCount, "pages", p.table, p.pgi + p.pgo, p.command, p.args)

			continue

		# locks

		if( p.lineType == "locks"  ):

			# for interest sake

			if( verboseLogging ):
				if( p.lrh > 100 ): print(lineCount, "readheld", p.table, p.lrh, p.command, p.args )
				if( p.lwh > 100 ): print(lineCount, "writeheld", p.table, p.lwh, p.command, p.args )
				if( p.lrw > 100 ): print(lineCount, "readwait", p.table, p.lrw, p.command, p.args )
				if( p.lww > 100 ): print(lineCount, "writewait", p.table, p.lww, p.command, p.args)

			# remember count for this table for this command
			# and add it to the grand total

			tc = tableCounts( p.lrw, p.lrh, p.lww, p.lwh )
			commandTables[ p.table ] = tc
			commandTotal |= tc
			grandTotal += tc

			# tally by table and by command name

			timesByTable.tally( p.table, tc )
			timesByCommand.tally( p.table, p.command, tc )

		# per command tallying

		if( p.lineType == "end" ):

			if( showHisto ):
				locksByTime.tally( commandTables, p.date )

			if( showWaitVsHeld ):
				waitVsHeld.tally( commandTables, verboseLogging, lineCount, p.command )

			if( showDelays ):
				waitDelays.tally( commandTotal, verboseLogging, lineCount, p.command )

			commandTotal = tableCounts( 0, 0, 0, 0 )
			commandTables = {}

			p.reset()

	# Dump it all out

	if( showTally ):
		print(">>> Log file summary")
		print("Lines,Commands,Pages Read,Pages Written,First Date,Last Date")
		print("%d,%d,%d,%d,%s,%s" % ( lineCount, commandCount, pagesIn, pagesOut, firstDate, lastDate ))

	if( showDelays ):
		waitDelays.dump()

	timesByTable.dump( grandTotal )

	if( showWaitVsHeld ):
		waitVsHeld.dump( timesByTable.byTable )

	if( showByCommand ):
		timesByCommand.dump()

	if( showHisto ):
		locksByTime.dump( spannerInterval )

main()

# Change User Description Committed
#1 34000 Perforce maintenance Populate -r -S //psla/dev.
//psla/main/p4clog.py
#1 33998 C. Thomas Tyler Populate -b PSLA_Classic_to_Streams.
//guest/perforce_software/log-analyzer/psla/p4clog.py
#2 27832 C. Thomas Tyler Remade change on behalf of cmclouth in new post-restrucred path.
 Change is
Charlie's original content, just in a new file path. (A 'p4 filelog' was done
to confirm Charlie's change was the latest change pre-refactoring, and also no
changes were made post-refactoring, so no merge work is needed for this change).

Change passes basic testing (works on Python3, generates reasonable results with
track=1).

cmclouth improvements #review-11090 @lester_cheung @cmclouth
* Resolved whitespace indentation problems that prevent it from working on Windows.
* Converted to Python 3.x
** convert print to print()
** convert iteritems() to items()
** change exception handling
** cast float to int (line 175)
* Changed Table sort to consider the combination of both ReadWaitTime and WriteWaitTime (line 423.)
#1 25216 Robert Cowham Branch files to Workshop mandated path for project
//guest/perforce_software/utils/log_analyzer/p4clog.py
#2 9735 Lester Cheung Updating p4clog.py with our latest version
#1 8280 Matt Attaway Add our new log analyszer, p4clog.