Merge lp:~canonical-platform-qa/qakit/app_startup_poc into lp:qakit
- app_startup_poc
- Merge into trunk
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 |
Related bugs: |
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://
Run eve by doing python qakit/appstartu
Download and install mongodb (https:/
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.
2- Make sure the os allow to route to localnet by doing:
sudo sysctl -w net.ipv4.
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_
Then, having a test-results.
The html report will be generated in ~/Desktop/
Description of the change
The eve service is currently available on:
http://
Jenkins job available on:
- 48. By Sergio Cazzolato
-
Merge with trunk and conflicts resolved
- 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
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/
What does dao mean?
More comments tomorrow, just wanted to get started ;) .
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
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.
Preview Diff
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 | ) |
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: bazaar. launchpad. net/~canonical- platform- qa/qakit/ trunk/view/ head:/README
http://