Clone in a terminal:
#! /usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2007-2010 (ita)

"""
Debugging helper for parallel compilation, outputs
a file named pdebug.svg in the source directory::

	def options(opt):
		opt.load('parallel_debug')
	def configure(conf):
		conf.load('parallel_debug')
	def build(bld):
   	 ...
"""

import os, time, sys
try: from Queue import Queue
except: from queue import Queue
from waflib import Runner, Options, Utils, Task, Logs, Errors

#import random
#random.seed(100)

def options(opt):
	opt.add_option('--dtitle', action='store', default='Parallel build representation for %r' % ' '.join(sys.argv),
		help='title for the svg diagram', dest='dtitle')
	opt.add_option('--dwidth', action='store', type='int', help='diagram width', default=800, dest='dwidth')
	opt.add_option('--dtime', action='store', type='float', help='recording interval in seconds', default=0.009, dest='dtime')
	opt.add_option('--dband', action='store', type='int', help='band width', default=22, dest='dband')
	opt.add_option('--dmaxtime', action='store', type='float', help='maximum time, for drawing fair comparisons', default=0, dest='dmaxtime')

# red   #ff4d4d
# green #4da74d
# lila  #a751ff

color2code = {
	'GREEN'  : '#4da74d',
	'YELLOW' : '#fefe44',
	'PINK'   : '#a751ff',
	'RED'    : '#cc1d1d',
	'BLUE'   : '#6687bb',
	'CYAN'   : '#34e2e2',
}

mp = {}
info = [] # list of (text,color)

def map_to_color(name):
	if name in mp:
		return mp[name]
	try:
		cls = Task.classes[name]
	except KeyError:
		return color2code['RED']
	if cls.color in mp:
		return mp[cls.color]
	if cls.color in color2code:
		return color2code[cls.color]
	return color2code['RED']

def process(self):
	m = self.master
	if m.stop:
		m.out.put(self)
		return

	self.master.set_running(1, id(Utils.threading.currentThread()), self)

	# remove the task signature immediately before it is executed
	# in case of failure the task will be executed again
	try:
		del self.generator.bld.task_sigs[self.uid()]
	except:
		pass

	try:
		self.generator.bld.returned_tasks.append(self)
		self.log_display(self.generator.bld)
		ret = self.run()
	except Exception:
		self.err_msg = Utils.ex_stack()
		self.hasrun = Task.EXCEPTION

		# TODO cleanup
		m.error_handler(self)
		m.out.put(self)
		return

	if ret:
		self.err_code = ret
		self.hasrun = Task.CRASHED
	else:
		try:
			self.post_run()
		except Errors.WafError:
			pass
		except Exception:
			self.err_msg = Utils.ex_stack()
			self.hasrun = Task.EXCEPTION
		else:
			self.hasrun = Task.SUCCESS
	if self.hasrun != Task.SUCCESS:
		m.error_handler(self)

	self.master.set_running(-1, id(Utils.threading.currentThread()), self)
	m.out.put(self)
Task.TaskBase.process_back = Task.TaskBase.process
Task.TaskBase.process = process

old_start = Runner.Parallel.start
def do_start(self):
	try:
		Options.options.dband
	except AttributeError:
		self.bld.fatal('use def options(opt): opt.load("parallel_debug")!')

	self.taskinfo = Queue()
	old_start(self)
	if self.dirty:
		process_colors(self)
Runner.Parallel.start = do_start

def set_running(self, by, i, tsk):
	self.taskinfo.put( (i, id(tsk), time.time(), tsk.__class__.__name__, self.processed, self.count, by)  )
Runner.Parallel.set_running = set_running

def name2class(name):
	return name.replace(' ', '_').replace('.', '_')

