1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Lifrom __future__ import division 3*9c5db199SXin Lifrom __future__ import print_function 4*9c5db199SXin Li 5*9c5db199SXin Liimport json 6*9c5db199SXin Liimport os 7*9c5db199SXin Li 8*9c5db199SXin Lifrom autotest_lib.server.hosts import file_store 9*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 10*9c5db199SXin Lifrom autotest_lib.tko import tast 11*9c5db199SXin Lifrom autotest_lib.tko import utils as tko_utils 12*9c5db199SXin Liimport six 13*9c5db199SXin Li 14*9c5db199SXin Li 15*9c5db199SXin Liclass HostKeyvalError(Exception): 16*9c5db199SXin Li """Raised when the host keyval cannot be read.""" 17*9c5db199SXin Li 18*9c5db199SXin Li 19*9c5db199SXin Liclass job(object): 20*9c5db199SXin Li """Represents a job.""" 21*9c5db199SXin Li 22*9c5db199SXin Li def __init__(self, dir, user, label, machine, queued_time, started_time, 23*9c5db199SXin Li finished_time, machine_owner, machine_group, aborted_by, 24*9c5db199SXin Li aborted_on, keyval_dict): 25*9c5db199SXin Li self.dir = dir 26*9c5db199SXin Li self.tests = [] 27*9c5db199SXin Li self.user = user 28*9c5db199SXin Li self.label = label 29*9c5db199SXin Li self.machine = machine 30*9c5db199SXin Li self.queued_time = queued_time 31*9c5db199SXin Li self.started_time = started_time 32*9c5db199SXin Li self.finished_time = finished_time 33*9c5db199SXin Li self.machine_owner = machine_owner 34*9c5db199SXin Li self.machine_group = machine_group 35*9c5db199SXin Li self.aborted_by = aborted_by 36*9c5db199SXin Li self.aborted_on = aborted_on 37*9c5db199SXin Li self.keyval_dict = keyval_dict 38*9c5db199SXin Li self.afe_parent_job_id = None 39*9c5db199SXin Li self.build_version = None 40*9c5db199SXin Li self.suite = None 41*9c5db199SXin Li self.board = None 42*9c5db199SXin Li self.job_idx = None 43*9c5db199SXin Li # id of the corresponding tko_task_references entry. 44*9c5db199SXin Li # This table is used to refer to skylab task / afe job corresponding to 45*9c5db199SXin Li # this tko_job. 46*9c5db199SXin Li self.task_reference_id = None 47*9c5db199SXin Li 48*9c5db199SXin Li @staticmethod 49*9c5db199SXin Li def read_keyval(dir): 50*9c5db199SXin Li """ 51*9c5db199SXin Li Read job keyval files. 52*9c5db199SXin Li 53*9c5db199SXin Li @param dir: String name of directory containing job keyval files. 54*9c5db199SXin Li 55*9c5db199SXin Li @return A dictionary containing job keyvals. 56*9c5db199SXin Li 57*9c5db199SXin Li """ 58*9c5db199SXin Li dir = os.path.normpath(dir) 59*9c5db199SXin Li top_dir = tko_utils.find_toplevel_job_dir(dir) 60*9c5db199SXin Li if not top_dir: 61*9c5db199SXin Li top_dir = dir 62*9c5db199SXin Li assert(dir.startswith(top_dir)) 63*9c5db199SXin Li 64*9c5db199SXin Li # Pull in and merge all the keyval files, with higher-level 65*9c5db199SXin Li # overriding values in the lower-level ones. 66*9c5db199SXin Li keyval = {} 67*9c5db199SXin Li while True: 68*9c5db199SXin Li try: 69*9c5db199SXin Li upper_keyval = utils.read_keyval(dir) 70*9c5db199SXin Li # HACK: exclude hostname from the override - this is a special 71*9c5db199SXin Li # case where we want lower to override higher. 72*9c5db199SXin Li if 'hostname' in upper_keyval and 'hostname' in keyval: 73*9c5db199SXin Li del upper_keyval['hostname'] 74*9c5db199SXin Li keyval.update(upper_keyval) 75*9c5db199SXin Li except IOError: 76*9c5db199SXin Li pass # If the keyval can't be read just move on to the next. 77*9c5db199SXin Li if dir == top_dir: 78*9c5db199SXin Li break 79*9c5db199SXin Li else: 80*9c5db199SXin Li assert(dir != '/') 81*9c5db199SXin Li dir = os.path.dirname(dir) 82*9c5db199SXin Li return keyval 83*9c5db199SXin Li 84*9c5db199SXin Li 85*9c5db199SXin Liclass kernel(object): 86*9c5db199SXin Li """Represents a kernel.""" 87*9c5db199SXin Li 88*9c5db199SXin Li def __init__(self, base, patches, kernel_hash): 89*9c5db199SXin Li self.base = base 90*9c5db199SXin Li self.patches = patches 91*9c5db199SXin Li self.kernel_hash = kernel_hash 92*9c5db199SXin Li 93*9c5db199SXin Li 94*9c5db199SXin Li @staticmethod 95*9c5db199SXin Li def compute_hash(base, hashes): 96*9c5db199SXin Li """Compute a hash given the base string and hashes for each patch. 97*9c5db199SXin Li 98*9c5db199SXin Li @param base: A string representing the kernel base. 99*9c5db199SXin Li @param hashes: A list of hashes, where each hash is associated with a 100*9c5db199SXin Li patch of this kernel. 101*9c5db199SXin Li 102*9c5db199SXin Li @return A string representing the computed hash. 103*9c5db199SXin Li 104*9c5db199SXin Li """ 105*9c5db199SXin Li key_string = ','.join([base] + hashes) 106*9c5db199SXin Li return utils.hash('md5', key_string).hexdigest() 107*9c5db199SXin Li 108*9c5db199SXin Li 109*9c5db199SXin Liclass test(object): 110*9c5db199SXin Li """Represents a test.""" 111*9c5db199SXin Li 112*9c5db199SXin Li def __init__(self, subdir, testname, status, reason, test_kernel, 113*9c5db199SXin Li machine, started_time, finished_time, iterations, 114*9c5db199SXin Li attributes, perf_values, labels): 115*9c5db199SXin Li self.subdir = subdir 116*9c5db199SXin Li self.testname = testname 117*9c5db199SXin Li self.status = status 118*9c5db199SXin Li self.reason = reason 119*9c5db199SXin Li self.kernel = test_kernel 120*9c5db199SXin Li self.machine = machine 121*9c5db199SXin Li self.started_time = started_time 122*9c5db199SXin Li self.finished_time = finished_time 123*9c5db199SXin Li self.iterations = iterations 124*9c5db199SXin Li self.attributes = attributes 125*9c5db199SXin Li self.perf_values = perf_values 126*9c5db199SXin Li self.labels = labels 127*9c5db199SXin Li 128*9c5db199SXin Li 129*9c5db199SXin Li @staticmethod 130*9c5db199SXin Li def load_iterations(keyval_path): 131*9c5db199SXin Li """Abstract method to load a list of iterations from a keyval file. 132*9c5db199SXin Li 133*9c5db199SXin Li @param keyval_path: String path to a keyval file. 134*9c5db199SXin Li 135*9c5db199SXin Li @return A list of iteration objects. 136*9c5db199SXin Li 137*9c5db199SXin Li """ 138*9c5db199SXin Li raise NotImplementedError 139*9c5db199SXin Li 140*9c5db199SXin Li 141*9c5db199SXin Li @classmethod 142*9c5db199SXin Li def parse_test(cls, job, subdir, testname, status, reason, test_kernel, 143*9c5db199SXin Li started_time, finished_time, existing_instance=None): 144*9c5db199SXin Li """ 145*9c5db199SXin Li Parse test result files to construct a complete test instance. 146*9c5db199SXin Li 147*9c5db199SXin Li Given a job and the basic metadata about the test that can be 148*9c5db199SXin Li extracted from the status logs, parse the test result files (keyval 149*9c5db199SXin Li files and perf measurement files) and use them to construct a complete 150*9c5db199SXin Li test instance. 151*9c5db199SXin Li 152*9c5db199SXin Li @param job: A job object. 153*9c5db199SXin Li @param subdir: The string subdirectory name for the given test. 154*9c5db199SXin Li @param testname: The name of the test. 155*9c5db199SXin Li @param status: The status of the test. 156*9c5db199SXin Li @param reason: The reason string for the test. 157*9c5db199SXin Li @param test_kernel: The kernel of the test. 158*9c5db199SXin Li @param started_time: The start time of the test. 159*9c5db199SXin Li @param finished_time: The finish time of the test. 160*9c5db199SXin Li @param existing_instance: An existing test instance. 161*9c5db199SXin Li 162*9c5db199SXin Li @return A test instance that has the complete information. 163*9c5db199SXin Li 164*9c5db199SXin Li """ 165*9c5db199SXin Li tko_utils.dprint("parsing test %s %s" % (subdir, testname)) 166*9c5db199SXin Li 167*9c5db199SXin Li if tast.is_tast_test(testname): 168*9c5db199SXin Li attributes, perf_values = tast.load_tast_test_aux_results(job, 169*9c5db199SXin Li testname) 170*9c5db199SXin Li iterations = [] 171*9c5db199SXin Li elif subdir: 172*9c5db199SXin Li # Grab iterations from the results keyval. 173*9c5db199SXin Li iteration_keyval = os.path.join(job.dir, subdir, 174*9c5db199SXin Li 'results', 'keyval') 175*9c5db199SXin Li iterations = cls.load_iterations(iteration_keyval) 176*9c5db199SXin Li 177*9c5db199SXin Li # Grab perf values from the perf measurements file. 178*9c5db199SXin Li perf_values_file = os.path.join(job.dir, subdir, 179*9c5db199SXin Li 'results', 'results-chart.json') 180*9c5db199SXin Li perf_values = {} 181*9c5db199SXin Li if os.path.exists(perf_values_file): 182*9c5db199SXin Li with open(perf_values_file, 'r') as fp: 183*9c5db199SXin Li contents = fp.read() 184*9c5db199SXin Li if contents: 185*9c5db199SXin Li perf_values = json.loads(contents) 186*9c5db199SXin Li 187*9c5db199SXin Li # Grab test attributes from the subdir keyval. 188*9c5db199SXin Li test_keyval = os.path.join(job.dir, subdir, 'keyval') 189*9c5db199SXin Li attributes = test.load_attributes(test_keyval) 190*9c5db199SXin Li else: 191*9c5db199SXin Li iterations = [] 192*9c5db199SXin Li perf_values = {} 193*9c5db199SXin Li attributes = {} 194*9c5db199SXin Li 195*9c5db199SXin Li # Grab test+host attributes from the host keyval. 196*9c5db199SXin Li host_keyval = cls.parse_host_keyval(job.dir, job.machine) 197*9c5db199SXin Li attributes.update(dict(('host-%s' % k, v) 198*9c5db199SXin Li for k, v in six.iteritems(host_keyval))) 199*9c5db199SXin Li 200*9c5db199SXin Li if existing_instance: 201*9c5db199SXin Li def constructor(*args, **dargs): 202*9c5db199SXin Li """Initializes an existing test instance.""" 203*9c5db199SXin Li existing_instance.__init__(*args, **dargs) 204*9c5db199SXin Li return existing_instance 205*9c5db199SXin Li else: 206*9c5db199SXin Li constructor = cls 207*9c5db199SXin Li 208*9c5db199SXin Li return constructor(subdir, testname, status, reason, test_kernel, 209*9c5db199SXin Li job.machine, started_time, finished_time, 210*9c5db199SXin Li iterations, attributes, perf_values, []) 211*9c5db199SXin Li 212*9c5db199SXin Li 213*9c5db199SXin Li @classmethod 214*9c5db199SXin Li def parse_partial_test(cls, job, subdir, testname, reason, test_kernel, 215*9c5db199SXin Li started_time): 216*9c5db199SXin Li """ 217*9c5db199SXin Li Create a test instance representing a partial test result. 218*9c5db199SXin Li 219*9c5db199SXin Li Given a job and the basic metadata available when a test is 220*9c5db199SXin Li started, create a test instance representing the partial result. 221*9c5db199SXin Li Assume that since the test is not complete there are no results files 222*9c5db199SXin Li actually available for parsing. 223*9c5db199SXin Li 224*9c5db199SXin Li @param job: A job object. 225*9c5db199SXin Li @param subdir: The string subdirectory name for the given test. 226*9c5db199SXin Li @param testname: The name of the test. 227*9c5db199SXin Li @param reason: The reason string for the test. 228*9c5db199SXin Li @param test_kernel: The kernel of the test. 229*9c5db199SXin Li @param started_time: The start time of the test. 230*9c5db199SXin Li 231*9c5db199SXin Li @return A test instance that has partial test information. 232*9c5db199SXin Li 233*9c5db199SXin Li """ 234*9c5db199SXin Li tko_utils.dprint('parsing partial test %s %s' % (subdir, testname)) 235*9c5db199SXin Li 236*9c5db199SXin Li return cls(subdir, testname, 'RUNNING', reason, test_kernel, 237*9c5db199SXin Li job.machine, started_time, None, [], {}, [], []) 238*9c5db199SXin Li 239*9c5db199SXin Li 240*9c5db199SXin Li @staticmethod 241*9c5db199SXin Li def load_attributes(keyval_path): 242*9c5db199SXin Li """ 243*9c5db199SXin Li Load test attributes from a test keyval path. 244*9c5db199SXin Li 245*9c5db199SXin Li Load the test attributes into a dictionary from a test 246*9c5db199SXin Li keyval path. Does not assume that the path actually exists. 247*9c5db199SXin Li 248*9c5db199SXin Li @param keyval_path: The string path to a keyval file. 249*9c5db199SXin Li 250*9c5db199SXin Li @return A dictionary representing the test keyvals. 251*9c5db199SXin Li 252*9c5db199SXin Li """ 253*9c5db199SXin Li if not os.path.exists(keyval_path): 254*9c5db199SXin Li return {} 255*9c5db199SXin Li return utils.read_keyval(keyval_path) 256*9c5db199SXin Li 257*9c5db199SXin Li 258*9c5db199SXin Li @staticmethod 259*9c5db199SXin Li def _parse_keyval(job_dir, sub_keyval_path): 260*9c5db199SXin Li """ 261*9c5db199SXin Li Parse a file of keyvals. 262*9c5db199SXin Li 263*9c5db199SXin Li @param job_dir: The string directory name of the associated job. 264*9c5db199SXin Li @param sub_keyval_path: Path to a keyval file relative to job_dir. 265*9c5db199SXin Li 266*9c5db199SXin Li @return A dictionary representing the keyvals. 267*9c5db199SXin Li 268*9c5db199SXin Li """ 269*9c5db199SXin Li # The "real" job dir may be higher up in the directory tree. 270*9c5db199SXin Li job_dir = tko_utils.find_toplevel_job_dir(job_dir) 271*9c5db199SXin Li if not job_dir: 272*9c5db199SXin Li return {} # We can't find a top-level job dir with job keyvals. 273*9c5db199SXin Li 274*9c5db199SXin Li # The keyval is <job_dir>/`sub_keyval_path` if it exists. 275*9c5db199SXin Li keyval_path = os.path.join(job_dir, sub_keyval_path) 276*9c5db199SXin Li if os.path.isfile(keyval_path): 277*9c5db199SXin Li return utils.read_keyval(keyval_path) 278*9c5db199SXin Li else: 279*9c5db199SXin Li return {} 280*9c5db199SXin Li 281*9c5db199SXin Li 282*9c5db199SXin Li @staticmethod 283*9c5db199SXin Li def _is_multimachine(job_dir): 284*9c5db199SXin Li """ 285*9c5db199SXin Li Determine whether the job is a multi-machine job. 286*9c5db199SXin Li 287*9c5db199SXin Li @param job_dir: The string directory name of the associated job. 288*9c5db199SXin Li 289*9c5db199SXin Li @return True, if the job is a multi-machine job, or False if not. 290*9c5db199SXin Li 291*9c5db199SXin Li """ 292*9c5db199SXin Li machines_path = os.path.join(job_dir, '.machines') 293*9c5db199SXin Li if os.path.exists(machines_path): 294*9c5db199SXin Li with open(machines_path, 'r') as fp: 295*9c5db199SXin Li line_count = len(fp.read().splitlines()) 296*9c5db199SXin Li if line_count > 1: 297*9c5db199SXin Li return True 298*9c5db199SXin Li return False 299*9c5db199SXin Li 300*9c5db199SXin Li 301*9c5db199SXin Li @staticmethod 302*9c5db199SXin Li def parse_host_keyval(job_dir, hostname): 303*9c5db199SXin Li """ 304*9c5db199SXin Li Parse host keyvals. 305*9c5db199SXin Li 306*9c5db199SXin Li @param job_dir: The string directory name of the associated job. 307*9c5db199SXin Li @param hostname: The string hostname. 308*9c5db199SXin Li 309*9c5db199SXin Li @return A dictionary representing the host keyvals. 310*9c5db199SXin Li 311*9c5db199SXin Li @raises HostKeyvalError if the host keyval is not found. 312*9c5db199SXin Li 313*9c5db199SXin Li """ 314*9c5db199SXin Li keyval_path = os.path.join('host_keyvals', hostname) 315*9c5db199SXin Li hostinfo_path = os.path.join(job_dir, 'host_info_store', 316*9c5db199SXin Li hostname + '.store') 317*9c5db199SXin Li # Skylab uses hostinfo. If this is not present, try falling back to the 318*9c5db199SXin Li # host keyval file (moblab), or an empty host keyval for multi-machine 319*9c5db199SXin Li # tests (jetstream). 320*9c5db199SXin Li if os.path.exists(hostinfo_path): 321*9c5db199SXin Li tko_utils.dprint('Reading keyvals from hostinfo.') 322*9c5db199SXin Li return _parse_hostinfo_keyval(hostinfo_path) 323*9c5db199SXin Li elif os.path.exists(os.path.join(job_dir, keyval_path)): 324*9c5db199SXin Li tko_utils.dprint('Reading keyvals from %s.' % keyval_path) 325*9c5db199SXin Li return test._parse_keyval(job_dir, keyval_path) 326*9c5db199SXin Li elif test._is_multimachine(job_dir): 327*9c5db199SXin Li tko_utils.dprint('Multimachine job, no keyvals.') 328*9c5db199SXin Li return {} 329*9c5db199SXin Li raise HostKeyvalError('Host keyval not found') 330*9c5db199SXin Li 331*9c5db199SXin Li 332*9c5db199SXin Li @staticmethod 333*9c5db199SXin Li def parse_job_keyval(job_dir): 334*9c5db199SXin Li """ 335*9c5db199SXin Li Parse job keyvals. 336*9c5db199SXin Li 337*9c5db199SXin Li @param job_dir: The string directory name of the associated job. 338*9c5db199SXin Li 339*9c5db199SXin Li @return A dictionary representing the job keyvals. 340*9c5db199SXin Li 341*9c5db199SXin Li """ 342*9c5db199SXin Li # The job keyval is <job_dir>/keyval if it exists. 343*9c5db199SXin Li return test._parse_keyval(job_dir, 'keyval') 344*9c5db199SXin Li 345*9c5db199SXin Li 346*9c5db199SXin Lidef _parse_hostinfo_keyval(hostinfo_path): 347*9c5db199SXin Li """ 348*9c5db199SXin Li Parse host keyvals from hostinfo. 349*9c5db199SXin Li 350*9c5db199SXin Li @param hostinfo_path: The string path to the host info store file. 351*9c5db199SXin Li 352*9c5db199SXin Li @return A dictionary representing the host keyvals. 353*9c5db199SXin Li 354*9c5db199SXin Li """ 355*9c5db199SXin Li store = file_store.FileStore(hostinfo_path) 356*9c5db199SXin Li hostinfo = store.get() 357*9c5db199SXin Li # TODO(ayatane): Investigate if urllib.quote is better. 358*9c5db199SXin Li label_string = ','.join(label.replace(':', '%3A') 359*9c5db199SXin Li for label in hostinfo.labels) 360*9c5db199SXin Li return { 361*9c5db199SXin Li 'labels': label_string, 362*9c5db199SXin Li 'platform': hostinfo.model, 363*9c5db199SXin Li 'board': hostinfo.board 364*9c5db199SXin Li } 365*9c5db199SXin Li 366*9c5db199SXin Li 367*9c5db199SXin Liclass patch(object): 368*9c5db199SXin Li """Represents a patch.""" 369*9c5db199SXin Li 370*9c5db199SXin Li def __init__(self, spec, reference, hash): 371*9c5db199SXin Li self.spec = spec 372*9c5db199SXin Li self.reference = reference 373*9c5db199SXin Li self.hash = hash 374*9c5db199SXin Li 375*9c5db199SXin Li 376*9c5db199SXin Liclass iteration(object): 377*9c5db199SXin Li """Represents an iteration.""" 378*9c5db199SXin Li 379*9c5db199SXin Li def __init__(self, index, attr_keyval, perf_keyval): 380*9c5db199SXin Li self.index = index 381*9c5db199SXin Li self.attr_keyval = attr_keyval 382*9c5db199SXin Li self.perf_keyval = perf_keyval 383*9c5db199SXin Li 384*9c5db199SXin Li 385*9c5db199SXin Li @staticmethod 386*9c5db199SXin Li def parse_line_into_dicts(line, attr_dict, perf_dict): 387*9c5db199SXin Li """ 388*9c5db199SXin Li Abstract method to parse a keyval line and insert it into a dictionary. 389*9c5db199SXin Li 390*9c5db199SXin Li @param line: The string line to parse. 391*9c5db199SXin Li @param attr_dict: Dictionary of generic iteration attributes. 392*9c5db199SXin Li @param perf_dict: Dictionary of iteration performance results. 393*9c5db199SXin Li 394*9c5db199SXin Li """ 395*9c5db199SXin Li raise NotImplementedError 396*9c5db199SXin Li 397*9c5db199SXin Li 398*9c5db199SXin Li @classmethod 399*9c5db199SXin Li def load_from_keyval(cls, keyval_path): 400*9c5db199SXin Li """ 401*9c5db199SXin Li Load a list of iterations from an iteration keyval file. 402*9c5db199SXin Li 403*9c5db199SXin Li Keyval data from separate iterations is separated by blank 404*9c5db199SXin Li lines. Makes use of the parse_line_into_dicts method to 405*9c5db199SXin Li actually parse the individual lines. 406*9c5db199SXin Li 407*9c5db199SXin Li @param keyval_path: The string path to a keyval file. 408*9c5db199SXin Li 409*9c5db199SXin Li @return A list of iteration objects. 410*9c5db199SXin Li 411*9c5db199SXin Li """ 412*9c5db199SXin Li if not os.path.exists(keyval_path): 413*9c5db199SXin Li return [] 414*9c5db199SXin Li 415*9c5db199SXin Li iterations = [] 416*9c5db199SXin Li index = 1 417*9c5db199SXin Li attr, perf = {}, {} 418*9c5db199SXin Li with open(keyval_path, 'r') as kp: 419*9c5db199SXin Li lines = kp.readlines() 420*9c5db199SXin Li for line in lines: 421*9c5db199SXin Li line = line.strip() 422*9c5db199SXin Li if line: 423*9c5db199SXin Li cls.parse_line_into_dicts(line, attr, perf) 424*9c5db199SXin Li else: 425*9c5db199SXin Li iterations.append(cls(index, attr, perf)) 426*9c5db199SXin Li index += 1 427*9c5db199SXin Li attr, perf = {}, {} 428*9c5db199SXin Li if attr or perf: 429*9c5db199SXin Li iterations.append(cls(index, attr, perf)) 430*9c5db199SXin Li return iterations 431*9c5db199SXin Li 432*9c5db199SXin Li 433*9c5db199SXin Liclass perf_value_iteration(object): 434*9c5db199SXin Li """Represents a perf value iteration.""" 435*9c5db199SXin Li 436*9c5db199SXin Li def __init__(self, index, perf_measurements): 437*9c5db199SXin Li """ 438*9c5db199SXin Li Initializes the perf values for a particular test iteration. 439*9c5db199SXin Li 440*9c5db199SXin Li @param index: The integer iteration number. 441*9c5db199SXin Li @param perf_measurements: A list of dictionaries, where each dictionary 442*9c5db199SXin Li contains the information for a measured perf metric from the 443*9c5db199SXin Li current iteration. 444*9c5db199SXin Li 445*9c5db199SXin Li """ 446*9c5db199SXin Li self.index = index 447*9c5db199SXin Li self.perf_measurements = perf_measurements 448*9c5db199SXin Li 449*9c5db199SXin Li 450*9c5db199SXin Li @staticmethod 451*9c5db199SXin Li def parse_line_into_dict(line): 452*9c5db199SXin Li """ 453*9c5db199SXin Li Abstract method to parse an individual perf measurement line. 454*9c5db199SXin Li 455*9c5db199SXin Li @param line: A string line from the perf measurement output file. 456*9c5db199SXin Li 457*9c5db199SXin Li @return A dicionary representing the information for a measured perf 458*9c5db199SXin Li metric from one line of the perf measurement output file, or an 459*9c5db199SXin Li empty dictionary if the line cannot be parsed successfully. 460*9c5db199SXin Li 461*9c5db199SXin Li """ 462*9c5db199SXin Li raise NotImplementedError 463