Merge lp:~canonical-platform-qa/qakit/performance_solution into lp:qakit

Proposed by Sergio Cazzolato
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
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

To post a comment you must log in.
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 )

Subscribers

People subscribed via source and target branches

to all changes: