Merge lp:~canonical-platform-qa/qakit/performance_solution into lp:qakit
- performance_solution
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 73 |
Proposed branch: | lp:~canonical-platform-qa/qakit/performance_solution |
Merge into: | lp:qakit |
Diff against target: |
1981 lines (+1068/-661) 33 files modified
README (+15/-2) qakit/appstartup/__init__.py (+0/-33) qakit/appstartup/dao.py (+0/-102) qakit/appstartup/data_processor.py (+0/-74) qakit/appstartup/eve/__init__.py (+0/-16) qakit/appstartup/eve/run.py (+0/-22) qakit/appstartup/eve/settings.py (+0/-89) qakit/appstartup/orchestrator.py (+0/-99) qakit/appstartup/parser.py (+0/-65) qakit/appstartup/plotter.py (+0/-63) qakit/appstartup/report.py (+0/-72) qakit/dashboard/css/kpi.css (+1/-1) qakit/dashboard/index.html (+58/-19) qakit/dashboard/js/appstartup.js (+3/-3) qakit/dashboard/js/main.js (+10/-0) qakit/dashboard/js/performance.js (+31/-0) qakit/dashboard/js/scalability.js (+31/-0) qakit/perf/__init__.py (+30/-0) qakit/perf/engine/__init__.py (+16/-0) qakit/perf/engine/dao.py (+104/-0) qakit/perf/engine/data_processor.py (+81/-0) qakit/perf/engine/parser.py (+72/-0) qakit/perf/engine/plotter.py (+93/-0) qakit/perf/engine/report.py (+54/-0) qakit/perf/eve/__init__.py (+16/-0) qakit/perf/eve/run.py (+24/-0) qakit/perf/eve/settings.py (+92/-0) qakit/perf/orchestration/__init__.py (+84/-0) qakit/perf/orchestration/app_startup.py (+79/-0) qakit/perf/orchestration/performance.py (+80/-0) qakit/perf/orchestration/scalability.py (+78/-0) qakit/practitest/subunit_results.py (+15/-0) setup.py (+1/-1) |
To merge this branch: | bzr merge lp:~canonical-platform-qa/qakit/performance_solution |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical Platform QA Team | Pending | ||
Review via email: mp+289117@code.launchpad.net |
Commit message
This branch involves many changes in order to extend the performance features that was previously used just for app startup, supporting now different kind of times based tests such as the scalability ones. The change includes:
. Adding scalability to qakit
. html report removed
. Generic report generator created in the engine dir
. Dashboard updated
. configuration moved to config file (see README)
. readme file updated
Description of the change
- 56. By Sergio Cazzolato
-
Simplify the settings config for the eve service
- 57. By Sergio Cazzolato
-
Adding missing files and updating README
- 58. By Sergio Cazzolato
-
Editing the scalability names used in the README
- 59. By Sergio Cazzolato
-
Fix based on error doing requests.post in jenkins due to an old version installed there
- 60. By Sergio Cazzolato
-
Force matplotlib to not use any Xwindows backend
- 61. By Sergio Cazzolato
-
Force matplotlib to not use any Xwindows backend
- 62. By Sergio Cazzolato
-
Minor fixes on dashboard index js
- 63. By Sergio Cazzolato
-
Minor fixes on dashboard index js
- 64. By Sergio Cazzolato
-
Using double quotes for jquery locators
- 65. By Sergio Cazzolato
-
Updating js to create pdf
- 66. By Sergio Cazzolato
-
Adding support for perfomance tests and improving plotting sticks
- 67. By Sergio Cazzolato
-
minor change in css to center performance graphs
- 68. By Sergio Cazzolato
-
Changing name of report
- 69. By Sergio Cazzolato
-
Adding missing files
- 70. By Sergio Cazzolato
-
Fix unintentional deletion
- 71. By Sergio Cazzolato
-
Allow to join reports
- 72. By Sergio Cazzolato
-
Updating config values for performance
- 73. By Sergio Cazzolato
-
Fix reports path dir
Preview Diff
1 | === modified file 'README' |
2 | --- README 2016-02-18 14:19:19 +0000 |
3 | +++ README 2016-04-22 20:41:08 +0000 |
4 | @@ -88,11 +88,24 @@ |
5 | [APP_STARTUP] |
6 | EVE_HOST = [dest_ip] |
7 | EVE_PORT = [dest_port] |
8 | - EVE_PATH = appstartup |
9 | - HTML_REPORT = True |
10 | + EVE_PATH = perf |
11 | JSON_REPORT = True |
12 | BUILDS_TO_PLOT = 10 |
13 | |
14 | + [APP_STARTUP] |
15 | + ALLOWED_APPS = webbrowser, address_book, calculator, here, dialer, clock, messaging, ebay, camera, system_settings, music, gallery |
16 | + TYPES = cold, hot |
17 | + TITLES = Cold Start, Hot Start |
18 | + COLORS = 3366FF, FF1A00 |
19 | + ECOLORS = FF1A00, 3366FF |
20 | + |
21 | + [SCALABILITY] |
22 | + ALLOWED_NAMES = pictures_processed, pictures_gallery, my_pictures, pictures_carousel |
23 | + LOADS = 1024, 2048, 4096, 8192, 16384 |
24 | + TITLES = Load 1024, Load 2048, Load 4096, Load 8192, Load 16384 |
25 | + COLORS = FFBF00, 81F7D8, 9F81F7, F781BE, 0040FF |
26 | + ECOLORS = 0040FF, F781BE, 9F81F7, 81F7D8, FFBF00 |
27 | + |
28 | Having a test-results.subunit file, run as following: python3 qakit/appstartup/orchestrator.py pathto/test-results.subunit |
29 | |
30 | HTML and json reports are generated in the subunit directory. All the pictures will be stored in the appstartup_data dir. |
31 | |
32 | === removed directory 'qakit/appstartup' |
33 | === removed file 'qakit/appstartup/__init__.py' |
34 | --- qakit/appstartup/__init__.py 2016-01-22 20:54:59 +0000 |
35 | +++ qakit/appstartup/__init__.py 1970-01-01 00:00:00 +0000 |
36 | @@ -1,33 +0,0 @@ |
37 | -#!/usr/bin/python3 |
38 | -# UESQA Metrics |
39 | -# Copyright (C) 2016 Canonical |
40 | -# |
41 | -# This program is free software: you can redistribute it and/or modify |
42 | -# it under the terms of the GNU General Public License as published by |
43 | -# the Free Software Foundation, either version 3 of the License, or |
44 | -# (at your option) any later version. |
45 | -# |
46 | -# This program is distributed in the hope that it will be useful, |
47 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
48 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
49 | -# GNU General Public License for more details. |
50 | -# |
51 | -# You should have received a copy of the GNU General Public License |
52 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
53 | - |
54 | -import configparser |
55 | - |
56 | -import qakit.config as qakit_config |
57 | - |
58 | -allowed_apps = ['webbrowser', 'address_book', 'calculator', 'here', 'dialer', |
59 | - 'clock', 'messaging', 'ebay', 'camera', 'system_settings', |
60 | - 'music', 'gallery'] |
61 | - |
62 | - |
63 | -def _read_config(config_filepath): |
64 | - """ Parse the config at the given filepath and return it """ |
65 | - config_file = configparser.ConfigParser() |
66 | - config_file.read(config_filepath) |
67 | - return config_file |
68 | - |
69 | -config_dict = _read_config(qakit_config.get_config_file_location()) |
70 | |
71 | === removed file 'qakit/appstartup/dao.py' |
72 | --- qakit/appstartup/dao.py 2016-02-18 14:19:19 +0000 |
73 | +++ qakit/appstartup/dao.py 1970-01-01 00:00:00 +0000 |
74 | @@ -1,102 +0,0 @@ |
75 | -#!/usr/bin/python3 |
76 | -# UESQA Metrics |
77 | -# Copyright (C) 2016 Canonical |
78 | -# |
79 | -# This program is free software: you can redistribute it and/or modify |
80 | -# it under the terms of the GNU General Public License as published by |
81 | -# the Free Software Foundation, either version 3 of the License, or |
82 | -# (at your option) any later version. |
83 | -# |
84 | -# This program is distributed in the hope that it will be useful, |
85 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
86 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
87 | -# GNU General Public License for more details. |
88 | -# |
89 | -# You should have received a copy of the GNU General Public License |
90 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
91 | - |
92 | -import logging |
93 | -import requests |
94 | - |
95 | -from qakit.appstartup import config_dict as config |
96 | - |
97 | -BASE_URL = 'http://{}:{}/{}'.format(config['APP_STARTUP']['EVE_HOST'], |
98 | - config['APP_STARTUP']['EVE_PORT'], |
99 | - config['APP_STARTUP']['EVE_PATH']) |
100 | - |
101 | -logger = logging.getLogger(__name__) |
102 | - |
103 | - |
104 | -def get_headers(): |
105 | - return {'Content-Type': 'application/json'} |
106 | - |
107 | - |
108 | -def _get_where_param(**params): |
109 | - """ Get a where parameter used to filter when getting a query to mongo |
110 | - :param params: A dict with all the parameters used to filter |
111 | - """ |
112 | - where_param = "{" |
113 | - for (k, v) in params.items(): |
114 | - where_param += '"{}":"{}",'.format(k, v) |
115 | - return where_param[:-1] + '}' |
116 | - |
117 | - |
118 | -def save_record_to_db(global_info, startup_times): |
119 | - """ Save the record information in the mongo db. The information saved is |
120 | - composed by joining both dicts received as parameter |
121 | - """ |
122 | - logger.info('Saving records to db') |
123 | - for instance in startup_times: |
124 | - instance.update(global_info) |
125 | - r = requests.post('{}'.format(BASE_URL), |
126 | - json=instance, |
127 | - headers=get_headers()) |
128 | - if not r.status_code == requests.codes.created: |
129 | - logger.error('Error uploading instance data: {}'.format(instance)) |
130 | - logger.error(r.status_code, r.reason) |
131 | - |
132 | - |
133 | -def is_run_saved(run_id): |
134 | - """ Indicate if there is at least one record with the run_id saved in the |
135 | - mongo db |
136 | - """ |
137 | - where_param = _get_where_param(run_id=run_id) |
138 | - r = requests.get(BASE_URL, params={'where': where_param}, |
139 | - headers=get_headers()) |
140 | - if r.status_code == requests.codes.ok: |
141 | - return r.json()['_meta']['total'] > 0 |
142 | - else: |
143 | - logger.error('Error retrieving information for run: {}'.format(run_id)) |
144 | - logger.error(r.status_code, r.reason) |
145 | - |
146 | - |
147 | -def get_records_for_app(app, device, channel): |
148 | - """ Retrieve the records from the mongo db for an specific app, device and |
149 | - channel. The records by default are ordered by build number |
150 | - :param app: The app used to filter the results |
151 | - :param device: The device used to filter the results |
152 | - :param channel: The channel used to filter the results |
153 | - """ |
154 | - where_param = _get_where_param(app=app, device=device, channel=channel) |
155 | - |
156 | - stop = False |
157 | - page_iter = 1 |
158 | - records = [] |
159 | - while not stop: |
160 | - r = requests.get(BASE_URL, |
161 | - params={'where': where_param, 'max_results': 50, |
162 | - 'page': page_iter}, |
163 | - headers=get_headers()) |
164 | - if r.status_code == requests.codes.ok: |
165 | - records.extend(r.json()['_items']) |
166 | - else: |
167 | - logger.error('Error retrieving data from url: {}'.format(r.url)) |
168 | - logger.error(r.status_code, r.reason) |
169 | - |
170 | - metadata = r.json()['_meta'] |
171 | - if metadata['max_results'] * metadata['page'] >= metadata['total']: |
172 | - stop = True |
173 | - else: |
174 | - page_iter += 1 |
175 | - |
176 | - return records |
177 | |
178 | === removed file 'qakit/appstartup/data_processor.py' |
179 | --- qakit/appstartup/data_processor.py 2016-02-18 14:19:19 +0000 |
180 | +++ qakit/appstartup/data_processor.py 1970-01-01 00:00:00 +0000 |
181 | @@ -1,74 +0,0 @@ |
182 | -#!/usr/bin/python3 |
183 | -# UESQA Metrics |
184 | -# Copyright (C) 2016 Canonical |
185 | -# |
186 | -# This program is free software: you can redistribute it and/or modify |
187 | -# it under the terms of the GNU General Public License as published by |
188 | -# the Free Software Foundation, either version 3 of the License, or |
189 | -# (at your option) any later version. |
190 | -# |
191 | -# This program is distributed in the hope that it will be useful, |
192 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
193 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
194 | -# GNU General Public License for more details. |
195 | -# |
196 | -# You should have received a copy of the GNU General Public License |
197 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
198 | - |
199 | -import statistics |
200 | - |
201 | -from qakit.appstartup import config_dict as config |
202 | - |
203 | -BUILDS_TO_PLOT = config['APP_STARTUP']['BUILDS_TO_PLOT'] |
204 | - |
205 | - |
206 | -def get_times_by_build(app_records): |
207 | - """ Calculate the times for the different builds |
208 | - :return: [builds], [mean cold], [stdev cold], [mean hot], [stdev hot] |
209 | - """ |
210 | - build_times = {} |
211 | - for record in app_records: |
212 | - build = record['build_number'] |
213 | - if build in build_times: |
214 | - build_times[build].append(record) |
215 | - else: |
216 | - build_times[build] = [record] |
217 | - |
218 | - times = _process_build_times(build_times) |
219 | - if times: |
220 | - return zip(*times) |
221 | - else: |
222 | - return (), (), (), (), () |
223 | - |
224 | - |
225 | -def _process_build_times(build_times): |
226 | - """ Process and filter build times """ |
227 | - times = [] |
228 | - for build in sorted(build_times)[- int(BUILDS_TO_PLOT):]: |
229 | - hot_times = _get_times_by_type(build_times[build], 'hot') |
230 | - cold_times = _get_times_by_type(build_times[build], 'cold') |
231 | - |
232 | - times.append((build, _get_average(cold_times), |
233 | - _get_stdev(cold_times), |
234 | - _get_average(hot_times), |
235 | - _get_stdev(hot_times))) |
236 | - return times |
237 | - |
238 | - |
239 | -def _get_average(times): |
240 | - if times: |
241 | - return statistics.mean(times) |
242 | - else: |
243 | - return 0 |
244 | - |
245 | - |
246 | -def _get_stdev(times): |
247 | - if len(times) >= 2: |
248 | - return statistics.stdev(times) |
249 | - else: |
250 | - return 0 |
251 | - |
252 | - |
253 | -def _get_times_by_type(app_records, type): |
254 | - return [app_record['time'] for app_record in app_records if |
255 | - app_record['type'] == type] |
256 | |
257 | === removed directory 'qakit/appstartup/eve' |
258 | === removed file 'qakit/appstartup/eve/__init__.py' |
259 | --- qakit/appstartup/eve/__init__.py 2016-01-19 20:21:59 +0000 |
260 | +++ qakit/appstartup/eve/__init__.py 1970-01-01 00:00:00 +0000 |
261 | @@ -1,16 +0,0 @@ |
262 | -#!/usr/bin/python3 |
263 | -# UESQA Metrics |
264 | -# Copyright (C) 2016 Canonical |
265 | -# |
266 | -# This program is free software: you can redistribute it and/or modify |
267 | -# it under the terms of the GNU General Public License as published by |
268 | -# the Free Software Foundation, either version 3 of the License, or |
269 | -# (at your option) any later version. |
270 | -# |
271 | -# This program is distributed in the hope that it will be useful, |
272 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
273 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
274 | -# GNU General Public License for more details. |
275 | -# |
276 | -# You should have received a copy of the GNU General Public License |
277 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
278 | |
279 | === removed file 'qakit/appstartup/eve/run.py' |
280 | --- qakit/appstartup/eve/run.py 2016-01-19 20:21:59 +0000 |
281 | +++ qakit/appstartup/eve/run.py 1970-01-01 00:00:00 +0000 |
282 | @@ -1,22 +0,0 @@ |
283 | -#!/usr/bin/python3 |
284 | -# UESQA Metrics |
285 | -# Copyright (C) 2016 Canonical |
286 | -# |
287 | -# This program is free software: you can redistribute it and/or modify |
288 | -# it under the terms of the GNU General Public License as published by |
289 | -# the Free Software Foundation, either version 3 of the License, or |
290 | -# (at your option) any later version. |
291 | -# |
292 | -# This program is distributed in the hope that it will be useful, |
293 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
294 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
295 | -# GNU General Public License for more details. |
296 | -# |
297 | -# You should have received a copy of the GNU General Public License |
298 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
299 | - |
300 | -from eve import Eve |
301 | -app = Eve() |
302 | - |
303 | -if __name__ == '__main__': |
304 | - app.run() |
305 | |
306 | === removed file 'qakit/appstartup/eve/settings.py' |
307 | --- qakit/appstartup/eve/settings.py 2016-01-28 05:47:05 +0000 |
308 | +++ qakit/appstartup/eve/settings.py 1970-01-01 00:00:00 +0000 |
309 | @@ -1,89 +0,0 @@ |
310 | -#!/usr/bin/python3 |
311 | -# UESQA Metrics |
312 | -# Copyright (C) 2016 Canonical |
313 | -# |
314 | -# This program is free software: you can redistribute it and/or modify |
315 | -# it under the terms of the GNU General Public License as published by |
316 | -# the Free Software Foundation, either version 3 of the License, or |
317 | -# (at your option) any later version. |
318 | -# |
319 | -# This program is distributed in the hope that it will be useful, |
320 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
321 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
322 | -# GNU General Public License for more details. |
323 | -# |
324 | -# You should have received a copy of the GNU General Public License |
325 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
326 | - |
327 | -import os |
328 | - |
329 | -MONGO_HOST = os.getenv('EVE_MONGO_HOST', '127.0.0.1') |
330 | -MONGO_PORT = os.getenv('EVE_MONGO_PORT', '27017') |
331 | -MONGO_USERNAME = os.getenv('EVE_MONGO_USERNAME', 'ubuntuUser') |
332 | -MONGO_PASSWORD = os.getenv('EVE_MONGO_PASSWORD', 'ubuntuPassword') |
333 | -MONGO_DBNAME = os.getenv('EVE_MONGO_DBNAME', 'test') |
334 | -SERVER_NAME = os.getenv('EVE_SERVER_NAME', None) |
335 | - |
336 | - |
337 | -appstartup_schema = { |
338 | - # Schema definition, based on Cerberus grammar. Check the Cerberus project |
339 | - # (https://github.com/nicolaiarocci/cerberus) for details. |
340 | - 'run_id': { |
341 | - 'type': 'string', |
342 | - 'required': True, |
343 | - }, |
344 | - 'time': { |
345 | - 'type': 'number', |
346 | - 'required': True, |
347 | - }, |
348 | - 'type': { |
349 | - 'type': 'string', |
350 | - 'required': True, |
351 | - }, |
352 | - 'iteration': { |
353 | - 'type': 'number', |
354 | - 'required': True, |
355 | - }, |
356 | - 'app': { |
357 | - 'type': 'string', |
358 | - 'required': False, |
359 | - }, |
360 | - 'build_number': { |
361 | - 'type': 'string', |
362 | - 'required': True, |
363 | - }, |
364 | - 'device': { |
365 | - 'type': 'string', |
366 | - 'required': True, |
367 | - }, |
368 | - 'channel': { |
369 | - 'type': 'string', |
370 | - 'required': True, |
371 | - }, |
372 | -} |
373 | - |
374 | -appstartup = { |
375 | - # 'title' tag used in item links. Defaults to the resource title minus |
376 | - # the final, plural 's' (works fine in most cases but not for 'people') |
377 | - 'item_title': 'appstartup', |
378 | - |
379 | - # by default the standard item entry point is defined as |
380 | - # '/people/<ObjectId>'. We leave it untouched, and we also enable an |
381 | - # additional read-only entry point. This way consumers can also perform |
382 | - # GET requests at '/people/<lastname>'. |
383 | - 'additional_lookup': { |
384 | - 'url': 'regex("[\w]+")', |
385 | - 'field': 'app' |
386 | - }, |
387 | - |
388 | - # We choose to override global cache-control directives for this resource. |
389 | - 'cache_control': 'max-age=10,must-revalidate', |
390 | - 'cache_expires': 10, |
391 | - |
392 | - # most global settings can be overridden at resource level |
393 | - 'schema': appstartup_schema, |
394 | -} |
395 | - |
396 | -DOMAIN = {'appstartup': appstartup} |
397 | -RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] |
398 | -ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE'] |
399 | |
400 | === removed file 'qakit/appstartup/orchestrator.py' |
401 | --- qakit/appstartup/orchestrator.py 2016-02-12 17:01:08 +0000 |
402 | +++ qakit/appstartup/orchestrator.py 1970-01-01 00:00:00 +0000 |
403 | @@ -1,99 +0,0 @@ |
404 | -#!/usr/bin/python3 |
405 | -# UESQA Metrics |
406 | -# Copyright (C) 2016 Canonical |
407 | -# |
408 | -# This program is free software: you can redistribute it and/or modify |
409 | -# it under the terms of the GNU General Public License as published by |
410 | -# the Free Software Foundation, either version 3 of the License, or |
411 | -# (at your option) any later version. |
412 | -# |
413 | -# This program is distributed in the hope that it will be useful, |
414 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
415 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
416 | -# GNU General Public License for more details. |
417 | -# |
418 | -# You should have received a copy of the GNU General Public License |
419 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
420 | - |
421 | -from argparse import ArgumentParser |
422 | -import logging |
423 | -import os |
424 | - |
425 | -from qakit.appstartup import allowed_apps |
426 | -from qakit.appstartup import config_dict as config |
427 | -from qakit.appstartup import dao |
428 | -from qakit.appstartup import data_processor |
429 | -from qakit.appstartup import parser |
430 | -from qakit.appstartup import plotter |
431 | -from qakit.appstartup import report |
432 | -from qakit.practitest import subunit_results |
433 | - |
434 | -logger = logging.getLogger(__name__) |
435 | - |
436 | -HTML_REPORT = config['APP_STARTUP']['HTML_REPORT'] |
437 | -JSON_REPORT = config['APP_STARTUP']['JSON_REPORT'] |
438 | - |
439 | - |
440 | -def save_results_if_needed(global_info, startup_times): |
441 | - if startup_times: |
442 | - run_id = global_info['run_id'] |
443 | - if not dao.is_run_saved(run_id): |
444 | - dao.save_record_to_db(global_info, startup_times) |
445 | - else: |
446 | - logger.warning( |
447 | - 'Test results already in the db for the run_id {}'.format( |
448 | - run_id)) |
449 | - |
450 | - |
451 | -def _collect_records_by_app(global_info): |
452 | - app_records = {} |
453 | - device = global_info['device'] |
454 | - channel = global_info['channel'] |
455 | - for app in allowed_apps: |
456 | - app_records[app] = dao.get_records_for_app(app, device, channel) |
457 | - return app_records |
458 | - |
459 | - |
460 | -def _plot_records_by_apps(records_by_apps, results_dir): |
461 | - app_plots = {} |
462 | - |
463 | - for app in allowed_apps: |
464 | - (builds, cold_times, cold_stdevs, hot_times, hot_stdevs) = \ |
465 | - data_processor.get_times_by_build(records_by_apps[app]) |
466 | - app_plots[app] = plotter.plot_app_records(app, builds, cold_times, |
467 | - cold_stdevs, hot_times, |
468 | - hot_stdevs, results_dir) |
469 | - return app_plots |
470 | - |
471 | - |
472 | -def _create_graphics_dir(results_dir): |
473 | - dir = os.path.join(results_dir, 'appstartup_data') |
474 | - if not os.path.exists(dir): |
475 | - os.mkdir(dir) |
476 | - return dir |
477 | - |
478 | - |
479 | -def generate_reports(subunit_log, global_info, records_by_apps): |
480 | - if HTML_REPORT or JSON_REPORT: |
481 | - graphics_dir = _create_graphics_dir(os.path.dirname(subunit_log)) |
482 | - pics = _plot_records_by_apps(records_by_apps, graphics_dir) |
483 | - if HTML_REPORT: |
484 | - report.create_html_report(pics, os.path.dirname(subunit_log), |
485 | - report_info=global_info) |
486 | - if JSON_REPORT: |
487 | - report.create_json_report(pics, os.path.dirname(subunit_log), |
488 | - report_info=global_info) |
489 | - |
490 | - |
491 | -def main(): |
492 | - args_parser = ArgumentParser("Parse app startup times from logs.") |
493 | - args_parser.add_argument("subunit_log") |
494 | - args = args_parser.parse_args() |
495 | - results_list = subunit_results.parse(args.subunit_log) |
496 | - global_info, startup_times = parser.parse_startup_times(results_list) |
497 | - save_results_if_needed(global_info, startup_times) |
498 | - records_by_apps = _collect_records_by_app(global_info) |
499 | - generate_reports(args.subunit_log, global_info, records_by_apps) |
500 | - |
501 | -if __name__ == "__main__": |
502 | - main() |
503 | |
504 | === removed file 'qakit/appstartup/parser.py' |
505 | --- qakit/appstartup/parser.py 2016-01-22 20:54:59 +0000 |
506 | +++ qakit/appstartup/parser.py 1970-01-01 00:00:00 +0000 |
507 | @@ -1,65 +0,0 @@ |
508 | -#!/usr/bin/python3 |
509 | -# UESQA Metrics |
510 | -# Copyright (C) 2016 Canonical |
511 | -# |
512 | -# This program is free software: you can redistribute it and/or modify |
513 | -# it under the terms of the GNU General Public License as published by |
514 | -# the Free Software Foundation, either version 3 of the License, or |
515 | -# (at your option) any later version. |
516 | -# |
517 | -# This program is distributed in the hope that it will be useful, |
518 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
519 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
520 | -# GNU General Public License for more details. |
521 | -# |
522 | -# You should have received a copy of the GNU General Public License |
523 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
524 | - |
525 | -import ast |
526 | - |
527 | - |
528 | -def parse_startup_times(results): |
529 | - """ Retrieve the global run information and a list with all the history |
530 | - instances parsed from the results_list in the subunit test log |
531 | - """ |
532 | - startup_times = [] |
533 | - tests_logs = _get_results_test_logs(results) |
534 | - global_info = _get_global_information(tests_logs) |
535 | - |
536 | - for test_log in tests_logs: |
537 | - times = _get_times(test_log) |
538 | - if global_info and times: |
539 | - startup_times.extend(times) |
540 | - return global_info, startup_times |
541 | - |
542 | - |
543 | -def _get_global_information(tests_logs): |
544 | - for test_log in tests_logs: |
545 | - markers = [line for line in test_log if '<=' in line] |
546 | - if markers: |
547 | - values = markers[0].split('<=')[1].strip() |
548 | - return ast.literal_eval(values) |
549 | - raise RuntimeError('Global info not provided for tests') |
550 | - |
551 | - |
552 | -def _get_results_test_logs(results): |
553 | - test_logs = [] |
554 | - for result in results: |
555 | - test_log = result['details'].get('test-log') |
556 | - if test_log: |
557 | - test_logs.append(test_log.as_text().split('\n')) |
558 | - return test_logs |
559 | - |
560 | - |
561 | -def _get_times(test_log): |
562 | - markers = [line for line in test_log if '=>' in line] |
563 | - times = [] |
564 | - |
565 | - iteration = 1 |
566 | - for mark in markers: |
567 | - values = mark.split('=>')[1].strip() |
568 | - values_dict = ast.literal_eval(values) |
569 | - values_dict['iteration'] = iteration |
570 | - times.append(values_dict) |
571 | - iteration += 1 |
572 | - return times |
573 | |
574 | === removed file 'qakit/appstartup/plotter.py' |
575 | --- qakit/appstartup/plotter.py 2016-02-10 20:18:30 +0000 |
576 | +++ qakit/appstartup/plotter.py 1970-01-01 00:00:00 +0000 |
577 | @@ -1,63 +0,0 @@ |
578 | -#!/usr/bin/python3 |
579 | -# UESQA Metrics |
580 | -# Copyright (C) 2016 Canonical |
581 | -# |
582 | -# This program is free software: you can redistribute it and/or modify |
583 | -# it under the terms of the GNU General Public License as published by |
584 | -# the Free Software Foundation, either version 3 of the License, or |
585 | -# (at your option) any later version. |
586 | -# |
587 | -# This program is distributed in the hope that it will be useful, |
588 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
589 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
590 | -# GNU General Public License for more details. |
591 | -# |
592 | -# You should have received a copy of the GNU General Public License |
593 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
594 | - |
595 | -import matplotlib.pyplot as plt |
596 | -import numpy as np |
597 | -import os |
598 | - |
599 | - |
600 | -def plot_app_records(app, builds, cold_times, cold_stdevs, hot_times, |
601 | - hot_stdevs, plots_dir): |
602 | - """ Generate the graphic and return the graphic path. |
603 | - It is supposed the records are already ordered by build number. |
604 | - """ |
605 | - |
606 | - # Set descriptions |
607 | - fig, ax = plt.subplots() |
608 | - ax.set_title('Times for {} app'.format(app)) |
609 | - ax.set_xlabel('Build Number') |
610 | - ax.set_ylabel('Time (seconds)') |
611 | - |
612 | - # Config the graphic |
613 | - ind = np.arange(len(builds)) |
614 | - width = 0.35 |
615 | - ax.set_xticks(ind + width) |
616 | - ax.set_xticklabels(builds) |
617 | - ax.set_xmargin(0.1) |
618 | - ax.yaxis.grid(True) |
619 | - |
620 | - rects_cold = ax.bar(ind, cold_times, width, color='#3366FF', |
621 | - yerr=cold_stdevs, ecolor='#FF1A00', capsize=5, |
622 | - error_kw={'elinewidth': 2}) |
623 | - rects_hot = ax.bar(ind + width, hot_times, width, color='#FF1A00', |
624 | - yerr=hot_stdevs, ecolor='#3366FF', capsize=5, |
625 | - error_kw={'elinewidth': 2}) |
626 | - |
627 | -# Shrink current axis's height by 10% on the bottom |
628 | - box = ax.get_position() |
629 | - ax.set_position([box.x0, box.y0 + box.height * 0.1, |
630 | - box.width, box.height * 0.9]) |
631 | - |
632 | - if rects_cold and rects_hot: |
633 | - ax.legend((rects_cold[0], rects_hot[0]), ('Cold Start', 'Hot Start'), |
634 | - loc='upper center', bbox_to_anchor=(0.85, -0.05), |
635 | - fancybox=True, shadow=True) |
636 | - |
637 | - # Save the graphic |
638 | - dst_file = os.path.join(plots_dir, 'barchart_{}.png'.format(app)) |
639 | - plt.savefig(dst_file) |
640 | - return dst_file |
641 | |
642 | === removed file 'qakit/appstartup/report.py' |
643 | --- qakit/appstartup/report.py 2016-02-12 17:08:11 +0000 |
644 | +++ qakit/appstartup/report.py 1970-01-01 00:00:00 +0000 |
645 | @@ -1,72 +0,0 @@ |
646 | -#!/usr/bin/python3 |
647 | -# UESQA Metrics |
648 | -# Copyright (C) 2016 Canonical |
649 | -# |
650 | -# This program is free software: you can redistribute it and/or modify |
651 | -# it under the terms of the GNU General Public License as published by |
652 | -# the Free Software Foundation, either version 3 of the License, or |
653 | -# (at your option) any later version. |
654 | -# |
655 | -# This program is distributed in the hope that it will be useful, |
656 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
657 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
658 | -# GNU General Public License for more details. |
659 | -# |
660 | -# You should have received a copy of the GNU General Public License |
661 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
662 | - |
663 | -import json |
664 | -import os |
665 | - |
666 | -html_header = '<html><head><title>{}</title></head><body><div align="center"><table>' # NOQA |
667 | -html_footer = '</table></div></body></html>' |
668 | -html_picture = '<tr><td><br/><br/><font size="7" color="#8B005A"><center>{}</center></font><br/><img src="{}"/></td></tr>' # NOQA |
669 | -html_logo = '<img src="http://design.ubuntu.com/wp-content/uploads/canonical-logo1.png">' # NOQA |
670 | -html_title = '<br/><br/><div><font size="7" color="#8B005A">Apps StartUp Execution Report</font></div>' # NOQA |
671 | -html_info_title_header = '<br/><br/><div><font size="6" color="#8B005A">General information</font>' # NOQA |
672 | -html_info_title_footer = '</div>' |
673 | -html_info = '<div><br/><font size="4">{}: {}</font></div>' |
674 | - |
675 | - |
676 | -def create_html_report(out_files, report_dir, |
677 | - report_file="app_startup_report.html", |
678 | - report_info=None): |
679 | - report_path = os.path.join(report_dir, report_file) |
680 | - |
681 | - with open(report_path, "w") as report: |
682 | - # write out the html header |
683 | - report.write(html_header.format('App startup times report')) |
684 | - report.write(html_logo) |
685 | - |
686 | - report.write(html_title) |
687 | - report.write(html_info_title_header) |
688 | - for info in sorted(report_info.keys()): |
689 | - report.write(html_info.format(info.replace('_', ' ').title(), |
690 | - report_info[info])) |
691 | - report.write(html_info_title_footer) |
692 | - |
693 | - for app, path in out_files.items(): |
694 | - report.write(html_picture.format(app.title(), path)) |
695 | - |
696 | - report.write(html_footer) |
697 | - |
698 | - return report_path |
699 | - |
700 | - |
701 | -def create_json_report(out_files, report_dir, |
702 | - report_file="app_startup_report.json", |
703 | - report_info=None): |
704 | - |
705 | - report_path = os.path.join(report_dir, report_file) |
706 | - data = dict() |
707 | - data['channel'] = report_info['channel'] |
708 | - data['device'] = report_info['device'] |
709 | - data['build_number'] = report_info['build_number'] |
710 | - |
711 | - charts = [] |
712 | - for app, path in out_files.items(): |
713 | - charts.append({"title": app.title(), "path": path}) |
714 | - data['charts'] = charts |
715 | - |
716 | - with open(report_path, "w") as report: |
717 | - report.write(json.dumps(data, ensure_ascii=False)) |
718 | |
719 | === modified file 'qakit/dashboard/css/kpi.css' |
720 | --- qakit/dashboard/css/kpi.css 2016-02-11 21:23:21 +0000 |
721 | +++ qakit/dashboard/css/kpi.css 2016-04-22 20:41:08 +0000 |
722 | @@ -27,6 +27,6 @@ |
723 | list-style-type: none !important; |
724 | } |
725 | |
726 | -.app-startup-elem { |
727 | +.perf-elem { |
728 | text-align: center; |
729 | } |
730 | |
731 | === modified file 'qakit/dashboard/index.html' |
732 | --- qakit/dashboard/index.html 2016-02-12 17:24:39 +0000 |
733 | +++ qakit/dashboard/index.html 2016-04-22 20:41:08 +0000 |
734 | @@ -27,6 +27,8 @@ |
735 | <script src="js/test-library.js"></script> |
736 | <script src="js/test-plans.js"></script> |
737 | <script src="js/appstartup.js"></script> |
738 | + <script src="js/scalability.js"></script> |
739 | + <script src="js/performance.js"></script> |
740 | |
741 | <style type="text/css"> |
742 | .bs-example{ |
743 | @@ -104,6 +106,20 @@ |
744 | <ul id="app-startup-sublinks" class="sub-links"> |
745 | </ul> |
746 | </div> |
747 | + <div class="link-blue" id="scalability-link"> |
748 | + <a href="#scalability-section"> |
749 | + <i class="fa fa-tachometer"></i>Scalability |
750 | + </a> |
751 | + <ul id="scalability-sublinks" class="sub-links"> |
752 | + </ul> |
753 | + </div> |
754 | + <div class="link-blue" id="performance-link"> |
755 | + <a href="#performance-section"> |
756 | + <i class="fa fa-tachometer"></i>Performance |
757 | + </a> |
758 | + <ul id="performance-sublinks" class="sub-links"> |
759 | + </ul> |
760 | + </div> |
761 | </div> |
762 | </aside> |
763 | <div id="main-content" class="main-content"> |
764 | @@ -279,6 +295,39 @@ |
765 | </div> |
766 | </div> <!-- app-startup-section --> |
767 | |
768 | + <div id="scalability-section" class="content-section"> |
769 | + <div class="panel panel-default"> |
770 | + <div class="panel-heading"><h1>Scalability Report</h1></div> |
771 | + <div class="panel-body"> |
772 | + <div class="well well-sm"> |
773 | + <h3 id="scalability-device"></h3> |
774 | + <h3 id="scalability-channel"></h3> |
775 | + <h3 id="scalability-build-number"></h3> |
776 | + <p><font color="#FFBF00">Load 1024 elements</font></p> |
777 | + <p><font color="#81F7D8">Load 2048 elements</font></p> |
778 | + <p><font color="#9F81F7">Load 4096 elements</font></p> |
779 | + <p><font color="#F781BE">Load 8192 elements</font></p> |
780 | + <p><font color="#0040FF">Load 16384 elements</font></p> |
781 | + </div> |
782 | + <table id="ScalabilityTable" class="table table-striped table-bordered"></table> |
783 | + </div> |
784 | + </div> |
785 | + </div> <!-- scalability-section --> |
786 | + |
787 | + <div id="performance-section" class="content-section"> |
788 | + <div class="panel panel-default"> |
789 | + <div class="panel-heading"><h1>Performance Report</h1></div> |
790 | + <div class="panel-body"> |
791 | + <div class="well well-sm"> |
792 | + <h3 id="performance-device"></h3> |
793 | + <h3 id="performance-channel"></h3> |
794 | + <h3 id="performance-build-number"></h3> |
795 | + </div> |
796 | + <table id="PerformanceTable" class="table table-striped table-bordered"></table> |
797 | + </div> |
798 | + </div> |
799 | + </div> <!-- performance-section --> |
800 | + |
801 | <div id="applications-manual-section" class="content-section"> |
802 | <div class="panel panel-default"> |
803 | <div class="panel-heading"><h1>Tests by Application Manual</h1></div> |
804 | @@ -326,25 +375,15 @@ |
805 | </div> <!-- main-content --> |
806 | |
807 | <script> |
808 | - $(function () { |
809 | - var links = $('.sidebar-links > div'); |
810 | - links.on('click', function () { |
811 | - links.removeClass('selected'); |
812 | - $(this).addClass('selected'); |
813 | - }); |
814 | - }); |
815 | - </script> |
816 | - <script> |
817 | - |
818 | -// show only the content-section of id matching hash |
819 | -$(window).on("hashchange", function (e) { |
820 | - moveToSection(); |
821 | - |
822 | -}); |
823 | - |
824 | -window.onload = function() { |
825 | - drawSections(true); |
826 | -} |
827 | + // show only the content-section of id matching hash |
828 | + window.onhashchange = function (e) { |
829 | + moveToSection(); |
830 | + } |
831 | + |
832 | + window.onload = function() { |
833 | + selectSection() |
834 | + drawSections(true); |
835 | + } |
836 | </script> |
837 | </body> |
838 | </html> |
839 | |
840 | === modified file 'qakit/dashboard/js/appstartup.js' |
841 | --- qakit/dashboard/js/appstartup.js 2016-02-12 17:24:39 +0000 |
842 | +++ qakit/dashboard/js/appstartup.js 2016-04-22 20:41:08 +0000 |
843 | @@ -3,7 +3,7 @@ |
844 | function drawAppStartUpSection(firstDraw) { |
845 | $.getJSON("./data/app_startup_report.json", function(data) { |
846 | if (firstDraw){ |
847 | - process(data); |
848 | + process_app_startup(data); |
849 | } |
850 | }).fail(function(data, status) { |
851 | console.log("Hiding Appstartup section"); |
852 | @@ -12,7 +12,7 @@ |
853 | }); |
854 | } |
855 | |
856 | -function process(data) { |
857 | +function process_app_startup(data) { |
858 | $('#app-startup-channel').text('Channel: ' + data.channel); |
859 | $('#app-startup-device').text('Device: ' + data.device); |
860 | $('#app-startup-build-number').text('Build Number: ' + data.build_number); |
861 | @@ -22,7 +22,7 @@ |
862 | title = charts[chart].title |
863 | path = "./data/" + basename(charts[chart].path) |
864 | id = title.toLowerCase() + "-app-startup-section" |
865 | - section = '<div class="app-startup-elem" id="' + id + '" parent-section="app-startup-section"><h2>' + title + '</h2><img src="' + path + '"/></div>' |
866 | + section = '<div class="perf-elem" id="' + id + '" parent-section="app-startup-section"><h2>' + title + '</h2><img src="' + path + '"/></div>' |
867 | $( "#AppStartUpTable" ).append(section); |
868 | |
869 | link = '<li><a href="#' + id + '">' + title + '</a></li>' |
870 | |
871 | === modified file 'qakit/dashboard/js/main.js' |
872 | --- qakit/dashboard/js/main.js 2016-02-11 21:30:40 +0000 |
873 | +++ qakit/dashboard/js/main.js 2016-04-22 20:41:08 +0000 |
874 | @@ -9,6 +9,8 @@ |
875 | drawApplicationsSection(); |
876 | drawDomainsSection(); |
877 | drawAppStartUpSection(firstDraw); |
878 | + drawScalabilitySection(firstDraw) |
879 | + drawPerformanceSection(firstDraw) |
880 | |
881 | hideUnusedLinks(); |
882 | } |
883 | @@ -49,4 +51,12 @@ |
884 | if (!window.location.hash){ |
885 | drawSections(false); |
886 | } |
887 | +} |
888 | + |
889 | +function selectSection() { |
890 | + var links = $(".sidebar-links > div"); |
891 | + links.on('click', function () { |
892 | + links.removeClass('selected'); |
893 | + $(this).addClass('selected'); |
894 | + }); |
895 | } |
896 | \ No newline at end of file |
897 | |
898 | === added file 'qakit/dashboard/js/performance.js' |
899 | --- qakit/dashboard/js/performance.js 1970-01-01 00:00:00 +0000 |
900 | +++ qakit/dashboard/js/performance.js 2016-04-22 20:41:08 +0000 |
901 | @@ -0,0 +1,31 @@ |
902 | + |
903 | + |
904 | +function drawPerformanceSection(firstDraw) { |
905 | + $.getJSON("./data/performance_report.json", function(data) { |
906 | + if (firstDraw){ |
907 | + process_performance(data); |
908 | + } |
909 | + }).fail(function(data, status) { |
910 | + console.log("Hiding Performance section"); |
911 | + $("#performance-section").hide(); |
912 | + $("#performance-link").hide(); |
913 | + }); |
914 | +} |
915 | + |
916 | +function process_performance(data) { |
917 | + $('#performance-channel').text('Channel: ' + data.channel); |
918 | + $('#performance-device').text('Device: ' + data.device); |
919 | + $('#performance-build-number').text('Build Number: ' + data.build_number); |
920 | + |
921 | + charts = data.charts |
922 | + for (var chart in charts) { |
923 | + title = charts[chart].title |
924 | + path = "./data/" + basename(charts[chart].path) |
925 | + id = title.toLowerCase() + "-performance-section" |
926 | + section = '<div class="perf-elem" id="' + id + '" parent-section="performance-section"><h2>' + title + '</h2><img src="' + path + '"/></div>' |
927 | + $( "#PerformanceTable" ).append(section); |
928 | + |
929 | + link = '<li><a href="#' + id + '">' + title + '</a></li>' |
930 | + $( "#performance-sublinks" ).append(link); |
931 | + } |
932 | +} |
933 | |
934 | === added file 'qakit/dashboard/js/scalability.js' |
935 | --- qakit/dashboard/js/scalability.js 1970-01-01 00:00:00 +0000 |
936 | +++ qakit/dashboard/js/scalability.js 2016-04-22 20:41:08 +0000 |
937 | @@ -0,0 +1,31 @@ |
938 | + |
939 | + |
940 | +function drawScalabilitySection(firstDraw) { |
941 | + $.getJSON("./data/scalability_report.json", function(data) { |
942 | + if (firstDraw){ |
943 | + process_scalability(data); |
944 | + } |
945 | + }).fail(function(data, status) { |
946 | + console.log("Hiding Scalability section"); |
947 | + $("#scalability-section").hide(); |
948 | + $("#scalability-link").hide(); |
949 | + }); |
950 | +} |
951 | + |
952 | +function process_scalability(data) { |
953 | + $('#scalability-channel').text('Channel: ' + data.channel); |
954 | + $('#scalability-device').text('Device: ' + data.device); |
955 | + $('#scalability-build-number').text('Build Number: ' + data.build_number); |
956 | + |
957 | + charts = data.charts |
958 | + for (var chart in charts) { |
959 | + title = charts[chart].title |
960 | + path = "./data/" + basename(charts[chart].path) |
961 | + id = title.toLowerCase() + "-scalability-section" |
962 | + section = '<div class="perf-elem" id="' + id + '" parent-section="scalability-section"><h2>' + title.replace('_', ' ') + '</h2><img src="' + path + '"/></div>' |
963 | + $( "#ScalabilityTable" ).append(section); |
964 | + |
965 | + link = '<li><a href="#' + id + '">' + title.replace('_', ' ') + '</a></li>' |
966 | + $( "#scalability-sublinks" ).append(link); |
967 | + } |
968 | +} |
969 | |
970 | === added directory 'qakit/perf' |
971 | === added file 'qakit/perf/__init__.py' |
972 | --- qakit/perf/__init__.py 1970-01-01 00:00:00 +0000 |
973 | +++ qakit/perf/__init__.py 2016-04-22 20:41:08 +0000 |
974 | @@ -0,0 +1,30 @@ |
975 | +#!/usr/bin/python3 |
976 | +# UESQA Metrics |
977 | +# Copyright (C) 2016 Canonical |
978 | +# |
979 | +# This program is free software: you can redistribute it and/or modify |
980 | +# it under the terms of the GNU General Public License as published by |
981 | +# the Free Software Foundation, either version 3 of the License, or |
982 | +# (at your option) any later version. |
983 | +# |
984 | +# This program is distributed in the hope that it will be useful, |
985 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
986 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
987 | +# GNU General Public License for more details. |
988 | +# |
989 | +# You should have received a copy of the GNU General Public License |
990 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
991 | + |
992 | +import configparser |
993 | + |
994 | +from qakit import config as qakit_config |
995 | + |
996 | + |
997 | +def _read_config(config_filepath): |
998 | + """ Parse the config at the given filepath and return it """ |
999 | + config_file = configparser.ConfigParser() |
1000 | + config_file.read(config_filepath) |
1001 | + return config_file |
1002 | + |
1003 | + |
1004 | +config_dict = _read_config(qakit_config.get_config_file_location()) |
1005 | |
1006 | === added directory 'qakit/perf/engine' |
1007 | === added file 'qakit/perf/engine/__init__.py' |
1008 | --- qakit/perf/engine/__init__.py 1970-01-01 00:00:00 +0000 |
1009 | +++ qakit/perf/engine/__init__.py 2016-04-22 20:41:08 +0000 |
1010 | @@ -0,0 +1,16 @@ |
1011 | +#!/usr/bin/python3 |
1012 | +# UESQA Metrics |
1013 | +# Copyright (C) 2016 Canonical |
1014 | +# |
1015 | +# This program is free software: you can redistribute it and/or modify |
1016 | +# it under the terms of the GNU General Public License as published by |
1017 | +# the Free Software Foundation, either version 3 of the License, or |
1018 | +# (at your option) any later version. |
1019 | +# |
1020 | +# This program is distributed in the hope that it will be useful, |
1021 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1022 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1023 | +# GNU General Public License for more details. |
1024 | +# |
1025 | +# You should have received a copy of the GNU General Public License |
1026 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1027 | |
1028 | === added file 'qakit/perf/engine/dao.py' |
1029 | --- qakit/perf/engine/dao.py 1970-01-01 00:00:00 +0000 |
1030 | +++ qakit/perf/engine/dao.py 2016-04-22 20:41:08 +0000 |
1031 | @@ -0,0 +1,104 @@ |
1032 | +#!/usr/bin/python3 |
1033 | +# UESQA Metrics |
1034 | +# Copyright (C) 2016 Canonical |
1035 | +# |
1036 | +# This program is free software: you can redistribute it and/or modify |
1037 | +# it under the terms of the GNU General Public License as published by |
1038 | +# the Free Software Foundation, either version 3 of the License, or |
1039 | +# (at your option) any later version. |
1040 | +# |
1041 | +# This program is distributed in the hope that it will be useful, |
1042 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1043 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1044 | +# GNU General Public License for more details. |
1045 | +# |
1046 | +# You should have received a copy of the GNU General Public License |
1047 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1048 | + |
1049 | +import json |
1050 | +import logging |
1051 | +import requests |
1052 | +from qakit.perf import config_dict as config |
1053 | + |
1054 | +BASE_URL = 'http://{}:{}/{}'.format(config['PERF']['EVE_HOST'], |
1055 | + config['PERF']['EVE_PORT'], |
1056 | + config['PERF']['EVE_PATH']) |
1057 | + |
1058 | +logger = logging.getLogger(__name__) |
1059 | + |
1060 | + |
1061 | +def get_headers(): |
1062 | + return {'Content-Type': 'application/json'} |
1063 | + |
1064 | + |
1065 | +def _get_where_param(**params): |
1066 | + """ Get a where parameter used to filter when getting a query to mongo |
1067 | + :param params: A dict with all the parameters used to filter |
1068 | + """ |
1069 | + where_param = "{" |
1070 | + for (k, v) in params.items(): |
1071 | + where_param += '"{}":"{}",'.format(k, v) |
1072 | + return where_param[:-1] + '}' |
1073 | + |
1074 | + |
1075 | +def save_record_to_db(global_info, records): |
1076 | + """ Save the record information in the mongo db. The information saved is |
1077 | + composed by joining both dicts received as parameter |
1078 | + """ |
1079 | + logger.info('Saving records to db') |
1080 | + for record in records: |
1081 | + record.update(global_info) |
1082 | + r = requests.post('{}'.format(BASE_URL), data=json.dumps(record), |
1083 | + headers=get_headers()) |
1084 | + if not r.status_code == requests.codes.created: |
1085 | + logger.error('Error uploading record: {}'.format(record)) |
1086 | + logger.error(r.status_code, r.reason) |
1087 | + |
1088 | + |
1089 | +def is_run_saved(run_id): |
1090 | + """ Indicate if there is at least one record with the run_id saved in the |
1091 | + mongo db |
1092 | + """ |
1093 | + where_param = _get_where_param(run_id=run_id) |
1094 | + r = requests.get(BASE_URL, params={'where': where_param}, |
1095 | + headers=get_headers()) |
1096 | + if r.status_code == requests.codes.ok: |
1097 | + return r.json()['_meta']['total'] > 0 |
1098 | + else: |
1099 | + logger.error('Error retrieving information for run: {}'.format(run_id)) |
1100 | + logger.error(r.status_code, r.reason) |
1101 | + |
1102 | + |
1103 | +def get_records_for_key(key, value, device, channel): |
1104 | + """ Retrieve the records from the mongo db for an specific key and value, |
1105 | + device and channel. The records by default are ordered by build number |
1106 | + :param device: The device used to filter the results |
1107 | + :param channel: The channel used to filter the results |
1108 | + :param key: the key used to filter the results |
1109 | + :param value: the value used to filter the results |
1110 | + |
1111 | + """ |
1112 | + args = {'device': device, 'channel': channel, key: value} |
1113 | + where_param = _get_where_param(**args) |
1114 | + |
1115 | + stop = False |
1116 | + page_iter = 1 |
1117 | + records = [] |
1118 | + while not stop: |
1119 | + r = requests.get(BASE_URL, |
1120 | + params={'where': where_param, 'max_results': 50, |
1121 | + 'page': page_iter}, |
1122 | + headers=get_headers()) |
1123 | + if r.status_code == requests.codes.ok: |
1124 | + records.extend(r.json()['_items']) |
1125 | + else: |
1126 | + logger.error('Error retrieving data from url: {}'.format(r.url)) |
1127 | + logger.error(r.status_code, r.reason) |
1128 | + |
1129 | + metadata = r.json()['_meta'] |
1130 | + if metadata['max_results'] * metadata['page'] >= metadata['total']: |
1131 | + stop = True |
1132 | + else: |
1133 | + page_iter += 1 |
1134 | + |
1135 | + return records |
1136 | |
1137 | === added file 'qakit/perf/engine/data_processor.py' |
1138 | --- qakit/perf/engine/data_processor.py 1970-01-01 00:00:00 +0000 |
1139 | +++ qakit/perf/engine/data_processor.py 2016-04-22 20:41:08 +0000 |
1140 | @@ -0,0 +1,81 @@ |
1141 | +#!/usr/bin/python3 |
1142 | +# UESQA Metrics |
1143 | +# Copyright (C) 2016 Canonical |
1144 | +# |
1145 | +# This program is free software: you can redistribute it and/or modify |
1146 | +# it under the terms of the GNU General Public License as published by |
1147 | +# the Free Software Foundation, either version 3 of the License, or |
1148 | +# (at your option) any later version. |
1149 | +# |
1150 | +# This program is distributed in the hope that it will be useful, |
1151 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1152 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1153 | +# GNU General Public License for more details. |
1154 | +# |
1155 | +# You should have received a copy of the GNU General Public License |
1156 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1157 | + |
1158 | +import statistics |
1159 | +from qakit.perf import config_dict as config |
1160 | + |
1161 | +BUILDS_TO_PLOT = config['PERF']['BUILDS_TO_PLOT'] |
1162 | + |
1163 | + |
1164 | +class BuildTime: |
1165 | + |
1166 | + def __init__(self, build, time, stdev): |
1167 | + self.build = build |
1168 | + self.time = time |
1169 | + self.stdev = stdev |
1170 | + |
1171 | + |
1172 | +def get_times_by_build(records, key, values): |
1173 | + """ Calculate the times for the different builds and values |
1174 | + :param records: the set of time records used to get the desired times |
1175 | + :param key: the key to filter |
1176 | + :param values: the values to group the results |
1177 | + :return: a dict of BuildTime objects by key |
1178 | + """ |
1179 | + build_times = {} |
1180 | + for record in records: |
1181 | + build = record['build_number'] |
1182 | + if build in build_times: |
1183 | + build_times[build].append(record) |
1184 | + else: |
1185 | + build_times[build] = [record] |
1186 | + |
1187 | + times = {} |
1188 | + for value in values: |
1189 | + times[value] = _process_build_times(build_times, key, value) |
1190 | + |
1191 | + return times |
1192 | + |
1193 | + |
1194 | +def _process_build_times(build_times, key, value): |
1195 | + """ Process and filter build times """ |
1196 | + times = [] |
1197 | + for build in sorted(build_times)[- int(BUILDS_TO_PLOT):]: |
1198 | + times_by_key = _get_times_by_key(build_times[build], key, value) |
1199 | + |
1200 | + times.append(BuildTime(build, |
1201 | + _get_average(times_by_key), |
1202 | + _get_stdev(times_by_key))) |
1203 | + return times |
1204 | + |
1205 | + |
1206 | +def _get_average(times): |
1207 | + if times: |
1208 | + return statistics.mean(times) |
1209 | + else: |
1210 | + return 0 |
1211 | + |
1212 | + |
1213 | +def _get_stdev(times): |
1214 | + if len(times) >= 2: |
1215 | + return statistics.stdev(times) |
1216 | + else: |
1217 | + return 0 |
1218 | + |
1219 | + |
1220 | +def _get_times_by_key(records, key, value): |
1221 | + return [record['time'] for record in records if record[key] == value] |
1222 | |
1223 | === added file 'qakit/perf/engine/parser.py' |
1224 | --- qakit/perf/engine/parser.py 1970-01-01 00:00:00 +0000 |
1225 | +++ qakit/perf/engine/parser.py 2016-04-22 20:41:08 +0000 |
1226 | @@ -0,0 +1,72 @@ |
1227 | +#!/usr/bin/python3 |
1228 | +# UESQA Metrics |
1229 | +# Copyright (C) 2016 Canonical |
1230 | +# |
1231 | +# This program is free software: you can redistribute it and/or modify |
1232 | +# it under the terms of the GNU General Public License as published by |
1233 | +# the Free Software Foundation, either version 3 of the License, or |
1234 | +# (at your option) any later version. |
1235 | +# |
1236 | +# This program is distributed in the hope that it will be useful, |
1237 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1238 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1239 | +# GNU General Public License for more details. |
1240 | +# |
1241 | +# You should have received a copy of the GNU General Public License |
1242 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1243 | + |
1244 | +import ast |
1245 | +import os |
1246 | + |
1247 | + |
1248 | +def parse_log_times(log_lines, times_class): |
1249 | + """ Retrieve the global run information and a list with all the history |
1250 | + instances parsed from the results_list in the subunit test log |
1251 | + :param log_lines: the lines in the log |
1252 | + :param times_class: the class used to filter the times to consider |
1253 | + :return: the global information of the run and the times for this run |
1254 | + """ |
1255 | + tests_times = [] |
1256 | + global_info = _get_global_information(log_lines) |
1257 | + |
1258 | + times = _get_times(log_lines, times_class) |
1259 | + if global_info and times: |
1260 | + tests_times.extend(times) |
1261 | + return global_info, tests_times |
1262 | + |
1263 | + |
1264 | +def parse_log_file(log_file): |
1265 | + """ Return a list of lines for the log """ |
1266 | + if not os.path.exists(log_file): |
1267 | + raise RuntimeError('Log file does not exist {}'.format(log_file)) |
1268 | + with open(log_file, 'r') as file: |
1269 | + return file.read().splitlines() |
1270 | + |
1271 | + |
1272 | +def _get_global_information(log_lines): |
1273 | + markers = [line for line in log_lines if '<=' in line] |
1274 | + if markers: |
1275 | + values = markers[0].split('<=')[1].strip() |
1276 | + return ast.literal_eval(values) |
1277 | + raise RuntimeError('Global info not provided for tests') |
1278 | + |
1279 | + |
1280 | +def _get_results_test_logs(results): |
1281 | + test_logs = [] |
1282 | + for result in results: |
1283 | + test_log = result['details'].get('test-log') |
1284 | + if test_log: |
1285 | + test_logs.append(test_log.as_text().split('\n')) |
1286 | + return test_logs |
1287 | + |
1288 | + |
1289 | +def _get_times(test_log, times_class): |
1290 | + markers = [line for line in test_log if '=>' in line] |
1291 | + times = [] |
1292 | + |
1293 | + for mark in markers: |
1294 | + values = mark.split('=>')[1].strip() |
1295 | + values_dict = ast.literal_eval(values) |
1296 | + if 'class' in values_dict and values_dict['class'] == times_class: |
1297 | + times.append(values_dict) |
1298 | + return times |
1299 | |
1300 | === added file 'qakit/perf/engine/plotter.py' |
1301 | --- qakit/perf/engine/plotter.py 1970-01-01 00:00:00 +0000 |
1302 | +++ qakit/perf/engine/plotter.py 2016-04-22 20:41:08 +0000 |
1303 | @@ -0,0 +1,93 @@ |
1304 | +#!/usr/bin/python3 |
1305 | +# UESQA Metrics |
1306 | +# Copyright (C) 2016 Canonical |
1307 | +# |
1308 | +# This program is free software: you can redistribute it and/or modify |
1309 | +# it under the terms of the GNU General Public License as published by |
1310 | +# the Free Software Foundation, either version 3 of the License, or |
1311 | +# (at your option) any later version. |
1312 | +# |
1313 | +# This program is distributed in the hope that it will be useful, |
1314 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1315 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1316 | +# GNU General Public License for more details. |
1317 | +# |
1318 | +# You should have received a copy of the GNU General Public License |
1319 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1320 | + |
1321 | + |
1322 | +import numpy as np |
1323 | +import os |
1324 | + |
1325 | +# Force matplotlib to not use any Xwindows backend. |
1326 | +import matplotlib |
1327 | +matplotlib.use('Agg') |
1328 | +import matplotlib.pyplot as plt # NOQA |
1329 | + |
1330 | + |
1331 | +class TimesBar: |
1332 | + |
1333 | + def __init__(self, times, stdevs, title, color, stdevcolor): |
1334 | + self.times = times |
1335 | + self.stdevs = stdevs |
1336 | + self.title = title |
1337 | + self.color = color |
1338 | + self.stdevcolor = stdevcolor |
1339 | + |
1340 | + |
1341 | +def _get_xspacing(width, bars): |
1342 | + if len(bars) <= 1: |
1343 | + return width / 2 |
1344 | + else: |
1345 | + return (len(bars) - 1) * width |
1346 | + |
1347 | + |
1348 | +def plot_records(name, title, builds, bars, plots_dir): |
1349 | + """ Generate the graphic and return the graphic path. |
1350 | + It is supposed the records are already ordered by build number. |
1351 | + :param name: The name which identifies the graph |
1352 | + :param title: The title for the graph |
1353 | + :param builds: The list of build numbers to plot |
1354 | + :param bars: A list of TimesBar objects used to plot bars |
1355 | + :param plots_dir: The dir where the graph is stored |
1356 | + :return: The destination path of the created graph |
1357 | + """ |
1358 | + |
1359 | + # Set descriptions |
1360 | + fig, ax = plt.subplots() |
1361 | + ax.set_title(title) |
1362 | + ax.set_xlabel('Build Number') |
1363 | + ax.set_ylabel('Time (seconds)') |
1364 | + |
1365 | + # Config the graphic |
1366 | + ind = np.arange(len(builds)) |
1367 | + width = 0.35 |
1368 | + |
1369 | + ax.set_xticks(ind + _get_xspacing(width, bars)) |
1370 | + ax.set_xticklabels(builds) |
1371 | + ax.set_xmargin(0.1) |
1372 | + ax.yaxis.grid(True) |
1373 | + |
1374 | + rects = [] |
1375 | + for bar in bars: |
1376 | + if bar.times: |
1377 | + rects.append(ax.bar(ind + bars.index(bar) * width, bar.times, |
1378 | + width, color=bar.color, yerr=bar.stdevs, |
1379 | + ecolor=bar.stdevcolor, capsize=5, |
1380 | + error_kw={'elinewidth': 2})) |
1381 | + |
1382 | + # Shrink current axis's height by 10% on the bottom |
1383 | + box = ax.get_position() |
1384 | + ax.set_position([box.x0, box.y0 + box.height * 0.1, |
1385 | + box.width, box.height * 0.9]) |
1386 | + |
1387 | + if rects and len(rects) <= 2: |
1388 | + ax.legend([rect[0] for rect in rects], |
1389 | + [bar.title for bar in bars], |
1390 | + loc='upper center', bbox_to_anchor=(0.85, -0.05), |
1391 | + fancybox=True, shadow=True) |
1392 | + |
1393 | + # Save the graphic |
1394 | + dst_file = os.path.join(plots_dir, 'barchart_{}.png'.format(name)) |
1395 | + plt.savefig(dst_file) |
1396 | + return dst_file |
1397 | |
1398 | === added file 'qakit/perf/engine/report.py' |
1399 | --- qakit/perf/engine/report.py 1970-01-01 00:00:00 +0000 |
1400 | +++ qakit/perf/engine/report.py 2016-04-22 20:41:08 +0000 |
1401 | @@ -0,0 +1,54 @@ |
1402 | +#!/usr/bin/python3 |
1403 | +# UESQA Metrics |
1404 | +# Copyright (C) 2016 Canonical |
1405 | +# |
1406 | +# This program is free software: you can redistribute it and/or modify |
1407 | +# it under the terms of the GNU General Public License as published by |
1408 | +# the Free Software Foundation, either version 3 of the License, or |
1409 | +# (at your option) any later version. |
1410 | +# |
1411 | +# This program is distributed in the hope that it will be useful, |
1412 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1413 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1414 | +# GNU General Public License for more details. |
1415 | +# |
1416 | +# You should have received a copy of the GNU General Public License |
1417 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1418 | + |
1419 | +import json |
1420 | +import os |
1421 | + |
1422 | + |
1423 | +def create_json_report(out_files, report_dir, report_file, report_info): |
1424 | + report_path = os.path.join(report_dir, report_file) |
1425 | + |
1426 | + data = dict() |
1427 | + data['channel'] = report_info['channel'] |
1428 | + data['device'] = report_info['device'] |
1429 | + data['build_number'] = report_info['build_number'] |
1430 | + |
1431 | + charts = [] |
1432 | + if os.path.exists(report_path): |
1433 | + charts = get_charts_already_created(report_path, data) |
1434 | + os.remove(report_path) |
1435 | + |
1436 | + for name, path in out_files.items(): |
1437 | + charts.append({"title": name.title().replace('_', ' '), "path": path}) |
1438 | + data['charts'] = charts |
1439 | + |
1440 | + with open(report_path, "w") as report: |
1441 | + report.write(json.dumps(data, ensure_ascii=False)) |
1442 | + |
1443 | + |
1444 | +def get_charts_already_created(report_path, current_data): |
1445 | + try: |
1446 | + with open(report_path) as report_file: |
1447 | + report_data = json.load(report_file) |
1448 | + if report_data['channel'] == current_data['channel'] and \ |
1449 | + report_data['device'] == current_data['device'] and \ |
1450 | + report_data['build_number'] == current_data['build_number']: |
1451 | + return report_data['charts'] |
1452 | + else: |
1453 | + return [] |
1454 | + except Exception: |
1455 | + return [] |
1456 | |
1457 | === added directory 'qakit/perf/eve' |
1458 | === added file 'qakit/perf/eve/__init__.py' |
1459 | --- qakit/perf/eve/__init__.py 1970-01-01 00:00:00 +0000 |
1460 | +++ qakit/perf/eve/__init__.py 2016-04-22 20:41:08 +0000 |
1461 | @@ -0,0 +1,16 @@ |
1462 | +#!/usr/bin/python3 |
1463 | +# UESQA Metrics |
1464 | +# Copyright (C) 2016 Canonical |
1465 | +# |
1466 | +# This program is free software: you can redistribute it and/or modify |
1467 | +# it under the terms of the GNU General Public License as published by |
1468 | +# the Free Software Foundation, either version 3 of the License, or |
1469 | +# (at your option) any later version. |
1470 | +# |
1471 | +# This program is distributed in the hope that it will be useful, |
1472 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1473 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1474 | +# GNU General Public License for more details. |
1475 | +# |
1476 | +# You should have received a copy of the GNU General Public License |
1477 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1478 | |
1479 | === added file 'qakit/perf/eve/run.py' |
1480 | --- qakit/perf/eve/run.py 1970-01-01 00:00:00 +0000 |
1481 | +++ qakit/perf/eve/run.py 2016-04-22 20:41:08 +0000 |
1482 | @@ -0,0 +1,24 @@ |
1483 | +#!/usr/bin/python3 |
1484 | +# UESQA Metrics |
1485 | +# Copyright (C) 2016 Canonical |
1486 | +# |
1487 | +# This program is free software: you can redistribute it and/or modify |
1488 | +# it under the terms of the GNU General Public License as published by |
1489 | +# the Free Software Foundation, either version 3 of the License, or |
1490 | +# (at your option) any later version. |
1491 | +# |
1492 | +# This program is distributed in the hope that it will be useful, |
1493 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1494 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1495 | +# GNU General Public License for more details. |
1496 | +# |
1497 | +# You should have received a copy of the GNU General Public License |
1498 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1499 | + |
1500 | +from eve import Eve |
1501 | + |
1502 | +app = Eve() |
1503 | + |
1504 | + |
1505 | +if __name__ == '__main__': |
1506 | + app.run() |
1507 | |
1508 | === added file 'qakit/perf/eve/settings.py' |
1509 | --- qakit/perf/eve/settings.py 1970-01-01 00:00:00 +0000 |
1510 | +++ qakit/perf/eve/settings.py 2016-04-22 20:41:08 +0000 |
1511 | @@ -0,0 +1,92 @@ |
1512 | +#!/usr/bin/python3 |
1513 | +# UESQA Metrics |
1514 | +# Copyright (C) 2016 Canonical |
1515 | +# |
1516 | +# This program is free software: you can redistribute it and/or modify |
1517 | +# it under the terms of the GNU General Public License as published by |
1518 | +# the Free Software Foundation, either version 3 of the License, or |
1519 | +# (at your option) any later version. |
1520 | +# |
1521 | +# This program is distributed in the hope that it will be useful, |
1522 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1523 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1524 | +# GNU General Public License for more details. |
1525 | +# |
1526 | +# You should have received a copy of the GNU General Public License |
1527 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1528 | + |
1529 | +import os |
1530 | + |
1531 | +MONGO_HOST = os.getenv('EVE_MONGO_HOST', '127.0.0.1') |
1532 | +MONGO_PORT = os.getenv('EVE_MONGO_PORT', '27017') |
1533 | +MONGO_USERNAME = os.getenv('EVE_MONGO_USERNAME', 'ubuntuUser') |
1534 | +MONGO_PASSWORD = os.getenv('EVE_MONGO_PASSWORD', 'ubuntuPassword') |
1535 | +MONGO_DBNAME = os.getenv('EVE_MONGO_DBNAME', 'test') |
1536 | +SERVER_NAME = os.getenv('EVE_SERVER_NAME', None) |
1537 | + |
1538 | +perf_schema = { |
1539 | + # Schema definition, based on Cerberus grammar. Check the Cerberus project |
1540 | + # (https://github.com/nicolaiarocci/cerberus) for details. |
1541 | + 'run_id': { |
1542 | + 'type': 'string', |
1543 | + 'required': True, |
1544 | + }, |
1545 | + 'class': { |
1546 | + 'type': 'string', |
1547 | + 'required': True, |
1548 | + }, |
1549 | + 'time': { |
1550 | + 'type': 'number', |
1551 | + 'required': True, |
1552 | + }, |
1553 | + 'type': { |
1554 | + 'type': 'string', |
1555 | + 'required': False, |
1556 | + }, |
1557 | + 'name': { |
1558 | + 'type': 'string', |
1559 | + 'required': True, |
1560 | + }, |
1561 | + 'load': { |
1562 | + 'type': 'number', |
1563 | + 'required': False, |
1564 | + }, |
1565 | + 'build_number': { |
1566 | + 'type': 'string', |
1567 | + 'required': True, |
1568 | + }, |
1569 | + 'device': { |
1570 | + 'type': 'string', |
1571 | + 'required': True, |
1572 | + }, |
1573 | + 'channel': { |
1574 | + 'type': 'string', |
1575 | + 'required': True, |
1576 | + }, |
1577 | +} |
1578 | + |
1579 | +perf = { |
1580 | + # 'title' tag used in item links. Defaults to the resource title minus |
1581 | + # the final, plural 's' (works fine in most cases but not for 'people') |
1582 | + 'item_title': 'perf', |
1583 | + |
1584 | + # by default the standard item entry point is defined as |
1585 | + # '/people/<ObjectId>'. We leave it untouched, and we also enable an |
1586 | + # additional read-only entry point. This way consumers can also perform |
1587 | + # GET requests at '/people/<lastname>'. |
1588 | + 'additional_lookup': { |
1589 | + 'url': 'regex("[\w]+")', |
1590 | + 'field': 'name' |
1591 | + }, |
1592 | + |
1593 | + # We choose to override global cache-control directives for this resource. |
1594 | + 'cache_control': 'max-age=10,must-revalidate', |
1595 | + 'cache_expires': 10, |
1596 | + |
1597 | + # most global settings can be overridden at resource level |
1598 | + 'schema': perf_schema, |
1599 | +} |
1600 | + |
1601 | +DOMAIN = {'perf': perf} |
1602 | +RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] |
1603 | +ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE'] |
1604 | |
1605 | === added directory 'qakit/perf/orchestration' |
1606 | === added file 'qakit/perf/orchestration/__init__.py' |
1607 | --- qakit/perf/orchestration/__init__.py 1970-01-01 00:00:00 +0000 |
1608 | +++ qakit/perf/orchestration/__init__.py 2016-04-22 20:41:08 +0000 |
1609 | @@ -0,0 +1,84 @@ |
1610 | +#!/usr/bin/python3 |
1611 | +# UESQA Metrics |
1612 | +# Copyright (C) 2016 Canonical |
1613 | +# |
1614 | +# This program is free software: you can redistribute it and/or modify |
1615 | +# it under the terms of the GNU General Public License as published by |
1616 | +# the Free Software Foundation, either version 3 of the License, or |
1617 | +# (at your option) any later version. |
1618 | +# |
1619 | +# This program is distributed in the hope that it will be useful, |
1620 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1621 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1622 | +# GNU General Public License for more details. |
1623 | +# |
1624 | +# You should have received a copy of the GNU General Public License |
1625 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1626 | + |
1627 | +from argparse import ArgumentParser |
1628 | +import logging |
1629 | +import os |
1630 | + |
1631 | +from qakit.practitest import subunit_results |
1632 | +from qakit.perf import config_dict as config |
1633 | +from qakit.perf.engine import dao, parser |
1634 | + |
1635 | +logger = logging.getLogger(__name__) |
1636 | + |
1637 | +json_report = config['PERF']['JSON_REPORT'] |
1638 | + |
1639 | + |
1640 | +def save_results_if_needed(global_info, times): |
1641 | + if times: |
1642 | + run_id = global_info['run_id'] |
1643 | + if not dao.is_run_saved(run_id): |
1644 | + dao.save_record_to_db(global_info, times) |
1645 | + else: |
1646 | + logger.warning( |
1647 | + 'Test results already in the db for the run_id {}'.format( |
1648 | + run_id)) |
1649 | + |
1650 | + |
1651 | +def collect_records_by_name(global_info, allowed_names): |
1652 | + records = {} |
1653 | + device = global_info['device'] |
1654 | + channel = global_info['channel'] |
1655 | + for name in allowed_names: |
1656 | + records[name] = dao.get_records_for_key('name', name, device, channel) |
1657 | + return records |
1658 | + |
1659 | + |
1660 | +def _parse_input(input_class, parser_method): |
1661 | + args_parser = ArgumentParser("Parse times from logs.") |
1662 | + args_parser.add_argument("log") |
1663 | + args = args_parser.parse_args() |
1664 | + |
1665 | + log_lines = parser_method(args.log) |
1666 | + output_dir = os.path.dirname(args.log) |
1667 | + return parser.parse_log_times(log_lines, input_class), output_dir |
1668 | + |
1669 | + |
1670 | +def parse_subunit_input(input_class): |
1671 | + return _parse_input(input_class, subunit_results.get_log_lines) |
1672 | + |
1673 | + |
1674 | +def parse_text_input(input_class): |
1675 | + return _parse_input(input_class, parser.parse_log_file) |
1676 | + |
1677 | + |
1678 | +def create_graphics_dir(results_dir, dirname): |
1679 | + dir = os.path.join(results_dir, dirname) |
1680 | + if not os.path.exists(dir): |
1681 | + os.mkdir(dir) |
1682 | + return dir |
1683 | + |
1684 | + |
1685 | +def get_from_build_times(build_times, key, attr): |
1686 | + """ Get the list of values stored in the attr for every BuildTime stored |
1687 | + for the desired key in the build_times |
1688 | + """ |
1689 | + return [getattr(time, attr) for time in build_times[key]] |
1690 | + |
1691 | + |
1692 | +def strip_string_list(string_list): |
1693 | + return [string.strip() for string in string_list.split(',')] |
1694 | |
1695 | === added file 'qakit/perf/orchestration/app_startup.py' |
1696 | --- qakit/perf/orchestration/app_startup.py 1970-01-01 00:00:00 +0000 |
1697 | +++ qakit/perf/orchestration/app_startup.py 2016-04-22 20:41:08 +0000 |
1698 | @@ -0,0 +1,79 @@ |
1699 | +#!/usr/bin/python3 |
1700 | +# UESQA Metrics |
1701 | +# Copyright (C) 2016 Canonical |
1702 | +# |
1703 | +# This program is free software: you can redistribute it and/or modify |
1704 | +# it under the terms of the GNU General Public License as published by |
1705 | +# the Free Software Foundation, either version 3 of the License, or |
1706 | +# (at your option) any later version. |
1707 | +# |
1708 | +# This program is distributed in the hope that it will be useful, |
1709 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1710 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1711 | +# GNU General Public License for more details. |
1712 | +# |
1713 | +# You should have received a copy of the GNU General Public License |
1714 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1715 | + |
1716 | +import logging |
1717 | + |
1718 | +from qakit.perf import config_dict as config |
1719 | +from qakit.perf.engine import report, plotter, data_processor |
1720 | +from qakit.perf import orchestration as orch |
1721 | + |
1722 | +logger = logging.getLogger(__name__) |
1723 | + |
1724 | +eve_path = 'app_startup' |
1725 | +allowed_apps = orch.strip_string_list(config['APP_STARTUP']['ALLOWED_APPS']) |
1726 | +types = orch.strip_string_list(config['APP_STARTUP']['TYPES']) |
1727 | +titles = orch.strip_string_list(config['APP_STARTUP']['TITLES']) |
1728 | +colors = orch.strip_string_list(config['APP_STARTUP']['COLORS']) |
1729 | +ecolors = orch.strip_string_list(config['APP_STARTUP']['ECOLORS']) |
1730 | + |
1731 | + |
1732 | +def _plot_records_by_apps(records_by_apps, results_dir): |
1733 | + app_plots = {} |
1734 | + |
1735 | + for app in allowed_apps: |
1736 | + title = 'Times for {} app'.format(app) |
1737 | + build_times = data_processor.get_times_by_build(records_by_apps[app], |
1738 | + 'type', types) |
1739 | + builds = [times.build for times in build_times[types[0]]] |
1740 | + |
1741 | + bars = [] |
1742 | + iter = 0 |
1743 | + for type in types: |
1744 | + bars.append(plotter.TimesBar( |
1745 | + orch.get_from_build_times(build_times, type, 'time'), |
1746 | + orch.get_from_build_times(build_times, type, 'stdev'), |
1747 | + titles[iter], |
1748 | + '#{}'.format(colors[iter]), |
1749 | + '#{}'.format(ecolors[iter]))) |
1750 | + iter += 1 |
1751 | + |
1752 | + app_plots[app] = plotter.plot_records(app, title, builds, bars, |
1753 | + results_dir) |
1754 | + |
1755 | + return app_plots |
1756 | + |
1757 | + |
1758 | +def generate_reports(output_dir, global_info, records_by_apps): |
1759 | + if orch.json_report: |
1760 | + graphics_dir = orch.create_graphics_dir(output_dir, 'appstartup_data') |
1761 | + pics = _plot_records_by_apps(records_by_apps, graphics_dir) |
1762 | + report.create_json_report(pics, output_dir, |
1763 | + report_file='app_startup_report.json', |
1764 | + report_info=global_info) |
1765 | + |
1766 | + |
1767 | +def main(): |
1768 | + (global_info, startup_times), output_dir = orch.parse_subunit_input( |
1769 | + 'app_startup') |
1770 | + |
1771 | + orch.save_results_if_needed(global_info, startup_times) |
1772 | + records_by_apps = orch.collect_records_by_name(global_info, allowed_apps) |
1773 | + generate_reports(output_dir, global_info, records_by_apps) |
1774 | + |
1775 | + |
1776 | +if __name__ == "__main__": |
1777 | + main() |
1778 | |
1779 | === added file 'qakit/perf/orchestration/performance.py' |
1780 | --- qakit/perf/orchestration/performance.py 1970-01-01 00:00:00 +0000 |
1781 | +++ qakit/perf/orchestration/performance.py 2016-04-22 20:41:08 +0000 |
1782 | @@ -0,0 +1,80 @@ |
1783 | +#!/usr/bin/python3 |
1784 | +# UESQA Metrics |
1785 | +# Copyright (C) 2016 Canonical |
1786 | +# |
1787 | +# This program is free software: you can redistribute it and/or modify |
1788 | +# it under the terms of the GNU General Public License as published by |
1789 | +# the Free Software Foundation, either version 3 of the License, or |
1790 | +# (at your option) any later version. |
1791 | +# |
1792 | +# This program is distributed in the hope that it will be useful, |
1793 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1794 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1795 | +# GNU General Public License for more details. |
1796 | +# |
1797 | +# You should have received a copy of the GNU General Public License |
1798 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1799 | + |
1800 | +import logging |
1801 | + |
1802 | +from qakit.perf import config_dict as config |
1803 | +from qakit.perf.engine import report, plotter, data_processor |
1804 | +from qakit.perf import orchestration as orch |
1805 | + |
1806 | +logger = logging.getLogger(__name__) |
1807 | + |
1808 | +eve_path = 'performance' |
1809 | +allowed_scenarios = orch.strip_string_list( |
1810 | + config['PERFORMANCE']['PERFORMANCE_ALLOWED_SCENARIOS']) |
1811 | +types = orch.strip_string_list(config['PERFORMANCE']['PERFORMANCE_TYPES']) |
1812 | +titles = orch.strip_string_list(config['PERFORMANCE']['PERFORMANCE_TITLES']) |
1813 | +colors = orch.strip_string_list(config['PERFORMANCE']['PERFORMANCE_COLORS']) |
1814 | +ecolors = orch.strip_string_list(config['PERFORMANCE']['PERFORMANCE_ECOLORS']) |
1815 | + |
1816 | + |
1817 | +def _plot_records_by_scenarios(records_by_apps, results_dir): |
1818 | + plots = {} |
1819 | + |
1820 | + for scenario in allowed_scenarios: |
1821 | + title = 'Times for {} scenario'.format(scenario) |
1822 | + build_times = data_processor.get_times_by_build( |
1823 | + records_by_apps[scenario], 'type', types) |
1824 | + builds = [times.build for times in build_times[types[0]]] |
1825 | + |
1826 | + bars = [] |
1827 | + iter = 0 |
1828 | + for type in types: |
1829 | + bars.append(plotter.TimesBar( |
1830 | + orch.get_from_build_times(build_times, type, 'time'), |
1831 | + orch.get_from_build_times(build_times, type, 'stdev'), |
1832 | + titles[iter], |
1833 | + '#{}'.format(colors[iter]), |
1834 | + '#{}'.format(ecolors[iter]))) |
1835 | + iter += 1 |
1836 | + |
1837 | + plots[scenario] = plotter.plot_records(scenario, title, builds, bars, |
1838 | + results_dir) |
1839 | + |
1840 | + return plots |
1841 | + |
1842 | + |
1843 | +def generate_reports(output_dir, global_info, records): |
1844 | + if orch.json_report: |
1845 | + graphics_dir = orch.create_graphics_dir(output_dir, 'performance_data') |
1846 | + pics = _plot_records_by_scenarios(records, graphics_dir) |
1847 | + report.create_json_report(pics, output_dir, |
1848 | + report_file='performance_report.json', |
1849 | + report_info=global_info) |
1850 | + |
1851 | + |
1852 | +def main(): |
1853 | + (global_info, startup_times), output_dir = orch.parse_text_input( |
1854 | + 'performance') |
1855 | + |
1856 | + orch.save_results_if_needed(global_info, startup_times) |
1857 | + records = orch.collect_records_by_name(global_info, allowed_scenarios) |
1858 | + generate_reports(output_dir, global_info, records) |
1859 | + |
1860 | + |
1861 | +if __name__ == "__main__": |
1862 | + main() |
1863 | |
1864 | === added file 'qakit/perf/orchestration/scalability.py' |
1865 | --- qakit/perf/orchestration/scalability.py 1970-01-01 00:00:00 +0000 |
1866 | +++ qakit/perf/orchestration/scalability.py 2016-04-22 20:41:08 +0000 |
1867 | @@ -0,0 +1,78 @@ |
1868 | +#!/usr/bin/python3 |
1869 | +# UESQA Metrics |
1870 | +# Copyright (C) 2016 Canonical |
1871 | +# |
1872 | +# This program is free software: you can redistribute it and/or modify |
1873 | +# it under the terms of the GNU General Public License as published by |
1874 | +# the Free Software Foundation, either version 3 of the License, or |
1875 | +# (at your option) any later version. |
1876 | +# |
1877 | +# This program is distributed in the hope that it will be useful, |
1878 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1879 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1880 | +# GNU General Public License for more details. |
1881 | +# |
1882 | +# You should have received a copy of the GNU General Public License |
1883 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1884 | + |
1885 | +import logging |
1886 | + |
1887 | +from qakit.perf import config_dict as config |
1888 | +from qakit.perf.engine import report, plotter, data_processor |
1889 | +from qakit.perf import orchestration as orch |
1890 | + |
1891 | +logger = logging.getLogger(__name__) |
1892 | + |
1893 | +allowed_names = orch.strip_string_list(config['SCALABILITY']['ALLOWED_NAMES']) |
1894 | +loads = [int(load) for load in config['SCALABILITY']['LOADS'].split(',')] |
1895 | +titles = orch.strip_string_list(config['SCALABILITY']['TITLES']) |
1896 | +colors = orch.strip_string_list(config['SCALABILITY']['COLORS']) |
1897 | +ecolors = orch.strip_string_list(config['SCALABILITY']['ECOLORS']) |
1898 | + |
1899 | + |
1900 | +def _plot_records_by_name(records_by_apps, results_dir): |
1901 | + plots = {} |
1902 | + |
1903 | + for name in allowed_names: |
1904 | + title = 'Times for {} scenario'.format(name.replace('_', ' ')) |
1905 | + build_times = data_processor.get_times_by_build(records_by_apps[name], |
1906 | + 'load', loads) |
1907 | + builds = [times.build for times in build_times[loads[0]]] |
1908 | + |
1909 | + bars = [] |
1910 | + iter = 0 |
1911 | + for load in loads: |
1912 | + bars.append(plotter.TimesBar( |
1913 | + orch.get_from_build_times(build_times, load, 'time'), |
1914 | + orch.get_from_build_times(build_times, load, 'stdev'), |
1915 | + titles[iter], |
1916 | + '#{}'.format(colors[iter]), |
1917 | + '#{}'.format(ecolors[iter]))) |
1918 | + iter += 1 |
1919 | + |
1920 | + plots[name] = plotter.plot_records(name, title, builds, bars, |
1921 | + results_dir) |
1922 | + |
1923 | + return plots |
1924 | + |
1925 | + |
1926 | +def generate_reports(output_dir, global_info, records_by_apps): |
1927 | + if orch.json_report: |
1928 | + graphics_dir = orch.create_graphics_dir(output_dir, 'scalability_data') |
1929 | + pics = _plot_records_by_name(records_by_apps, graphics_dir) |
1930 | + report.create_json_report(pics, output_dir, |
1931 | + report_file='scalability_report.json', |
1932 | + report_info=global_info) |
1933 | + |
1934 | + |
1935 | +def main(): |
1936 | + (global_info, scal_times), output_dir = orch.parse_subunit_input( |
1937 | + 'scalability') |
1938 | + |
1939 | + orch.save_results_if_needed(global_info, scal_times) |
1940 | + records_by_apps = orch.collect_records_by_name(global_info, allowed_names) |
1941 | + generate_reports(output_dir, global_info, records_by_apps) |
1942 | + |
1943 | + |
1944 | +if __name__ == "__main__": |
1945 | + main() |
1946 | |
1947 | === modified file 'qakit/practitest/subunit_results.py' |
1948 | --- qakit/practitest/subunit_results.py 2015-04-15 19:03:17 +0000 |
1949 | +++ qakit/practitest/subunit_results.py 2016-04-22 20:41:08 +0000 |
1950 | @@ -41,3 +41,18 @@ |
1951 | if result['id'] == test_name: |
1952 | return result |
1953 | return None |
1954 | + |
1955 | + |
1956 | +def get_log_lines(filepath): |
1957 | + """ Parse the subunit file at the given path and return all the lines in |
1958 | + the log details for all the tests |
1959 | + """ |
1960 | + results = parse(filepath) |
1961 | + test_logs = [] |
1962 | + for result in results: |
1963 | + test_log = result['details'].get('test-log') |
1964 | + if test_log: |
1965 | + test_logs.append(test_log.as_text().split('\n')) |
1966 | + |
1967 | + log_lines = [line for sublist in test_logs for line in sublist] |
1968 | + return log_lines |
1969 | |
1970 | === modified file 'setup.py' |
1971 | --- setup.py 2015-12-09 11:33:15 +0000 |
1972 | +++ setup.py 2016-04-22 20:41:08 +0000 |
1973 | @@ -39,7 +39,7 @@ |
1974 | ('subunit-practitest-export = ' |
1975 | 'qakit.practitest.report_subunit_results_to_practitest:main'), |
1976 | ('parse-app-startup = ' |
1977 | - 'qakit.appstartup.parse_startup_times:main'), |
1978 | + 'qakit.perf.parse_startup_times:main'), |
1979 | ] |
1980 | } |
1981 | ) |