1#!/usr/bin/env python3
2#
3#  BLIS
4#  An object-based framework for developing high-performance BLAS-like
5#  libraries.
6#
7#  Copyright (C) 2018, The University of Texas at Austin
8#  Copyright (C) 2018 - 2019, Advanced Micro Devices, Inc.
9#
10#  Redistribution and use in source and binary forms, with or without
11#  modification, are permitted provided that the following conditions are
12#  met:
13#   - Redistributions of source code must retain the above copyright
14#     notice, this list of conditions and the following disclaimer.
15#   - Redistributions in binary form must reproduce the above copyright
16#     notice, this list of conditions and the following disclaimer in the
17#     documentation and/or other materials provided with the distribution.
18#   - Neither the name(s) of the copyright holder(s) nor the names of its
19#     contributors may be used to endorse or promote products derived
20#     from this software without specific prior written permission.
21#
22#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26#  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33#
34#
35
36# Import modules
37import os
38import sys
39import getopt
40import re
41import subprocess
42import time
43import statistics
44
45
46def print_usage():
47
48	my_print( " " )
49	my_print( " %s" % script_name )
50	my_print( " " )
51	my_print( " Field G. Van Zee" )
52	my_print( " " )
53	my_print( " Repeatedly run a test driver and accumulate statistics for the" )
54	my_print( " output." )
55	my_print( " " )
56	my_print( " Usage:" )
57	my_print( " " )
58	my_print( "   %s [options] drivername" % script_name )
59	my_print( " " )
60	my_print( " Arguments:" )
61	my_print( " " )
62	my_print( "   drivername    The filename/path of the test driver to run. The" )
63	my_print( "                 test driver must output its performance data to" )
64	my_print( "                 standard output." )
65	my_print( " " )
66	my_print( " The following options are accepted:" )
67	my_print( " " )
68	my_print( "   -c num      performance column index" )
69	my_print( "                 Find the performance result in column index <num> of" )
70	my_print( "                 the test driver's output. Here, a column is defined" )
71	my_print( "                 as a contiguous sequence of non-whitespace characters," )
72	my_print( "                 with the column indices beginning at 0. By default," )
73	my_print( "                 the second-to-last column index in the output is used." )
74	my_print( " " )
75	my_print( "   -d delay    sleep() delay" )
76	my_print( "                 Wait <delay> seconds after each execution of the" )
77	my_print( "                 test driver. The default delay is 0." )
78	my_print( " " )
79	my_print( "   -n niter    number of iterations" )
80	my_print( "                 Execute the test driver <niter> times. The default" )
81	my_print( "                 value is 10." )
82	my_print( " " )
83	my_print( "   -q          quiet; summary only" )
84	my_print( "                 Do not output statistics after every new execution of" )
85	my_print( "                 the test driver; instead, only output the final values" )
86	my_print( "                 after all iterations are complete. The default is to" )
87	my_print( "                 output updated statistics after each iteration." )
88	my_print( " " )
89	my_print( "   -h          help" )
90	my_print( "                 Output this information and exit." )
91	my_print( " " )
92
93
94# ------------------------------------------------------------------------------
95
96def my_print( s ):
97
98	sys.stdout.write( "%s\n" % s )
99	#sys.stdout.flush()
100
101# ------------------------------------------------------------------------------
102
103# Global variables.
104script_name    = None
105output_name    = None
106
107def main():
108
109	global script_name
110	global output_name
111
112	# Obtain the script name.
113	path, script_name = os.path.split(sys.argv[0])
114
115	output_name = script_name
116
117	# Default values for optional arguments.
118	#perf_col = 9
119	perf_col = -1
120	delay    = 0
121	niter    = 10
122	quiet    = False
123
124	# Process our command line options.
125	try:
126		opts, args = getopt.getopt( sys.argv[1:], "c:d:n:hq" )
127
128	except getopt.GetoptError as err:
129		# print help information and exit:
130		my_print( str(err) ) # will print something like "option -a not recognized"
131		print_usage()
132		sys.exit(2)
133
134	for opt, optarg in opts:
135		if   opt == "-c":
136			perf_col = optarg
137		elif opt == "-d":
138			delay = optarg
139		elif opt == "-n":
140			niter = optarg
141		elif opt == "-q":
142			quiet = True
143		elif opt == "-h":
144			print_usage()
145			sys.exit()
146		else:
147			print_usage()
148			sys.exit()
149
150	# Print usage if we don't have exactly one argument.
151	if len( args ) != 1:
152		print_usage()
153		sys.exit()
154
155	# Acquire our only mandatory argument: the name of the test driver.
156	driverfile = args[0]
157
158	#my_print( "test driver: %s" % driverfile )
159	#my_print( "column num:  %s" % perf_col )
160	#my_print( "delay:       %s" % delay )
161	#my_print( "num iter:    %s" % niter )
162
163	# Build a list of iterations.
164	iters = range( int(niter) )
165
166	# Run the test driver once to detect the number of lines of output.
167	p = subprocess.run( driverfile, stdout=subprocess.PIPE )
168	lines0 = p.stdout.decode().splitlines()
169	num_lines0 = int(len(lines0))
170
171	# Initialize the list of lists (one list per performance result).
172	aperf = []
173	for i in range( num_lines0 ):
174		aperf.append( [] )
175
176	for it in iters:
177
178		# Run the test driver.
179		p = subprocess.run( driverfile, stdout=subprocess.PIPE )
180
181		# Acquire the lines of output.
182		lines = p.stdout.decode().splitlines()
183
184		# Accumulate the test driver's latest results into aperf.
185		for i in range( num_lines0 ):
186
187			# Parse the current line to find the performance value.
188			line  = lines[i]
189			words = line.split()
190			if perf_col == -1:
191				perf  = words[ len(words)-2 ]
192			else:
193				perf  = words[ int(perf_col) ]
194
195			# As unlikely as it is, guard against Inf and NaN.
196			if float(perf) ==  float('Inf') or \
197			   float(perf) == -float('Inf') or \
198			   float(perf) ==  float('NaN'): perf = 0.0
199
200			# Add the performance value to the list at the ith entry of aperf.
201			aperf[i].append( float(perf) )
202
203			# Compute stats for the current line.
204			avgp = statistics.mean( aperf[i] )
205			maxp =             max( aperf[i] )
206			minp =             min( aperf[i] )
207
208			# Only compute stdev() when we have two or more data points.
209			if len( aperf[i] ) > 1: stdp = statistics.stdev( aperf[i] )
210			else:                   stdp = 0.0
211
212			# Construct a string to match the performance value and then
213			# use that string to search-and-replace with four format specs
214			# for the min, avg, max, and stdev values computed above.
215			search = '%8s' % perf
216			newline = re.sub( str(search), ' %7.2f %7.2f %7.2f %6.2f', line )
217
218			# Search for the column index range that would be present if this were
219			# matlab-compatible output. The index range will typically be 1:n,
220			# where n is the number of columns of data.
221			found_index = False
222			for word in words:
223				if re.match( '1:', word ):
224					index_str = word
225					found_index = True
226					break
227
228			# If we find the column index range, we need to update it to reflect
229			# the replacement of one column of data with four, for a net increase
230			# of columns. We do so via another instance of re.sub() in which we
231			# search for the old index string and replace it with the new one.
232			if found_index:
233				last_col = int(index_str[2]) + 3
234				new_index_str = '1:%1s' % last_col
235				newline = re.sub( index_str, new_index_str, newline )
236
237			# If the quiet flag was not give, output the intermediate results.
238			if not quiet:
239				print( newline % ( float(minp), float(avgp), float(maxp), float(stdp) ) )
240
241		# Flush stdout after each set of output prior to sleeping.
242		sys.stdout.flush()
243
244		# Sleep for a bit until the next iteration.
245		time.sleep( int(delay) )
246
247	# If the quiet flag was given, output the final results.
248	if quiet:
249
250		for i in range( num_lines0 ):
251
252			# Parse the current line to find the performance value (only
253			# needed for call to re.sub() below).
254			line  = lines0[i]
255			words = line.split()
256			if perf_col == -1:
257				perf  = words[ len(words)-2 ]
258			else:
259				perf  = words[ int(perf_col) ]
260
261			# Compute stats for the current line.
262			avgp = statistics.mean( aperf[i] )
263			maxp =             max( aperf[i] )
264			minp =             min( aperf[i] )
265
266			# Only compute stdev() when we have two or more data points.
267			if len( aperf[i] ) > 1: stdp = statistics.stdev( aperf[i] )
268			else:                   stdp = 0.0
269
270			# Construct a string to match the performance value and then
271			# use that string to search-and-replace with four format specs
272			# for the min, avg, max, and stdev values computed above.
273			search = '%8s' % perf
274			newline = re.sub( str(search), ' %7.2f %7.2f %7.2f %6.2f', line )
275
276			# Search for the column index range that would be present if this were
277			# matlab-compatible output. The index range will typically be 1:n,
278			# where n is the number of columns of data.
279			found_index = False
280			for word in words:
281				if re.match( '1:', word ):
282					index_str = word
283					found_index = True
284					break
285
286			# If we find the column index range, we need to update it to reflect
287			# the replacement of one column of data with four, for a net increase
288			# of columns. We do so via another instance of re.sub() in which we
289			# search for the old index string and replace it with the new one.
290			if found_index:
291				last_col = int(index_str[2]) + 3
292				new_index_str = '1:%1s' % last_col
293				newline = re.sub( index_str, new_index_str, newline )
294
295			# Output the results for the current line.
296			print( newline % ( float(minp), float(avgp), float(maxp), float(stdp) ) )
297
298		# Flush stdout afterwards.
299		sys.stdout.flush()
300
301
302	# Return from main().
303	return 0
304
305
306
307
308if __name__ == "__main__":
309	main()
310