| 1 | #!/usr/bin/python3
 | 
|---|
| 2 | """
 | 
|---|
| 3 | Python Script to implement R.M.I.T. testing : Randomized Multiple Interleaved Trials
 | 
|---|
| 4 | 
 | 
|---|
| 5 | ./rmit.py run COMMAND CANDIDATES
 | 
|---|
| 6 | -t trials
 | 
|---|
| 7 | -o option:values
 | 
|---|
| 8 | """
 | 
|---|
| 9 | 
 | 
|---|
| 10 | 
 | 
|---|
| 11 | import argparse
 | 
|---|
| 12 | import datetime
 | 
|---|
| 13 | import itertools
 | 
|---|
| 14 | import json
 | 
|---|
| 15 | import os
 | 
|---|
| 16 | import random
 | 
|---|
| 17 | import re
 | 
|---|
| 18 | import socket
 | 
|---|
| 19 | import subprocess
 | 
|---|
| 20 | import sys
 | 
|---|
| 21 | 
 | 
|---|
| 22 | 
 | 
|---|
| 23 | def parse_range(x):
 | 
|---|
| 24 |     result = []
 | 
|---|
| 25 |     for part in x.split(','):
 | 
|---|
| 26 |         if '-' in part:
 | 
|---|
| 27 |             a, b = part.split('-')
 | 
|---|
| 28 |             a, b = int(a), int(b)
 | 
|---|
| 29 |             result.extend(range(a, b + 1))
 | 
|---|
| 30 |         else:
 | 
|---|
| 31 |             a = int(part)
 | 
|---|
| 32 |             result.append(a)
 | 
|---|
| 33 |     return result
 | 
|---|
| 34 | 
 | 
|---|
| 35 | class DependentOpt:
 | 
|---|
| 36 |         def __init__(self, key, value):
 | 
|---|
| 37 |                 self.key = key
 | 
|---|
| 38 |                 self.value = value
 | 
|---|
| 39 |                 self.vars = re.findall("[a-zA-Z]", value)
 | 
|---|
| 40 | 
 | 
|---|
| 41 | def parse_option(key, values):
 | 
|---|
| 42 |         try:
 | 
|---|
| 43 |                 num = int(values)
 | 
|---|
| 44 |                 return key, [num]
 | 
|---|
| 45 |         except:
 | 
|---|
| 46 |                 pass
 | 
|---|
| 47 | 
 | 
|---|
| 48 |         if re.search("^[0-9-,]+$", values):
 | 
|---|
| 49 |                 values = parse_range(values)
 | 
|---|
| 50 |                 return key, [v for v in values]
 | 
|---|
| 51 |         else:
 | 
|---|
| 52 |                 return key, DependentOpt(key, values)
 | 
|---|
| 53 | 
 | 
|---|
| 54 | def eval_one(fmt, vals):
 | 
|---|
| 55 |         orig = fmt
 | 
|---|
| 56 |         for k, v in vals:
 | 
|---|
| 57 |                 fmt = fmt.replace(k, str(v))
 | 
|---|
| 58 | 
 | 
|---|
| 59 |         if not re.search("^[0-9-/*+ ]+$", fmt):
 | 
|---|
| 60 |                 print('ERROR: pattern option {} (interpreted as {}) could not be evaluated'.format(orig, fmt), file=sys.stderr)
 | 
|---|
| 61 |                 sys.exit(1)
 | 
|---|
| 62 | 
 | 
|---|
| 63 |         return eval(fmt)
 | 
|---|
| 64 | 
 | 
|---|
| 65 | def eval_options(opts):
 | 
|---|
| 66 |         dependents = [d for d in opts.values() if type(d) is DependentOpt]
 | 
|---|
| 67 |         processed = []
 | 
|---|
| 68 |         nopts = []
 | 
|---|
| 69 |         for d in dependents:
 | 
|---|
| 70 |                 processed.append(d.key)
 | 
|---|
| 71 |                 lists = []
 | 
|---|
| 72 |                 for dvar in d.vars:
 | 
|---|
| 73 |                         if not dvar in opts.keys():
 | 
|---|
| 74 |                                 print('ERROR: extra pattern option {}:{} uses unknown key {}'.format(d.key,d.value,dvar), file=sys.stderr)
 | 
|---|
| 75 |                                 sys.exit(1)
 | 
|---|
| 76 | 
 | 
