| 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 values.startswith('\\'):
 | 
|---|
| 49 |                 return key, values[1:].split(',')
 | 
|---|
| 50 |         elif re.search("^[0-9-,]+$", values):
 | 
|---|
| 51 |                 values = parse_range(values)
 | 
|---|
| 52 |                 return key, [v for v in values]
 | 
|---|
| 53 |         else:
 | 
|---|
| 54 |                 return key, DependentOpt(key, values)
 | 
|---|
| 55 | 
 | 
|---|
| 56 | def eval_one(fmt, vals):
 | 
|---|
| 57 |         orig = fmt
 | 
|---|
| 58 |         for k, v in vals:
 | 
|---|
| 59 |                 fmt = fmt.replace(k, str(v))
 | 
|---|
| 60 | 
 | 
|---|
| 61 |         if not re.search("^[0-9-/*+ ]+$", fmt):
 | 
|---|
| 62 |                 print('ERROR: pattern option {} (interpreted as {}) could not be evaluated'.format(orig, fmt), file=sys.stderr)
 | 
|---|
| 63 |                 sys.exit(1)
 | 
|---|
| 64 | 
 | 
|---|
| 65 |         return eval(fmt)
 | 
|---|
| 66 | 
 | 
|---|
| 67 | # Evaluate all the options
 | 
|---|
| 68 | # options can be of the for key = val or key = some_math(other_key)
 | 
|---|
| 69 | # produce a list of all the options to replace some_math(other_key) with actual value
 | 
|---|
| 70 | def eval_options(opts):
 | 
|---|
| 71 |         # Find all the options with dependencies
 | 
|---|
| 72 |         dependents = [d for d in opts.values() if type(d) is DependentOpt]
 | 
|---|
| 73 | 
 | 
|---|
| 74 |         # we need to find all the straglers
 | 
|---|
| 75 |         processed = []
 | 
|---|
| 76 | 
 | 
|---|
| 77 |         # extract all the necessary inputs
 | 
|---|
| 78 |         input_keys = {}
 | 
|---|
| 79 |         for d in dependents:
 | 
|---|
| 80 |                 # Mark the dependent as seen
 | 
|---|
| 81 |                 processed.append(d.key)
 | 
|---|
| 82 | 
 | 
|---|
| 83 |                 # process each of the dependencies
 | 
|---|
| 84 |                 for dvar in d.vars:
 | 
|---|
| 85 |                         # Check that it depends on something that exists
 | 
|---|
| 86 |                         if not dvar in opts.keys():
 | 
|---|
| 87 |                                 print('ERROR: extra pattern option {}:{} uses unknown key {}'.format(d.key,d.value,dvar), file=sys.stderr)
 | 
|---|
| 88 |                                 sys.exit(1)
 | 
|---|
| 89 | 
 | 
|---|
| 90 |                         # Check that it's not nested
 | 
|---|
| 91 |                         if type(dvar) is DependentOpt:
 | 
|---|
| 92 |                                 print('ERROR: dependent options cannot be nested {}:{} uses key {}'.format(d.key,d.value,dvar), file=sys.stderr)
 | 
|---|
| 93 |                                 sys.exit(1)
 | 
|---|
| 94 | 
 | 
|---|
| 95 |                         # Add the values to the input keys
 | 
|---|
| 96 |                         if dvar not in input_keys:
 | 
|---|
| 97 |                                 input_keys[dvar] = opts[dvar]
 | 
|---|
| 98 |                         else :
 | 
|---|
| 99 |                                 if input_keys[dvar] != opts[dvar]:
 | 
|---|
| 100 |                                         print('INTERNAL ERROR: repeat input do not match {}:{} vs {}'.format(dvar,opts[dvar],input_keys[dvar]), file=sys.stderr)
 | 
|---|
| 101 |                                         sys.exit(1)
 | 
|---|
| 102 | 
 | 
|---|
| 103 |                         # Mark the input as seen
 | 
|---|
| 104 |                         processed.append(dvar)
 | 
|---|
| 105 | 
 | 
|---|
| 106 |         # add in all the straglers they should cause too many problems
 | 
|---|
| 107 |         for k, v in opts.items():
 | 
|---|
| 108 |                 if type(v) is DependentOpt:
 | 
|---|
| 109 |                         continue
 | 
|---|
| 110 | 
 | 
|---|
| 111 |                 if k in processed:
 | 
|---|
| 112 |                         # consistency check
 | 
|---|
| 113 |                         if k not in input_keys:
 | 
|---|
| 114 |                                 print('INTERNAL ERROR: key \'{}\' marked as processed but not in input_keys'.format(k), file=sys.stderr)
 | 
|---|
| 115 |                                 sys.exit(1)
 | 
