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

Proposed by Sergio Cazzolato
Status: Merged
Approved by: Allan LeSage
Approved revision: 64
Merged at revision: 49
Proposed branch: lp:~canonical-platform-qa/qakit/app_startup_poc
Merge into: lp:qakit
Diff against target: 782 lines (+691/-2)
14 files modified
README (+48/-0)
debian/control (+2/-0)
qakit/appstartup/__init__.py (+33/-0)
qakit/appstartup/dao.py (+102/-0)
qakit/appstartup/data_processor.py (+74/-0)
qakit/appstartup/eve/__init__.py (+16/-0)
qakit/appstartup/eve/run.py (+22/-0)
qakit/appstartup/eve/settings.py (+89/-0)
qakit/appstartup/orchestrator.py (+99/-0)
qakit/appstartup/parser.py (+65/-0)
qakit/appstartup/plotter.py (+63/-0)
qakit/appstartup/report.py (+72/-0)
qakit/config.py (+4/-2)
setup.py (+2/-0)
To merge this branch: bzr merge lp:~canonical-platform-qa/qakit/app_startup_poc
Reviewer Review Type Date Requested Status
Allan LeSage (community) Approve
Richard Huddie (community) Needs Fixing
Review via email: mp+283698@code.launchpad.net

Commit message

Process to parse logs, upload data to mongo db and plot the app startup times for the last builds.

Dependencies:
Install pip by doing: "sudo apt-get install python-pip python-dev build-essential"