|---|
| 77 |                         lists.append([(dvar, o) for o in opts[dvar]])
 | 
|---|
| 78 |                         processed.append(dvar)
 | 
|---|
| 79 | 
 | 
|---|
| 80 |                 kopt = []
 | 
|---|
| 81 |                 for vals in list(itertools.product(*lists)):
 | 
|---|
| 82 |                         res = ['-{}'.format(d.key), "{}".format(eval_one(d.value, vals))]
 | 
|---|
| 83 |                         for k, v in vals:
 | 
|---|
| 84 |                                 res.extend(['-{}'.format(k), "{}".format(v)])
 | 
|---|
| 85 |                         kopt.append(res)
 | 
|---|
| 86 |                 nopts.append(kopt)
 | 
|---|
| 87 | 
 | 
|---|
| 88 | 
 | 
|---|
| 89 |         for k, vals in opts.items():
 | 
|---|
| 90 |                 if k not in processed:
 | 
|---|
| 91 |                         kopt = []
 | 
|---|
| 92 |                         for v in vals:
 | 
|---|
| 93 |                                 kopt.append(['-{}'.format(k), "{}".format(v)])
 | 
|---|
| 94 |                         nopts.append(kopt)
 | 
|---|
| 95 | 
 | 
|---|
| 96 |         return nopts
 | 
|---|
| 97 | 
 | 
|---|
| 98 | # returns the first option with key 'opt'
 | 
|---|
| 99 | def search_option(action, opt):
 | 
|---|
| 100 |         i = 0
 | 
|---|
| 101 |         while i < len(action):
 | 
|---|
| 102 |                 if action[i] == opt:
 | 
|---|
| 103 |                         i += 1
 | 
|---|
| 104 |                         if i != len(action):
 | 
|---|
| 105 |                                 return action[i]
 | 
|---|
| 106 |                 i += 1
 | 
|---|
| 107 | 
 | 
|---|
| 108 |         return None
 | 
|---|
| 109 | 
 | 
|---|
| 110 | def actions_eta(actions):
 | 
|---|
| 111 |         time = 0
 | 
|---|
| 112 |         for a in actions:
 | 
|---|
| 113 |                 o = search_option(a, '-d')
 | 
|---|
| 114 |                 if o :
 | 
|---|
| 115 |                         time += int(o)
 | 
|---|
| 116 |         return time
 | 
|---|
| 117 | 
 | 
|---|
| 118 | taskset_maps = None
 | 
|---|
| 119 | 
 | 
|---|
| 120 | def init_taskset_maps():
 | 
|---|
| 121 |         global taskset_maps
 | 
|---|
| 122 |         known_hosts = {
 | 
|---|
| 123 |                 "jax": {
 | 
|---|
| 124 |                         range(  1,  24) : "48-71",
 | 
|---|
| 125 |                         range( 25,  48) : "48-71,144-167",
 | 
|---|
| 126 |                         range( 49,  96) : "48-95,144-191",
 | 
|---|
| 127 |                         range( 97, 144) : "24-95,120-191",
 | 
|---|
| 128 |                         range(145, 192) : "0-95,96-191",
 | 
|---|
| 129 |                 },
 | 
|---|
| 130 |         }
 | 
|---|
| 131 | 
 | 
|---|
| 132 |         if (host := socket.gethostname()) in known_hosts:
 | 
|---|
| 133 |                 taskset_maps = known_hosts[host]
 | 
|---|
| 134 |                 return True
 | 
|---|
| 135 | 
 | 
|---|
| 136 |         print("Warning unknown host '{}', disable taskset usage".format(host), file=sys.stderr)
 | 
|---|
| 137 |         return False
 | 
|---|
| 138 | 
 | 
|---|
| 139 | 
 | 
|---|
| 140 | def settaskset_one(action):
 | 
|---|
| 141 |         o = search_option(action, '-p')
 | 
|---|
| 142 |         if not o:
 | 
|---|
| 143 |                 return action
 | 
|---|
| 144 |         try:
 | 
|---|
| 145 |                 oi = int(o)
 | 
|---|
| 146 |         except ValueError:
 | 
|---|
| 147 |                 return action
 | 
|---|
| 148 | 
 | 
|---|
| 149 |         m = "Not found"
 | 
|---|
| 150 |         for key in taskset_maps:
 | 
|---|
| 151 |                 if oi in key:
 | 
|---|
| 152 |                         return ['taskset', '-c', taskset_maps[key], *action]
 | 