|---|
| 116 |                         continue
 | 
|---|
| 117 | 
 | 
|---|
| 118 |                 # consistency check
 | 
|---|
| 119 |                 if k in input_keys:
 | 
|---|
| 120 |                         print('INTERNAL ERROR: key \'{}\' in input_keys but not marked as processed'.format(k), file=sys.stderr)
 | 
|---|
| 121 |                         sys.exit(1)
 | 
|---|
| 122 | 
 | 
|---|
| 123 |                 # add the straggler
 | 
|---|
| 124 |                 input_keys[k] = v
 | 
|---|
| 125 | 
 | 
|---|
| 126 |         # flatten the dict into a list of pairs so it's easier to work with
 | 
|---|
| 127 |         input_list = []
 | 
|---|
| 128 |         for k, v in input_keys.items():
 | 
|---|
| 129 |                 input_list.append([(k, o) for o in v])
 | 
|---|
| 130 | 
 | 
|---|
| 131 |         # evaluate all the dependents
 | 
|---|
| 132 |         # they are not allowed to produce new values so it's a one-to-one mapping from here
 | 
|---|
| 133 |         evaluated = []
 | 
|---|
| 134 |         for inputs in list(itertools.product(*input_list)):
 | 
|---|
| 135 |                 this_eval = list(inputs)
 | 
|---|
| 136 |                 for d in dependents:
 | 
|---|
| 137 |                         this_eval.append((d.key, eval_one(d.value, inputs)))
 | 
|---|
| 138 | 
 | 
|---|
| 139 |                 evaluated.append(this_eval)
 | 
|---|
| 140 | 
 | 
|---|
| 141 |         # reformat everything to a list of arguments
 | 
|---|
| 142 |         formated = []
 | 
|---|
| 143 |         for o in evaluated:
 | 
|---|
| 144 |                 inner = []
 | 
|---|
| 145 |                 for k,v in o:
 | 
|---|
| 146 |                         inner.append("-{}".format(k))
 | 
|---|
| 147 |                         inner.append("{}".format(v))
 | 
|---|
| 148 | 
 | 
|---|
| 149 |                 # print(inner)
 | 
|---|
| 150 |                 formated.append(inner)
 | 
|---|
| 151 | 
 | 
|---|
| 152 |         return formated
 | 
|---|
| 153 | 
 | 
|---|
| 154 | # returns the first option with key 'opt'
 | 
|---|
| 155 | def search_option(action, opt):
 | 
|---|
| 156 |         i = 0
 | 
|---|
| 157 |         while i < len(action):
 | 
|---|
| 158 |                 if action[i] == opt:
 | 
|---|
| 159 |                         i += 1
 | 
|---|
| 160 |                         if i != len(action):
 | 
|---|
| 161 |                                 return action[i]
 | 
|---|
| 162 |                 i += 1
 | 
|---|
| 163 | 
 | 
|---|
| 164 |         return None
 | 
|---|
| 165 | 
 | 
|---|
| 166 | def actions_eta(actions):
 | 
|---|
| 167 |         time = 0
 | 
|---|
| 168 |         for a in actions:
 | 
|---|
| 169 |                 o = search_option(a, '-d')
 | 
|---|
| 170 |                 if o :
 | 
|---|
| 171 |                         time += int(o)
 | 
|---|
| 172 |         return time
 | 
|---|
| 173 | 
 | 
|---|
| 174 | taskset_maps = None
 | 
|---|
| 175 | 
 | 
|---|
| 176 | def init_taskset_maps():
 | 
|---|
| 177 |         global taskset_maps
 | 
|---|
| 178 |         known_hosts = {
 | 
|---|
| 179 |                 "jax": {
 | 
|---|
| 180 |                         range(  1,  25) : "48-71",
 | 
|---|
| 181 |                         range( 25,  49) : "48-71,144-167",
 | 
|---|
| 182 |                         range( 49,  97) : "48-95,144-191",
 | 
|---|
| 183 |                         range( 97, 145) : "24-95,120-191",
 | 
|---|
| 184 |                         range(145, 193) : "0-95,96-191",
 | 
|---|
| 185 |                 },
 | 
|---|
| 186 |         }
 | 
|---|
| 187 | 
 | 
|---|
| 188 |         if (host := socket.gethostname()) in known_hosts:
 | 
|---|
| 189 |                 taskset_maps = known_hosts[host]
 | 
|---|
| 190 |                 return True
 | 
|---|
| 191 | 
 | 
|---|
| 192 |         print("Warning unknown host '{}', disable taskset usage".format(host), file=sys.stderr)
 | 
|---|
| 193 |         return False
 | 
|---|
| 194 | 
 | 
