From b148b4601024f2a92f9d9b6ba711f8c26719781d Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 27 Sep 2023 13:37:43 -0400 Subject: [PATCH] benchmarks working --- benchmark.py | 91 ++++++++++++++++++++++++++++++++ github_client.py | 23 ++++++-- main.py | 8 ++- media/benchmark_output.png | Bin 0 -> 44002 bytes readme.md | 58 ++++++++++++++------ results_parser.py | 46 ++++++++++++++++ tests/results_parser_test.py | 25 +++++++++ tests/test_utilities.py | 99 ++++++++++++++++++++++++++++++++++- utilities.py | 13 +++++ 9 files changed, 338 insertions(+), 25 deletions(-) create mode 100644 benchmark.py create mode 100644 media/benchmark_output.png create mode 100644 results_parser.py create mode 100644 tests/results_parser_test.py diff --git a/benchmark.py b/benchmark.py new file mode 100644 index 0000000..378d448 --- /dev/null +++ b/benchmark.py @@ -0,0 +1,91 @@ +from typing import List + +import matplotlib.pyplot as plt +import argparse +from github_client import GithubClient +from results_parser import parse +from single_file_cache import SingleFileCache +from utilities import get_dates_between, convert_to_plot +from datetime import datetime +from collections import defaultdict + +COMMIT_CACHE_FILE = 'cache/benchmark-date-commit-cache.json' +REPORT_CACHE_FILE = 'cache/benchmark-report-cache.json' + + +class BenchmarkApp: + def __init__(self, file_path: str): + self.client = GithubClient() + self.commit_cache = SingleFileCache(location=COMMIT_CACHE_FILE) + self.report_cache = SingleFileCache(location=REPORT_CACHE_FILE) + self.file_path = file_path + + def get_commit_by_date(self, repository, date): + find_commit = self.commit_cache.retrieve_value(date) + if not find_commit: + find_commit = self.client.get_most_recent_commit(repository, date, "gh-pages") + if find_commit: + self.commit_cache.add_to_cache(date, find_commit) + + return find_commit + + def get_report_by_commit(self, repository, commit): + repo_data = self.report_cache.retrieve_value(commit) + + if not repo_data: + repo_data = self.client.get_file_at_commit(repository=repository, filepath=self.file_path, commit_sha=commit) + self.report_cache.add_to_cache(commit, repo_data) + + return repo_data + + +def main(args): + file_path = "benchmark-overhead/results/release/summary.txt" + + metrics = [ + "Min heap used (MB)", + "Max heap used (MB)" + ] + + timeframe = get_dates_between(args.start, datetime.now().date(), args.interval) + result = defaultdict(dict) + + app = BenchmarkApp(file_path=file_path) + + for snapshot in timeframe: + commit = app.get_commit_by_date(date=snapshot, repository=args.repo) + + report = app.get_report_by_commit(repository=args.repo, commit=commit) + parsed = parse(report, metrics) + if parsed: + result[snapshot]["date"] = snapshot + for metric in metrics: + result[snapshot][metric] = parsed.metrics[metric] + + dates, metric_values = convert_to_plot(result, metrics) + + for metric, values in metric_values.items(): + plt.plot(dates, values, label=metric) + + plt.xlabel('Date') + plt.ylabel('MB') + plt.title('Benchmark Metrics') + plt.xticks(rotation=45) + plt.legend() + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Benchmark Tracker') + parser.add_argument("-r", "--repo", + help="Repository name. " + "ex: open-telemetry/opentelemetry-java-instrumentation", + required=True) + parser.add_argument("-s", "--start", + help="Starting Date (will calculate from this date until now)", + required=True) + parser.add_argument("-i", "--interval", + help="Interval (in days) between data points", required=True) + arguments = parser.parse_args() + main(arguments) diff --git a/github_client.py b/github_client.py index 131ecd0..58c6342 100644 --- a/github_client.py +++ b/github_client.py @@ -1,3 +1,5 @@ +import base64 + import requests import os @@ -17,13 +19,14 @@ def _get(self, url, params=None): except Exception as e: print(e) - def get_most_recent_commit(self, repo, timestamp) -> requests.models.Response: + def get_most_recent_commit(self, repo: str, timestamp: str, branch: str) -> requests.models.Response: api_url = f"{self.base_url}/repos/{repo}/commits" params = { "per_page": 1, "until": timestamp, - "order": "desc" + "order": "desc", + "sha": branch } response = self._get(api_url, params=params) @@ -40,7 +43,7 @@ def get_most_recent_commit(self, repo, timestamp) -> requests.models.Response: print(f"Error: {response.status_code}") return None - def get_repository_at_commit(self, repository, commit_sha): + def get_repository_at_commit(self, repository: str, commit_sha: str): api_url = f"{self.base_url}/repos/{repository}/git/trees/{commit_sha}?recursive=1" response = self._get(api_url) @@ -50,3 +53,17 @@ def get_repository_at_commit(self, repository, commit_sha): else: print(f"Error: {response.status_code}") return None + + def get_file_at_commit(self, repository: str, filepath: str, commit_sha: str): + api_url = f"{self.base_url}/repos/{repository}/contents/{filepath}" + + response = self._get(api_url, params={"ref": commit_sha}) + + if response.status_code == 200: + # File content is base64 encoded, decode it + content = response.json().get("content", "") + content = base64.b64decode(content) + return str(content, encoding='utf-8') + else: + print(f"Error: {response.status_code}") + return None diff --git a/main.py b/main.py index 83f8312..cff5324 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ from data_filter import DataFilter from multi_file_cache import MultiFileCache -from utilities import count_by_file_extension, get_dates_between +from utilities import count_by_file_extension, get_dates_between, convert_to_plot from single_file_cache import SingleFileCache from github_client import GithubClient @@ -27,7 +27,7 @@ def __init__(self, languages: List[str], path_prefix: str, keyword: str): def get_commit_by_date(self, repository, date): find_commit = self.commit_cache.retrieve_value(date) if not find_commit: - find_commit = self.client.get_most_recent_commit(repository, date) + find_commit = self.client.get_most_recent_commit(repository, date, "main") if find_commit: self.commit_cache.add_to_cache(date, find_commit) @@ -74,9 +74,7 @@ def main(args): except Exception as e: print(f"Error for {snapshot}, {e}") - dates = [] - - language_counts = {} + dates, language_counts = convert_to_plot(result, languages) for item in result.values(): dates.append(item["date"][:10]) diff --git a/media/benchmark_output.png b/media/benchmark_output.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd0a52bb8cbcd144cdb10c05b0209e8cbb1494e GIT binary patch literal 44002 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 zⅅ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 literal 0 HcmV?d00001 diff --git a/readme.md b/readme.md index 93fb746..5d4582d 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,10 @@ -# Code Migration Tracker +# Repo Metrics -Goal: Given a repository, a timeframe, and any filtering rules, track a goal over time. +Random collection of tools to pull and visualize various data about a repository as timeseries metrics. + +Functionality: +* Timeseries file count tracking: for tracking migration projects from one language to another. +* Pull benchmark data and visualize as timeseries ## Setup @@ -11,7 +15,23 @@ export GITHUB_TOKEN="insert-your-token" make install ``` -## Arguments +## Test / linting + +``` +# Make sure you install pytest and ruff +make install + +# Tests +make test + +# Linting +make lint +``` + + +## File Count Tracking + +### Arguments | Argument | Command | Description | Example | |------------|----------------|----------------------------------------------------------------------------|------------------------------------------------------------| @@ -20,7 +40,7 @@ make install | Interval | -i, --interval | Interval (in days) between data points | --interval 14 | -## Example Usage: +### Example Usage: In the `open-telemetry/opentelemetry-java-instrumentation` repository, track the conversion of tests from groovy to java in the `instrumentation` directory starting from 2022-11-15 with a data point every 2 weeks. @@ -32,19 +52,28 @@ Output: ![Example](./media/example_output.png) -## Test / linting +## Benchmark Visualization -``` -# Make sure you install pytest and ruff -make install +This is very specific to the open-telemetry/opentelemetry-java-instrumentation repo -# Tests -make test +### Arguments -# Linting -make lint -``` +| Argument | Command | Description | Example | +|------------|----------------|----------------------------------------------------------------------------|------------------------------------------------------------| +| Repository | -r, --repo | Repository name. | --repo "open-telemetry/opentelemetry-java-instrumentation" | +| Start Date | -s, --start | Starting Date in format %Y-%m-%d (will calculate from this date until now) | --start "2022-11-15" | +| Interval | -i, --interval | Interval (in days) between data points | --interval 14 | + + +### Example Usage: + +Chart Min and max heap starting from 2022-02-14 with a data point every 30 days. +`python benchmark.py -r "open-telemetry/opentelemetry-java-instrumentation" -s "2022-02-14" -i 30` + +Output: + +![Example](./media/benchmark_output.png) ## Approach @@ -54,6 +83,3 @@ make lint - Cache this data locally to avoid repeated api calls - Generate Graph to show results over time frame - -## Data Filters - diff --git a/results_parser.py b/results_parser.py new file mode 100644 index 0000000..e413cf5 --- /dev/null +++ b/results_parser.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import List + + +class ReportMetrics: + def __init__(self, date: str): + self.date = date + self.metrics = {} + + +def parse(report: str, metrics: List[str]) -> ReportMetrics: + if report is None: + return None + + split = report.split("----------------------------------------------------------\n") + + metrics_split = split[2].split("\n") + date = convert_to_desired_format(split[1].split("Run at ")[1].split("\n")[0]) + + report_metrics = ReportMetrics(date=date) + + try: + for line in metrics_split: + for metric in metrics: + + if line.startswith(metric): + values = line.split(":") + report_metrics.metrics[metric] = float(values[1].split()[1]) + except IndexError: + return None + + return report_metrics + + +def convert_to_desired_format(date_str): + # Define the input and output date formats + input_format = "%a %b %d %H:%M:%S UTC %Y" + output_format = "%Y-%m-%d" + + try: + parsed_date = datetime.strptime(date_str, input_format) + formatted_date = parsed_date.strftime(output_format) + return formatted_date + except ValueError: + print("Invalid date format") + return None diff --git a/tests/results_parser_test.py b/tests/results_parser_test.py new file mode 100644 index 0000000..dfa3cb0 --- /dev/null +++ b/tests/results_parser_test.py @@ -0,0 +1,25 @@ +import unittest + +from results_parser import parse + + +class ResultsParserTestCase(unittest.TestCase): + + def __init__(self, *args, **kwargs): + self.example = """----------------------------------------------------------\n Run at Sat Sep 23 05:22:19 UTC 2023\n release : compares no agent, latest stable, and latest snapshot agents\n 5 users, 5000 iterations\n----------------------------------------------------------\nAgent : none latest snapshot\nRun duration : 00:02:27 00:02:57 00:03:06\nAvg. CPU (user) % : 0.46024063 0.48809186 0.49900937\nMax. CPU (user) % : 0.5527638 0.5891089 0.6\nAvg. mch tot cpu % : 0.9943353 0.99306744 0.9932704\nStartup time (ms) : 19598 16351 17050\nTotal allocated MB : 27799.50 34195.20 58039.97\nMin heap used (MB) : 88.10 115.85 112.63\nMax heap used (MB) : 365.90 557.00 478.78\nThread switch rate : 28534.94 29848.291 32354.986\nGC time (ms) : 1800 3014 2928\nGC pause time (ms) : 1814 3052 2959\nReq. mean (ms) : 10.74 12.82 13.51\nReq. p95 (ms) : 32.04 38.45 40.28\nIter. mean (ms) : 144.60 173.90 182.65\nIter. p95 (ms) : 233.74 275.94 291.89\nNet read avg (bps) : 5441971.00 4728712.00 4507975.00\nNet write avg (bps) : 7256048.00 25533599.00 24434992.00\nPeak threads : 43 55 56\n""" + self.metrics = [ + "Min heap used (MB)", + "Max heap used (MB)" + ] + super(ResultsParserTestCase, self).__init__(*args, **kwargs) + + def test_parse_metrics_from_summary(self): + result = parse(report=self.example, metrics=self.metrics) + self.assertEqual(557.00, result.metrics["Max heap used (MB)"]) + self.assertEqual(115.85, result.metrics["Min heap used (MB)"]) + + def test_parse_date_from_summary(self): + result = parse(report=self.example, metrics=self.metrics) + self.assertEqual("2023-09-23", result.date) + + diff --git a/tests/test_utilities.py b/tests/test_utilities.py index b25200a..aafcfdf 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,6 +1,7 @@ +from collections import defaultdict from unittest import TestCase -from utilities import get_dates_between, count_by_file_extension +from utilities import get_dates_between, count_by_file_extension, convert_to_plot from datetime import datetime @@ -56,3 +57,99 @@ def test_count_by_file_extension(self): self.assertEqual(2, result['groovy']) self.assertEqual(1, result['txt']) + def test_report_generator(self): + metrics = [ + "Min heap used (MB)", + "Max heap used (MB)" + ] + + expected_dates = ['2022-11-16', '2022-11-30', '2022-12-14', '2022-12-28', + '2023-01-11', '2023-01-25', '2023-02-08', '2023-02-22', + '2023-03-08', '2023-03-22', '2023-04-05', '2023-04-19', + '2023-05-03', '2023-05-17', '2023-05-31', '2023-06-14', + '2023-06-28', '2023-07-12', '2023-07-26', '2023-08-09', + '2023-08-23', '2023-09-06', '2023-09-20'] + + test = {'2022-11-16T00:00:00Z': {'date': '2022-11-16T00:00:00Z', + 'Min heap used (MB)': 88.37, + 'Max heap used (MB)': 360.79}, + '2022-11-30T00:00:00Z': {'date': '2022-11-30T00:00:00Z', + 'Min heap used (MB)': 93.24, + 'Max heap used (MB)': 357.57}, + '2022-12-14T00:00:00Z': {'date': '2022-12-14T00:00:00Z', + 'Min heap used (MB)': 93.12, + 'Max heap used (MB)': 489.0}, + '2022-12-28T00:00:00Z': {'date': '2022-12-28T00:00:00Z', + 'Min heap used (MB)': 93.36, + 'Max heap used (MB)': 339.98}, + '2023-01-11T00:00:00Z': {'date': '2023-01-11T00:00:00Z', + 'Min heap used (MB)': 90.34, + 'Max heap used (MB)': 448.27}, + '2023-01-25T00:00:00Z': {'date': '2023-01-25T00:00:00Z', + 'Min heap used (MB)': 90.57, + 'Max heap used (MB)': 343.99}, + '2023-02-08T00:00:00Z': {'date': '2023-02-08T00:00:00Z', + 'Min heap used (MB)': 88.31, + 'Max heap used (MB)': 389.67}, + '2023-02-22T00:00:00Z': {'date': '2023-02-22T00:00:00Z', + 'Min heap used (MB)': 89.99, + 'Max heap used (MB)': 334.34}, + '2023-03-08T00:00:00Z': {'date': '2023-03-08T00:00:00Z', + 'Min heap used (MB)': 85.4, + 'Max heap used (MB)': 340.25}, + '2023-03-22T00:00:00Z': {'date': '2023-03-22T00:00:00Z', + 'Min heap used (MB)': 94.67, + 'Max heap used (MB)': 362.89}, + '2023-04-05T00:00:00Z': {'date': '2023-04-05T00:00:00Z', + 'Min heap used (MB)': 83.31, + 'Max heap used (MB)': 406.11}, + '2023-04-19T00:00:00Z': {'date': '2023-04-19T00:00:00Z', + 'Min heap used (MB)': 108.68, + 'Max heap used (MB)': 474.59}, + '2023-05-03T00:00:00Z': {'date': '2023-05-03T00:00:00Z', + 'Min heap used (MB)': 90.29, + 'Max heap used (MB)': 396.23}, + '2023-05-17T00:00:00Z': {'date': '2023-05-17T00:00:00Z', + 'Min heap used (MB)': 96.5, + 'Max heap used (MB)': 448.06}, + '2023-05-31T00:00:00Z': {'date': '2023-05-31T00:00:00Z', + 'Min heap used (MB)': 94.72, + 'Max heap used (MB)': 362.23}, + '2023-06-14T00:00:00Z': {'date': '2023-06-14T00:00:00Z', + 'Min heap used (MB)': 112.4, + 'Max heap used (MB)': 483.55}, + '2023-06-28T00:00:00Z': {'date': '2023-06-28T00:00:00Z', + 'Min heap used (MB)': 109.7, + 'Max heap used (MB)': 478.83}, + '2023-07-12T00:00:00Z': {'date': '2023-07-12T00:00:00Z', + 'Min heap used (MB)': 115.64, + 'Max heap used (MB)': 511.06}, + '2023-07-26T00:00:00Z': {'date': '2023-07-26T00:00:00Z', + 'Min heap used (MB)': 117.18, + 'Max heap used (MB)': 545.65}, + '2023-08-09T00:00:00Z': {'date': '2023-08-09T00:00:00Z', + 'Min heap used (MB)': 111.84, + 'Max heap used (MB)': 483.33}, + '2023-08-23T00:00:00Z': {'date': '2023-08-23T00:00:00Z', + 'Min heap used (MB)': 114.26, + 'Max heap used (MB)': 503.5}, + '2023-09-06T00:00:00Z': {'date': '2023-09-06T00:00:00Z', + 'Min heap used (MB)': 116.66, + 'Max heap used (MB)': 554.82}, + '2023-09-20T00:00:00Z': {'date': '2023-09-20T00:00:00Z', + 'Min heap used (MB)': 103.79, + 'Max heap used (MB)': 521.74}} + + expected_plots = { + 'Min heap used (MB)': [88.37, 93.24, 93.12, 93.36, 90.34, 90.57, 88.31, + 89.99, 85.4, 94.67, 83.31, 108.68, 90.29, 96.5, + 94.72, 112.4, 109.7, 115.64, 117.18, 111.84, 114.26, + 116.66, 103.79], + 'Max heap used (MB)': [360.79, 357.57, 489.0, 339.98, 448.27, 343.99, + 389.67, 334.34, 340.25, 362.89, 406.11, 474.59, + 396.23, 448.06, 362.23, 483.55, 478.83, 511.06, + 545.65, 483.33, 503.5, 554.82, 521.74]} + + dates, plots = convert_to_plot(test, metrics) + self.assertEqual(expected_plots, plots) + self.assertEqual(expected_dates, dates) diff --git a/utilities.py b/utilities.py index a8ccf1f..651fef9 100644 --- a/utilities.py +++ b/utilities.py @@ -35,3 +35,16 @@ def count_by_file_extension(files: List[str], languages: List[str]) -> dict: if file.endswith(extension): file_counts[ext] += 1 return file_counts + + +def convert_to_plot(input_dict: dict, items): + result = {} + dates = [] + for entry in input_dict.values(): + dates.append(entry["date"][:10]) + for item in items: + try: + result[item].append(entry[item]) + except KeyError: + result[item] = [entry[item]] + return dates, result