-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create summary.json of results from K6 run #8
Open
waleedqk
wants to merge
12
commits into
kserve:main
Choose a base branch
from
waleedqk:wqk_prom_scrape
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
2037ee7
First draft for adding scraper to create summary.json of results
3591b41
add sleep to end of job to give time to exec into job if need be
ca1d4bb
add option to move data to persistant directory in cluster if flag pa…
307c56a
set the persistent dir flag when calling the scraper script
7290768
Add prometheus_api_client package to be installed in Docker image for…
71511ce
add mlflow to be installed in cluster for summary upload to server
e93e008
Update perf tests readme
1b9432d
Remove the extra scraper invocation from the runHowitzer script
1d165a4
remove unwated dependencies from repo
8e42f37
Add unit test fro scraper, and update README. Remove sleep since its …
d2a86bd
Run unit tests via auto discovery
17a526c
Merge branch 'main' into wqk_prom_scrape
aluu317 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
"""This module scrapes the K6 test results and aggregates them into a Markdown | ||
document that is well-organized and easily read. The output of this module | ||
is the deliverable of a Howitzer launch. | ||
|
||
echo $CONFIG_FILE | ||
perf_test/configs/inference-test.json | ||
|
||
python3 -m perf_test.scripts.scraper -r results -s summary -c $CONFIG_FILE | ||
""" | ||
import argparse | ||
import json | ||
import os | ||
import sys | ||
import textwrap | ||
import re | ||
import shutil | ||
|
||
TEST_CONFIGS_FILENAME = 'test_configs.dict' | ||
OUTPUT_FILENAME = 'output.md' | ||
SUMMARY_FILENAME = 'summary.json' | ||
|
||
# Between K6_DELIMITER_INIT_START and K6_DELIMITER_INIT_END is the init info section | ||
# with VU, duration info, etc. | ||
# After K6_DELIMITER_CHECKS is the checks info | ||
K6_DELIMITER_INIT_START = '.io' | ||
K6_DELIMITER_INIT_END = 'running' | ||
K6_DELIMITER_CHECKS = 'default ✓' | ||
SECTION_HEADER = '<!---MD_SECTION--->' | ||
|
||
def retrieve_k6_results(data, start_time, end_time): | ||
"""Extracts raw information from the K6 test. Howitzer pipes the K6 output from | ||
stdout only. Currently, K6 tends to use the following as delimiters for | ||
processing: [----------------------------------------------------------]. | ||
This occurs when the test is loading options, as well as when it is starting. | ||
We use this delimiter to remove the splash screen ASCII art, parse out the | ||
inital params (i.e. num virtual users, duration, and so on), as well as the | ||
load test's results. The result is a formatted string. NOTE: Although the | ||
Markdown string is formatted in yaml, the result is NOT yaml; it just happens | ||
to align well with the way K6 prints results out. | ||
|
||
Parameters: | ||
data (list): List of lines in the K6 file output file being read. | ||
start_time (str): Date at which the K6 test being considered began. | ||
end_time (str): Date at which the K6 test being considered ended. | ||
|
||
Returns: | ||
str: Formatted code block (with date info) to be rendered into Markdown. | ||
""" | ||
init_start_idx = [i for i, val in enumerate(data) if K6_DELIMITER_INIT_START in val][-1] | ||
init_end_idx = [i for i, val in enumerate(data) if K6_DELIMITER_INIT_END in val][0] | ||
check_idx = [i for i, val in enumerate(data) if K6_DELIMITER_CHECKS in val][0] | ||
init_info = [x for x in data[init_start_idx+1:init_end_idx]] | ||
results = [x for x in data[check_idx+1:]] | ||
formatted = textwrap.dedent(''.join(init_info + ['\n'] + results)) | ||
start_tstr = 'Test start time: {}'.format(start_time) | ||
end_tstr = 'Test end time: {}'.format(end_time) | ||
return '\n```yaml\n{}{}{}```'.format(formatted, start_tstr, end_tstr) | ||
|
||
|
||
def write_document_intro(json_configs, out_file): | ||
"""Writes the header of the output markdown file, which consists of the overall | ||
objective and the JSON file required for reproducing the tests. | ||
|
||
Args: | ||
json_configs (dict): parsed JSON configuration file. | ||
out_file (file): opened output file being written. | ||
""" | ||
if 'rendering_configs' not in json_configs: | ||
raise KeyError('Rendering configs not present in configmap! Scraping failed.') | ||
out_file.write('# ' + json_configs['rendering_configs']['title'] + '\n') | ||
out_file.write(SECTION_HEADER + '\n') | ||
if 'description' in json_configs['rendering_configs']: | ||
out_file.write(json_configs['rendering_configs']['description'] + '\n ') | ||
out_file.write('## JSON Used by Howitzer Configmap \n') | ||
out_file.write('```json\n'+json.dumps(json_configs, indent=2, sort_keys=True)+'\n```\n') | ||
|
||
def write_document_tests(json_configs, out_file, results_dir: str): | ||
"""Writes the remainder of the file (K6 test results). Groups each named block | ||
as they are defined in the config file. Also renders in optional block | ||
descriptions. | ||
|
||
Args: | ||
json_configs (dict): parsed JSON configuration file. | ||
out_file (file): opened output file being written. | ||
""" | ||
if 'test_configs' not in json_configs: | ||
raise KeyError('Rendering configs not present in configmap! Scraping failed.') | ||
result_files = [x for x in os.listdir(results_dir) if x.endswith(".txt")] | ||
out_file.write(SECTION_HEADER + '\n') | ||
out_file.write('## Load Test Results') | ||
# Iterate over all testing groups. Make a drop down menu for each one. | ||
for test_opts in json_configs['test_configs']: | ||
out_file.write('\n<details>\n') | ||
out_file.write('<summary>'+ test_opts['name'] +'</summary>\n') | ||
if 'description' in test_opts: | ||
out_file.write(test_opts['description'] + '\n\n') | ||
# Iterate over all K6 subtests that were rendered and processed. | ||
matching_files = [x for x in result_files if x[:x.rindex('_')] == test_opts['name']] | ||
for res_file in natural_sort(matching_files): | ||
with open(os.path.join(results_dir, res_file), 'r') as results: | ||
start_time, *contents, end_time = results.readlines() | ||
k6_dump = retrieve_k6_results(contents, start_time, end_time) | ||
out_file.write('{}\n\n'.format(k6_dump)) | ||
out_file.write('</details>\n') | ||
|
||
def natural_sort(filenames): | ||
"""Naturally sorts the filenames so that they appear in a logical order once they | ||
are rendered into the output markdown document. | ||
|
||
Args: | ||
filenames (list): list of filenames to be sorted. | ||
""" | ||
convert = lambda x: int(x) if x.isdigit() else x.lower() | ||
alphanum_key = lambda x: [ convert(c) for c in re.split('([0-9]+)', x) ] | ||
return sorted(filenames, key = alphanum_key) | ||
|
||
def generate_md_output(config_file: str, results_dir: str): | ||
"""Writes the output document to be retrieved by the user after running K6 tests. | ||
The document will be stored in the top level Howitzer directory. | ||
""" | ||
config_contents = json.load(open(config_file, 'r')) | ||
with open(OUTPUT_FILENAME, 'w') as out_file: | ||
write_document_intro(config_contents, out_file) | ||
write_document_tests(config_contents, out_file, results_dir) | ||
|
||
def write_summary_tests(summaries_dir: str, json_configs, out_file, results_dir: str): | ||
"""Writes the summary.json file with consolidated test info, config and results | ||
|
||
Args: | ||
json_configs (dict): parsed JSON configuration file. | ||
out_file (file): opened output file being written. | ||
""" | ||
if 'rendering_configs' not in json_configs: | ||
raise KeyError('Rendering configs with title and description not present in configmap! Scraping failed.') | ||
|
||
summary_files = [x for x in os.listdir(summaries_dir) if x[-4:] == 'json'] | ||
test_configs_file = os.path.join(summaries_dir, TEST_CONFIGS_FILENAME) | ||
test_configs = json.load(open(test_configs_file, 'r')) | ||
summary_configs = {} | ||
|
||
# Iterate over all testing summaries and adds test info, config and results for each test | ||
for test_summary in summary_files: | ||
test_name = test_summary[:-5] | ||
summary_configs[test_name] = {} | ||
|
||
if 'rendering_configs' in json_configs: | ||
summary_configs[test_name]['rendering_configs'] = json_configs['rendering_configs'] | ||
|
||
summary_configs[test_name] = test_configs[test_name] | ||
|
||
#Get start and end time for test from test result file in RESULTS_DIR | ||
with open(os.path.join(results_dir, test_name + '.txt'), 'r') as result_file: | ||
start_time, *contents, end_time = result_file.readlines() | ||
if start_time: summary_configs[test_name]['start_time'] = start_time.strip() | ||
if end_time: summary_configs[test_name]['end_time'] = end_time.strip() | ||
|
||
summary_configs[test_name]['test_results'] = json.load(open(os.path.join(summaries_dir, test_name + '.json'), 'r')) | ||
|
||
# Per k6 version 0.31.0, we don't support the http_req_duration sub-metric with label: http_req_duration{expected_response:true} | ||
if "http_req_duration{expected_response:true}" in summary_configs[test_name]['test_results']["metrics"]: | ||
del summary_configs[test_name]['test_results']["metrics"]["http_req_duration{expected_response:true}"] | ||
|
||
json.dump(summary_configs, out_file, indent = 4) | ||
print("Dumping json results to a log line for persistence in logDNA") | ||
print(json.dumps(summary_configs)) | ||
|
||
def generate_summary_output(summaries_dir: str, config_file: str, results_dir: str): | ||
"""Writes the output summary document to be retrieved by the user after running K6 tests. | ||
The document will be stored in the top level Howitzer directory. | ||
""" | ||
config_contents = json.load(open(config_file, 'r')) | ||
with open(SUMMARY_FILENAME, 'w') as out_file: | ||
write_summary_tests(summaries_dir, config_contents, out_file, results_dir) | ||
|
||
if __name__ == '__main__': | ||
|
||
parser = argparse.ArgumentParser(add_help=False, | ||
description= | ||
'Render some mother trucking K6 tests.') | ||
|
||
parser.add_argument('--help', action='help', help='Show this help message and exit.') | ||
|
||
parser.add_argument('-r', '--results-dir', metavar='RESULTS_DIR', | ||
help='The directory to find the k6 test results in', | ||
required=True) | ||
parser.add_argument('-s', '--summaries-dir', metavar='SUMMARIES_DIR', | ||
help='The directory to place the k6 per-test summary files into', | ||
required=True) | ||
parser.add_argument('-c', '--config-file', metavar='CONFIG_FILE', | ||
help='The absolute path to the one true test config file to be exploded', | ||
required=True) | ||
|
||
args = parser.parse_args() | ||
|
||
generate_md_output(args.config_file, args.results_dir) | ||
generate_summary_output(args.summaries_dir, args.config_file, args.results_dir) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's another
python3 -m perf_test.scripts.scraper -r results -s summary -c $CONFIG_FILE
down below that basically does the same command. Is there a difference here? Maybe we only need one?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, so testing it out locally and understanding the code as well, I think we can just remove the invocation in the loop and just running it at the end will suffice
Testing it on the perf cluster to confirm no change in behaviour