|---|
| 195 | 
 | 
|---|
| 196 | def settaskset_one(action):
 | 
|---|
| 197 |         o = search_option(action, '-p')
 | 
|---|
| 198 |         if not o:
 | 
|---|
| 199 |                 return action
 | 
|---|
| 200 |         try:
 | 
|---|
| 201 |                 oi = int(o)
 | 
|---|
| 202 |         except ValueError:
 | 
|---|
| 203 |                 return action
 | 
|---|
| 204 | 
 | 
|---|
| 205 |         m = "Not found"
 | 
|---|
| 206 |         for key in taskset_maps:
 | 
|---|
| 207 |                 if oi in key:
 | 
|---|
| 208 |                         return ['taskset', '-c', taskset_maps[key], *action]
 | 
|---|
| 209 | 
 | 
|---|
| 210 |         print("Warning no mapping for {} cores".format(oi), file=sys.stderr)
 | 
|---|
| 211 |         return action
 | 
|---|
| 212 | 
 | 
|---|
| 213 | def settaskset(actions):
 | 
|---|
| 214 |         return [settaskset_one(a) for a in actions]
 | 
|---|
| 215 | 
 | 
|---|
| 216 | if __name__ == "__main__":
 | 
|---|
| 217 |         # ================================================================================
 | 
|---|
| 218 |         # parse command line arguments
 | 
|---|
| 219 |         formats = ['raw', 'csv', 'json']
 | 
|---|
| 220 |         parser = argparse.ArgumentParser(description='Python Script to implement R.M.I.T. testing : Randomized Multiple Interleaved Trials')
 | 
|---|
| 221 |         parser.add_argument('--list', help='List all the commands that would be run', action='store_true')
 | 
|---|
| 222 |         parser.add_argument('--file', nargs='?', type=argparse.FileType('w'), default=sys.stdout)
 | 
|---|
| 223 |         parser.add_argument('--trials', help='Number of trials to run per combinaison', type=int, default=3)
 | 
|---|
| 224 |         parser.add_argument('--notaskset', help='If specified, the trial will not use taskset to match the -p option', action='store_true')
 | 
|---|
| 225 |         parser.add_argument('command', metavar='command', type=str, nargs=1, help='the command prefix to run')
 | 
|---|
| 226 |         parser.add_argument('candidates', metavar='candidates', type=str, nargs='*', help='the candidate suffix to run')
 | 
|---|
| 227 | 
 | 
|---|
| 228 |         try:
 | 
|---|
| 229 |                 options, unknown =  parser.parse_known_args()
 | 
|---|
| 230 | 
 | 
|---|
| 231 |                 options.option = []
 | 
|---|
| 232 |                 while unknown:
 | 
|---|
| 233 |                         key = unknown.pop(0)
 | 
|---|
| 234 |                         val = unknown.pop(0)
 | 
|---|
| 235 | 
 | 
|---|
| 236 |                         if key[0] != '-':
 | 
|---|
| 237 |                                 raise ValueError
 | 
|---|
| 238 | 
 | 
|---|
| 239 |                         options.option.append((key[1:], val))
 | 
|---|
| 240 | 
 | 
|---|
| 241 |         except:
 | 
|---|
| 242 |                 sys.exit(1)
 | 
|---|
| 243 | 
 | 
|---|
| 244 |         # ================================================================================
 | 
|---|
| 245 |         # Identify the commands to run
 | 
|---|
| 246 |         command = './' + options.command[0]
 | 
|---|
| 247 |         if options.candidates:
 | 
|---|
| 248 |                 commands = [command + "-" + c for c in options.candidates]
 | 
|---|
| 249 |         else:
 | 
|---|
| 250 |                 commands = [command]
 | 
|---|
| 251 |         for c in commands:
 | 
|---|
| 252 |                 if not os.path.isfile(c):
 | 
|---|
| 253 |                         print('ERROR: invalid command {}, file does not exist'.format(c), file=sys.stderr)
 | 
|---|
| 254 |                         sys.exit(1)
 | 
|---|
| 255 | 
 | 
|---|
| 256 |                 if not os.access(c, os.X_OK):
 | 
|---|
| 257 |                         print('ERROR: invalid command {}, file not executable'.format(c), file=sys.stderr)
 | 
|---|
| 258 |                         sys.exit(1)
 | 
|---|
| 259 | 
 | 
|---|
| 260 | 
 | 
|---|
| 261 |         # ================================================================================
 | 
|---|
| 262 |         # Identify the options to run
 | 
|---|
| 263 |         opts = dict([parse_option(k, v) for k, v in options.option])
 | 
|---|
| 264 | 
 | 
|---|
| 265 |         # Evaluate the options (options can depend on the value of other options)
 | 
