| [7a2a3af] | 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
 | 
|---|
| [dbb1073] | 12 | import datetime
 | 
|---|
| [7a2a3af] | 13 | import itertools
 | 
|---|
| [883c4d9] | 14 | import json
 | 
|---|
| [7a2a3af] | 15 | import os
 | 
|---|
 | 16 | import random
 | 
|---|
 | 17 | import re
 | 
|---|
| [3e9ec44] | 18 | import socket
 | 
|---|
| [7a2a3af] | 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 | 
 | 
|---|
| [af333e3] | 41 | def parse_option(key, values):
 | 
|---|
| [7a2a3af] | 42 |         try:
 | 
|---|
 | 43 |                 num = int(values)
 | 
|---|
 | 44 |                 return key, [num]
 | 
|---|
 | 45 |         except:
 | 
|---|
 | 46 |                 pass
 | 
|---|
 | 47 | 
 | 
|---|
| [3613e25] | 48 |         if values.startswith('\\'):
 | 
|---|
 | 49 |                 return key, values[1:].split(',')
 | 
|---|
 | 50 |         elif re.search("^[0-9-,]+$", values):
 | 
|---|
| [7a2a3af] | 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 | 
 | 
|---|
| [f56101f] | 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
 | 
|---|
| [7a2a3af] | 70 | def eval_options(opts):
 | 
|---|
| [f56101f] | 71 |         # Find all the options with dependencies
 | 
|---|
| [7a2a3af] | 72 |         dependents = [d for d in opts.values() if type(d) is DependentOpt]
 | 
|---|
| [f56101f] | 73 | 
 | 
|---|
 | 74 |         # we need to find all the straglers
 | 
|---|
| [7a2a3af] | 75 |         processed = []
 | 
|---|
| [f56101f] | 76 | 
 | 
|---|
 | 77 |         # extract all the necessary inputs
 | 
|---|
 | 78 |         input_keys = {}
 | 
|---|
| [7a2a3af] | 79 |         for d in dependents:
 | 
|---|
| [f56101f] | 80 |                 # Mark the dependent as seen
 | 
|---|
| [7a2a3af] | 81 |                 processed.append(d.key)
 | 
|---|
| [f56101f] | 82 | 
 | 
|---|
 | 83 |                 # process each of the dependencies
 | 
|---|
| [7a2a3af] | 84 |                 for dvar in d.vars:
 | 
|---|
| [f56101f] | 85 |                         # Check that it depends on something that exists
 | 
|---|
| [7a2a3af] | 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 | 
 | 
|---|
| [f56101f] | 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
 | 
|---|
| [7a2a3af] | 104 |                         processed.append(dvar)
 | 
|---|
 | 105 | 
 | 
|---|
| [f56101f] | 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)
 | 
|---|
| [7a2a3af] | 140 | 
 | 
|---|
| [f56101f] | 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))
 | 
|---|
| [7a2a3af] | 148 | 
 | 
|---|
| [f56101f] | 149 |                 # print(inner)
 | 
|---|
 | 150 |                 formated.append(inner)
 | 
|---|
| [7a2a3af] | 151 | 
 | 
|---|
| [f56101f] | 152 |         return formated
 | 
|---|
| [7a2a3af] | 153 | 
 | 
|---|
| [3e9ec44] | 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 | 
 | 
|---|
| [dbb1073] | 166 | def actions_eta(actions):
 | 
|---|
 | 167 |         time = 0
 | 
|---|
 | 168 |         for a in actions:
 | 
|---|
| [3e9ec44] | 169 |                 o = search_option(a, '-d')
 | 
|---|
 | 170 |                 if o :
 | 
|---|
 | 171 |                         time += int(o)
 | 
|---|
| [dbb1073] | 172 |         return time
 | 
|---|
 | 173 | 
 | 
|---|
| [3e9ec44] | 174 | taskset_maps = None
 | 
|---|
 | 175 | 
 | 
|---|
 | 176 | def init_taskset_maps():
 | 
|---|
 | 177 |         global taskset_maps
 | 