Download and Install eve (http://python-eve.org/install.html)
Run eve by doing python qakit/appstartup/eve/run.py (you can define the env variables, see in file settings.py)

Download and install mongodb (https://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/)
Create a user for mongo and remember to update the eve settings file
To create the mongo user type: "mongo" and then
db.createUser(
   {
     user: "ubuntuUser",
     pwd: "ubuntuPassword",
     roles: [ { role: "readWrite", db: "test" } ]
   }
)
Note: eve code is ready to run with python 2.7 and 3.X

Network configuration when the eve machine is hosted in canonistack:

  1- Allow ip routing by doing:
  sysctl -w net.ipv4.ip_forward=1

  2- Make sure the os allow to route to localnet by doing:
  sudo sysctl -w net.ipv4.conf.eth0.route_localnet=1

  3- configure iptables by doing:
  sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 5000 -j DNAT
  --to-destination 127.0.0.1:5000

To run the import and plotting process it is needed to run the following tests with the command "./run-system-tests ubuntu_system_tests.tests.app_startup.test_application_startup" for the branch lp:~canonical-platform-qa/ubuntu-system-tests/app_startup_poc.

Then, having a test-results.subunit file, run as following: python3 qakit/appstartup/orchestrator.py ~/Desktop/app_startup/test-results.subunit

The html report will be generated in ~/Desktop/app_startup/app_startup_report.html

Description of the change

The eve service is currently available on:

  http://10.55.61.109:5000/appstartup

Jenkins job available on:

  https://platform-qa-jenkins.ubuntu.com/job/cachio-appstarup

To post a comment you must log in.
48. By Sergio Cazzolato

Merge with trunk and conflicts resolved

Revision history for this message
Richard Huddie (rhuddie) wrote :

Could you add a README with all the setup steps listed similar to above description? Or update existing one?

I found it a little confusing as the existing qakit README mentions only python 2 support:
http://bazaar.launchpad.net/~canonical-platform-qa/qakit/trunk/view/head:/README

review: Needs Fixing
49. By Sergio Cazzolato

Supporting the run type

50. By Sergio Cazzolato

Adding env variable for qakit config file

51. By Sergio Cazzolato

Making configurable the ip and port used by eve

52. By Sergio Cazzolato

Change to access from external ip to the local service

53. By Sergio Cazzolato

Adding missing file to create a html report

54. By Sergio Cazzolato

Making url path configurable

55. By Sergio Cazzolato

Changing graphics colors

56. By Sergio Cazzolato

Adding some styles to the report

57. By Sergio Cazzolato

Minor changes in the report

58. By Sergio Cazzolato

Adding json report

59. By Sergio Cazzolato

Improving how reports are managed

60. By Sergio Cazzolato

Adding docs to the README

61. By Sergio Cazzolato

Adding missing docs to the README

62. By Sergio Cazzolato

Adding configs for reports and adding more info json report

63. By Sergio Cazzolato

updating json report

Revision history for this message
Allan LeSage (allanlesage) wrote :

I have yet to get this working all the way but some comments to deal with meanwhile:

When you're using requests.get or .post, just pass the data as a named keyword argument *params* with a dict of data, it's much clearer than formatting the URL yourself IMO.

For the HTML rendering, it'd be cleaner to use a jinja2 template, instead of concatenating the HTML together piecemeal. Happy to help get that going, or you could just drag the metrics stuff into a shared location? Would you have a look at qakit/metrics/reports/__init__.py:render_html ?

What does dao mean?

More comments tomorrow, just wanted to get started ;) .

review: Needs Fixing
Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote :

Going to use params to format the url.

The html report is going to disappear once we have it in trunk as we are going to use the dashboard. SO I don't see the need to change this implementation, don't you?

64. By Sergio Cazzolato

Adding paramter to filter number of builds to consider and using params to format the urls

Revision history for this message
Allan LeSage (allanlesage) wrote :

Thanks for that change, propose to merge this--it's just a proof of concept after all ;) --and it doesn't affect any existing code.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2014-10-08 01:40:03 +0000
3+++ README 2016-02-18 14:19:39 +0000
4@@ -48,3 +48,51 @@
5 | ubuntu-rtm/landing-007 - qtbase-opensource-src,qtbase-opensource-src-gles : Mirv | Need QA Sign-off |
6 | ubuntu-rtm/landing-009 - ubuntu-touch-session : tedg | Need QA Sign-off |
7 +---------------------------------------------------------------------------------------------------------------------------------------------------------+------------------+
8+
9+How do I process appstartup logs
10+================================
11+
12+Environment configuration
13+-------------------------
14+
15+Install pip by doing: "sudo apt-get install python-pip python-dev build-essential"
16+
17+Download and Install eve (http://python-eve.org/install.html)
18+Run eve by doing python qakit/appstartup/eve/run.py (you can define the env variables, see in file settings.py)
19+
20+Download and install mongodb (https://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/)
21+Create a user for mongo and remember to update the eve settings file
22+To create the mongo user type: "mongo" and then
23+db.createUser(
24+ {
25+ user: "ubuntuUser",
26+ pwd: "ubuntuPassword",
27+ roles: [ { role: "readWrite", db: "test" } ]
28+ }
29+)
30+Note: eve code is ready to run with python 2.7 and 3.X
31+
32+In case it is needed to open ports where the eve machine is hosted:
33+
34+ 1- Allow ip routing by doing:
35+ sysctl -w net.ipv4.ip_forward=1
36+
37+ 2- Make sure the os allow to route to localnet by doing:
38+ sudo sysctl -w net.ipv4.conf.eth0.route_localnet=1
39+
40+ 3- configure iptables by doing:
41+ sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 5000 -j DNAT
42+ --to-destination 127.0.0.1:5000
43+
44+Add to the config the following:
45+ [APP_STARTUP]
46+ EVE_HOST = [dest_ip]
47+ EVE_PORT = [dest_port]
48+ EVE_PATH = appstartup
49+ HTML_REPORT = True
50+ JSON_REPORT = True
51+ BUILDS_TO_PLOT = 10
52+
53+Having a test-results.subunit file, run as following: python3 qakit/appstartup/orchestrator.py pathto/test-results.subunit
54+
55+HTML and json reports are generated in the subunit directory. All the pictures will be stored in the appstartup_data dir.
56
57=== modified file 'debian/control'
58--- debian/control 2016-01-12 12:11:16 +0000
59+++ debian/control 2016-02-18 14:19:39 +0000
60@@ -24,6 +24,8 @@
61 python3-bson,
62 python3-dateutil,
63 python3-jinja2,
64+ python3-matplotlib,
65+ python3-numpy,
66 python3-pymongo,
67 python3-requests,
68 python3-subunit,
69
70=== added directory 'qakit/appstartup'
71=== added file 'qakit/appstartup/__init__.py'
72--- qakit/appstartup/__init__.py 1970-01-01 00:00:00 +0000
73+++ qakit/appstartup/__init__.py 2016-02-18 14:19:39 +0000
74@@ -0,0 +1,33 @@
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 configparser
93+
94+import qakit.config as qakit_config
95+
96+allowed_apps = ['webbrowser', 'address_book', 'calculator', 'here', 'dialer',
97+ 'clock', 'messaging', 'ebay', 'camera', 'system_settings',
98+ 'music', 'gallery']
99+
100+
101+def _read_config(config_filepath):
102+ """ Parse the config at the given filepath and return it """
103+ config_file = configparser.ConfigParser()
104+ config_file.read(config_filepath)
105+ return config_file
106+
107+config_dict = _read_config(qakit_config.get_config_file_location())
108
109=== added file 'qakit/appstartup/dao.py'
110--- qakit/appstartup/dao.py 1970-01-01 00:00:00 +0000
111+++ qakit/appstartup/dao.py 2016-02-18 14:19:39 +0000
112@@ -0,0 +1,102 @@
113+#!/usr/bin/python3
114+# UESQA Metrics
115+# Copyright (C) 2016 Canonical
116+#
117+# This program is free software: you can redistribute it and/or modify
118+# it under the terms of the GNU General Public License as published by
119+# the Free Software Foundation, either version 3 of the License, or
120+# (at your option) any later version.
121+#
122+# This program is distributed in the hope that it will be useful,
123+# but WITHOUT ANY WARRANTY; without even the implied warranty of
124+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
125+# GNU General Public License for more details.
126+#
127+# You should have received a copy of the GNU General Public License
128+# along with this program. If not, see <http://www.gnu.org/licenses/>.
129+
130+import logging
131+import requests
132+
133+from qakit.appstartup import config_dict as config
134+
135+BASE_URL = 'http://{}:{}/{}'.format(config['APP_STARTUP']['EVE_HOST'],
136+ config['APP_STARTUP']['EVE_PORT'],
137+ config['APP_STARTUP']['EVE_PATH'])
138+
139+logger = logging.getLogger(__name__)
140+
141+
142+def get_headers():
143+ return {'Content-Type': 'application/json'}
144+
145+
146+def _get_where_param(**params):
147+ """ Get a where parameter used to filter when getting a query to mongo
148+ :param params: A dict with all the parameters used to filter
149+ """
150+ where_param = "{"
151+ for (k, v) in params.items():
152+ where_param += '"{}":"{}",'.format(k, v)
153+ return where_param[:-1] + '}'
154+
155+
156+def save_record_to_db(global_info, startup_times):
157+ """ Save the record information in the mongo db. The information saved is
158+ composed by joining both dicts received as parameter
159+ """
160+ logger.info('Saving records to db')
161+ for instance in startup_times:
162+ instance.update(global_info)
163+ r = requests.post('{}'.format(BASE_URL),
164+ json=instance,
165+ headers=get_headers())
166+ if not r.status_code == requests.codes.created:
167+ logger.error('Error uploading instance data: {}'.format(instance))
168+ logger.error(r.status_code, r.reason)
169+
170+
171+def is_run_saved(run_id):
172+ """ Indicate if there is at least one record with the run_id saved in the
173+ mongo db
174+ """
175+ where_param = _get_where_param(run_id=run_id)
176+ r = requests.get(BASE_URL, params={'where': where_param},
177+ headers=get_headers())
178+ if r.status_code == requests.codes.ok:
179+ return r.json()['_meta']['total'] > 0
180+ else:
181+ logger.error('Error retrieving information for run: {}'.format(run_id))
182+ logger.error(r.status_code, r.reason)
183+
184+
185+def get_records_for_app(app, device, channel):
186+ """ Retrieve the records from the mongo db for an specific app, device and
187+ channel. The records by default are ordered by build number
188+ :param app: The app used to filter the results
189+ :param device: The device used to filter the results
190+ :param channel: The channel used to filter the results
191+ """
192+ where_param = _get_where_param(app=app, device=device, channel=channel)
193+
194+ stop = False
195+ page_iter = 1
196+ records = []
197+ while not stop:
198+ r = requests.get(BASE_URL,
199+ params={'where': where_param, 'max_results': 50,
200+ 'page': page_iter},
201+ headers=get_headers())
202+ if r.status_code == requests.codes.ok:
203+ records.extend(r.json()['_items'])
204+ else:
205+ logger.error('Error retrieving data from url: {}'.format(r.url))
206+ logger.error(r.status_code, r.reason)
207+
208+ metadata = r.json()['_meta']
209+ if metadata['max_results'] * metadata['page'] >= metadata['total']:
210+ stop = True
211+ else:
212+ page_iter += 1
213+
214+ return records
215
216=== added file 'qakit/appstartup/data_processor.py'
217--- qakit/appstartup/data_processor.py 1970-01-01 00:00:00 +0000
218+++ qakit/appstartup/data_processor.py 2016-02-18 14:19:39 +0000
219@@ -0,0 +1,74 @@
220+#!/usr/bin/python3
221+# UESQA Metrics
222+# Copyright (C) 2016 Canonical
223+#
224+# This program is free software: you can redistribute it and/or modify
225+# it under the terms of the GNU General Public License as published by
226+# the Free Software Foundation, either version 3 of the License, or
227+# (at your option) any later version.
228+#
229+# This program is distributed in the hope that it will be useful,
230+# but WITHOUT ANY WARRANTY; without even the implied warranty of
231+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
232+# GNU General Public License for more details.
233+#
234+# You should have received a copy of the GNU General Public License
235+# along with this program. If not, see <http://www.gnu.org/licenses/>.
236+
237+import statistics
238+
239+from qakit.appstartup import config_dict as config
240+
241+BUILDS_TO_PLOT = config['APP_STARTUP']['BUILDS_TO_PLOT']
242+
243+
244+def get_times_by_build(app_records):
245+ """ Calculate the times for the different builds
246+ :return: [builds], [mean cold], [stdev cold], [mean hot], [stdev hot]
247+ """
248+ build_times = {}
249+ for record in app_records:
250+ build = record['build_number']
251+ if build in build_times:
252+ build_times[build].append(record)
253+ else:
254+ build_times[build] = [record]
255+
256+ times = _process_build_times(build_times)
257+ if times:
258+ return zip(*times)
259+ else:
260+ return (), (), (), (), ()
261+
262+
263+def _process_build_times(build_times):
264+ """ Process and filter build times """
265+ times = []
266+ for build in sorted(build_times)[- int(BUILDS_TO_PLOT):]:
267+ hot_times = _get_times_by_type(build_times[build], 'hot')
268+ cold_times = _get_times_by_type(build_times[build], 'cold')
269+
270+ times.append((build, _get_average(cold_times),
271+ _get_stdev(cold_times),
272+ _get_average(hot_times),
273+ _get_stdev(hot_times)))
274+ return times
275+
276+
277+def _get_average(times):
278+ if times:
279+ return statistics.mean(times)
280+ else:
281+ return 0
282+
283+
284+def _get_stdev(times):
285+ if len(times) >= 2:
286+ return statistics.stdev(times)
287+ else:
288+ return 0
289+
290+
291+def _get_times_by_type(app_records, type):
292+ return [app_record['time'] for app_record in app_records if
293+ app_record['type'] == type]
294
295=== added directory 'qakit/appstartup/eve'
296=== added file 'qakit/appstartup/eve/__init__.py'
297--- qakit/appstartup/eve/__init__.py 1970-01-01 00:00:00 +0000
298+++ qakit/appstartup/eve/__init__.py 2016-02-18 14:19:39 +0000
299@@ -0,0 +1,16 @@
300+#!/usr/bin/python3
301+# UESQA Metrics
302+# Copyright (C) 2016 Canonical
303+#
304+# This program is free software: you can redistribute it and/or modify
305+# it under the terms of the GNU General Public License as published by
306+# the Free Software Foundation, either version 3 of the License, or
307+# (at your option) any later version.
308+#
309+# This program is distributed in the hope that it will be useful,
310+# but WITHOUT ANY WARRANTY; without even the implied warranty of
311+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
312+# GNU General Public License for more details.
313+#
314+# You should have received a copy of the GNU General Public License
315+# along with this program. If not, see <http://www.gnu.org/licenses/>.
316
317=== added file 'qakit/appstartup/eve/run.py'
318--- qakit/appstartup/eve/run.py 1970-01-01 00:00:00 +0000
319+++ qakit/appstartup/eve/run.py 2016-02-18 14:19:39 +0000
320@@ -0,0 +1,22 @@
321+#!/usr/bin/python3
322+# UESQA Metrics
323+# Copyright (C) 2016 Canonical
324+#
325+# This program is free software: you can redistribute it and/or modify
326+# it under the terms of the GNU General Public License as published by
327+# the Free Software Foundation, either version 3 of the License, or
328+# (at your option) any later version.
329+#
330+# This program is distributed in the hope that it will be useful,
331+# but WITHOUT ANY WARRANTY; without even the implied warranty of
332+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
333+# GNU General Public License for more details.
334+#
335+# You should have received a copy of the GNU General Public License
336+# along with this program. If not, see <http://www.gnu.org/licenses/>.
337+
338+from eve import Eve
339+app = Eve()
340+
341+if __name__ == '__main__':
342+ app.run()
343
344=== added file 'qakit/appstartup/eve/settings.py'
345--- qakit/appstartup/eve/settings.py 1970-01-01 00:00:00 +0000
346+++ qakit/appstartup/eve/settings.py 2016-02-18 14:19:39 +0000
347@@ -0,0 +1,89 @@
348+#!/usr/bin/python3
349+# UESQA Metrics
350+# Copyright (C) 2016 Canonical
351+#
352+# This program is free software: you can redistribute it and/or modify
353+# it under the terms of the GNU General Public License as published by
354+# the Free Software Foundation, either version 3 of the License, or
355+# (at your option) any later version.
356+#
357+# This program is distributed in the hope that it will be useful,
358+# but WITHOUT ANY WARRANTY; without even the implied warranty of
359+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
360+# GNU General Public License for more details.
361+#
362+# You should have received a copy of the GNU General Public License
363+# along with this program. If not, see <http://www.gnu.org/licenses/>.
364+
365+import os
366+
367+MONGO_HOST = os.getenv('EVE_MONGO_HOST', '127.0.0.1')
368+MONGO_PORT = os.getenv('EVE_MONGO_PORT', '27017')
369+MONGO_USERNAME = os.getenv('EVE_MONGO_USERNAME', 'ubuntuUser')
370+MONGO_PASSWORD = os.getenv('EVE_MONGO_PASSWORD', 'ubuntuPassword')
371+MONGO_DBNAME = os.getenv('EVE_MONGO_DBNAME', 'test')
372+SERVER_NAME = os.getenv('EVE_SERVER_NAME', None)
373+
374+
375+appstartup_schema = {
376+ # Schema definition, based on Cerberus grammar. Check the Cerberus project
377+ # (https://github.com/nicolaiarocci/cerberus) for details.
378+ 'run_id': {
379+ 'type': 'string',
380+ 'required': True,
381+ },
382+ 'time': {
383+ 'type': 'number',
384+ 'required': True,
385+ },
386+ 'type': {
387+ 'type': 'string',
388+ 'required': True,
389+ },
390+ 'iteration': {
391+ 'type': 'number',
392+ 'required': True,
393+ },
394+ 'app': {
395+ 'type': 'string',
396+ 'required': False,
397+ },
398+ 'build_number': {
399+ 'type': 'string',
400+ 'required': True,
401+ },
402+ 'device': {
403+ 'type': 'string',
404+ 'required': True,
405+ },
406+ 'channel': {
407+ 'type': 'string',
408+ 'required': True,
409+ },
410+}
411+
412+appstartup = {
413+ # 'title' tag used in item links. Defaults to the resource title minus
414+ # the final, plural 's' (works fine in most cases but not for 'people')
415+ 'item_title': 'appstartup',
416+
417+ # by default the standard item entry point is defined as
418+ # '/people/<ObjectId>'. We leave it untouched, and we also enable an
419+ # additional read-only entry point. This way consumers can also perform
420+ # GET requests at '/people/<lastname>'.
421+ 'additional_lookup': {
422+ 'url': 'regex("[\w]+")',
423+ 'field': 'app'
424+ },
425+
426+ # We choose to override global cache-control directives for this resource.
427+ 'cache_control': 'max-age=10,must-revalidate',
428+ 'cache_expires': 10,
429+
430+ # most global settings can be overridden at resource level
431+ 'schema': appstartup_schema,
432+}
433+
434+DOMAIN = {'appstartup': appstartup}
435+RESOURCE_METHODS = ['GET', 'POST', 'DELETE']
436+ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE']
437
438=== added file 'qakit/appstartup/orchestrator.py'
439--- qakit/appstartup/orchestrator.py 1970-01-01 00:00:00 +0000
440+++ qakit/appstartup/orchestrator.py 2016-02-18 14:19:39 +0000
441@@ -0,0 +1,99 @@
442+#!/usr/bin/python3
443+# UESQA Metrics
444+# Copyright (C) 2016 Canonical
445+#
446+# This program is free software: you can redistribute it and/or modify
447+# it under the terms of the GNU General Public License as published by
448+# the Free Software Foundation, either version 3 of the License, or
449+# (at your option) any later version.
450+#
451+# This program is distributed in the hope that it will be useful,
452+# but WITHOUT ANY WARRANTY; without even the implied warranty of
453+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
454+# GNU General Public License for more details.
455+#
456+# You should have received a copy of the GNU General Public License
457+# along with this program. If not, see <http://www.gnu.org/licenses/>.
458+
459+from argparse import ArgumentParser
460+import logging
461+import os
462+
463+from qakit.appstartup import allowed_apps
464+from qakit.appstartup import config_dict as config
465+from qakit.appstartup import dao
466+from qakit.appstartup import data_processor
467+from qakit.appstartup import parser
468+from qakit.appstartup import plotter
469+from qakit.appstartup import report
470+from qakit.practitest import subunit_results
471+
472+logger = logging.getLogger(__name__)
473+
474+HTML_REPORT = config['APP_STARTUP']['HTML_REPORT']
475+JSON_REPORT = config['APP_STARTUP']['JSON_REPORT']
476+
477+
478+def save_results_if_needed(global_info, startup_times):
479+ if startup_times:
480+ run_id = global_info['run_id']
481+ if not dao.is_run_saved(run_id):
482+ dao.save_record_to_db(global_info, startup_times)
483+ else:
484+ logger.warning(
485+ 'Test results already in the db for the run_id {}'.format(
486+ run_id))
487+
488+
489+def _collect_records_by_app(global_info):
490+ app_records = {}
491+ device = global_info['device']
492+ channel = global_info['channel']
493+ for app in allowed_apps:
494+ app_records[app] = dao.get_records_for_app(app, device, channel)
495+ return app_records
496+
497+
498+def _plot_records_by_apps(records_by_apps, results_dir):
499+ app_plots = {}
500+
501+ for app in allowed_apps:
502+ (builds, cold_times, cold_stdevs, hot_times, hot_stdevs) = \
503+ data_processor.get_times_by_build(records_by_apps[app])
504+ app_plots[app] = plotter.plot_app_records(app, builds, cold_times,
505+ cold_stdevs, hot_times,
506+ hot_stdevs, results_dir)
507+ return app_plots
508+
509+
510+def _create_graphics_dir(results_dir):
511+ dir = os.path.join(results_dir, 'appstartup_data')
512+ if not os.path.exists(dir):
513+ os.mkdir(dir)
514+ return dir
515+
516+
517+def generate_reports(subunit_log, global_info, records_by_apps):
518+ if HTML_REPORT or JSON_REPORT:
519+ graphics_dir = _create_graphics_dir(os.path.dirname(subunit_log))
520+ pics = _plot_records_by_apps(records_by_apps, graphics_dir)
521+ if HTML_REPORT:
522+ report.create_html_report(pics, os.path.dirname(subunit_log),
523+ report_info=global_info)
524+ if JSON_REPORT:
525+ report.create_json_report(pics, os.path.dirname(subunit_log),
526+ report_info=global_info)
527+
528+
529+def main():
530+ args_parser = ArgumentParser("Parse app startup times from logs.")
531+ args_parser.add_argument("subunit_log")
532+ args = args_parser.parse_args()
533+ results_list = subunit_results.parse(args.subunit_log)
534+ global_info, startup_times = parser.parse_startup_times(results_list)
535+ save_results_if_needed(global_info, startup_times)
536+ records_by_apps = _collect_records_by_app(global_info)
537+ generate_reports(args.subunit_log, global_info, records_by_apps)
538+
539+if __name__ == "__main__":
540+ main()
541
542=== added file 'qakit/appstartup/parser.py'
543--- qakit/appstartup/parser.py 1970-01-01 00:00:00 +0000
544+++ qakit/appstartup/parser.py 2016-02-18 14:19:39 +0000
545@@ -0,0 +1,65 @@
546+#!/usr/bin/python3
547+# UESQA Metrics
548+# Copyright (C) 2016 Canonical
549+#
550+# This program is free software: you can redistribute it and/or modify
551+# it under the terms of the GNU General Public License as published by
552+# the Free Software Foundation, either version 3 of the License, or
553+# (at your option) any later version.
554+#
555+# This program is distributed in the hope that it will be useful,
556+# but WITHOUT ANY WARRANTY; without even the implied warranty of
557+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
558+# GNU General Public License for more details.
559+#
560+# You should have received a copy of the GNU General Public License
561+# along with this program. If not, see <http://www.gnu.org/licenses/>.
562+
563+import ast
564+
565+
566+def parse_startup_times(results):
567+ """ Retrieve the global run information and a list with all the history
568+ instances parsed from the results_list in the subunit test log
569+ """
570+ startup_times = []
571+ tests_logs = _get_results_test_logs(results)
572+ global_info = _get_global_information(tests_logs)
573+
574+ for test_log in tests_logs:
575+ times = _get_times(test_log)
576+ if global_info and times:
577+ startup_times.extend(times)
578+ return global_info, startup_times
579+
580+
581+def _get_global_information(tests_logs):
582+ for test_log in tests_logs:
583+ markers = [line for line in test_log if '<=' in line]
584+ if markers:
585+ values = markers[0].split('<=')[1].strip()
586+ return ast.literal_eval(values)
587+ raise RuntimeError('Global info not provided for tests')
588+
589+
590+def _get_results_test_logs(results):
591+ test_logs = []
592+ for result in results:
593+ test_log = result['details'].get('test-log')
594+ if test_log:
595+ test_logs.append(test_log.as_text().split('\n'))
596+ return test_logs
597+
598+
599+def _get_times(test_log):
600+ markers = [line for line in test_log if '=>' in line]
601+ times = []
602+
603+ iteration = 1
604+ for mark in markers:
605+ values = mark.split('=>')[1].strip()
606+ values_dict = ast.literal_eval(values)
607+ values_dict['iteration'] = iteration
608+ times.append(values_dict)
609+ iteration += 1
610+ return times
611
612=== added file 'qakit/appstartup/plotter.py'
613--- qakit/appstartup/plotter.py 1970-01-01 00:00:00 +0000
614+++ qakit/appstartup/plotter.py 2016-02-18 14:19:39 +0000
615@@ -0,0 +1,63 @@
616+#!/usr/bin/python3
617+# UESQA Metrics
618+# Copyright (C) 2016 Canonical
619+#
620+# This program is free software: you can redistribute it and/or modify
621+# it under the terms of the GNU General Public License as published by
622+# the Free Software Foundation, either version 3 of the License, or
623+# (at your option) any later version.
624+#
625+# This program is distributed in the hope that it will be useful,
626+# but WITHOUT ANY WARRANTY; without even the implied warranty of
627+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
628+# GNU General Public License for more details.
629+#
630+# You should have received a copy of the GNU General Public License
631+# along with this program. If not, see <http://www.gnu.org/licenses/>.
632+
633+import matplotlib.pyplot as plt
634+import numpy as np
635+import os
636+
637+
638+def plot_app_records(app, builds, cold_times, cold_stdevs, hot_times,
639+ hot_stdevs, plots_dir):
640+ """ Generate the graphic and return the graphic path.
641+ It is supposed the records are already ordered by build number.
642+ """
643+
644+ # Set descriptions
645+ fig, ax = plt.subplots()
646+ ax.set_title('Times for {} app'.format(app))
647+ ax.set_xlabel('Build Number')
648+ ax.set_ylabel('Time (seconds)')
649+
650+ # Config the graphic
651+ ind = np.arange(len(builds))
652+ width = 0.35
653+ ax.set_xticks(ind + width)
654+ ax.set_xticklabels(builds)
655+ ax.set_xmargin(0.1)
656+ ax.yaxis.grid(True)
657+
658+ rects_cold = ax.bar(ind, cold_times, width, color='#3366FF',
659+ yerr=cold_stdevs, ecolor='#FF1A00', capsize=5,
660+ error_kw={'elinewidth': 2})
661+ rects_hot = ax.bar(ind + width, hot_times, width, color='#FF1A00',
662+ yerr=hot_stdevs, ecolor='#3366FF', capsize=5,
663+ error_kw={'elinewidth': 2})
664+
665+# Shrink current axis's height by 10% on the bottom
666+ box = ax.get_position()
667+ ax.set_position([box.x0, box.y0 + box.height * 0.1,
668+ box.width, box.height * 0.9])
669+
670+ if rects_cold and rects_hot:
671+ ax.legend((rects_cold[0], rects_hot[0]), ('Cold Start', 'Hot Start'),
672+ loc='upper center', bbox_to_anchor=(0.85, -0.05),
673+ fancybox=True, shadow=True)
674+
675+ # Save the graphic
676+ dst_file = os.path.join(plots_dir, 'barchart_{}.png'.format(app))
677+ plt.savefig(dst_file)
678+ return dst_file
679
680=== added file 'qakit/appstartup/report.py'
681--- qakit/appstartup/report.py 1970-01-01 00:00:00 +0000
682+++ qakit/appstartup/report.py 2016-02-18 14:19:39 +0000
683@@ -0,0 +1,72 @@
684+#!/usr/bin/python3
685+# UESQA Metrics
686+# Copyright (C) 2016 Canonical
687+#
688+# This program is free software: you can redistribute it and/or modify
689+# it under the terms of the GNU General Public License as published by
690+# the Free Software Foundation, either version 3 of the License, or
691+# (at your option) any later version.
692+#
693+# This program is distributed in the hope that it will be useful,
694+# but WITHOUT ANY WARRANTY; without even the implied warranty of
695+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
696+# GNU General Public License for more details.
697+#
698+# You should have received a copy of the GNU General Public License
699+# along with this program. If not, see <http://www.gnu.org/licenses/>.
700+
701+import json
702+import os
703+
704+html_header = '<html><head><title>{}</title></head><body><div align="center"><table>' # NOQA
705+html_footer = '</table></div></body></html>'
706+html_picture = '<tr><td><br/><br/><font size="7" color="#8B005A"><center>{}</center></font><br/><img src="{}"/></td></tr>' # NOQA
707+html_logo = '<img src="http://design.ubuntu.com/wp-content/uploads/canonical-logo1.png">' # NOQA
708+html_title = '<br/><br/><div><font size="7" color="#8B005A">Apps StartUp Execution Report</font></div>' # NOQA
709+html_info_title_header = '<br/><br/><div><font size="6" color="#8B005A">General information</font>' # NOQA
710+html_info_title_footer = '</div>'
711+html_info = '<div><br/><font size="4">{}: {}</font></div>'
712+
713+
714+def create_html_report(out_files, report_dir,
715+ report_file="app_startup_report.html",
716+ report_info=None):
717+ report_path = os.path.join(report_dir, report_file)
718+
719+ with open(report_path, "w") as report:
720+ # write out the html header
721+ report.write(html_header.format('App startup times report'))
722+ report.write(html_logo)
723+
724+ report.write(html_title)
725+ report.write(html_info_title_header)
726+ for info in sorted(report_info.keys()):
727+ report.write(html_info.format(info.replace('_', ' ').title(),
728+ report_info[info]))
729+ report.write(html_info_title_footer)
730+
731+ for app, path in out_files.items():
732+ report.write(html_picture.format(app.title(), path))
733+
734+ report.write(html_footer)
735+
736+ return report_path
737+
738+
739+def create_json_report(out_files, report_dir,
740+ report_file="app_startup_report.json",
741+ report_info=None):
742+
743+ report_path = os.path.join(report_dir, report_file)
744+ data = dict()
745+ data['channel'] = report_info['channel']
746+ data['device'] = report_info['device']
747+ data['build_number'] = report_info['build_number']
748+
749+ charts = []
750+ for app, path in out_files.items():
751+ charts.append({"title": app.title(), "path": path})
752+ data['charts'] = charts
753+
754+ with open(report_path, "w") as report:
755+ report.write(json.dumps(data, ensure_ascii=False))
756
757=== modified file 'qakit/config.py'
758--- qakit/config.py 2014-10-08 01:30:28 +0000
759+++ qakit/config.py 2016-02-18 14:19:39 +0000
760@@ -21,5 +21,7 @@
761
762
763 def get_config_file_location():
764- """Returns the full, absolute path to the config file location."""
765- return os.path.expanduser('~/.config/qakit.ini')
766+ """ Return the full, absolute path to the config file location. When the
767+ QAKIT_CONFIG env variable is defined, it is be used, otherwise it is
768+ used the default config file in .config dir"""
769+ return os.getenv('QAKIT_CONFIG', os.path.expanduser('~/.config/qakit.ini'))
770
771=== modified file 'setup.py'
772--- setup.py 2015-11-25 20:51:47 +0000
773+++ setup.py 2016-02-18 14:19:39 +0000
774@@ -38,6 +38,8 @@
775 'console_scripts': [
776 ('subunit-practitest-export = '
777 'qakit.practitest.report_subunit_results_to_practitest:main'),
778+ ('parse-app-startup = '
779+ 'qakit.appstartup.parse_startup_times:main'),
780 ]
781 }
782 )

Subscribers

People subscribed via source and target branches

to all changes: