Skip to content

Commit

Permalink
Merge branch 'main' into upgrade-to-python-3.12
Browse files Browse the repository at this point in the history
  • Loading branch information
liamtoozer authored Oct 8, 2024
2 parents 42b5653 + d5b3d85 commit bb1094c
Show file tree
Hide file tree
Showing 10 changed files with 633 additions and 107 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
include = scripts/*

[report]
fail_under = 80
fail_under = 85
10 changes: 7 additions & 3 deletions requests/test_benchmark_business_happy_path.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"method": "POST",
"url": "/questionnaire/block-383/",
"data": {
"answer-439": "Hi eQ team \ud83d\ude0d"
"answer-439": "Hi eQ Team"
}
},
{
Expand Down Expand Up @@ -119,8 +119,8 @@
"method": "POST",
"url": "/questionnaire/people/add-person/",
"data": {
"first-name": "Erling",
"last-name": "Haaland"
"first-name": "Kylian",
"last-name": "Mbappé"
}
},
{
Expand Down Expand Up @@ -540,6 +540,10 @@
"method": "GET",
"url": "/submitted/view-response/"
},
{
"method": "GET",
"url": "/submitted/download-pdf"
},
{
"method": "GET",
"url": "/submitted/thank-you/"
Expand Down
115 changes: 76 additions & 39 deletions scripts/benchmark_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,55 @@ def __init__(self, folder_paths: List[str]):
"POST": {"response_times": [], "total": 0},
}

self._session_percentile: List[int] = []
self._pdf_percentile: List[int] = []
self._total_failures: int = 0
self._percentiles: Mapping[int : List[float]] = defaultdict(list) # noqa: E203

self._process_file_data()

def __str__(self):
formatted_percentiles = "\n".join(
self._formatted_percentiles: str = "\n".join(
f"{percentile}th: {self.percentiles[percentile]}ms"
for percentile in self.PERCENTILES_TO_REPORT
)

self.formatted_average_get: str = self.formatted_percentile(self.average_get)
self.formatted_average_post: str = self.formatted_percentile(self.average_post)
self.formatted_average_pdf_percentile: str = self.formatted_percentile(
self.average_pdf_percentile
)
self.formatted_average_session_percentile: str = self.formatted_percentile(
self.average_session_percentile
)

def __str__(self):
if self.output_to_github:
formatted_percentiles = formatted_percentiles.replace(os.linesep, "<br />")
formatted_percentiles = self._formatted_percentiles.replace(
os.linesep, "<br />"
)
return (
f'{{"body": "'
f"**Benchmark Results**<br /><br />"
f"Percentile Averages:<br />"
f"{formatted_percentiles}<br />"
f"GETs (99th): {self.average_get}ms<br />"
f"POSTs (99th): {self.average_post}ms<br /><br />"
f"GETs (99th): {self.formatted_average_get}<br />"
f"POSTs (99th): {self.formatted_average_post}<br /><br />"
f"PDF: {self.formatted_average_pdf_percentile}<br />"
f"Session: {self.formatted_average_session_percentile}<br /><br />"
f"Total Requests: {self.total_requests:,}<br />"
f"Total Failures: {self._total_failures:,}<br />"
f'Error Percentage: {(round(self.error_percentage, 2))}%<br />"}}'
)
return (
f"---\n"
f"Percentile Averages:\n"
f"{formatted_percentiles}\n"
f"{self._formatted_percentiles}\n"
f"---\n"
f"GETs (99th): {self.formatted_average_get}\n"
f"POSTs (99th): {self.formatted_average_post}\n"
f"---\n"
f"GETs (99th): {self.average_get}ms\n"
f"POSTs (99th): {self.average_post}ms\n"
f"PDF: {self.formatted_average_pdf_percentile}\n"
f"Session: {self.formatted_average_session_percentile}\n"
f"---\n"
f"Total Requests: {self.total_requests:,}\n"
f"Total Failures: {self._total_failures:,}\n"
Expand All @@ -60,34 +79,48 @@ def _process_file_data(self):
for file in self.files:
with open(file) as fp:
for row in DictReader(fp, delimiter=","):
request_count = int(
row.get("Request Count") or row.get("# requests")
)
if row["Name"] == "Aggregated":
failure_count = row.get("Failure Count") or row.get(
"# failures"
)
self._total_failures += int(failure_count)
else:
weighted_request_count = self._get_weighted_request_count(
request_count
)
for percentile in self.PERCENTILES_TO_REPORT:
weighted_percentile = (
float(row[f"{percentile}%"]) * weighted_request_count
)
self._percentiles[percentile].append(weighted_percentile)

percentile_response_time = float(
row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"]
)
weighted_response_time = (
percentile_response_time * weighted_request_count
)
self._requests[row["Type"]]["response_times"].append(
weighted_response_time
)
self._requests[row["Type"]]["total"] += request_count
self._process_row(row)

def _process_row(self, row):
# Handle special 'Aggregated' row
if row["Name"] == "Aggregated":
failure_count = row["Failure Count"] or row["# failures"]
self._total_failures += int(failure_count)
return

if row["Name"] == "/submitted/download-pdf":
self._pdf_percentile.append(
int(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
)
elif row["Name"] == "/session":
self._session_percentile.append(
int(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
)

request_count = int(row.get("Request Count") or row.get("# requests"))
weighted_request_count = self._get_weighted_request_count(request_count)

for percentile in self.PERCENTILES_TO_REPORT:
weighted_percentile = float(row[f"{percentile}%"]) * weighted_request_count
self._percentiles[percentile].append(weighted_percentile)

percentile_response_time = float(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
weighted_response_time = percentile_response_time * weighted_request_count
self._requests[row["Type"]]["response_times"].append(weighted_response_time)
self._requests[row["Type"]]["total"] += request_count

@property
def average_pdf_percentile(self) -> int | None:
if self._pdf_percentile:
average_pdf_percentile = sum(self._pdf_percentile) / len(
self._pdf_percentile
)
return round(average_pdf_percentile)
return None

@property
def average_session_percentile(self) -> int:
return round(sum(self._session_percentile) / len(self._session_percentile))

@property
def files(self) -> List[str]:
Expand All @@ -96,7 +129,7 @@ def files(self) -> List[str]:
@property
def percentiles(self) -> Mapping:
return {
percentile: int(
percentile: round(
sum(values) / self._get_weighted_request_count(self.total_requests)
)
for percentile, values in self._percentiles.items()
Expand All @@ -108,14 +141,14 @@ def total_requests(self) -> int:

@property
def average_get(self) -> int:
return int(
return round(
sum(self._requests["GET"]["response_times"])
/ self._get_weighted_request_count(self._requests["GET"]["total"])
)

@property
def average_post(self) -> int:
return int(
return round(
sum(self._requests["POST"]["response_times"])
/ self._get_weighted_request_count(self._requests["POST"]["total"])
)
Expand All @@ -124,5 +157,9 @@ def average_post(self) -> int:
def error_percentage(self) -> float:
return (self._total_failures * 100) / self.total_requests

@staticmethod
def formatted_percentile(percentile) -> str:
return f"{percentile}ms" if percentile else "N/A"

def _get_weighted_request_count(self, request_count: int) -> float:
return request_count * self.PERCENTILE_TO_USE_FOR_AVERAGES / 100
73 changes: 57 additions & 16 deletions scripts/visualise_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,69 @@
from scripts.get_summary import get_results, parse_environment_variables

PERCENTILES_TO_GRAPH = (50, 90, 95, 99)
PERCENTILES_TO_PLOT = ("50th", "90th", "95th", "99th")

ADDITIONAL_METRICS_TO_GRAPH = ("PDF", "Session")


class GraphGenerationFailed(Exception):
pass


def plot_data(df, number_of_days_to_plot):
try:
plt.style.use("fast")
def plot_data(dataframes, number_of_days_to_plot):
plt.style.use("fast")
fig, axes = plt.subplots(nrows=1, ncols=len(dataframes), figsize=(15, 6))

for i, dataframe in enumerate(dataframes):
"""
We use the .subplot() method below to switch the indexes of the plots themselves,
if we do not switch plots or remove this method, the visuals (such as the graph background)
and, most importantly, the axes values will only be applied to the second subplot.
"""
plt.subplot(1, len(dataframes), i + 1)
if (
number_of_days_to_plot and number_of_days_to_plot <= 45
): # To make the chart still easily digestible
df.plot.line(marker="o", markersize=8)
dataframe.plot.line(
marker="o", markersize=8, ax=axes[i] if len(dataframes) > 1 else axes
)
plt.grid(True, axis="both", alpha=0.3)
else:
df.plot.line()
dataframe.plot.line(ax=axes[i] if len(dataframes) > 1 else axes)

plt.margins(0.03, 0.07)
plt.legend(frameon=True, prop={"size": 17})
plt.xticks(df.index, df["DATE"], size="small", rotation=90)
plt.legend(frameon=True, prop={"size": 10})
plt.xticks(dataframe.index, dataframe["DATE"], size="small", rotation=90)
plt.yticks(size="small")
plt.ylabel("Average Response Time (ms)")
plt.xlabel("Run Date (YYYY-MM-DD)", labelpad=13)

plt.savefig("performance_graph.png", bbox_inches="tight")
print("Graph saved as performance_graph.png")

def create_graph(dataframes, number_of_days_to_plot, filename):
try:
plot_data(dataframes, number_of_days_to_plot)
plt.savefig(filename, bbox_inches="tight")
print("Graph saved as", filename)
except Exception as e:
raise GraphGenerationFailed from e


def get_data_frame(results):
def get_dataframes(folders, number_of_days):
results = list(get_results(folders, number_of_days))
performance_dataframe = get_performance_data_frame(results)
additional_metrics_dataframe = get_additional_metrics_data_frame(results)

return [performance_dataframe, additional_metrics_dataframe]


def create_dataframe(result_fields, values_to_plot):
return DataFrame(
result_fields,
columns=["DATE", *(f"{percentile}" for percentile in values_to_plot)],
)


def get_performance_data_frame(results):
result_fields = [
[
result.date,
Expand All @@ -49,16 +80,26 @@ def get_data_frame(results):
for result in results
]

percentile_columns = (f"{percentile}th" for percentile in PERCENTILES_TO_GRAPH)
return DataFrame(result_fields, columns=["DATE", *percentile_columns])
return create_dataframe(result_fields, PERCENTILES_TO_PLOT)


def get_additional_metrics_data_frame(results):
result_fields = [
[
result.date,
result.statistics.average_pdf_percentile,
result.statistics.average_session_percentile,
]
for result in results
]

return create_dataframe(result_fields, ADDITIONAL_METRICS_TO_GRAPH)


if __name__ == "__main__":
parsed_variables = parse_environment_variables()
number_of_days = parsed_variables["number_of_days"]

folders = sorted(glob(f"{parsed_variables['output_dir']}/*"))
results = get_results(folders, number_of_days)
dataframe = get_data_frame(results)

plot_data(dataframe, number_of_days)
dataframes = get_dataframes(folders, number_of_days)
create_graph(dataframes, number_of_days, "performance_graph.png")
Loading

0 comments on commit bb1094c

Please sign in to comment.