|---|
| 153 | 
 | 
|---|
| 154 |         print("Warning no mapping for {} cores".format(oi), file=sys.stderr)
 | 
|---|
| 155 |         return action
 | 
|---|
| 156 | 
 | 
|---|
| 157 | def settaskset(actions):
 | 
|---|
| 158 |         return [settaskset_one(a) for a in actions]
 | 
|---|
| 159 | 
 | 
|---|
| 160 | if __name__ == "__main__":
 | 
|---|
| 161 |         # ================================================================================
 | 
|---|
| 162 |         # parse command line arguments
 | 
|---|
| 163 |         formats = ['raw', 'csv', 'json']
 | 
|---|
| 164 |         parser = argparse.ArgumentParser(description='Python Script to implement R.M.I.T. testing : Randomized Multiple Interleaved Trials')
 | 
|---|
| 165 |         parser.add_argument('--list', help='List all the commands that would be run', action='store_true')
 | 
|---|
| 166 |         parser.add_argument('--file', nargs='?', type=argparse.FileType('w'), default=sys.stdout)
 | 
|---|
| 167 |         parser.add_argument('--trials', help='Number of trials to run per combinaison', type=int, default=3)
 | 
|---|
| 168 |         parser.add_argument('--notaskset', help='If specified, the trial will not use taskset to match the -p option', action='store_true')
 | 
|---|
| 169 |         parser.add_argument('command', metavar='command', type=str, nargs=1, help='the command prefix to run')
 | 
|---|
| 170 |         parser.add_argument('candidates', metavar='candidates', type=str, nargs='*', help='the candidate suffix to run')
 | 
|---|
| 171 | 
 | 
|---|
| 172 |         try:
 | 
|---|
| 173 |                 options, unknown =  parser.parse_known_args()
 | 
|---|
| 174 | 
 | 
|---|
| 175 |                 options.option = []
 | 
|---|
| 176 |                 while unknown:
 | 
|---|
| 177 |                         key = unknown.pop(0)
 | 
|---|
| 178 |                         val = unknown.pop(0)
 | 
|---|
| 179 | 
 | 
|---|
| 180 |                         if key[0] != '-':
 | 
|---|
| 181 |                                 raise ValueError
 | 
|---|
| 182 | 
 | 
|---|
| 183 |                         options.option.append((key[1:], val))
 | 
|---|
| 184 | 
 | 
|---|
| 185 |         except:
 | 
|---|
| 186 |                 print('ERROR: invalid arguments', file=sys.stderr)
 | 
|---|
| 187 |                 parser.print_help(sys.stderr)
 | 
|---|
| 188 |                 sys.exit(1)
 | 
|---|
| 189 | 
 | 
|---|
| 190 |         # ================================================================================
 | 
|---|
| 191 |         # Identify the commands to run
 | 
|---|
| 192 |         command = './' + options.command[0]
 | 
|---|
| 193 |         if options.candidates:
 | 
|---|
| 194 |                 commands = [command + "-" + c for c in options.candidates]
 | 
|---|
| 195 |         else:
 | 
|---|
| 196 |                 commands = [command]
 | 
|---|
| 197 |         for c in commands:
 | 
|---|
| 198 |                 if not os.path.isfile(c):
 | 
|---|
| 199 |                         print('ERROR: invalid command {}, file does not exist'.format(c), file=sys.stderr)
 | 
|---|
| 200 |                         sys.exit(1)
 | 
|---|
| 201 | 
 | 
|---|
| 202 |                 if not os.access(c, os.X_OK):
 | 
|---|
| 203 |                         print('ERROR: invalid command {}, file not executable'.format(c), file=sys.stderr)
 | 
|---|
| 204 |                         sys.exit(1)
 | 
|---|
| 205 | 
 | 
|---|
| 206 | 
 | 
|---|
| 207 |         # ================================================================================
 | 
|---|
| 208 |         # Identify the options to run
 | 
|---|
| 209 |         opts = dict([parse_option(k, v) for k, v in options.option])
 | 
|---|
| 210 | 
 | 
|---|
| 211 |         # Evaluate the options (options can depend on the value of other options)
 | 
|---|
| 212 |         opts = eval_options(opts)
 | 
|---|
| 213 | 
 | 
|---|
| 214 |         # ================================================================================
 | 
|---|
| 215 |         # Figure out all the combinations to run
 | 
