1 # SPDX-License-Identifier: GPL-2.0
6 from pathlib
import Path
11 def __init__(self
, metric
: list[str], wl
: str, value
: list[float], low
: float, up
=float('nan'), description
=str()):
12 self
.metric
: list = metric
# multiple metrics in relationship type tests
13 self
.workloads
= [wl
] # multiple workloads possible
14 self
.collectedValue
: list = value
15 self
.valueLowBound
= low
16 self
.valueUpBound
= up
17 self
.description
= description
19 def __repr__(self
) -> str:
20 if len(self
.metric
) > 1:
21 return "\nMetric Relationship Error: \tThe collected value of metric {0}\n\
22 \tis {1} in workload(s): {2} \n\
23 \tbut expected value range is [{3}, {4}]\n\
24 \tRelationship rule description: \'{5}\'".format(self
.metric
, self
.collectedValue
, self
.workloads
,
25 self
.valueLowBound
, self
.valueUpBound
, self
.description
)
26 elif len(self
.collectedValue
) == 0:
27 return "\nNo Metric Value Error: \tMetric {0} returns with no value \n\
28 \tworkload(s): {1}".format(self
.metric
, self
.workloads
)
30 return "\nWrong Metric Value Error: \tThe collected value of metric {0}\n\
31 \tis {1} in workload(s): {2}\n\
32 \tbut expected value range is [{3}, {4}]"\
33 .format(self
.metric
, self
.collectedValue
, self
.workloads
,
34 self
.valueLowBound
, self
.valueUpBound
)
38 def __init__(self
, rulefname
, reportfname
='', t
=5, debug
=False, datafname
='', fullrulefname
='', workload
='true', metrics
=''):
39 self
.rulefname
= rulefname
40 self
.reportfname
= reportfname
42 self
.collectlist
: str = metrics
43 self
.metrics
= self
.__set
_metrics
(metrics
)
47 self
.workloads
= [x
for x
in workload
.split(",") if x
]
48 self
.wlidx
= 0 # idx of current workloads
49 self
.allresults
= dict() # metric results of all workload
50 self
.alltotalcnt
= dict()
51 self
.allpassedcnt
= dict()
53 self
.results
= dict() # metric results of current workload
54 # vars for test pass/failure statistics
55 # metrics with no results or negative results, neg result counts failed tests
56 self
.ignoremetrics
= set()
62 # vars for Rule Generator
63 self
.pctgmetrics
= set() # Percentage rule
66 self
.datafname
= datafname
68 self
.fullrulefname
= fullrulefname
70 def __set_metrics(self
, metrics
=''):
72 return set(metrics
.split(","))
76 def read_json(self
, filename
: str) -> dict:
78 with
open(Path(filename
).resolve(), "r") as f
:
79 data
= json
.loads(f
.read())
81 print(f
"Error when reading file {e}")
86 def json_dump(self
, data
, output_file
):
87 parent
= Path(output_file
).parent
88 if not parent
.exists():
89 parent
.mkdir(parents
=True)
91 with
open(output_file
, "w+") as output_file
:
97 def get_results(self
, idx
: int = 0):
98 return self
.results
.get(idx
)
100 def get_bounds(self
, lb
, ub
, error
, alias
={}, ridx
: int = 0) -> list:
102 Get bounds and tolerance from lb, ub, and error.
103 If missing lb, use 0.0; missing ub, use float('inf); missing error, use self.tolerance.
105 @param lb: str/float, lower bound
106 @param ub: str/float, upper bound
107 @param error: float/str, error tolerance
108 @returns: lower bound, return inf if the lower bound is a metric value and is not collected
109 upper bound, return -1 if the upper bound is a metric value and is not collected
110 tolerance, denormalized base on upper bound value
112 # init ubv and lbv to invalid values
113 def get_bound_value(bound
, initval
, ridx
):
115 if isinstance(bound
, int) or isinstance(bound
, float):
117 elif isinstance(bound
, str):
121 vall
= self
.get_value(alias
[ub
], ridx
)
124 elif bound
.replace('.', '1').isdigit():
127 print("Wrong bound: {0}".format(bound
))
129 print("Wrong bound: {0}".format(bound
))
132 ubv
= get_bound_value(ub
, -1, ridx
)
133 lbv
= get_bound_value(lb
, float('inf'), ridx
)
134 t
= get_bound_value(error
, self
.tolerance
, ridx
)
136 # denormalize error threshold
137 denormerr
= t
* ubv
/ 100 if ubv
!= 100 and ubv
> 0 else t
139 return lbv
, ubv
, denormerr
141 def get_value(self
, name
: str, ridx
: int = 0) -> list:
143 Get value of the metric from self.results.
144 If result of this metric is not provided, the metric name will be added into self.ignoremetics.
145 All future test(s) on this metric will fail.
147 @param name: name of the metric
148 @returns: list with value found in self.results; list is empty when value is not found.
151 data
= self
.results
[ridx
] if ridx
in self
.results
else self
.results
[0]
152 if name
not in self
.ignoremetrics
:
154 results
.append(data
[name
])
155 elif name
.replace('.', '1').isdigit():
156 results
.append(float(name
))
158 self
.ignoremetrics
.add(name
)
161 def check_bound(self
, val
, lb
, ub
, err
):
162 return True if val
<= ub
+ err
and val
>= lb
- err
else False
164 # Positive Value Sanity check
165 def pos_val_test(self
):
167 Check if metrics value are non-negative.
168 One metric is counted as one test.
169 Failure: when metric value is negative or not provided.
170 Metrics with negative value will be added into self.ignoremetrics.
176 results
= self
.get_results()
179 for name
, val
in results
.items():
181 negmetric
[name
] = val
186 # The first round collect_perf() run these metrics with simple workload
187 # "true". We give metrics a second chance with a longer workload if less
188 # than 20 metrics failed positive test.
189 if len(rerun
) > 0 and len(rerun
) < 20:
190 second_results
= dict()
191 self
.second_test(rerun
, second_results
)
192 for name
, val
in second_results
.items():
193 if name
not in negmetric
:
199 if len(negmetric
.keys()):
200 self
.ignoremetrics
.update(negmetric
.keys())
202 [TestError([m
], self
.workloads
[self
.wlidx
], negmetric
[m
], 0) for m
in negmetric
.keys()])
206 def evaluate_formula(self
, formula
: str, alias
: dict, ridx
: int = 0):
208 Evaluate the value of formula.
210 @param formula: the formula to be evaluated
211 @param alias: the dict has alias to metric name mapping
212 @returns: value of the formula is success; -1 if the one or more metric value not provided
220 # TODO: support parenthesis?
221 for i
in range(len(formula
)):
222 if i
+1 == len(formula
) or formula
[i
] in ('+', '-', '*', '/'):
223 s
= alias
[formula
[b
:i
]] if i
+ \
224 1 < len(formula
) else alias
[formula
[b
:]]
225 v
= self
.get_value(s
, ridx
)
229 f
= f
+ "{0}(={1:.4f})".format(s
, v
[0])
231 stack
[-1] = stack
[-1] * v
233 stack
[-1] = stack
[-1] / v
238 if i
+ 1 < len(formula
):
244 return -1, "Metric value missing: "+','.join(errs
)
249 # Relationships Tests
250 def relationship_test(self
, rule
: dict):
252 Validate if the metrics follow the required relationship in the rule.
253 eg. lower_bound <= eval(formula)<= upper_bound
254 One rule is counted as ont test.
255 Failure: when one or more metric result(s) not provided, or when formula evaluated outside of upper/lower bounds.
257 @param rule: dict with metric name(+alias), formula, and required upper and lower bounds.
260 for m
in rule
['Metrics']:
261 alias
[m
['Alias']] = m
['Name']
262 lbv
, ubv
, t
= self
.get_bounds(
263 rule
['RangeLower'], rule
['RangeUpper'], rule
['ErrorThreshold'], alias
, ridx
=rule
['RuleIndex'])
264 val
, f
= self
.evaluate_formula(
265 rule
['Formula'], alias
, ridx
=rule
['RuleIndex'])
267 lb
= rule
['RangeLower']
268 ub
= rule
['RangeUpper']
269 if isinstance(lb
, str):
272 if isinstance(ub
, str):
277 self
.errlist
.append(TestError([m
['Name'] for m
in rule
['Metrics']], self
.workloads
[self
.wlidx
], [],
278 lb
, ub
, rule
['Description']))
279 elif not self
.check_bound(val
, lbv
, ubv
, t
):
280 self
.errlist
.append(TestError([m
['Name'] for m
in rule
['Metrics']], self
.workloads
[self
.wlidx
], [val
],
281 lb
, ub
, rule
['Description']))
289 def single_test(self
, rule
: dict):
291 Validate if the metrics are in the required value range.
292 eg. lower_bound <= metrics_value <= upper_bound
293 One metric is counted as one test in this type of test.
294 One rule may include one or more metrics.
295 Failure: when the metric value not provided or the value is outside the bounds.
296 This test updates self.total_cnt.
298 @param rule: dict with metrics to validate and the value range requirement
300 lbv
, ubv
, t
= self
.get_bounds(
301 rule
['RangeLower'], rule
['RangeUpper'], rule
['ErrorThreshold'])
302 metrics
= rule
['Metrics']
309 result
= self
.get_value(m
['Name'])
310 if len(result
) > 0 and self
.check_bound(result
[0], lbv
, ubv
, t
) or m
['Name'] in self
.skiplist
:
313 failures
[m
['Name']] = result
314 rerun
.append(m
['Name'])
316 if len(rerun
) > 0 and len(rerun
) < 20:
317 second_results
= dict()
318 self
.second_test(rerun
, second_results
)
319 for name
, val
in second_results
.items():
320 if name
not in failures
:
322 if self
.check_bound(val
, lbv
, ubv
, t
):
326 failures
[name
] = [val
]
327 self
.results
[0][name
] = val
329 self
.totalcnt
+= totalcnt
330 self
.passedcnt
+= passcnt
331 if len(failures
.keys()) != 0:
332 self
.errlist
.extend([TestError([name
], self
.workloads
[self
.wlidx
], val
,
333 rule
['RangeLower'], rule
['RangeUpper']) for name
, val
in failures
.items()])
337 def create_report(self
):
339 Create final report and write into a JSON file.
344 allres
= [{"Workload": self
.workloads
[i
], "Results": self
.allresults
[i
]}
345 for i
in range(0, len(self
.workloads
))]
346 self
.json_dump(allres
, self
.datafname
)
348 def check_rule(self
, testtype
, metric_list
):
350 Check if the rule uses metric(s) that not exist in current platform.
352 @param metric_list: list of metrics from the rule.
353 @return: False when find one metric out in Metric file. (This rule should not skipped.)
354 True when all metrics used in the rule are found in Metric file.
356 if testtype
== "RelationshipTest":
357 for m
in metric_list
:
358 if m
['Name'] not in self
.metrics
:
362 # Start of Collector and Converter
363 def convert(self
, data
: list, metricvalues
: dict):
365 Convert collected metric data from the -j output to dict of {metric_name:value}.
367 for json_string
in data
:
369 result
= json
.loads(json_string
)
370 if "metric-unit" in result
and result
["metric-unit"] != "(null)" and result
["metric-unit"] != "":
371 name
= result
["metric-unit"].split(" ")[1] if len(result
["metric-unit"].split(" ")) > 1 \
372 else result
["metric-unit"]
373 metricvalues
[name
.lower()] = float(result
["metric-value"])
374 except ValueError as error
:
378 def _run_perf(self
, metric
, workload
: str):
380 command
= [tool
, 'stat', '-j', '-M', f
"{metric}", "-a"]
381 wl
= workload
.split()
383 print(" ".join(command
))
384 cmd
= subprocess
.run(command
, stderr
=subprocess
.PIPE
, encoding
='utf-8')
385 data
= [x
+'}' for x
in cmd
.stderr
.split('}\n') if x
]
386 if data
[0][0] != '{':
387 data
[0] = data
[0][data
[0].find('{'):]
390 def collect_perf(self
, workload
: str):
392 Collect metric data with "perf stat -M" on given workload with -a and -j.
394 self
.results
= dict()
395 print(f
"Starting perf collection")
396 print(f
"Long workload: {workload}")
398 if self
.collectlist
!= "":
399 collectlist
[0] = {x
for x
in self
.collectlist
.split(",")}
401 collectlist
[0] = set(list(self
.metrics
))
402 # Create metric set for relationship rules
403 for rule
in self
.rules
:
404 if rule
["TestType"] == "RelationshipTest":
405 metrics
= [m
["Name"] for m
in rule
["Metrics"]]
406 if not any(m
not in collectlist
[0] for m
in metrics
):
407 collectlist
[rule
["RuleIndex"]] = [
408 ",".join(list(set(metrics
)))]
410 for idx
, metrics
in collectlist
.items():
415 for metric
in metrics
:
416 data
= self
._run
_perf
(metric
, wl
)
417 if idx
not in self
.results
:
418 self
.results
[idx
] = dict()
419 self
.convert(data
, self
.results
[idx
])
422 def second_test(self
, collectlist
, second_results
):
423 workload
= self
.workloads
[self
.wlidx
]
424 for metric
in collectlist
:
425 data
= self
._run
_perf
(metric
, workload
)
426 self
.convert(data
, second_results
)
428 # End of Collector and Converter
430 # Start of Rule Generator
431 def parse_perf_metrics(self
):
433 Read and parse perf metric file:
434 1) find metrics with '1%' or '100%' as ScaleUnit for Percent check
435 2) create metric name list
437 command
= ['perf', 'list', '-j', '--details', 'metrics']
438 cmd
= subprocess
.run(command
, stdout
=subprocess
.PIPE
,
439 stderr
=subprocess
.PIPE
, encoding
='utf-8')
441 data
= json
.loads(cmd
.stdout
)
443 if 'MetricName' not in m
:
444 print("Warning: no metric name")
446 name
= m
['MetricName'].lower()
447 self
.metrics
.add(name
)
448 if 'ScaleUnit' in m
and (m
['ScaleUnit'] == '1%' or m
['ScaleUnit'] == '100%'):
449 self
.pctgmetrics
.add(name
.lower())
450 except ValueError as error
:
451 print(f
"Error when parsing metric data")
456 def remove_unsupported_rules(self
, rules
):
460 for m
in rule
["Metrics"]:
461 if m
["Name"] in self
.skiplist
or m
["Name"] not in self
.metrics
:
465 new_rules
.append(rule
)
468 def create_rules(self
):
470 Create full rules which includes:
471 1) All the rules from the "relationshi_rules" file
472 2) SingleMetric rule for all the 'percent' metrics
474 Reindex all the rules to avoid repeated RuleIndex
476 data
= self
.read_json(self
.rulefname
)
477 rules
= data
['RelationshipRules']
478 self
.skiplist
= set([name
.lower() for name
in data
['SkipList']])
479 self
.rules
= self
.remove_unsupported_rules(rules
)
480 pctgrule
= {'RuleIndex': 0,
481 'TestType': 'SingleMetricTest',
484 'ErrorThreshold': self
.tolerance
,
485 'Description': 'Metrics in percent unit have value with in [0, 100]',
486 'Metrics': [{'Name': m
.lower()} for m
in self
.pctgmetrics
]}
487 self
.rules
.append(pctgrule
)
489 # Re-index all rules to avoid repeated RuleIndex
496 # TODO: need to test and generate file name correctly
497 data
= {'RelationshipRules': self
.rules
, 'SupportedMetrics': [
498 {"MetricName": name
} for name
in self
.metrics
]}
499 self
.json_dump(data
, self
.fullrulefname
)
502 # End of Rule Generator
504 def _storewldata(self
, key
):
506 Store all the data of one workload into the corresponding data structure for all workloads.
507 @param key: key to the dictionaries (index of self.workloads).
509 self
.allresults
[key
] = self
.results
510 self
.alltotalcnt
[key
] = self
.totalcnt
511 self
.allpassedcnt
[key
] = self
.passedcnt
513 # Initialize data structures before data validation of each workload
514 def _init_data(self
):
516 testtypes
= ['PositiveValueTest',
517 'RelationshipTest', 'SingleMetricTest']
518 self
.results
= dict()
519 self
.ignoremetrics
= set()
520 self
.errlist
= list()
526 The real entry point of the test framework.
527 This function loads the validation rule JSON file and Standard Metric file to create rules for
528 testing and namemap dictionaries.
529 It also reads in result JSON file for testing.
531 In the test process, it passes through each rule and launch correct test function bases on the
532 'TestType' field of the rule.
534 The final report is written into a JSON file.
536 if not self
.collectlist
:
537 self
.parse_perf_metrics()
539 print("No metric found for testing")
542 for i
in range(0, len(self
.workloads
)):
545 self
.collect_perf(self
.workloads
[i
])
546 # Run positive value test
549 # skip rules that uses metrics not exist in this platform
550 testtype
= r
['TestType']
551 if not self
.check_rule(testtype
, r
['Metrics']):
553 if testtype
== 'RelationshipTest':
554 self
.relationship_test(r
)
555 elif testtype
== 'SingleMetricTest':
558 print("Unsupported Test Type: ", testtype
)
559 print("Workload: ", self
.workloads
[i
])
560 print("Total Test Count: ", self
.totalcnt
)
561 print("Passed Test Count: ", self
.passedcnt
)
564 return len(self
.errlist
) > 0
565 # End of Class Validator
569 parser
= argparse
.ArgumentParser(
570 description
="Launch metric value validation")
573 "-rule", help="Base validation rule file", required
=True)
575 "-output_dir", help="Path for validator output file, report file", required
=True)
576 parser
.add_argument("-debug", help="Debug run, save intermediate data to files",
577 action
="store_true", default
=False)
579 "-wl", help="Workload to run while data collection", default
="true")
580 parser
.add_argument("-m", help="Metric list to validate", default
="")
581 args
= parser
.parse_args()
582 outpath
= Path(args
.output_dir
)
583 reportf
= Path
.joinpath(outpath
, 'perf_report.json')
584 fullrule
= Path
.joinpath(outpath
, 'full_rule.json')
585 datafile
= Path
.joinpath(outpath
, 'perf_data.json')
587 validator
= Validator(args
.rule
, reportf
, debug
=args
.debug
,
588 datafname
=datafile
, fullrulefname
=fullrule
, workload
=args
.wl
,
590 ret
= validator
.test()
595 if __name__
== "__main__":