|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import sys |
| 4 | +from os.path import dirname |
| 5 | +import argparse |
| 6 | +import re |
| 7 | +from glob import glob |
| 8 | +import json |
| 9 | +import math |
| 10 | + |
| 11 | +################################################################ |
| 12 | +# # |
| 13 | +# Script to exctract CPU/MEM resource estimates from # |
| 14 | +# the log_time files left from the individual tasks # |
| 15 | +# # |
| 16 | +# Outputs a json to be fed to the workflow runnner for dynamic # |
| 17 | +# scheduling decisions. # |
| 18 | +################################################################ |
| 19 | + |
| 20 | +# helper function to find files |
| 21 | +def find_files(path, search, depth=0): |
| 22 | + files = [] |
| 23 | + for d in range(depth + 1): |
| 24 | + wildcards = "/*" * d |
| 25 | + path_search = path + wildcards + f"/{search}" |
| 26 | + files.extend(glob(path_search)) |
| 27 | + return files |
| 28 | + |
| 29 | +# this functions extracts the important metrics from a single resource file (left by O2 jobutils/taskwrapper) |
| 30 | +def extract_time_single(path): |
| 31 | + r = {} |
| 32 | + with open(path, "r") as f: |
| 33 | + for l in f: |
| 34 | + if "walltime" in l: |
| 35 | + r["walltime"] = float(l.strip().split()[-1]) |
| 36 | + elif "CPU" in l: |
| 37 | + r["cpu"] = float(l.strip().split()[-1].split('%')[0]) |
| 38 | + elif "mem" in l: |
| 39 | + r["mem"] = float(l.strip().split()[-1]) |
| 40 | + return r |
| 41 | + |
| 42 | +def process(args): |
| 43 | + pipeline_dir = dirname(args.path) |
| 44 | + files = find_files(pipeline_dir, "*.log_time", 1) |
| 45 | + if not files: |
| 46 | + print(f"WARNING: Cannot find time logs in {pipeline_dir}.") |
| 47 | + return |
| 48 | + |
| 49 | + resource_accum = {} # accumulates resources per task name |
| 50 | + for f in files: |
| 51 | + # name from time log file |
| 52 | + name = f.split("/")[-1] |
| 53 | + name = re.sub("\.log_time$", "", name) |
| 54 | + name_notf = name.split("_")[0] |
| 55 | + resources = extract_time_single(f) |
| 56 | + resources["name"] = name |
| 57 | + resource_accum[name_notf] = resource_accum.get(name_notf,[]) |
| 58 | + resource_accum[name_notf].append(resources) |
| 59 | + |
| 60 | + |
| 61 | + # finalizes metrics; average for CPU; max for MEM; average for walltime |
| 62 | + # returns final metric result suitable for output to json |
| 63 | + def finalize(resource_list): |
| 64 | + """ |
| 65 | + input is map of lists of resource dictionaries |
| 66 | + output is map of final resource estimates |
| 67 | + """ |
| 68 | + result = {} |
| 69 | + for task in resource_list: |
| 70 | + finalr = {"walltime": 0, "cpu" : 0, "mem" : 0} |
| 71 | + for r in resource_list[task]: # r is a resource estimate |
| 72 | + finalr["walltime"] += r["walltime"] |
| 73 | + finalr["mem"] = max(r["mem"], finalr["mem"]) |
| 74 | + finalr["cpu"] += r["cpu"] |
| 75 | + finalr["walltime"] /= len(resource_list[task]) |
| 76 | + finalr["cpu"] /= 100*len(resource_list[task]) # we take the number of CPUs not the percent |
| 77 | + finalr["mem"] /= 1024 # we'd like to have it in MB |
| 78 | + finalr["mem"] = math.ceil(finalr["mem"]) |
| 79 | + result[task] = finalr |
| 80 | + return result |
| 81 | + |
| 82 | + estimate=finalize(resource_accum) |
| 83 | + print (estimate) |
| 84 | + |
| 85 | + # finally save to JSON |
| 86 | + with open(args.output, "w") as f: |
| 87 | + json.dump(estimate, f, indent=2) |
| 88 | + |
| 89 | + |
| 90 | +def main(): |
| 91 | + parser = argparse.ArgumentParser(description="Produce a O2DPG workflow resource file from existing time logs") |
| 92 | + parser.add_argument('-o','--output', help='Filename of output metric json file', default='learned_O2DPG_resource_metrics.json') |
| 93 | + parser.add_argument('-p','--path', help='Path to O2DPG workspace', default="./") |
| 94 | + args = parser.parse_args() |
| 95 | + process(args) |
| 96 | + |
| 97 | +if __name__ == "__main__": |
| 98 | + sys.exit(main()) |
0 commit comments