This repository has been archived by the owner on Jul 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsquad-track-duration
executable file
·443 lines (363 loc) · 15 KB
/
squad-track-duration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim: set ts=4
#
# Copyright 2024-present Linaro Limited
#
# SPDX-License-Identifier: MIT
import argparse
import json
import logging
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
import pandas as pd
import plotly.express as px
from squad_client.core.api import SquadApi
from squad_client.core.models import ALL, Squad
squad_host_url = "https://qa-reports.linaro.org/"
SquadApi.configure(cache=3600, url=os.getenv("SQUAD_HOST", squad_host_url))
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
ARTIFACTORIAL_FILENAME = "builds.json"
class MetaFigure:
def __init__(self, plotly_fig, title, description):
self.plotly_fig = plotly_fig
self.title = title
self.description = description
def fig(self):
return self.fig
def title(self):
return self.title
def description(self):
return self.description
def parse_datetime_from_string(datetime_string):
accepted_datetime_formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S"]
# Loop through each accepted datetime format and try parse it
for datetime_format in accepted_datetime_formats:
try:
# If the format parses successfully, return the datetime object
return datetime.strptime(datetime_string, datetime_format)
except ValueError:
pass
# If no format can be parsed, raise an argument error
raise argparse.ArgumentTypeError(
f"Unsupported datetime format {datetime_string}. Accepted formats are {accepted_datetime_formats}"
)
def parse_args():
parser = argparse.ArgumentParser(description="Track duration")
parser.add_argument(
"--group",
required=True,
help="squad group",
)
parser.add_argument(
"--project",
required=True,
help="squad project",
)
parser.add_argument(
"--start-datetime",
type=parse_datetime_from_string,
required=True,
help="Starting date time. Example: 2022-01-01 or 2022-01-01T00:00:00",
)
parser.add_argument(
"--end-datetime",
type=parse_datetime_from_string,
required=True,
help="Ending date time. Example: 2022-12-31 or 2022-12-31T00:00:00",
)
parser.add_argument(
"--build-name",
required=False,
default="gcc-13-lkftconfig",
help="Build name",
)
parser.add_argument(
"--debug",
action="store_true",
default=False,
help="Display debug messages",
)
return parser.parse_args()
def get_cache_from_artifactorial():
exists = os.path.exists(ARTIFACTORIAL_FILENAME)
if not exists:
return {}
with open(ARTIFACTORIAL_FILENAME, "r") as fp:
builds = json.load(fp)
return builds
return {}
def save_build_cache_to_artifactorial(data, days_ago=None):
with open(ARTIFACTORIAL_FILENAME, "w") as fp:
json.dump(data, fp)
def get_data(args, build_cache):
start_datetime = args.start_datetime
end_datetime = args.end_datetime
group = Squad().group(args.group)
project = group.project(args.project)
environments = project.environments(count=ALL).values()
first_start_day = True
final_end_date = False
tmp_data = []
# Set up a delta which determines how many days of data to read from SQUAD
# per loop. Minimum delta is 1 day and delta must be in whole days to keep
# this code easy to read, understand and debug.
delta = timedelta(days=1)
if delta.days < 1:
raise Exception("Minimum delta is 1 day for this code to work.")
if delta.seconds != 0 or delta.microseconds != 0:
raise Exception("Deltas must be whole days only.")
# Loops through each delta until the end date and filters the SQUAD data
# for that delta
while not final_end_date:
# If it is the first date in the range, use the provided start datetime
if first_start_day:
first_start_day = False
# Use the provided start time for the first day
tmp_start_datetime = start_datetime
else:
# For all other days, update the date by the delta then use the
# start of the day by zeroing hours, minutes and seconds
tmp_start_datetime += delta
tmp_start_datetime = tmp_start_datetime.replace(hour=0, minute=0, second=0)
# If the delta for this iteration sends us over the end of the range,
# use the provided end datetime
if tmp_start_datetime + delta >= end_datetime:
# We have reached the last day, so use this as the end date
tmp_end_datetime = end_datetime
final_end_date = True
else:
# Otherwise take the start time (with minutes zeroed) + delta
tmp_end_datetime = (
tmp_start_datetime.replace(hour=0, minute=0, second=0) + delta
)
logger.info(
f"Fetching builds from SQUAD, start_datetime: {tmp_start_datetime}, end_datetime: {tmp_end_datetime}"
)
filters = {
"created_at__lt": tmp_end_datetime.strftime("%Y-%m-%dT%H:%M:%S"),
"created_at__gt": tmp_start_datetime.strftime("%Y-%m-%dT%H:%M:%S"),
"count": ALL,
}
builds = project.builds(**filters)
device_dict = {}
# Loop through the environments and create a lookup table for URL -> device name (slug)
for env in environments:
device_dict[env.url] = env.slug
# Loop through the builds in the specified window and cache their data
# to a file if they are marked as finished. This will mean that we don't
# have to look them up again is SQUAD if we have already looked them up.
for build_id, build in builds.items():
if str(build_id) in build_cache.keys():
logger.debug(f"cached: {build_id}")
tmp_data = tmp_data + build_cache[str(build_id)]
else:
logger.debug(f"no-cache: {build_id}")
tmp_build_cache = []
testruns = build.testruns(count=ALL, prefetch_metadata=True)
for testrun_key, testrun in testruns.items():
device = device_dict[testrun.environment]
metadata = testrun.metadata
durations = metadata.durations
# Ignore testruns without duration data
if durations is None:
continue
build_name = metadata.build_name
# Ignore testruns without a build_name
if build_name is None:
continue
# Read the boot time from the duration data
boottime = durations["tests"]["boot"]
tmp = {
"build_id": build_id,
"build_name": build_name,
"git_describe": build.version.strip(),
"device": device,
"boottime": float(boottime),
"finished": build.finished,
"created_at": build.created_at,
}
tmp_data.append(tmp)
tmp_build_cache.append(tmp)
# Cache data for builds that are marked finished
if build.finished and len(tmp_build_cache) > 0:
build_cache[str(build_id)] = tmp_build_cache
logger.debug(f"finished: {build_id}, {build.finished}")
return tmp_data, build_cache
def combine_plotly_figs_to_html(
figs,
html_fname,
main_title,
main_description,
include_plotlyjs="cdn",
separator=None,
auto_open=False,
):
with open(html_fname, "w") as f:
f.write(f"<h1>{main_title}</h1>")
f.write(f"<div>{main_description}</div>")
index = 0
f.write("<h2>Page content</h2>")
f.write("<ul>")
for fig in figs[1:]:
index = index + 1
f.write(f'<li><a href="#fig{index}">{fig.title}</a></li>')
f.write("</ul>")
f.write(f'<h2><a id="fig0">{figs[0].title}</a></h2>')
f.write(f"<div>{figs[0].description}</div>")
f.write(figs[0].plotly_fig.to_html(include_plotlyjs=include_plotlyjs))
index = 0
for fig in figs[1:]:
index = index + 1
if separator:
f.write(separator)
f.write(f'<h2><a id="fig{index}">{fig.title}</a></h2>')
f.write(f"<div>{fig.description}</div>")
f.write(fig.plotly_fig.to_html(full_html=False, include_plotlyjs=False))
if auto_open:
import webbrowser
uri = Path(html_fname).absolute().as_uri()
webbrowser.open(uri)
def run():
args = parse_args()
if args.debug:
logger.setLevel(level=logging.DEBUG)
if args.start_datetime > args.end_datetime:
raise Exception("Start time must be earlier than end time.")
build_cache = get_cache_from_artifactorial()
data = []
data, build_cache = get_data(args, build_cache)
save_build_cache_to_artifactorial(build_cache)
# Turn the data (list of dicts) into a pandas DataFrame
df = pd.DataFrame(data)
logger.debug("***********************")
logger.debug(df)
logger.debug(df.info())
logger.debug("***********************")
# Generate a build_name_device column and add this as a column in the DataFrame
df["build_name_device"] = df.build_name + "-" + df.device
figure_colletion = []
# Filter the DataFrame by the desired build name(s)
filtered_df1 = df[df["build_name"].isin([args.build_name])]
# Create a DataFrame which groups by type then takes the mean of the boot
# time per type.
df_grouping1 = filtered_df1.groupby(
["created_at", "git_describe", "device", "build_name"]
)
mean_boottimes1 = df_grouping1["boottime"].mean()
# Convert the Series object back to a DataFrame then sort values first by
# device, then by created_at. This will make the graph legend alphabetised
# while also ensuring the dates for each line are ordered by created_at so
# the graph's lines will be drawn correctly.
mean_boottimes1 = mean_boottimes1.reset_index().sort_values(
by=["device", "created_at"]
)
# Calculate how many boottimes we averaged over per device
count_per_device1 = df_grouping1["boottime"].count().groupby("device").sum()
col_name_boottime_count = "Boottimes included in average"
count_per_device1 = count_per_device1.reset_index().rename(
columns={"boottime": col_name_boottime_count}
)
# Create a new column with the name and count, then stick together the
# counts and the averages
count_per_device1["device_count"] = (
count_per_device1.device
+ " ("
+ count_per_device1[col_name_boottime_count].astype(str)
+ ")"
)
mean_boottimes1 = mean_boottimes1.merge(
count_per_device1, on="device", how="inner", suffixes=("_1", "_2")
)
# Create the figure to display this data
figure_colletion.append(
MetaFigure(
px.line(
mean_boottimes1,
x="created_at",
y="boottime",
color="device_count",
markers=True,
labels={"device_count": "Device (number of boots in mean)"},
)
.update_xaxes(
tickvals=mean_boottimes1["created_at"],
ticktext=mean_boottimes1["git_describe"],
)
.update_layout(xaxis_title="Version", yaxis_title="Boot time"),
f"Line graph, {args.build_name}",
f"This line graph is generated from build_name {args.build_name}."
+ " The graph uses the average (mean) over a number of boots for each device. The number of boots included in the average is presented in the 'Device (number of boots in mean)' in the line graph legend.",
)
)
# Filter the DataFrame by the desired build name(s)
filtered_df2 = df[df["build_name"].str.endswith(args.build_name.split("-")[-1])]
# Group and the mean of the boot time for the desired type - this time it is
# grouped by build_name_device, too, since we want to look at both the build
# and what device this was run on.
df_grouping2 = filtered_df2.groupby(
["created_at", "git_describe", "device", "build_name_device", "build_name"]
)
mean_boottimes2 = df_grouping2["boottime"].mean()
# Convert the Series object back to a DataFrame then sort values first by
# build_name_device, then by created_at. This will make the graph legend
# alphabetised while also ensuring the dates for each line are ordered by
# created_at so the graph's lines will be drawn correctly.
mean_boottimes2 = mean_boottimes2.reset_index().sort_values(
by=["build_name_device", "created_at"]
)
logger.debug(mean_boottimes2.info())
logger.debug(mean_boottimes2)
# Calculate how many boottimes we averaged over per device
count_per_device2 = (
df_grouping2["boottime"].count().groupby("build_name_device").sum()
)
count_per_device2 = count_per_device2.reset_index().rename(
columns={"boottime": col_name_boottime_count}
)
# Create a new column with the name and count, then stick together the
# counts and the averages
count_per_device2["build_name_device_count"] = (
count_per_device2.build_name_device
+ " ("
+ count_per_device2[col_name_boottime_count].astype(str)
+ ")"
)
mean_boottimes2 = mean_boottimes2.merge(
count_per_device2, on="build_name_device", how="inner", suffixes=("_1", "_2")
)
# Create the figure for this visualisation
figure_colletion.append(
MetaFigure(
px.line(
mean_boottimes2,
x="created_at",
y="boottime",
color="build_name_device_count",
markers=True,
labels={
"build_name_device_count": "Build name - device (number of boots in mean)"
},
)
.update_xaxes(
tickvals=mean_boottimes2["created_at"],
ticktext=mean_boottimes2["git_describe"],
)
.update_layout(xaxis_title="Version", yaxis_title="Boot time"),
f"Line graph, {args.build_name.split('-')[-1]}",
f"This line graph is generated from \"{args.build_name.split('-')[-1]}\"."
+ " The graph uses the average (mean) over a number of boots for each build_name-device combination. The number of boots included in the average is presented in the 'Build name - device (number of boots in mean)' in the line graph legend.",
)
)
combine_plotly_figs_to_html(
figure_colletion,
"index.html",
"This page show some interesting data around LKFT's builds",
f"These graphs is based on LKFT's {args.project} branch",
)
exit(0)
if __name__ == "__main__":
sys.exit(run())