def process_colors(producer):
	# first, cast the parameters
	tmp = []
	try:
		while True:
			tup = producer.taskinfo.get(False)
			tmp.append(list(tup))
	except:
		pass

	try:
		ini = float(tmp[0][2])
	except:
		return

	if not info:
		seen = []
		for x in tmp:
			name = x[3]
			if not name in seen:
				seen.append(name)
			else:
				continue

			info.append((name, map_to_color(name)))
		info.sort(key=lambda x: x[0])

	thread_count = 0
	acc = []
	for x in tmp:
		thread_count += x[6]
		acc.append("%d %d %f %r %d %d %d" % (x[0], x[1], x[2] - ini, x[3], x[4], x[5], thread_count))
	data_node = producer.bld.path.make_node('pdebug.dat')
	data_node.write('\n'.join(acc))

	tmp = [lst[:2] + [float(lst[2]) - ini] + lst[3:] for lst in tmp]

	st = {}
	for l in tmp:
		if not l[0] in st:
			st[l[0]] = len(st.keys())
	tmp = [  [st[lst[0]]] + lst[1:] for lst in tmp ]
	THREAD_AMOUNT = len(st.keys())

	st = {}
	for l in tmp:
		if not l[1] in st:
			st[l[1]] = len(st.keys())
	tmp = [  [lst[0]] + [st[lst[1]]] + lst[2:] for lst in tmp ]


	BAND = Options.options.dband

	seen = {}
	acc = []
	for x in range(len(tmp)):
		line = tmp[x]
		id = line[1]

		if id in seen:
			continue
		seen[id] = True

		begin = line[2]
		thread_id = line[0]
		for y in range(x + 1, len(tmp)):
			line = tmp[y]
			if line[1] == id:
				end = line[2]
				#print id, thread_id, begin, end
				#acc.append(  ( 10*thread_id, 10*(thread_id+1), 10*begin, 10*end ) )
				acc.append( (BAND * begin, BAND*thread_id, BAND*end - BAND*begin, BAND, line[3]) )
				break

	if Options.options.dmaxtime < 0.1:
		gwidth = 1
		for x in tmp:
			m = BAND * x[2]
			if m > gwidth:
				gwidth = m
	else:
		gwidth = BAND * Options.options.dmaxtime

	ratio = float(Options.options.dwidth) / gwidth
	gwidth = Options.options.dwidth

	gheight = BAND * (THREAD_AMOUNT + len(info) + 1.5)

	out = []

	out.append("""<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"
\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">
<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.0\"
   x=\"%r\" y=\"%r\" width=\"%r\" height=\"%r\"
   id=\"svg602\" xml:space=\"preserve\">

<style type='text/css' media='screen'>
	g.over rect  { stroke:#FF0000; fill-opacity:0.4 }
</style>

<script type='text/javascript'><![CDATA[
var svg  = document.getElementsByTagName('svg')[0];

svg.addEventListener('mouseover', function(e) {
	var g = e.target.parentNode;
	var x = document.getElementById('r_' + g.id);
	if (x) {
		g.setAttribute('class', g.getAttribute('class') + ' over');
		x.setAttribute('class', x.getAttribute('class') + ' over');
		showInfo(e, g.id);
	}
}, false);

svg.addEventListener('mouseout', function(e) {
		var g = e.target.parentNode;
		var x = document.getElementById('r_' + g.id);
		if (x) {
			g.setAttribute('class', g.getAttribute('class').replace(' over', ''));
			x.setAttribute('class', x.getAttribute('class').replace(' over', ''));
			hideInfo(e);
		}
}, false);

function showInfo(evt, txt) {
	tooltip = document.getElementById('tooltip');

	var t = document.getElementById('tooltiptext');
	t.firstChild.data = txt;

	var x = evt.clientX + 9;
	if (x > 250) { x -= t.getComputedTextLength() + 16; }
	var y = evt.clientY + 20;
	tooltip.setAttribute("transform", "translate(" + x + "," + y + ")");
	tooltip.setAttributeNS(null, "visibility", "visible");

	var r = document.getElementById('tooltiprect');
	r.setAttribute('width', t.getComputedTextLength() + 6);
}

function hideInfo(evt) {
	var tooltip = document.getElementById('tooltip');
	tooltip.setAttributeNS(null,"visibility","hidden");
}
]]></script>

<!-- inkscape requires a big rectangle or it will not export the pictures properly -->
<rect
   x='%r' y='%r'
   width='%r' height='%r'
   style=\"font-size:10;fill:#ffffff;fill-opacity:0.01;fill-rule:evenodd;stroke:#ffffff;\"
   />\n

""" % (0, 0, gwidth + 4, gheight + 4,   0, 0, gwidth + 4, gheight + 4))

	# main title
	if Options.options.dtitle:
		out.append("""<text x="%d" y="%d" style="font-size:15px; text-anchor:middle; font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans">%s</text>
""" % (gwidth/2, gheight - 5, Options.options.dtitle))

	# the rectangles

	groups = {}
	for (x, y, w, h, clsname) in acc:
		try:
			groups[clsname].append((x, y, w, h))
		except:
			groups[clsname] = [(x, y, w, h)]
	for cls in groups:
		out.append("<g id='%s'>\n" % name2class(cls))

		for (x, y, w, h) in groups[cls]:
			out.append("""<rect
   x='%r' y='%r'
   width='%r' height='%r'
   style=\"font-size:10;fill:%s;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;\"
   />\n""" % (2 + x*ratio, 2 + y, w*ratio, h, map_to_color(cls)))
		out.append("</g>\n")

	# output the caption
	cnt = THREAD_AMOUNT

	for (text, color) in info:
		# caption box
		b = BAND/2
		out.append("""<g id='r_%s'><rect
		x='%r' y='%r'
		width='%r' height='%r'
		style=\"font-size:10;fill:%s;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;\"
  />\n""" %                       (name2class(text), 2 + BAND,     5 + (cnt + 0.5) * BAND, b, b, color))

		# caption text
		out.append("""<text
   style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
   x="%r" y="%d">%s</text></g>\n""" % (2 + 2 * BAND, 5 + (cnt + 0.5) * BAND + 10, text))
		cnt += 1

	out.append("""
<g transform="translate(0,0)" visibility="hidden" id="tooltip">
  <rect id="tooltiprect" y="-15" x="-3" width="1" height="20" style="stroke:black;fill:#edefc2;stroke-width:1"/>
  <text id="tooltiptext" style="font-family:Arial; font-size:12;fill:black;"> </text>
</g>""")

	out.append("\n</svg>")

	node = producer.bld.path.make_node('pdebug.svg')
	node.write("".join(out))
	Logs.warn('Created the diagram %r' % node.abspath())

	p = node.parent.abspath()
	producer.bld.exec_command(['convert', p + os.sep + 'pdebug.svg', p + os.sep + 'pdebug.png'])

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112