|---|
| 216 |         actions = []
 | 
|---|
| 217 |         for p in itertools.product(range(options.trials), commands, *opts):
 | 
|---|
| 218 |                 act = [p[1]]
 | 
|---|
| 219 |                 for o in p[2:]:
 | 
|---|
| 220 |                         act.extend(o)
 | 
|---|
| 221 |                 actions.append(act)
 | 
|---|
| 222 | 
 | 
|---|
| 223 |         # ================================================================================
 | 
|---|
| 224 |         # Fixup the different commands
 | 
|---|
| 225 | 
 | 
|---|
| 226 |         # Add tasksets
 | 
|---|
| 227 |         withtaskset = False
 | 
|---|
| 228 |         if not options.notaskset and init_taskset_maps():
 | 
|---|
| 229 |                 withtaskset = True
 | 
|---|
| 230 |                 actions = settaskset(actions)
 | 
|---|
| 231 | 
 | 
|---|
| 232 |         # ================================================================================
 | 
|---|
| 233 |         # Now that we know what to run, print it.
 | 
|---|
| 234 |         # find expected time
 | 
|---|
| 235 |         time = actions_eta(actions)
 | 
|---|
| 236 |         print("Running {} trials{}".format(len(actions), "" if time == 0 else " (expecting to take {})".format(str(datetime.timedelta(seconds=int(time)))) ))
 | 
|---|
| 237 | 
 | 
|---|
| 238 |         # dry run if options ask for it
 | 
|---|
| 239 |         if options.list:
 | 
|---|
| 240 |                 for a in actions:
 | 
|---|
| 241 |                         print(" ".join(a))
 | 
|---|
| 242 |                 sys.exit(0)
 | 
|---|
| 243 | 
 | 
|---|
| 244 | 
 | 
|---|
| 245 |         # ================================================================================
 | 
|---|
| 246 |         # Prepare to run
 | 
|---|
| 247 | 
 | 
|---|
| 248 |         random.shuffle(actions)
 | 
|---|
| 249 | 
 | 
|---|
| 250 |         # ================================================================================
 | 
|---|
| 251 |         # Run
 | 
|---|
| 252 |         options.file.write("[")
 | 
|---|
| 253 |         first = True
 | 
|---|
| 254 |         for i, a in enumerate(actions):
 | 
|---|
| 255 |                 sa = " ".join(a[3:] if withtaskset else a)
 | 
|---|
| 256 |                 if first:
 | 
|---|
| 257 |                         first = False
 | 
|---|
| 258 |                 else:
 | 
|---|
| 259 |                         options.file.write(",")
 | 
|---|
| 260 |                 if options.file != sys.stdout:
 | 
|---|
| 261 |                         print("{}/{} : {}          \r".format(i, len(actions), sa), end = '')
 | 
|---|
| 262 |                 fields = {}
 | 
|---|
| 263 |                 with subprocess.Popen( a, stdout  = subprocess.PIPE, stderr  = subprocess.PIPE) as proc:
 | 
|---|
| 264 |                         out, err = proc.communicate()
 | 
|---|
| 265 |                         if proc.returncode != 0:
 | 
|---|
| 266 |                                 print("ERROR: command '{}' encountered error, returned code {}".format(sa, proc.returncode), file=sys.stderr)
 | 
|---|
| 267 |                                 print(err.decode("utf-8"))
 | 
|---|
| 268 |                                 sys.exit(1)
 | 
|---|
| 269 |                         for s in out.decode("utf-8").splitlines():
 | 
|---|
| 270 |                                 match = re.search("^(.*):(.*)$", s)
 | 
|---|
| 271 |                                 if match:
 | 
|---|
| 272 |                                         try:
 | 
|---|
| 273 |                                                 fields[match.group(1).strip()] = float(match.group(2).strip().replace(',',''))
 | 
|---|
| 274 |                                         except:
 | 
|---|
| 275 |                                                 pass
 | 
|---|
| 276 | 
 | 
|---|
| 277 |                 options.file.write(json.dumps([a[3 if withtaskset else 0][2:], sa, fields]))
 | 
|---|
| 278 |                 options.file.flush()
 | 
|---|
| 279 | 
 | 
|---|
| 280 |         options.file.write("]\n")
 | 
|---|
| 281 | 
 | 
|---|
| 282 |         if options.file != sys.stdout:
 | 
|---|
| 283 |                 print("Done                                                                                ")
 | 
|---|