|---|
| 266 |         opts = eval_options(opts)
 | 
|---|
| 267 | 
 | 
|---|
| 268 |         # ================================================================================
 | 
|---|
| 269 |         # Figure out all the combinations to run
 | 
|---|
| 270 |         actions = []
 | 
|---|
| 271 |         for p in itertools.product(range(options.trials), commands, opts):
 | 
|---|
| 272 |                 act = [p[1]]
 | 
|---|
| 273 |                 for o in p[2:]:
 | 
|---|
| 274 |                         act.extend(o)
 | 
|---|
| 275 |                 actions.append(act)
 | 
|---|
| 276 | 
 | 
|---|
| 277 |         # ================================================================================
 | 
|---|
| 278 |         # Fixup the different commands
 | 
|---|
| 279 | 
 | 
|---|
| 280 |         # Add tasksets
 | 
|---|
| 281 |         withtaskset = False
 | 
|---|
| 282 |         if not options.notaskset and init_taskset_maps():
 | 
|---|
| 283 |                 withtaskset = True
 | 
|---|
| 284 |                 actions = settaskset(actions)
 | 
|---|
| 285 | 
 | 
|---|
| 286 |         # ================================================================================
 | 
|---|
| 287 |         # Now that we know what to run, print it.
 | 
|---|
| 288 |         # find expected time
 | 
|---|
| 289 |         time = actions_eta(actions)
 | 
|---|
| 290 |         print("Running {} trials{}".format(len(actions), "" if time == 0 else " (expecting to take {})".format(str(datetime.timedelta(seconds=int(time)))) ))
 | 
|---|
| 291 | 
 | 
|---|
| 292 |         # dry run if options ask for it
 | 
|---|
| 293 |         if options.list:
 | 
|---|
| 294 |                 for a in actions:
 | 
|---|
| 295 |                         print(" ".join(a))
 | 
|---|
| 296 |                 sys.exit(0)
 | 
|---|
| 297 | 
 | 
|---|
| 298 | 
 | 
|---|
| 299 |         # ================================================================================
 | 
|---|
| 300 |         # Prepare to run
 | 
|---|
| 301 | 
 | 
|---|
| 302 |         random.shuffle(actions)
 | 
|---|
| 303 | 
 | 
|---|
| 304 |         # ================================================================================
 | 
|---|
| 305 |         # Run
 | 
|---|
| 306 |         options.file.write("[")
 | 
|---|
| 307 |         first = True
 | 
|---|
| 308 |         for i, a in enumerate(actions):
 | 
|---|
| 309 |                 sa = " ".join(a[3:] if withtaskset else a)
 | 
|---|
| 310 |                 if first:
 | 
|---|
| 311 |                         first = False
 | 
|---|
| 312 |                 else:
 | 
|---|
| 313 |                         options.file.write(",")
 | 
|---|
| 314 |                 if options.file != sys.stdout:
 | 
|---|
| 315 |                         print("{}/{} : {}          \r".format(i, len(actions), sa), end = '')
 | 
|---|
| 316 |                 fields = {}
 | 
|---|
| 317 |                 with subprocess.Popen( a, stdout  = subprocess.PIPE, stderr  = subprocess.PIPE) as proc:
 | 
|---|
| 318 |                         out, err = proc.communicate()
 | 
|---|
| 319 |                         if proc.returncode != 0:
 | 
|---|
| 320 |                                 print("ERROR: command '{}' encountered error, returned code {}".format(sa, proc.returncode), file=sys.stderr)
 | 
|---|
| 321 |                                 print(err.decode("utf-8"))
 | 
|---|
| 322 |                                 sys.exit(1)
 | 
|---|
| 323 |                         for s in out.decode("utf-8").splitlines():
 | 
|---|
| 324 |                                 match = re.search("^(.*):(.*)$", s)
 | 
|---|
| 325 |                                 if match:
 | 
|---|
| 326 |                                         try:
 | 
|---|
| 327 |                                                 fields[match.group(1).strip()] = float(match.group(2).strip().replace(',',''))
 | 
|---|
| 328 |                                         except:
 | 
|---|
| 329 |                                                 pass
 | 
|---|
| 330 | 
 | 
|---|
| 331 |                 options.file.write(json.dumps([a[3 if withtaskset else 0][2:], sa, fields]))
 | 
|---|
| 332 |                 options.file.flush()
 | 
|---|
| 333 | 
 | 
|---|
| 334 |         options.file.write("]\n")
 | 
|---|
| 335 | 
 | 
|---|
| 336 |         if options.file != sys.stdout:
 | 
|---|
| 337 |                 print("Done                                                                                ")
 | 
|---|