|---|
 | 178 |         known_hosts = {
 | 
|---|
 | 179 |                 "jax": {
 | 
|---|
| [f56101f] | 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",
 | 
|---|
| [3e9ec44] | 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 | 
 | 
|---|
| [7a2a3af] | 216 | if __name__ == "__main__":
 | 
|---|
 | 217 |         # ================================================================================
 | 
|---|
 | 218 |         # parse command line arguments
 | 
|---|
| [883c4d9] | 219 |         formats = ['raw', 'csv', 'json']
 | 
|---|
| [7a2a3af] | 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)
 | 
|---|
| [af333e3] | 223 |         parser.add_argument('--trials', help='Number of trials to run per combinaison', type=int, default=3)
 | 
|---|
| [3e9ec44] | 224 |         parser.add_argument('--notaskset', help='If specified, the trial will not use taskset to match the -p option', action='store_true')
 | 
|---|
| [7a2a3af] | 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:
 | 
|---|
| [af333e3] | 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 | 
 | 
|---|
| [7a2a3af] | 241 |         except:
 | 
|---|
 | 242 |                 sys.exit(1)
 | 
|---|
 | 243 | 
 | 
|---|
 | 244 |         # ================================================================================
 | 
|---|
 | 245 |         # Identify the commands to run
 | 
|---|
| [6f27b67] | 246 |         command = './' + options.command[0]
 | 
|---|
 | 247 |         if options.candidates:
 | 
|---|
 | 248 |                 commands = [command + "-" + c for c in options.candidates]
 | 
|---|
 | 249 |         else:
 | 
|---|
 | 250 |                 commands = [command]
 | 
|---|
| [7a2a3af] | 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
 | 
|---|
| [af333e3] | 263 |         opts = dict([parse_option(k, v) for k, v in options.option])
 | 
|---|
| [7a2a3af] | 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 = []
 | 
|---|
| [f56101f] | 271 |         for p in itertools.product(range(options.trials), commands, opts):
 | 
|---|
| [7a2a3af] | 272 |                 act = [p[1]]
 | 
|---|
 | 273 |                 for o in p[2:]:
 | 
|---|
 | 274 |                         act.extend(o)
 | 
|---|
 | 275 |                 actions.append(act)
 | 
|---|
 | 276 | 
 | 
|---|
 | 277 |         # ================================================================================
 | 
|---|
| [3e9ec44] | 278 |         # Fixup the different commands
 | 
|---|
 | 279 | 
 | 
|---|
 | 280 |         # Add tasksets
 | 
|---|
| [f46b26b8] | 281 |         withtaskset = False
 | 
|---|
| [3e9ec44] | 282 |         if not options.notaskset and init_taskset_maps():
 | 
|---|
| [f46b26b8] | 283 |                 withtaskset = True
 | 
|---|
| [3e9ec44] | 284 |                 actions = settaskset(actions)
 | 
|---|
 | 285 | 
 | 
|---|
 | 286 |         # ================================================================================
 | 
|---|
 | 287 |         # Now that we know what to run, print it.
 | 
|---|
| [83b22b53] | 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
 | 
|---|
| [7a2a3af] | 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 | 
 | 
|---|
| [dbb1073] | 302 |         random.shuffle(actions)
 | 
|---|
| [7a2a3af] | 303 | 
 | 
|---|
 | 304 |         # ================================================================================
 | 
|---|
 | 305 |         # Run
 | 
|---|
| [af333e3] | 306 |         options.file.write("[")
 | 
|---|
 | 307 |         first = True
 | 
|---|
| [7a2a3af] | 308 |         for i, a in enumerate(actions):
 | 
|---|
| [f46b26b8] | 309 |                 sa = " ".join(a[3:] if withtaskset else a)
 | 
|---|
| [af333e3] | 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 = '')
 | 
|---|
| [7a2a3af] | 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:
 | 
|---|
| [fa6233a] | 326 |                                         try:
 | 
|---|
 | 327 |                                                 fields[match.group(1).strip()] = float(match.group(2).strip().replace(',',''))
 | 
|---|
 | 328 |                                         except:
 | 
|---|
 | 329 |                                                 pass
 | 
|---|
| [7a2a3af] | 330 | 
 | 
|---|
| [f46b26b8] | 331 |                 options.file.write(json.dumps([a[3 if withtaskset else 0][2:], sa, fields]))
 | 
|---|
| [af333e3] | 332 |                 options.file.flush()
 | 
|---|
| [7a2a3af] | 333 | 
 | 
|---|
| [af333e3] | 334 |         options.file.write("]\n")
 | 
|---|
| [7a2a3af] | 335 | 
 | 
|---|
| [af333e3] | 336 |         if options.file != sys.stdout:
 | 
|---|
| [f56101f] | 337 |                 print("Done                                                                                ")
 | 
|---|