summaryrefslogtreecommitdiffstats
path: root/scripts/pybootchartgui
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/pybootchartgui')
-rw-r--r--scripts/pybootchartgui/AUTHORS11
-rw-r--r--scripts/pybootchartgui/COPYING340
-rw-r--r--scripts/pybootchartgui/MAINTAINERS3
-rw-r--r--scripts/pybootchartgui/NEWS204
-rw-r--r--scripts/pybootchartgui/README.pybootchart37
-rwxr-xr-xscripts/pybootchartgui/pybootchartgui.py23
-rw-r--r--scripts/pybootchartgui/pybootchartgui/__init__.py0
-rw-r--r--scripts/pybootchartgui/pybootchartgui/batch.py46
-rw-r--r--scripts/pybootchartgui/pybootchartgui/draw.py894
-rw-r--r--scripts/pybootchartgui/pybootchartgui/gui.py350
l---------scripts/pybootchartgui/pybootchartgui/main.py1
-rw-r--r--scripts/pybootchartgui/pybootchartgui/main.py.in187
-rw-r--r--scripts/pybootchartgui/pybootchartgui/parsing.py740
-rw-r--r--scripts/pybootchartgui/pybootchartgui/process_tree.py292
-rw-r--r--scripts/pybootchartgui/pybootchartgui/samples.py151
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/parser_test.py105
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py92
17 files changed, 3476 insertions, 0 deletions
diff --git a/scripts/pybootchartgui/AUTHORS b/scripts/pybootchartgui/AUTHORS
new file mode 100644
index 0000000000..672b7e9520
--- /dev/null
+++ b/scripts/pybootchartgui/AUTHORS
@@ -0,0 +1,11 @@
1Michael Meeks <michael.meeks@novell.com>
2Anders Norgaard <anders.norgaard@gmail.com>
3Scott James Remnant <scott@ubuntu.com>
4Henning Niss <henningniss@gmail.com>
5Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
6
7Contributors:
8 Brian Ewins
9
10Based on work by:
11 Ziga Mahkovec
diff --git a/scripts/pybootchartgui/COPYING b/scripts/pybootchartgui/COPYING
new file mode 100644
index 0000000000..ed87acf948
--- /dev/null
+++ b/scripts/pybootchartgui/COPYING
@@ -0,0 +1,340 @@
1 GNU GENERAL PUBLIC LICENSE
2 Version 2, June 1991
3
4 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
6 Everyone is permitted to copy and distribute verbatim copies
7 of this license document, but changing it is not allowed.
8
9 Preamble
10
11 The licenses for most software are designed to take away your
12freedom to share and change it. By contrast, the GNU General Public
13License is intended to guarantee your freedom to share and change free
14software--to make sure the software is free for all its users. This
15General Public License applies to most of the Free Software
16Foundation's software and to any other program whose authors commit to
17using it. (Some other Free Software Foundation software is covered by
18the GNU Library General Public License instead.) You can apply it to
19your programs, too.
20
21 When we speak of free software, we are referring to freedom, not
22price. Our General Public Licenses are designed to make sure that you
23have the freedom to distribute copies of free software (and charge for
24this service if you wish), that you receive source code or can get it
25if you want it, that you can change the software or use pieces of it
26in new free programs; and that you know you can do these things.
27
28 To protect your rights, we need to make restrictions that forbid
29anyone to deny you these rights or to ask you to surrender the rights.
30These restrictions translate to certain responsibilities for you if you
31distribute copies of the software, or if you modify it.
32
33 For example, if you distribute copies of such a program, whether
34gratis or for a fee, you must give the recipients all the rights that
35you have. You must make sure that they, too, receive or can get the
36source code. And you must show them these terms so they know their
37rights.
38
39 We protect your rights with two steps: (1) copyright the software, and
40(2) offer you this license which gives you legal permission to copy,
41distribute and/or modify the software.
42
43 Also, for each author's protection and ours, we want to make certain
44that everyone understands that there is no warranty for this free
45software. If the software is modified by someone else and passed on, we
46want its recipients to know that what they have is not the original, so
47that any problems introduced by others will not reflect on the original
48authors' reputations.
49
50 Finally, any free program is threatened constantly by software
51patents. We wish to avoid the danger that redistributors of a free
52program will individually obtain patent licenses, in effect making the
53program proprietary. To prevent this, we have made it clear that any
54patent must be licensed for everyone's free use or not licensed at all.
55
56 The precise terms and conditions for copying, distribution and
57modification follow.
58
59 GNU GENERAL PUBLIC LICENSE
60 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
62 0. This License applies to any program or other work which contains
63a notice placed by the copyright holder saying it may be distributed
64under the terms of this General Public License. The "Program", below,
65refers to any such program or work, and a "work based on the Program"
66means either the Program or any derivative work under copyright law:
67that is to say, a work containing the Program or a portion of it,
68either verbatim or with modifications and/or translated into another
69language. (Hereinafter, translation is included without limitation in
70the term "modification".) Each licensee is addressed as "you".
71
72Activities other than copying, distribution and modification are not
73covered by this License; they are outside its scope. The act of
74running the Program is not restricted, and the output from the Program
75is covered only if its contents constitute a work based on the
76Program (independent of having been made by running the Program).
77Whether that is true depends on what the Program does.
78
79 1. You may copy and distribute verbatim copies of the Program's
80source code as you receive it, in any medium, provided that you
81conspicuously and appropriately publish on each copy an appropriate
82copyright notice and disclaimer of warranty; keep intact all the
83notices that refer to this License and to the absence of any warranty;
84and give any other recipients of the Program a copy of this License
85along with the Program.
86
87You may charge a fee for the physical act of transferring a copy, and
88you may at your option offer warranty protection in exchange for a fee.
89
90 2. You may modify your copy or copies of the Program or any portion
91of it, thus forming a work based on the Program, and copy and
92distribute such modifications or work under the terms of Section 1
93above, provided that you also meet all of these conditions:
94
95 a) You must cause the modified files to carry prominent notices
96 stating that you changed the files and the date of any change.
97
98 b) You must cause any work that you distribute or publish, that in
99 whole or in part contains or is derived from the Program or any
100 part thereof, to be licensed as a whole at no charge to all third
101 parties under the terms of this License.
102
103 c) If the modified program normally reads commands interactively
104 when run, you must cause it, when started running for such
105 interactive use in the most ordinary way, to print or display an
106 announcement including an appropriate copyright notice and a
107 notice that there is no warranty (or else, saying that you provide
108 a warranty) and that users may redistribute the program under
109 these conditions, and telling the user how to view a copy of this
110 License. (Exception: if the Program itself is interactive but
111 does not normally print such an announcement, your work based on
112 the Program is not required to print an announcement.)
113
114These requirements apply to the modified work as a whole. If
115identifiable sections of that work are not derived from the Program,
116and can be reasonably considered independent and separate works in
117themselves, then this License, and its terms, do not apply to those
118sections when you distribute them as separate works. But when you
119distribute the same sections as part of a whole which is a work based
120on the Program, the distribution of the whole must be on the terms of
121this License, whose permissions for other licensees extend to the
122entire whole, and thus to each and every part regardless of who wrote it.
123
124Thus, it is not the intent of this section to claim rights or contest
125your rights to work written entirely by you; rather, the intent is to
126exercise the right to control the distribution of derivative or
127collective works based on the Program.
128
129In addition, mere aggregation of another work not based on the Program
130with the Program (or with a work based on the Program) on a volume of
131a storage or distribution medium does not bring the other work under
132the scope of this License.
133
134 3. You may copy and distribute the Program (or a work based on it,
135under Section 2) in object code or executable form under the terms of
136Sections 1 and 2 above provided that you also do one of the following:
137
138 a) Accompany it with the complete corresponding machine-readable
139 source code, which must be distributed under the terms of Sections
140 1 and 2 above on a medium customarily used for software interchange; or,
141
142 b) Accompany it with a written offer, valid for at least three
143 years, to give any third party, for a charge no more than your
144 cost of physically performing source distribution, a complete
145 machine-readable copy of the corresponding source code, to be
146 distributed under the terms of Sections 1 and 2 above on a medium
147 customarily used for software interchange; or,
148
149 c) Accompany it with the information you received as to the offer
150 to distribute corresponding source code. (This alternative is
151 allowed only for noncommercial distribution and only if you
152 received the program in object code or executable form with such
153 an offer, in accord with Subsection b above.)
154
155The source code for a work means the preferred form of the work for
156making modifications to it. For an executable work, complete source
157code means all the source code for all modules it contains, plus any
158associated interface definition files, plus the scripts used to
159control compilation and installation of the executable. However, as a
160special exception, the source code distributed need not include
161anything that is normally distributed (in either source or binary
162form) with the major components (compiler, kernel, and so on) of the
163operating system on which the executable runs, unless that component
164itself accompanies the executable.
165
166If distribution of executable or object code is made by offering
167access to copy from a designated place, then offering equivalent
168access to copy the source code from the same place counts as
169distribution of the source code, even though third parties are not
170compelled to copy the source along with the object code.
171
172 4. You may not copy, modify, sublicense, or distribute the Program
173except as expressly provided under this License. Any attempt
174otherwise to copy, modify, sublicense or distribute the Program is
175void, and will automatically terminate your rights under this License.
176However, parties who have received copies, or rights, from you under
177this License will not have their licenses terminated so long as such
178parties remain in full compliance.
179
180 5. You are not required to accept this License, since you have not
181signed it. However, nothing else grants you permission to modify or
182distribute the Program or its derivative works. These actions are
183prohibited by law if you do not accept this License. Therefore, by
184modifying or distributing the Program (or any work based on the
185Program), you indicate your acceptance of this License to do so, and
186all its terms and conditions for copying, distributing or modifying
187the Program or works based on it.
188
189 6. Each time you redistribute the Program (or any work based on the
190Program), the recipient automatically receives a license from the
191original licensor to copy, distribute or modify the Program subject to
192these terms and conditions. You may not impose any further
193restrictions on the recipients' exercise of the rights granted herein.
194You are not responsible for enforcing compliance by third parties to
195this License.
196
197 7. If, as a consequence of a court judgment or allegation of patent
198infringement or for any other reason (not limited to patent issues),
199conditions are imposed on you (whether by court order, agreement or
200otherwise) that contradict the conditions of this License, they do not
201excuse you from the conditions of this License. If you cannot
202distribute so as to satisfy simultaneously your obligations under this
203License and any other pertinent obligations, then as a consequence you
204may not distribute the Program at all. For example, if a patent
205license would not permit royalty-free redistribution of the Program by
206all those who receive copies directly or indirectly through you, then
207the only way you could satisfy both it and this License would be to
208refrain entirely from distribution of the Program.
209
210If any portion of this section is held invalid or unenforceable under
211any particular circumstance, the balance of the section is intended to
212apply and the section as a whole is intended to apply in other
213circumstances.
214
215It is not the purpose of this section to induce you to infringe any
216patents or other property right claims or to contest validity of any
217such claims; this section has the sole purpose of protecting the
218integrity of the free software distribution system, which is
219implemented by public license practices. Many people have made
220generous contributions to the wide range of software distributed
221through that system in reliance on consistent application of that
222system; it is up to the author/donor to decide if he or she is willing
223to distribute software through any other system and a licensee cannot
224impose that choice.
225
226This section is intended to make thoroughly clear what is believed to
227be a consequence of the rest of this License.
228
229 8. If the distribution and/or use of the Program is restricted in
230certain countries either by patents or by copyrighted interfaces, the
231original copyright holder who places the Program under this License
232may add an explicit geographical distribution limitation excluding
233those countries, so that distribution is permitted only in or among
234countries not thus excluded. In such case, this License incorporates
235the limitation as if written in the body of this License.
236
237 9. The Free Software Foundation may publish revised and/or new versions
238of the General Public License from time to time. Such new versions will
239be similar in spirit to the present version, but may differ in detail to
240address new problems or concerns.
241
242Each version is given a distinguishing version number. If the Program
243specifies a version number of this License which applies to it and "any
244later version", you have the option of following the terms and conditions
245either of that version or of any later version published by the Free
246Software Foundation. If the Program does not specify a version number of
247this License, you may choose any version ever published by the Free Software
248Foundation.
249
250 10. If you wish to incorporate parts of the Program into other free
251programs whose distribution conditions are different, write to the author
252to ask for permission. For software which is copyrighted by the Free
253Software Foundation, write to the Free Software Foundation; we sometimes
254make exceptions for this. Our decision will be guided by the two goals
255of preserving the free status of all derivatives of our free software and
256of promoting the sharing and reuse of software generally.
257
258 NO WARRANTY
259
260 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268REPAIR OR CORRECTION.
269
270 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278POSSIBILITY OF SUCH DAMAGES.
279
280 END OF TERMS AND CONDITIONS
281
282 How to Apply These Terms to Your New Programs
283
284 If you develop a new program, and you want it to be of the greatest
285possible use to the public, the best way to achieve this is to make it
286free software which everyone can redistribute and change under these terms.
287
288 To do so, attach the following notices to the program. It is safest
289to attach them to the start of each source file to most effectively
290convey the exclusion of warranty; and each file should have at least
291the "copyright" line and a pointer to where the full notice is found.
292
293 <one line to give the program's name and a brief idea of what it does.>
294 Copyright (C) <year> <name of author>
295
296 This program is free software; you can redistribute it and/or modify
297 it under the terms of the GNU General Public License as published by
298 the Free Software Foundation; either version 2 of the License, or
299 (at your option) any later version.
300
301 This program is distributed in the hope that it will be useful,
302 but WITHOUT ANY WARRANTY; without even the implied warranty of
303 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 GNU General Public License for more details.
305
306 You should have received a copy of the GNU General Public License
307 along with this program; if not, write to the Free Software
308 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
309
310
311Also add information on how to contact you by electronic and paper mail.
312
313If the program is interactive, make it output a short notice like this
314when it starts in an interactive mode:
315
316 Gnomovision version 69, Copyright (C) year name of author
317 Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
318 This is free software, and you are welcome to redistribute it
319 under certain conditions; type `show c' for details.
320
321The hypothetical commands `show w' and `show c' should show the appropriate
322parts of the General Public License. Of course, the commands you use may
323be called something other than `show w' and `show c'; they could even be
324mouse-clicks or menu items--whatever suits your program.
325
326You should also get your employer (if you work as a programmer) or your
327school, if any, to sign a "copyright disclaimer" for the program, if
328necessary. Here is a sample; alter the names:
329
330 Yoyodyne, Inc., hereby disclaims all copyright interest in the program
331 `Gnomovision' (which makes passes at compilers) written by James Hacker.
332
333 <signature of Ty Coon>, 1 April 1989
334 Ty Coon, President of Vice
335
336This General Public License does not permit incorporating your program into
337proprietary programs. If your program is a subroutine library, you may
338consider it more useful to permit linking proprietary applications with the
339library. If this is what you want to do, use the GNU Library General
340Public License instead of this License.
diff --git a/scripts/pybootchartgui/MAINTAINERS b/scripts/pybootchartgui/MAINTAINERS
new file mode 100644
index 0000000000..c65e1315f1
--- /dev/null
+++ b/scripts/pybootchartgui/MAINTAINERS
@@ -0,0 +1,3 @@
1Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
2Michael Meeks <michael.meeks@novell.com>
3Harald Hoyer <harald@redhat.com>
diff --git a/scripts/pybootchartgui/NEWS b/scripts/pybootchartgui/NEWS
new file mode 100644
index 0000000000..7c5b2fc3a1
--- /dev/null
+++ b/scripts/pybootchartgui/NEWS
@@ -0,0 +1,204 @@
1bootchart2 0.14.5:
2 + pybootchartgui (Riccardo)
3 + Fix tests with python3
4 + Fix parsing of files with non-ascii bytes
5 + Robustness fixes to taskstats and meminfo parsing
6 + More python3 fixes
7
8bootchart2 0.14.4:
9 + bootchartd
10 + Add relevant EXIT_PROC for GNOME3, XFCE4, openbox
11 (Justin Lecher, Ben Eills)
12 + pybootchartgui (Riccardo)
13 + Fix some issues in --crop-after and --annotate
14 + Fix pybootchartgui process_tree tests
15 + More python3 fixes
16
17bootchart2 0.14.2:
18 + pybootchartgui
19 + Fix some crashes in parsing.py (Jakub Czaplicki, Riccardo)
20 + speedup a bit meminfo parsing (Riccardo)
21 + Fix indentation for python3.2 (Riccardo)
22
23bootchart2 0.14.1:
24 + bootchartd
25 + Expect dmesg only if started as init (Henry Yei)
26 + look for bootchart_init in the environment (Henry Gebhardt)
27 + pybootchartgui
28 + Fixup some tests (Riccardo)
29 + Support hp smart arrays block devices (Anders Norgaard,
30 Brian Murray)
31 + Fixes for -t, -o and -f options (Mladen Kuntner, Harald, Riccardo)
32
33bootchart2 0.14.0:
34 + bootchartd
35 + Add ability to define custom commands
36 (Lucian Muresan, Peter Hjalmarsson)
37 + collector
38 + fix tmpfs mount leakage (Peter Hjalmarsson)
39 + pybootchartgui
40 + render cumulative I/O time chart (Sankar P)
41 + python3 compatibility fixes (Riccardo)
42 + Misc (Michael)
43 + remove confusing, obsolete setup.py
44 + install docs to /usr/share/
45 + lot of fixes for easier packaging (Peter Hjalmarsson)
46 + add bootchart2, bootchartd and pybootchartgui manpages
47 (Francesca Ciceri, David Paleino)
48
49bootchart2 0.12.6:
50 + bootchartd
51 + better check for initrd (Riccardo Magliocchetti)
52 + code cleanup (Riccardo)
53 + make the list of processes we are waiting for editable
54 in config file by EXIT_PROC (Riccardo)
55 + fix parsing of cmdline for alternative init system (Riccardo)
56 + fixed calling init in initramfs (Harald)
57 + exit 0 for start, if the collector is already running (Harald)
58 + collector
59 + try harder with taskstats (Michael)
60 + plug some small leaks (Riccardo)
61 + fix missing PROC_EVENTS detection (Harald)
62 + pybootchartgui (Michael)
63 + add kernel bootchart tab to interactive gui
64 + report bootchart version in cli interface
65 + improve rendering performance
66 + GUI improvements
67 + lot of cleanups
68 + Makefile
69 + do not python compile if NO_PYTHON_COMPILE is set (Harald)
70 + systemd service files
71 + added them and install (Harald, Wulf C. Krueger)
72
73bootchart2 0.12.5:
74 + administrative snafu version; pull before pushing...
75
76bootchart2 0.12.4:
77 + bootchartd
78 + reduce overhead caused by pidof (Riccardo Magliocchetti)
79 + collector
80 + attempt to retry ptrace to avoid bogus ENOSYS (Michael)
81 + add meminfo polling (Dave Martin)
82 + pybootchartgui
83 + handle dmesg timestamps with big delta (Riccardo)
84 + avoid divide by zero when rendering I/O utilization (Riccardo)
85 + add process grouping in the cumulative chart (Riccardo)
86 + fix cpu time calculation in cumulative chart (Riccardo)
87 + get i/o statistics for flash based devices (Riccardo)
88 + prettier coloring for the cumulative graphs (Michael)
89 + fix interactive CPU rendering (Michael)
90 + render memory usage graph (Dave Martin)
91
92bootchart2 0.12.3
93 + collector
94 + pclose after popen (Riccardo Magliocchetti (xrmx))
95 + fix buffer overflow (xrmx)
96 + count 'processor:' in /proc/cpuinfo for ARM (Michael)
97 + get model name from that line too for ARM (xrmx)
98 + store /proc/cpuinfo in the boot-chart archive (xrmx)
99 + try harder to detect missing TASKSTATS (Michael)
100 + sanity-check invalid domain names (Michael)
101 + detect missing PROC_EVENTS more reliably (Michael)
102 + README fixes (xrmx, Michael)
103 + pybootchartgui
104 + make num_cpu parsing robust (Michael)
105
106bootchart2 0.12.2
107 + fix pthread compile / linking bug
108
109bootchart2 0.12.1
110 + pybootchartgui
111 + pylint cleanup
112 + handle empty traces more elegantly
113 + add '-t' / '--boot-time' argument (Matthew Bauer)
114 + collector
115 + now GPLv2
116 + add rdinit support for very early initrd tracing
117 + cleanup / re-factor code into separate modules
118 + re-factor arg parsing, and parse remote process args
119 + handle missing bootchartd.conf cleanly
120 + move much of bootchartd from shell -> C
121 + drop dmesg and uname usage
122 + avoid rpm/dpkg with native version reporting
123
124bootchart2 0.12.0 (Michael Meeks)
125 + collector
126 + use netlink PROC_EVENTS to generate parentage data
127 + finally kills any need for 'acct' et. al.
128 + also removes need to poll /proc => faster
129 + cleanup code to K&R, 8 stop tabs.
130 + pybootchartgui
131 + consume thread parentage data
132
133bootchart2 0.11.4 (Michael Meeks)
134 + collector
135 + if run inside an initrd detect when /dev is writable
136 and remount ourselves into that.
137 + overflow buffers more elegantly in extremis
138 + dump full process path and command-line args
139 + calm down debugging output
140 + pybootchartgui
141 + can render logs in a directory again
142 + has a 'show more' option to show command-lines
143
144bootchart2 0.11.3 (Michael Meeks)
145 + add $$ display to the bootchart header
146 + process command-line bits
147 + fix collection code, and rename stream to match
148 + enable parsing, add check button to UI, and --show-all
149 command-line option
150 + fix parsing of directories full of files.
151
152bootchart2 0.11.2 (Michael Meeks)
153 + fix initrd sanity check to use the right proc path
154 + don't return a bogus error value when dumping state
155 + add -c to aid manual console debugging
156
157bootchart2 0.11.1 (Michael Meeks)
158 + even simpler initrd setup
159 + create a single directory: /lib/bootchart/tmpfs
160
161bootchart2 0.11 (Michael Meeks)
162 + bootchartd
163 + far, far simpler, less shell, more robustness etc.
164 + bootchart-collector
165 + remove the -p argument - we always mount proc
166 + requires /lib/bootchart (make install-chroot) to
167 be present (also in the initrd) [ with a kmsg
168 node included ]
169 + add a --probe-running mode
170 + ptrace re-write
171 + gives -much- better early-boot-time resolution
172 + unconditional chroot /lib/bootchart/chroot
173 + we mount proc there ourselves
174 + log extraction requires no common file-system view
175
176
177bootchart2 0.10.1 (Kel Modderman)
178 + collector arg -m should mount /proc
179 + remove bogus vcsid code
180 + split collector install in Makefile
181 + remove bogus debug code
182 + accept process names containing spaces
183
184bootchart2 0.10.0
185 + rendering (Anders Norgaard)
186 + fix for unknown exceptions
187 + interactive UI (Michael)
188 + much faster rendering by manual clipping
189 + horizontal scaling
190 + remove annoying page-up/down bindings
191 + initrd portability & fixes (Federic Crozat)
192 + port to Mandriva
193 + improved process waiting
194 + inittab commenting fix
195 + improved initrd detection / jail tagging
196 + fix for un-detectable accton behaviour change
197 + implement a built-in usleep to help initrd deps (Michael)
198
199bootchart2 0.0.9
200 + fix initrd bug
201
202bootchart2 0.0.8
203 + add a filename string to the window title in interactive mode
204 + add a NEWS file
diff --git a/scripts/pybootchartgui/README.pybootchart b/scripts/pybootchartgui/README.pybootchart
new file mode 100644
index 0000000000..8642e64679
--- /dev/null
+++ b/scripts/pybootchartgui/README.pybootchart
@@ -0,0 +1,37 @@
1 PYBOOTCHARTGUI
2 ----------------
3
4pybootchartgui is a tool (now included as part of bootchart2) for
5visualization and analysis of the GNU/Linux boot process. It renders
6the output of the boot-logger tool bootchart (see
7http://www.bootchart.org/) to either the screen or files of various
8formats. Bootchart collects information about the processes, their
9dependencies, and resource consumption during boot of a GNU/Linux
10system. The pybootchartgui tools visualizes the process tree and
11overall resource utilization.
12
13pybootchartgui is a port of the visualization part of bootchart from
14Java to Python and Cairo.
15
16Adapted from the bootchart-documentation:
17
18 The CPU and disk statistics are used to render stacked area and line
19 charts. The process information is used to create a Gantt chart
20 showing process dependency, states and CPU usage.
21
22 A typical boot sequence consists of several hundred processes. Since
23 it is difficult to visualize such amount of data in a comprehensible
24 way, tree pruning is utilized. Idle background processes and
25 short-lived processes are removed. Similar processes running in
26 parallel are also merged together.
27
28 Finally, the performance and dependency charts are rendered as a
29 single image to either the screen or in PNG, PDF or SVG format.
30
31
32To get help for pybootchartgui, run
33
34$ pybootchartgui --help
35
36This code was originally hosted at:
37 http://code.google.com/p/pybootchartgui/
diff --git a/scripts/pybootchartgui/pybootchartgui.py b/scripts/pybootchartgui/pybootchartgui.py
new file mode 100755
index 0000000000..7ce1a5be40
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui.py
@@ -0,0 +1,23 @@
1#!/usr/bin/env python
2#
3# This file is part of pybootchartgui.
4
5# pybootchartgui is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9
10# pybootchartgui is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14
15# You should have received a copy of the GNU General Public License
16# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
17
18
19import sys
20from pybootchartgui.main import main
21
22if __name__ == '__main__':
23 sys.exit(main())
diff --git a/scripts/pybootchartgui/pybootchartgui/__init__.py b/scripts/pybootchartgui/pybootchartgui/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/__init__.py
diff --git a/scripts/pybootchartgui/pybootchartgui/batch.py b/scripts/pybootchartgui/pybootchartgui/batch.py
new file mode 100644
index 0000000000..05c714e95e
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/batch.py
@@ -0,0 +1,46 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16import cairo
17from . import draw
18from .draw import RenderOptions
19
20def render(writer, trace, app_options, filename):
21 handlers = {
22 "png": (lambda w, h: cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h), \
23 lambda sfc: sfc.write_to_png(filename)),
24 "pdf": (lambda w, h: cairo.PDFSurface(filename, w, h), lambda sfc: 0),
25 "svg": (lambda w, h: cairo.SVGSurface(filename, w, h), lambda sfc: 0)
26 }
27
28 if app_options.format is None:
29 fmt = filename.rsplit('.', 1)[1]
30 else:
31 fmt = app_options.format
32
33 if not (fmt in handlers):
34 writer.error ("Unknown format '%s'." % fmt)
35 return 10
36
37 make_surface, write_surface = handlers[fmt]
38 options = RenderOptions (app_options)
39 (w, h) = draw.extents (options, 1.0, trace)
40 w = max (w, draw.MIN_IMG_W)
41 surface = make_surface (w, h)
42 ctx = cairo.Context (surface)
43 draw.render (ctx, options, 1.0, trace)
44 write_surface (surface)
45 writer.status ("bootchart written to '%s'" % filename)
46
diff --git a/scripts/pybootchartgui/pybootchartgui/draw.py b/scripts/pybootchartgui/pybootchartgui/draw.py
new file mode 100644
index 0000000000..8c574be50c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/draw.py
@@ -0,0 +1,894 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17import cairo
18import math
19import re
20import random
21import colorsys
22from operator import itemgetter
23
24class RenderOptions:
25
26 def __init__(self, app_options):
27 # should we render a cumulative CPU time chart
28 self.cumulative = True
29 self.charts = True
30 self.kernel_only = False
31 self.app_options = app_options
32
33 def proc_tree (self, trace):
34 if self.kernel_only:
35 return trace.kernel_tree
36 else:
37 return trace.proc_tree
38
39# Process tree background color.
40BACK_COLOR = (1.0, 1.0, 1.0, 1.0)
41
42WHITE = (1.0, 1.0, 1.0, 1.0)
43# Process tree border color.
44BORDER_COLOR = (0.63, 0.63, 0.63, 1.0)
45# Second tick line color.
46TICK_COLOR = (0.92, 0.92, 0.92, 1.0)
47# 5-second tick line color.
48TICK_COLOR_BOLD = (0.86, 0.86, 0.86, 1.0)
49# Annotation colour
50ANNOTATION_COLOR = (0.63, 0.0, 0.0, 0.5)
51# Text color.
52TEXT_COLOR = (0.0, 0.0, 0.0, 1.0)
53
54# Font family
55FONT_NAME = "Bitstream Vera Sans"
56# Title text font.
57TITLE_FONT_SIZE = 18
58# Default text font.
59TEXT_FONT_SIZE = 12
60# Axis label font.
61AXIS_FONT_SIZE = 11
62# Legend font.
63LEGEND_FONT_SIZE = 12
64
65# CPU load chart color.
66CPU_COLOR = (0.40, 0.55, 0.70, 1.0)
67# IO wait chart color.
68IO_COLOR = (0.76, 0.48, 0.48, 0.5)
69# Disk throughput color.
70DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0)
71# CPU load chart color.
72FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0)
73# Mem cached color
74MEM_CACHED_COLOR = CPU_COLOR
75# Mem used color
76MEM_USED_COLOR = IO_COLOR
77# Buffers color
78MEM_BUFFERS_COLOR = (0.4, 0.4, 0.4, 0.3)
79# Swap color
80MEM_SWAP_COLOR = DISK_TPUT_COLOR
81
82# Process border color.
83PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0)
84# Waiting process color.
85PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5)
86# Running process color.
87PROC_COLOR_R = CPU_COLOR
88# Sleeping process color.
89PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0)
90# Stopped process color.
91PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0)
92# Zombie process color.
93PROC_COLOR_Z = (0.71, 0.71, 0.71, 1.0)
94# Dead process color.
95PROC_COLOR_X = (0.71, 0.71, 0.71, 0.125)
96# Paging process color.
97PROC_COLOR_W = (0.71, 0.71, 0.71, 0.125)
98
99# Process label color.
100PROC_TEXT_COLOR = (0.19, 0.19, 0.19, 1.0)
101# Process label font.
102PROC_TEXT_FONT_SIZE = 12
103
104# Signature color.
105SIG_COLOR = (0.0, 0.0, 0.0, 0.3125)
106# Signature font.
107SIG_FONT_SIZE = 14
108# Signature text.
109SIGNATURE = "http://github.com/mmeeks/bootchart"
110
111# Process dependency line color.
112DEP_COLOR = (0.75, 0.75, 0.75, 1.0)
113# Process dependency line stroke.
114DEP_STROKE = 1.0
115
116# Process description date format.
117DESC_TIME_FORMAT = "mm:ss.SSS"
118
119# Cumulative coloring bits
120HSV_MAX_MOD = 31
121HSV_STEP = 7
122
123# Configure task color
124TASK_COLOR_CONFIGURE = (1.0, 1.0, 0.00, 1.0)
125# Compile task color.
126TASK_COLOR_COMPILE = (0.0, 1.00, 0.00, 1.0)
127# Install task color
128TASK_COLOR_INSTALL = (1.0, 0.00, 1.00, 1.0)
129# Sysroot task color
130TASK_COLOR_SYSROOT = (0.0, 0.00, 1.00, 1.0)
131# Package task color
132TASK_COLOR_PACKAGE = (0.0, 1.00, 1.00, 1.0)
133# Package Write RPM/DEB/IPK task color
134TASK_COLOR_PACKAGE_WRITE = (0.0, 0.50, 0.50, 1.0)
135
136# Process states
137STATE_UNDEFINED = 0
138STATE_RUNNING = 1
139STATE_SLEEPING = 2
140STATE_WAITING = 3
141STATE_STOPPED = 4
142STATE_ZOMBIE = 5
143
144STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, \
145 PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W]
146
147# CumulativeStats Types
148STAT_TYPE_CPU = 0
149STAT_TYPE_IO = 1
150
151# Convert ps process state to an int
152def get_proc_state(flag):
153 return "RSDTZXW".find(flag) + 1
154
155def draw_text(ctx, text, color, x, y):
156 ctx.set_source_rgba(*color)
157 ctx.move_to(x, y)
158 ctx.show_text(text)
159
160def draw_fill_rect(ctx, color, rect):
161 ctx.set_source_rgba(*color)
162 ctx.rectangle(*rect)
163 ctx.fill()
164
165def draw_rect(ctx, color, rect):
166 ctx.set_source_rgba(*color)
167 ctx.rectangle(*rect)
168 ctx.stroke()
169
170def draw_legend_box(ctx, label, fill_color, x, y, s):
171 draw_fill_rect(ctx, fill_color, (x, y - s, s, s))
172 draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s))
173 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
174
175def draw_legend_line(ctx, label, fill_color, x, y, s):
176 draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3))
177 ctx.arc(x + (s + 1)/2.0, y - (s - 3)/2.0, 2.5, 0, 2.0 * math.pi)
178 ctx.fill()
179 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
180
181def draw_label_in_box(ctx, color, label, x, y, w, maxx):
182 label_w = ctx.text_extents(label)[2]
183 label_x = x + w / 2 - label_w / 2
184 if label_w + 10 > w:
185 label_x = x + w + 5
186 if label_x + label_w > maxx:
187 label_x = x - label_w - 5
188 draw_text(ctx, label, color, label_x, y)
189
190def draw_sec_labels(ctx, options, rect, sec_w, nsecs):
191 ctx.set_font_size(AXIS_FONT_SIZE)
192 prev_x = 0
193 for i in range(0, rect[2] + 1, sec_w):
194 if ((i / sec_w) % nsecs == 0) :
195 if options.app_options.as_minutes :
196 label = "%.1f" % (i / sec_w / 60.0)
197 else :
198 label = "%d" % (i / sec_w)
199 label_w = ctx.text_extents(label)[2]
200 x = rect[0] + i - label_w/2
201 if x >= prev_x:
202 draw_text(ctx, label, TEXT_COLOR, x, rect[1] - 2)
203 prev_x = x + label_w
204
205def draw_box_ticks(ctx, rect, sec_w):
206 draw_rect(ctx, BORDER_COLOR, tuple(rect))
207
208 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
209
210 for i in range(sec_w, rect[2] + 1, sec_w):
211 if ((i / sec_w) % 10 == 0) :
212 ctx.set_line_width(1.5)
213 elif sec_w < 5 :
214 continue
215 else :
216 ctx.set_line_width(1.0)
217 if ((i / sec_w) % 30 == 0) :
218 ctx.set_source_rgba(*TICK_COLOR_BOLD)
219 else :
220 ctx.set_source_rgba(*TICK_COLOR)
221 ctx.move_to(rect[0] + i, rect[1] + 1)
222 ctx.line_to(rect[0] + i, rect[1] + rect[3] - 1)
223 ctx.stroke()
224 ctx.set_line_width(1.0)
225
226 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
227
228def draw_annotations(ctx, proc_tree, times, rect):
229 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
230 ctx.set_source_rgba(*ANNOTATION_COLOR)
231 ctx.set_dash([4, 4])
232
233 for time in times:
234 if time is not None:
235 x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration)
236
237 ctx.move_to(rect[0] + x, rect[1] + 1)
238 ctx.line_to(rect[0] + x, rect[1] + rect[3] - 1)
239 ctx.stroke()
240
241 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
242 ctx.set_dash([])
243
244def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range):
245 ctx.set_line_width(0.5)
246 x_shift = proc_tree.start_time
247
248 def transform_point_coords(point, x_base, y_base, \
249 xscale, yscale, x_trans, y_trans):
250 x = (point[0] - x_base) * xscale + x_trans
251 y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3]
252 return x, y
253
254 max_x = max (x for (x, y) in data)
255 max_y = max (y for (x, y) in data)
256 # avoid divide by zero
257 if max_y == 0:
258 max_y = 1.0
259 xscale = float (chart_bounds[2]) / max_x
260 # If data_range is given, scale the chart so that the value range in
261 # data_range matches the chart bounds exactly.
262 # Otherwise, scale so that the actual data matches the chart bounds.
263 if data_range:
264 yscale = float(chart_bounds[3]) / (data_range[1] - data_range[0])
265 ybase = data_range[0]
266 else:
267 yscale = float(chart_bounds[3]) / max_y
268 ybase = 0
269
270 first = transform_point_coords (data[0], x_shift, ybase, xscale, yscale, \
271 chart_bounds[0], chart_bounds[1])
272 last = transform_point_coords (data[-1], x_shift, ybase, xscale, yscale, \
273 chart_bounds[0], chart_bounds[1])
274
275 ctx.set_source_rgba(*color)
276 ctx.move_to(*first)
277 for point in data:
278 x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \
279 chart_bounds[0], chart_bounds[1])
280 ctx.line_to(x, y)
281 if fill:
282 ctx.stroke_preserve()
283 ctx.line_to(last[0], chart_bounds[1]+chart_bounds[3])
284 ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3])
285 ctx.line_to(first[0], first[1])
286 ctx.fill()
287 else:
288 ctx.stroke()
289 ctx.set_line_width(1.0)
290
291bar_h = 55
292meminfo_bar_h = 2 * bar_h
293header_h = 60
294# offsets
295off_x, off_y = 220, 10
296sec_w_base = 1 # the width of a second
297proc_h = 16 # the height of a process
298leg_s = 10
299MIN_IMG_W = 800
300CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs
301OPTIONS = None
302
303def extents(options, xscale, trace):
304 start = min(trace.start.keys())
305 end = start
306
307 processes = 0
308 for proc in trace.processes:
309 if not options.app_options.show_all and \
310 trace.processes[proc][1] - trace.processes[proc][0] < options.app_options.mintime:
311 continue
312
313 if trace.processes[proc][1] > end:
314 end = trace.processes[proc][1]
315 processes += 1
316
317 if trace.min is not None and trace.max is not None:
318 start = trace.min
319 end = trace.max
320
321 w = int ((end - start) * sec_w_base * xscale) + 2 * off_x
322 h = proc_h * processes + header_h + 2 * off_y
323
324 return (w, h)
325
326def clip_visible(clip, rect):
327 xmax = max (clip[0], rect[0])
328 ymax = max (clip[1], rect[1])
329 xmin = min (clip[0] + clip[2], rect[0] + rect[2])
330 ymin = min (clip[1] + clip[3], rect[1] + rect[3])
331 return (xmin > xmax and ymin > ymax)
332
333def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w):
334 proc_tree = options.proc_tree(trace)
335
336 # render bar legend
337 ctx.set_font_size(LEGEND_FONT_SIZE)
338
339 draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s)
340 draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s)
341
342 # render I/O wait
343 chart_rect = (off_x, curr_y+30, w, bar_h)
344 if clip_visible (clip, chart_rect):
345 draw_box_ticks (ctx, chart_rect, sec_w)
346 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
347 draw_chart (ctx, IO_COLOR, True, chart_rect, \
348 [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \
349 proc_tree, None)
350 # render CPU load
351 draw_chart (ctx, CPU_COLOR, True, chart_rect, \
352 [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \
353 proc_tree, None)
354
355 curr_y = curr_y + 30 + bar_h
356
357 # render second chart
358 draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s)
359 draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s)
360
361 # render I/O utilization
362 chart_rect = (off_x, curr_y+30, w, bar_h)
363 if clip_visible (clip, chart_rect):
364 draw_box_ticks (ctx, chart_rect, sec_w)
365 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
366 draw_chart (ctx, IO_COLOR, True, chart_rect, \
367 [(sample.time, sample.util) for sample in trace.disk_stats], \
368 proc_tree, None)
369
370 # render disk throughput
371 max_sample = max (trace.disk_stats, key = lambda s: s.tput)
372 if clip_visible (clip, chart_rect):
373 draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, \
374 [(sample.time, sample.tput) for sample in trace.disk_stats], \
375 proc_tree, None)
376
377 pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration)
378
379 shift_x, shift_y = -20, 20
380 if (pos_x < off_x + 245):
381 shift_x, shift_y = 5, 40
382
383 label = "%dMB/s" % round ((max_sample.tput) / 1024.0)
384 draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y)
385
386 curr_y = curr_y + 30 + bar_h
387
388 # render mem usage
389 chart_rect = (off_x, curr_y+30, w, meminfo_bar_h)
390 mem_stats = trace.mem_stats
391 if mem_stats and clip_visible (clip, chart_rect):
392 mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats)
393 draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, off_x, curr_y+20, leg_s)
394 draw_legend_box(ctx, "Used", MEM_USED_COLOR, off_x + 240, curr_y+20, leg_s)
395 draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, off_x + 360, curr_y+20, leg_s)
396 draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \
397 MEM_SWAP_COLOR, off_x + 480, curr_y+20, leg_s)
398 draw_box_ticks(ctx, chart_rect, sec_w)
399 draw_annotations(ctx, proc_tree, trace.times, chart_rect)
400 draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \
401 [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \
402 proc_tree, [0, mem_scale])
403 draw_chart(ctx, MEM_USED_COLOR, True, chart_rect, \
404 [(sample.time, sample.records['MemTotal'] - sample.records['MemFree'] - sample.records['Buffers']) for sample in mem_stats], \
405 proc_tree, [0, mem_scale])
406 draw_chart(ctx, MEM_CACHED_COLOR, True, chart_rect, \
407 [(sample.time, sample.records['Cached']) for sample in mem_stats], \
408 proc_tree, [0, mem_scale])
409 draw_chart(ctx, MEM_SWAP_COLOR, False, chart_rect, \
410 [(sample.time, float(sample.records['SwapTotal'] - sample.records['SwapFree'])) for sample in mem_stats], \
411 proc_tree, None)
412
413 curr_y = curr_y + meminfo_bar_h
414
415 return curr_y
416
417def render_processes_chart(ctx, options, trace, curr_y, w, h, sec_w):
418 chart_rect = [off_x, curr_y+header_h, w, h - 2 * off_y - (curr_y+header_h) + proc_h]
419
420 draw_legend_box (ctx, "Configure", \
421 TASK_COLOR_CONFIGURE, off_x , curr_y + 45, leg_s)
422 draw_legend_box (ctx, "Compile", \
423 TASK_COLOR_COMPILE, off_x+120, curr_y + 45, leg_s)
424 draw_legend_box (ctx, "Install", \
425 TASK_COLOR_INSTALL, off_x+240, curr_y + 45, leg_s)
426 draw_legend_box (ctx, "Populate Sysroot", \
427 TASK_COLOR_SYSROOT, off_x+360, curr_y + 45, leg_s)
428 draw_legend_box (ctx, "Package", \
429 TASK_COLOR_PACKAGE, off_x+480, curr_y + 45, leg_s)
430 draw_legend_box (ctx, "Package Write",
431 TASK_COLOR_PACKAGE_WRITE, off_x+600, curr_y + 45, leg_s)
432
433 ctx.set_font_size(PROC_TEXT_FONT_SIZE)
434
435 draw_box_ticks(ctx, chart_rect, sec_w)
436 draw_sec_labels(ctx, options, chart_rect, sec_w, 30)
437
438 y = curr_y+header_h
439
440 offset = trace.min or min(trace.start.keys())
441 for s in sorted(trace.start.keys()):
442 for val in sorted(trace.start[s]):
443 if not options.app_options.show_all and \
444 trace.processes[val][1] - s < options.app_options.mintime:
445 continue
446 task = val.split(":")[1]
447 #print val
448 #print trace.processes[val][1]
449 #print s
450 x = chart_rect[0] + (s - offset) * sec_w
451 w = ((trace.processes[val][1] - s) * sec_w)
452
453 #print "proc at %s %s %s %s" % (x, y, w, proc_h)
454 col = None
455 if task == "do_compile":
456 col = TASK_COLOR_COMPILE
457 elif task == "do_configure":
458 col = TASK_COLOR_CONFIGURE
459 elif task == "do_install":
460 col = TASK_COLOR_INSTALL
461 elif task == "do_populate_sysroot":
462 col = TASK_COLOR_SYSROOT
463 elif task == "do_package":
464 col = TASK_COLOR_PACKAGE
465 elif task == "do_package_write_rpm" or \
466 task == "do_package_write_deb" or \
467 task == "do_package_write_ipk":
468 col = TASK_COLOR_PACKAGE_WRITE
469 else:
470 col = WHITE
471
472 if col:
473 draw_fill_rect(ctx, col, (x, y, w, proc_h))
474 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
475
476 draw_label_in_box(ctx, PROC_TEXT_COLOR, val, x, y + proc_h - 4, w, proc_h)
477 y = y + proc_h
478
479 return curr_y
480
481#
482# Render the chart.
483#
484def render(ctx, options, xscale, trace):
485 (w, h) = extents (options, xscale, trace)
486 global OPTIONS
487 OPTIONS = options.app_options
488
489 # x, y, w, h
490 clip = ctx.clip_extents()
491
492 sec_w = int (xscale * sec_w_base)
493 ctx.set_line_width(1.0)
494 ctx.select_font_face(FONT_NAME)
495 draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h))
496 w -= 2*off_x
497 curr_y = off_y;
498
499 curr_y = render_processes_chart (ctx, options, trace, curr_y, w, h, sec_w)
500
501 return
502
503 proc_tree = options.proc_tree (trace)
504
505 # draw the title and headers
506 if proc_tree.idle:
507 duration = proc_tree.idle
508 else:
509 duration = proc_tree.duration
510
511 if not options.kernel_only:
512 curr_y = draw_header (ctx, trace.headers, duration)
513 else:
514 curr_y = off_y;
515
516 if options.charts:
517 curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h, sec_w)
518
519 # draw process boxes
520 proc_height = h
521 if proc_tree.taskstats and options.cumulative:
522 proc_height -= CUML_HEIGHT
523
524 draw_process_bar_chart(ctx, clip, options, proc_tree, trace.times,
525 curr_y, w, proc_height, sec_w)
526
527 curr_y = proc_height
528 ctx.set_font_size(SIG_FONT_SIZE)
529 draw_text(ctx, SIGNATURE, SIG_COLOR, off_x + 5, proc_height - 8)
530
531 # draw a cumulative CPU-time-per-process graph
532 if proc_tree.taskstats and options.cumulative:
533 cuml_rect = (off_x, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2)
534 if clip_visible (clip, cuml_rect):
535 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_CPU)
536
537 # draw a cumulative I/O-time-per-process graph
538 if proc_tree.taskstats and options.cumulative:
539 cuml_rect = (off_x, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2)
540 if clip_visible (clip, cuml_rect):
541 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_IO)
542
543def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h, sec_w):
544 header_size = 0
545 if not options.kernel_only:
546 draw_legend_box (ctx, "Running (%cpu)",
547 PROC_COLOR_R, off_x , curr_y + 45, leg_s)
548 draw_legend_box (ctx, "Unint.sleep (I/O)",
549 PROC_COLOR_D, off_x+120, curr_y + 45, leg_s)
550 draw_legend_box (ctx, "Sleeping",
551 PROC_COLOR_S, off_x+240, curr_y + 45, leg_s)
552 draw_legend_box (ctx, "Zombie",
553 PROC_COLOR_Z, off_x+360, curr_y + 45, leg_s)
554 header_size = 45
555
556 chart_rect = [off_x, curr_y + header_size + 15,
557 w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h]
558 ctx.set_font_size (PROC_TEXT_FONT_SIZE)
559
560 draw_box_ticks (ctx, chart_rect, sec_w)
561 if sec_w > 100:
562 nsec = 1
563 else:
564 nsec = 5
565 draw_sec_labels (ctx, options, chart_rect, sec_w, nsec)
566 draw_annotations (ctx, proc_tree, times, chart_rect)
567
568 y = curr_y + 60
569 for root in proc_tree.process_tree:
570 draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect, clip)
571 y = y + proc_h * proc_tree.num_nodes([root])
572
573
574def draw_header (ctx, headers, duration):
575 toshow = [
576 ('system.uname', 'uname', lambda s: s),
577 ('system.release', 'release', lambda s: s),
578 ('system.cpu', 'CPU', lambda s: re.sub('model name\s*:\s*', '', s, 1)),
579 ('system.kernel.options', 'kernel options', lambda s: s),
580 ]
581
582 header_y = ctx.font_extents()[2] + 10
583 ctx.set_font_size(TITLE_FONT_SIZE)
584 draw_text(ctx, headers['title'], TEXT_COLOR, off_x, header_y)
585 ctx.set_font_size(TEXT_FONT_SIZE)
586
587 for (headerkey, headertitle, mangle) in toshow:
588 header_y += ctx.font_extents()[2]
589 if headerkey in headers:
590 value = headers.get(headerkey)
591 else:
592 value = ""
593 txt = headertitle + ': ' + mangle(value)
594 draw_text(ctx, txt, TEXT_COLOR, off_x, header_y)
595
596 dur = duration / 100.0
597 txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60))
598 if headers.get('system.maxpid') is not None:
599 txt = txt + ' max pid: %s' % (headers.get('system.maxpid'))
600
601 header_y += ctx.font_extents()[2]
602 draw_text (ctx, txt, TEXT_COLOR, off_x, header_y)
603
604 return header_y
605
606def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) :
607 x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration)
608 w = ((proc.duration) * rect[2] / proc_tree.duration)
609
610 draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip)
611 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
612 ipid = int(proc.pid)
613 if not OPTIONS.show_all:
614 cmdString = proc.cmd
615 else:
616 cmdString = ''
617 if (OPTIONS.show_pid or OPTIONS.show_all) and ipid is not 0:
618 cmdString = cmdString + " [" + str(ipid // 1000) + "]"
619 if OPTIONS.show_all:
620 if proc.args:
621 cmdString = cmdString + " '" + "' '".join(proc.args) + "'"
622 else:
623 cmdString = cmdString + " " + proc.exe
624
625 draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, rect[0] + rect[2])
626
627 next_y = y + proc_h
628 for child in proc.child_list:
629 if next_y > clip[1] + clip[3]:
630 break
631 child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect, clip)
632 draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h)
633 next_y = next_y + proc_h * proc_tree.num_nodes([child])
634
635 return x, y
636
637
638def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip):
639
640 if y > clip[1] + clip[3] or y + proc_h + 2 < clip[1]:
641 return
642
643 draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h))
644
645 last_tx = -1
646 for sample in proc.samples :
647 tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration))
648
649 # samples are sorted chronologically
650 if tx < clip[0]:
651 continue
652 if tx > clip[0] + clip[2]:
653 break
654
655 tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration))
656 if last_tx != -1 and abs(last_tx - tx) <= tw:
657 tw -= last_tx - tx
658 tx = last_tx
659 tw = max (tw, 1) # nice to see at least something
660
661 last_tx = tx + tw
662 state = get_proc_state( sample.state )
663
664 color = STATE_COLORS[state]
665 if state == STATE_RUNNING:
666 alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0)
667 color = tuple(list(PROC_COLOR_R[0:3]) + [alpha])
668# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha)
669 elif state == STATE_SLEEPING:
670 continue
671
672 draw_fill_rect(ctx, color, (tx, y, tw, proc_h))
673
674def draw_process_connecting_lines(ctx, px, py, x, y, proc_h):
675 ctx.set_source_rgba(*DEP_COLOR)
676 ctx.set_dash([2, 2])
677 if abs(px - x) < 3:
678 dep_off_x = 3
679 dep_off_y = proc_h / 4
680 ctx.move_to(x, y + proc_h / 2)
681 ctx.line_to(px - dep_off_x, y + proc_h / 2)
682 ctx.line_to(px - dep_off_x, py - dep_off_y)
683 ctx.line_to(px, py - dep_off_y)
684 else:
685 ctx.move_to(x, y + proc_h / 2)
686 ctx.line_to(px, y + proc_h / 2)
687 ctx.line_to(px, py)
688 ctx.stroke()
689 ctx.set_dash([])
690
691# elide the bootchart collector - it is quite distorting
692def elide_bootchart(proc):
693 return proc.cmd == 'bootchartd' or proc.cmd == 'bootchart-colle'
694
695class CumlSample:
696 def __init__(self, proc):
697 self.cmd = proc.cmd
698 self.samples = []
699 self.merge_samples (proc)
700 self.color = None
701
702 def merge_samples(self, proc):
703 self.samples.extend (proc.samples)
704 self.samples.sort (key = lambda p: p.time)
705
706 def next(self):
707 global palette_idx
708 palette_idx += HSV_STEP
709 return palette_idx
710
711 def get_color(self):
712 if self.color is None:
713 i = self.next() % HSV_MAX_MOD
714 h = 0.0
715 if i is not 0:
716 h = (1.0 * i) / HSV_MAX_MOD
717 s = 0.5
718 v = 1.0
719 c = colorsys.hsv_to_rgb (h, s, v)
720 self.color = (c[0], c[1], c[2], 1.0)
721 return self.color
722
723
724def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type):
725 global palette_idx
726 palette_idx = 0
727
728 time_hash = {}
729 total_time = 0.0
730 m_proc_list = {}
731
732 if stat_type is STAT_TYPE_CPU:
733 sample_value = 'cpu'
734 else:
735 sample_value = 'io'
736 for proc in proc_tree.process_list:
737 if elide_bootchart(proc):
738 continue
739
740 for sample in proc.samples:
741 total_time += getattr(sample.cpu_sample, sample_value)
742 if not sample.time in time_hash:
743 time_hash[sample.time] = 1
744
745 # merge pids with the same cmd
746 if not proc.cmd in m_proc_list:
747 m_proc_list[proc.cmd] = CumlSample (proc)
748 continue
749 s = m_proc_list[proc.cmd]
750 s.merge_samples (proc)
751
752 # all the sample times
753 times = sorted(time_hash)
754 if len (times) < 2:
755 print("degenerate boot chart")
756 return
757
758 pix_per_ns = chart_bounds[3] / total_time
759# print "total time: %g pix-per-ns %g" % (total_time, pix_per_ns)
760
761 # FIXME: we have duplicates in the process list too [!] - why !?
762
763 # Render bottom up, left to right
764 below = {}
765 for time in times:
766 below[time] = chart_bounds[1] + chart_bounds[3]
767
768 # same colors each time we render
769 random.seed (0)
770
771 ctx.set_line_width(1)
772
773 legends = []
774 labels = []
775
776 # render each pid in order
777 for cs in m_proc_list.values():
778 row = {}
779 cuml = 0.0
780
781 # print "pid : %s -> %g samples %d" % (proc.cmd, cuml, len (cs.samples))
782 for sample in cs.samples:
783 cuml += getattr(sample.cpu_sample, sample_value)
784 row[sample.time] = cuml
785
786 process_total_time = cuml
787
788 # hide really tiny processes
789 if cuml * pix_per_ns <= 2:
790 continue
791
792 last_time = times[0]
793 y = last_below = below[last_time]
794 last_cuml = cuml = 0.0
795
796 ctx.set_source_rgba(*cs.get_color())
797 for time in times:
798 render_seg = False
799
800 # did the underlying trend increase ?
801 if below[time] != last_below:
802 last_below = below[last_time]
803 last_cuml = cuml
804 render_seg = True
805
806 # did we move up a pixel increase ?
807 if time in row:
808 nc = round (row[time] * pix_per_ns)
809 if nc != cuml:
810 last_cuml = cuml
811 cuml = nc
812 render_seg = True
813
814# if last_cuml > cuml:
815# assert fail ... - un-sorted process samples
816
817 # draw the trailing rectangle from the last time to
818 # before now, at the height of the last segment.
819 if render_seg:
820 w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration) + 1
821 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
822 ctx.rectangle (x, below[last_time] - last_cuml, w, last_cuml)
823 ctx.fill()
824# ctx.stroke()
825 last_time = time
826 y = below [time] - cuml
827
828 row[time] = y
829
830 # render the last segment
831 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
832 y = below[last_time] - cuml
833 ctx.rectangle (x, y, chart_bounds[2] - x, cuml)
834 ctx.fill()
835# ctx.stroke()
836
837 # render legend if it will fit
838 if cuml > 8:
839 label = cs.cmd
840 extnts = ctx.text_extents(label)
841 label_w = extnts[2]
842 label_h = extnts[3]
843# print "Text extents %g by %g" % (label_w, label_h)
844 labels.append((label,
845 chart_bounds[0] + chart_bounds[2] - label_w - off_x * 2,
846 y + (cuml + label_h) / 2))
847 if cs in legends:
848 print("ARGH - duplicate process in list !")
849
850 legends.append ((cs, process_total_time))
851
852 below = row
853
854 # render grid-lines over the top
855 draw_box_ticks(ctx, chart_bounds, sec_w)
856
857 # render labels
858 for l in labels:
859 draw_text(ctx, l[0], TEXT_COLOR, l[1], l[2])
860
861 # Render legends
862 font_height = 20
863 label_width = 300
864 LEGENDS_PER_COL = 15
865 LEGENDS_TOTAL = 45
866 ctx.set_font_size (TITLE_FONT_SIZE)
867 dur_secs = duration / 100
868 cpu_secs = total_time / 1000000000
869
870 # misleading - with multiple CPUs ...
871# idle = ((dur_secs - cpu_secs) / dur_secs) * 100.0
872 if stat_type is STAT_TYPE_CPU:
873 label = "Cumulative CPU usage, by process; total CPU: " \
874 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
875 else:
876 label = "Cumulative I/O usage, by process; total I/O: " \
877 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
878
879 draw_text(ctx, label, TEXT_COLOR, chart_bounds[0] + off_x,
880 chart_bounds[1] + font_height)
881
882 i = 0
883 legends = sorted(legends, key=itemgetter(1), reverse=True)
884 ctx.set_font_size(TEXT_FONT_SIZE)
885 for t in legends:
886 cs = t[0]
887 time = t[1]
888 x = chart_bounds[0] + off_x + int (i/LEGENDS_PER_COL) * label_width
889 y = chart_bounds[1] + font_height * ((i % LEGENDS_PER_COL) + 2)
890 str = "%s - %.0f(ms) (%2.2f%%)" % (cs.cmd, time/1000000, (time/total_time) * 100.0)
891 draw_legend_box(ctx, str, cs.color, x, y, leg_s)
892 i = i + 1
893 if i >= LEGENDS_TOTAL:
894 break
diff --git a/scripts/pybootchartgui/pybootchartgui/gui.py b/scripts/pybootchartgui/pybootchartgui/gui.py
new file mode 100644
index 0000000000..7fedd232df
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/gui.py
@@ -0,0 +1,350 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16import gobject
17import gtk
18import gtk.gdk
19import gtk.keysyms
20from . import draw
21from .draw import RenderOptions
22
23class PyBootchartWidget(gtk.DrawingArea):
24 __gsignals__ = {
25 'expose-event': 'override',
26 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)),
27 'position-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)),
28 'set-scroll-adjustments' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment))
29 }
30
31 def __init__(self, trace, options, xscale):
32 gtk.DrawingArea.__init__(self)
33
34 self.trace = trace
35 self.options = options
36
37 self.set_flags(gtk.CAN_FOCUS)
38
39 self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
40 self.connect("button-press-event", self.on_area_button_press)
41 self.connect("button-release-event", self.on_area_button_release)
42 self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
43 self.connect("motion-notify-event", self.on_area_motion_notify)
44 self.connect("scroll-event", self.on_area_scroll_event)
45 self.connect('key-press-event', self.on_key_press_event)
46
47 self.connect('set-scroll-adjustments', self.on_set_scroll_adjustments)
48 self.connect("size-allocate", self.on_allocation_size_changed)
49 self.connect("position-changed", self.on_position_changed)
50
51 self.zoom_ratio = 1.0
52 self.xscale = xscale
53 self.x, self.y = 0.0, 0.0
54
55 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
56 self.hadj = None
57 self.vadj = None
58 self.hadj_changed_signal_id = None
59 self.vadj_changed_signal_id = None
60
61 def do_expose_event(self, event):
62 cr = self.window.cairo_create()
63
64 # set a clip region for the expose event
65 cr.rectangle(
66 event.area.x, event.area.y,
67 event.area.width, event.area.height
68 )
69 cr.clip()
70 self.draw(cr, self.get_allocation())
71 return False
72
73 def draw(self, cr, rect):
74 cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
75 cr.paint()
76 cr.scale(self.zoom_ratio, self.zoom_ratio)
77 cr.translate(-self.x, -self.y)
78 draw.render(cr, self.options, self.xscale, self.trace)
79
80 def position_changed(self):
81 self.emit("position-changed", self.x, self.y)
82
83 ZOOM_INCREMENT = 1.25
84
85 def zoom_image (self, zoom_ratio):
86 self.zoom_ratio = zoom_ratio
87 self._set_scroll_adjustments (self.hadj, self.vadj)
88 self.queue_draw()
89
90 def zoom_to_rect (self, rect):
91 zoom_ratio = float(rect.width)/float(self.chart_width)
92 self.zoom_image(zoom_ratio)
93 self.x = 0
94 self.position_changed()
95
96 def set_xscale(self, xscale):
97 old_mid_x = self.x + self.hadj.page_size / 2
98 self.xscale = xscale
99 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
100 new_x = old_mid_x
101 self.zoom_image (self.zoom_ratio)
102
103 def on_expand(self, action):
104 self.set_xscale (int(self.xscale * 1.5 + 0.5))
105
106 def on_contract(self, action):
107 self.set_xscale (max(int(self.xscale / 1.5), 1))
108
109 def on_zoom_in(self, action):
110 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
111
112 def on_zoom_out(self, action):
113 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
114
115 def on_zoom_fit(self, action):
116 self.zoom_to_rect(self.get_allocation())
117
118 def on_zoom_100(self, action):
119 self.zoom_image(1.0)
120 self.set_xscale(1.0)
121
122 def show_toggled(self, button):
123 self.options.app_options.show_all = button.get_property ('active')
124 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
125 self._set_scroll_adjustments(self.hadj, self.vadj)
126 self.queue_draw()
127
128 POS_INCREMENT = 100
129
130 def on_key_press_event(self, widget, event):
131 if event.keyval == gtk.keysyms.Left:
132 self.x -= self.POS_INCREMENT/self.zoom_ratio
133 elif event.keyval == gtk.keysyms.Right:
134 self.x += self.POS_INCREMENT/self.zoom_ratio
135 elif event.keyval == gtk.keysyms.Up:
136 self.y -= self.POS_INCREMENT/self.zoom_ratio
137 elif event.keyval == gtk.keysyms.Down:
138 self.y += self.POS_INCREMENT/self.zoom_ratio
139 else:
140 return False
141 self.queue_draw()
142 self.position_changed()
143 return True
144
145 def on_area_button_press(self, area, event):
146 if event.button == 2 or event.button == 1:
147 area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
148 self.prevmousex = event.x
149 self.prevmousey = event.y
150 if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
151 return False
152 return False
153
154 def on_area_button_release(self, area, event):
155 if event.button == 2 or event.button == 1:
156 area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
157 self.prevmousex = None
158 self.prevmousey = None
159 return True
160 return False
161
162 def on_area_scroll_event(self, area, event):
163 if event.state & gtk.gdk.CONTROL_MASK:
164 if event.direction == gtk.gdk.SCROLL_UP:
165 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
166 return True
167 if event.direction == gtk.gdk.SCROLL_DOWN:
168 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
169 return True
170 return False
171
172 def on_area_motion_notify(self, area, event):
173 state = event.state
174 if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
175 x, y = int(event.x), int(event.y)
176 # pan the image
177 self.x += (self.prevmousex - x)/self.zoom_ratio
178 self.y += (self.prevmousey - y)/self.zoom_ratio
179 self.queue_draw()
180 self.prevmousex = x
181 self.prevmousey = y
182 self.position_changed()
183 return True
184
185 def on_set_scroll_adjustments(self, area, hadj, vadj):
186 self._set_scroll_adjustments (hadj, vadj)
187
188 def on_allocation_size_changed(self, widget, allocation):
189 self.hadj.page_size = allocation.width
190 self.hadj.page_increment = allocation.width * 0.9
191 self.vadj.page_size = allocation.height
192 self.vadj.page_increment = allocation.height * 0.9
193
194 def _set_adj_upper(self, adj, upper):
195 changed = False
196 value_changed = False
197
198 if adj.upper != upper:
199 adj.upper = upper
200 changed = True
201
202 max_value = max(0.0, upper - adj.page_size)
203 if adj.value > max_value:
204 adj.value = max_value
205 value_changed = True
206
207 if changed:
208 adj.changed()
209 if value_changed:
210 adj.value_changed()
211
212 def _set_scroll_adjustments(self, hadj, vadj):
213 if hadj == None:
214 hadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
215 if vadj == None:
216 vadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
217
218 if self.hadj_changed_signal_id != None and \
219 self.hadj != None and hadj != self.hadj:
220 self.hadj.disconnect (self.hadj_changed_signal_id)
221 if self.vadj_changed_signal_id != None and \
222 self.vadj != None and vadj != self.vadj:
223 self.vadj.disconnect (self.vadj_changed_signal_id)
224
225 if hadj != None:
226 self.hadj = hadj
227 self._set_adj_upper (self.hadj, self.zoom_ratio * self.chart_width)
228 self.hadj_changed_signal_id = self.hadj.connect('value-changed', self.on_adjustments_changed)
229
230 if vadj != None:
231 self.vadj = vadj
232 self._set_adj_upper (self.vadj, self.zoom_ratio * self.chart_height)
233 self.vadj_changed_signal_id = self.vadj.connect('value-changed', self.on_adjustments_changed)
234
235 def on_adjustments_changed(self, adj):
236 self.x = self.hadj.value / self.zoom_ratio
237 self.y = self.vadj.value / self.zoom_ratio
238 self.queue_draw()
239
240 def on_position_changed(self, widget, x, y):
241 self.hadj.value = x * self.zoom_ratio
242 self.vadj.value = y * self.zoom_ratio
243
244PyBootchartWidget.set_set_scroll_adjustments_signal('set-scroll-adjustments')
245
246class PyBootchartShell(gtk.VBox):
247 ui = '''
248 <ui>
249 <toolbar name="ToolBar">
250 <toolitem action="Expand"/>
251 <toolitem action="Contract"/>
252 <separator/>
253 <toolitem action="ZoomIn"/>
254 <toolitem action="ZoomOut"/>
255 <toolitem action="ZoomFit"/>
256 <toolitem action="Zoom100"/>
257 </toolbar>
258 </ui>
259 '''
260 def __init__(self, window, trace, options, xscale):
261 gtk.VBox.__init__(self)
262
263 self.widget = PyBootchartWidget(trace, options, xscale)
264
265 # Create a UIManager instance
266 uimanager = self.uimanager = gtk.UIManager()
267
268 # Add the accelerator group to the toplevel window
269 accelgroup = uimanager.get_accel_group()
270 window.add_accel_group(accelgroup)
271
272 # Create an ActionGroup
273 actiongroup = gtk.ActionGroup('Actions')
274 self.actiongroup = actiongroup
275
276 # Create actions
277 actiongroup.add_actions((
278 ('Expand', gtk.STOCK_ADD, None, None, None, self.widget.on_expand),
279 ('Contract', gtk.STOCK_REMOVE, None, None, None, self.widget.on_contract),
280 ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
281 ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
282 ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget.on_zoom_fit),
283 ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
284 ))
285
286 # Add the actiongroup to the uimanager
287 uimanager.insert_action_group(actiongroup, 0)
288
289 # Add a UI description
290 uimanager.add_ui_from_string(self.ui)
291
292 # Scrolled window
293 scrolled = gtk.ScrolledWindow()
294 scrolled.add(self.widget)
295
296 # toolbar / h-box
297 hbox = gtk.HBox(False, 8)
298
299 # Create a Toolbar
300 toolbar = uimanager.get_widget('/ToolBar')
301 hbox.pack_start(toolbar, True, True)
302
303 if not options.kernel_only:
304 # Misc. options
305 button = gtk.CheckButton("Show more")
306 button.connect ('toggled', self.widget.show_toggled)
307 button.set_active(options.app_options.show_all)
308 hbox.pack_start (button, False, True)
309
310 self.pack_start(hbox, False)
311 self.pack_start(scrolled)
312 self.show_all()
313
314 def grab_focus(self, window):
315 window.set_focus(self.widget)
316
317
318class PyBootchartWindow(gtk.Window):
319
320 def __init__(self, trace, app_options):
321 gtk.Window.__init__(self)
322
323 window = self
324 window.set_title("Bootchart %s" % trace.filename)
325 window.set_default_size(750, 550)
326
327 tab_page = gtk.Notebook()
328 tab_page.show()
329 window.add(tab_page)
330
331 full_opts = RenderOptions(app_options)
332 full_tree = PyBootchartShell(window, trace, full_opts, 1.0)
333 tab_page.append_page (full_tree, gtk.Label("Full tree"))
334
335 if trace.kernel is not None and len (trace.kernel) > 2:
336 kernel_opts = RenderOptions(app_options)
337 kernel_opts.cumulative = False
338 kernel_opts.charts = False
339 kernel_opts.kernel_only = True
340 kernel_tree = PyBootchartShell(window, trace, kernel_opts, 5.0)
341 tab_page.append_page (kernel_tree, gtk.Label("Kernel boot"))
342
343 full_tree.grab_focus(self)
344 self.show()
345
346
347def show(trace, options):
348 win = PyBootchartWindow(trace, options)
349 win.connect('destroy', gtk.main_quit)
350 gtk.main()
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py b/scripts/pybootchartgui/pybootchartgui/main.py
new file mode 120000
index 0000000000..b45ae0a3d2
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/main.py
@@ -0,0 +1 @@
main.py.in \ No newline at end of file
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py.in b/scripts/pybootchartgui/pybootchartgui/main.py.in
new file mode 100644
index 0000000000..21bb0be3a7
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/main.py.in
@@ -0,0 +1,187 @@
1#
2# ***********************************************************************
3# Warning: This file is auto-generated from main.py.in - edit it there.
4# ***********************************************************************
5#
6# pybootchartgui is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10
11# pybootchartgui is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
18
19from __future__ import print_function
20
21import sys
22import os
23import optparse
24
25from . import parsing
26from . import batch
27
28def _mk_options_parser():
29 """Make an options parser."""
30 usage = "%prog [options] /path/to/tmp/buildstats/<recipe-machine>/<BUILDNAME>/"
31 version = "%prog v1.0.0"
32 parser = optparse.OptionParser(usage, version=version)
33 parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False,
34 help="start in active mode")
35 parser.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"],
36 help="image format (png, svg, pdf); default format png")
37 parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None,
38 help="output path (file or directory) where charts are stored")
39 parser.add_option("-s", "--split", dest="num", type=int, default=1,
40 help="split the output chart into <NUM> charts, only works with \"-o PATH\"")
41 parser.add_option("-m", "--mintime", dest="mintime", type=int, default=8,
42 help="only tasks longer than this time will be displayed")
43 parser.add_option("-M", "--minutes", action="store_true", dest="as_minutes", default=False,
44 help="display time in minutes instead of seconds")
45# parser.add_option("-n", "--no-prune", action="store_false", dest="prune", default=True,
46# help="do not prune the process tree")
47 parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False,
48 help="suppress informational messages")
49# parser.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False,
50# help="only display the boot time of the boot in text format (stdout)")
51 parser.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False,
52 help="suppress all messages except errors")
53 parser.add_option("--verbose", action="store_true", dest="verbose", default=False,
54 help="print all messages")
55# parser.add_option("--profile", action="store_true", dest="profile", default=False,
56# help="profile rendering of chart (only useful when in batch mode indicated by -f)")
57# parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False,
58# help="show process ids in the bootchart as 'processname [pid]'")
59 parser.add_option("--show-all", action="store_true", dest="show_all", default=False,
60 help="show all processes in the chart")
61# parser.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None,
62# help="crop chart when idle after PROCESS is started")
63# parser.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None,
64# help="annotate position where PROCESS is started; can be specified multiple times. " +
65# "To create a single annotation when any one of a set of processes is started, use commas to separate the names")
66# parser.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None,
67# help="filename to write annotation points to")
68 parser.add_option("-T", "--full-time", action="store_true", dest="full_time", default=False,
69 help="display the full time regardless of which processes are currently shown")
70 return parser
71
72class Writer:
73 def __init__(self, write, options):
74 self.write = write
75 self.options = options
76
77 def error(self, msg):
78 self.write(msg)
79
80 def warn(self, msg):
81 if not self.options.quiet:
82 self.write(msg)
83
84 def info(self, msg):
85 if self.options.verbose:
86 self.write(msg)
87
88 def status(self, msg):
89 if not self.options.quiet:
90 self.write(msg)
91
92def _mk_writer(options):
93 def write(s):
94 print(s)
95 return Writer(write, options)
96
97def _get_filename(path):
98 """Construct a usable filename for outputs"""
99 dname = "."
100 fname = "bootchart"
101 if path != None:
102 if os.path.isdir(path):
103 dname = path
104 else:
105 fname = path
106 return os.path.join(dname, fname)
107
108def main(argv=None):
109 try:
110 if argv is None:
111 argv = sys.argv[1:]
112
113 parser = _mk_options_parser()
114 options, args = parser.parse_args(argv)
115
116 # Default values for disabled options
117 options.prune = True
118 options.boottime = False
119 options.profile = False
120 options.show_pid = False
121 options.crop_after = None
122 options.annotate = None
123 options.annotate_file = None
124
125 writer = _mk_writer(options)
126
127 if len(args) == 0:
128 print("No path given, trying /var/log/bootchart.tgz")
129 args = [ "/var/log/bootchart.tgz" ]
130
131 res = parsing.Trace(writer, args, options)
132
133 if options.interactive or options.output == None:
134 from . import gui
135 gui.show(res, options)
136 elif options.boottime:
137 import math
138 proc_tree = res.proc_tree
139 if proc_tree.idle:
140 duration = proc_tree.idle
141 else:
142 duration = proc_tree.duration
143 dur = duration / 100.0
144 print('%02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)))
145 else:
146 if options.annotate_file:
147 f = open (options.annotate_file, "w")
148 try:
149 for time in res[4]:
150 if time is not None:
151 # output as ms
152 print(time * 10, file=f)
153 else:
154 print(file=f)
155 finally:
156 f.close()
157 filename = _get_filename(options.output)
158 res_list = parsing.split_res(res, options)
159 n = 1
160 width = len(str(len(res_list)))
161 s = "_%%0%dd." % width
162 for r in res_list:
163 if len(res_list) == 1:
164 f = filename + "." + options.format
165 else:
166 f = filename + s % n + options.format
167 n = n + 1
168 def render():
169 batch.render(writer, r, options, f)
170 if options.profile:
171 import cProfile
172 import pstats
173 profile = '%s.prof' % os.path.splitext(filename)[0]
174 cProfile.runctx('render()', globals(), locals(), profile)
175 p = pstats.Stats(profile)
176 p.strip_dirs().sort_stats('time').print_stats(20)
177 else:
178 render()
179
180 return 0
181 except parsing.ParseError as ex:
182 print(("Parse error: %s" % ex))
183 return 2
184
185
186if __name__ == '__main__':
187 sys.exit(main())
diff --git a/scripts/pybootchartgui/pybootchartgui/parsing.py b/scripts/pybootchartgui/pybootchartgui/parsing.py
new file mode 100644
index 0000000000..d423b9f77c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/parsing.py
@@ -0,0 +1,740 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17from __future__ import with_statement
18
19import os
20import string
21import re
22import sys
23import tarfile
24from time import clock
25from collections import defaultdict
26from functools import reduce
27
28from .samples import *
29from .process_tree import ProcessTree
30
31if sys.version_info >= (3, 0):
32 long = int
33
34# Parsing produces as its end result a 'Trace'
35
36class Trace:
37 def __init__(self, writer, paths, options):
38 self.processes = {}
39 self.start = {}
40 self.end = {}
41 self.min = None
42 self.max = None
43 self.headers = None
44 self.disk_stats = None
45 self.ps_stats = None
46 self.taskstats = None
47 self.cpu_stats = None
48 self.cmdline = None
49 self.kernel = None
50 self.kernel_tree = None
51 self.filename = None
52 self.parent_map = None
53 self.mem_stats = None
54
55 if len(paths):
56 parse_paths (writer, self, paths)
57 if not self.valid():
58 raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths))
59
60 if options.full_time:
61 self.min = min(self.start.keys())
62 self.max = max(self.end.keys())
63
64 return
65
66 # Turn that parsed information into something more useful
67 # link processes into a tree of pointers, calculate statistics
68 self.compile(writer)
69
70 # Crop the chart to the end of the first idle period after the given
71 # process
72 if options.crop_after:
73 idle = self.crop (writer, options.crop_after)
74 else:
75 idle = None
76
77 # Annotate other times as the first start point of given process lists
78 self.times = [ idle ]
79 if options.annotate:
80 for procnames in options.annotate:
81 names = [x[:15] for x in procnames.split(",")]
82 for proc in self.ps_stats.process_map.values():
83 if proc.cmd in names:
84 self.times.append(proc.start_time)
85 break
86 else:
87 self.times.append(None)
88
89 self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats,
90 self.ps_stats.sample_period,
91 self.headers.get("profile.process"),
92 options.prune, idle, self.taskstats,
93 self.parent_map is not None)
94
95 if self.kernel is not None:
96 self.kernel_tree = ProcessTree(writer, self.kernel, None, 0,
97 self.headers.get("profile.process"),
98 False, None, None, True)
99
100 def valid(self):
101 return len(self.processes) != 0
102 return self.headers != None and self.disk_stats != None and \
103 self.ps_stats != None and self.cpu_stats != None
104
105 def add_process(self, process, start, end):
106 self.processes[process] = [start, end]
107 if start not in self.start:
108 self.start[start] = []
109 if process not in self.start[start]:
110 self.start[start].append(process)
111 if end not in self.end:
112 self.end[end] = []
113 if process not in self.end[end]:
114 self.end[end].append(process)
115
116 def compile(self, writer):
117
118 def find_parent_id_for(pid):
119 if pid is 0:
120 return 0
121 ppid = self.parent_map.get(pid)
122 if ppid:
123 # many of these double forks are so short lived
124 # that we have no samples, or process info for them
125 # so climb the parent hierarcy to find one
126 if int (ppid * 1000) not in self.ps_stats.process_map:
127# print "Pid '%d' short lived with no process" % ppid
128 ppid = find_parent_id_for (ppid)
129# else:
130# print "Pid '%d' has an entry" % ppid
131 else:
132# print "Pid '%d' missing from pid map" % pid
133 return 0
134 return ppid
135
136 # merge in the cmdline data
137 if self.cmdline is not None:
138 for proc in self.ps_stats.process_map.values():
139 rpid = int (proc.pid // 1000)
140 if rpid in self.cmdline:
141 cmd = self.cmdline[rpid]
142 proc.exe = cmd['exe']
143 proc.args = cmd['args']
144# else:
145# print "proc %d '%s' not in cmdline" % (rpid, proc.exe)
146
147 # re-parent any stray orphans if we can
148 if self.parent_map is not None:
149 for process in self.ps_stats.process_map.values():
150 ppid = find_parent_id_for (int(process.pid // 1000))
151 if ppid:
152 process.ppid = ppid * 1000
153
154 # stitch the tree together with pointers
155 for process in self.ps_stats.process_map.values():
156 process.set_parent (self.ps_stats.process_map)
157
158 # count on fingers variously
159 for process in self.ps_stats.process_map.values():
160 process.calc_stats (self.ps_stats.sample_period)
161
162 def crop(self, writer, crop_after):
163
164 def is_idle_at(util, start, j):
165 k = j + 1
166 while k < len(util) and util[k][0] < start + 300:
167 k += 1
168 k = min(k, len(util)-1)
169
170 if util[j][1] >= 0.25:
171 return False
172
173 avgload = sum(u[1] for u in util[j:k+1]) / (k-j+1)
174 if avgload < 0.25:
175 return True
176 else:
177 return False
178 def is_idle(util, start):
179 for j in range(0, len(util)):
180 if util[j][0] < start:
181 continue
182 return is_idle_at(util, start, j)
183 else:
184 return False
185
186 names = [x[:15] for x in crop_after.split(",")]
187 for proc in self.ps_stats.process_map.values():
188 if proc.cmd in names or proc.exe in names:
189 writer.info("selected proc '%s' from list (start %d)"
190 % (proc.cmd, proc.start_time))
191 break
192 if proc is None:
193 writer.warn("no selected crop proc '%s' in list" % crop_after)
194
195
196 cpu_util = [(sample.time, sample.user + sample.sys + sample.io) for sample in self.cpu_stats]
197 disk_util = [(sample.time, sample.util) for sample in self.disk_stats]
198
199 idle = None
200 for i in range(0, len(cpu_util)):
201 if cpu_util[i][0] < proc.start_time:
202 continue
203 if is_idle_at(cpu_util, cpu_util[i][0], i) \
204 and is_idle(disk_util, cpu_util[i][0]):
205 idle = cpu_util[i][0]
206 break
207
208 if idle is None:
209 writer.warn ("not idle after proc '%s'" % crop_after)
210 return None
211
212 crop_at = idle + 300
213 writer.info ("cropping at time %d" % crop_at)
214 while len (self.cpu_stats) \
215 and self.cpu_stats[-1].time > crop_at:
216 self.cpu_stats.pop()
217 while len (self.disk_stats) \
218 and self.disk_stats[-1].time > crop_at:
219 self.disk_stats.pop()
220
221 self.ps_stats.end_time = crop_at
222
223 cropped_map = {}
224 for key, value in self.ps_stats.process_map.items():
225 if (value.start_time <= crop_at):
226 cropped_map[key] = value
227
228 for proc in cropped_map.values():
229 proc.duration = min (proc.duration, crop_at - proc.start_time)
230 while len (proc.samples) \
231 and proc.samples[-1].time > crop_at:
232 proc.samples.pop()
233
234 self.ps_stats.process_map = cropped_map
235
236 return idle
237
238
239
240class ParseError(Exception):
241 """Represents errors during parse of the bootchart."""
242 def __init__(self, value):
243 self.value = value
244
245 def __str__(self):
246 return self.value
247
248def _parse_headers(file):
249 """Parses the headers of the bootchart."""
250 def parse(acc, line):
251 (headers, last) = acc
252 if '=' in line:
253 last, value = map (lambda x: x.strip(), line.split('=', 1))
254 else:
255 value = line.strip()
256 headers[last] += value
257 return headers, last
258 return reduce(parse, file.read().decode('utf-8').split('\n'), (defaultdict(str),''))[0]
259
260def _parse_timed_blocks(file):
261 """Parses (ie., splits) a file into so-called timed-blocks. A
262 timed-block consists of a timestamp on a line by itself followed
263 by zero or more lines of data for that point in time."""
264 def parse(block):
265 lines = block.split('\n')
266 if not lines:
267 raise ParseError('expected a timed-block consisting a timestamp followed by data lines')
268 try:
269 return (int(lines[0]), lines[1:])
270 except ValueError:
271 raise ParseError("expected a timed-block, but timestamp '%s' is not an integer" % lines[0])
272 blocks = file.read().decode('utf-8').split('\n\n')
273 return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')]
274
275def _parse_proc_ps_log(writer, file):
276 """
277 * See proc(5) for details.
278 *
279 * {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime,
280 * cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack,
281 * kstkesp, kstkeip}
282 """
283 processMap = {}
284 ltime = 0
285 timed_blocks = _parse_timed_blocks(file)
286 for time, lines in timed_blocks:
287 for line in lines:
288 if not line: continue
289 tokens = line.split(' ')
290 if len(tokens) < 21:
291 continue
292
293 offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0]
294 pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset])
295 userCpu, sysCpu, stime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset])
296
297 # magic fixed point-ness ...
298 pid *= 1000
299 ppid *= 1000
300 if pid in processMap:
301 process = processMap[pid]
302 process.cmd = cmd.strip('()') # why rename after latest name??
303 else:
304 process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime))
305 processMap[pid] = process
306
307 if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None:
308 userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime))
309 cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0)
310 process.samples.append(ProcessSample(time, state, cpuSample))
311
312 process.last_user_cpu_time = userCpu
313 process.last_sys_cpu_time = sysCpu
314 ltime = time
315
316 if len (timed_blocks) < 2:
317 return None
318
319 startTime = timed_blocks[0][0]
320 avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1)
321
322 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
323
324def _parse_taskstats_log(writer, file):
325 """
326 * See bootchart-collector.c for details.
327 *
328 * { pid, ppid, comm, cpu_run_real_total, blkio_delay_total, swapin_delay_total }
329 *
330 """
331 processMap = {}
332 pidRewrites = {}
333 ltime = None
334 timed_blocks = _parse_timed_blocks(file)
335 for time, lines in timed_blocks:
336 # we have no 'stime' from taskstats, so prep 'init'
337 if ltime is None:
338 process = Process(writer, 1, '[init]', 0, 0)
339 processMap[1000] = process
340 ltime = time
341# continue
342 for line in lines:
343 if not line: continue
344 tokens = line.split(' ')
345 if len(tokens) != 6:
346 continue
347
348 opid, ppid, cmd = int(tokens[0]), int(tokens[1]), tokens[2]
349 cpu_ns, blkio_delay_ns, swapin_delay_ns = long(tokens[-3]), long(tokens[-2]), long(tokens[-1]),
350
351 # make space for trees of pids
352 opid *= 1000
353 ppid *= 1000
354
355 # when the process name changes, we re-write the pid.
356 if opid in pidRewrites:
357 pid = pidRewrites[opid]
358 else:
359 pid = opid
360
361 cmd = cmd.strip('(').strip(')')
362 if pid in processMap:
363 process = processMap[pid]
364 if process.cmd != cmd:
365 pid += 1
366 pidRewrites[opid] = pid
367# print "process mutation ! '%s' vs '%s' pid %s -> pid %s\n" % (process.cmd, cmd, opid, pid)
368 process = process.split (writer, pid, cmd, ppid, time)
369 processMap[pid] = process
370 else:
371 process.cmd = cmd;
372 else:
373 process = Process(writer, pid, cmd, ppid, time)
374 processMap[pid] = process
375
376 delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns)
377 delta_blkio_delay_ns = (float) (blkio_delay_ns - process.last_blkio_delay_ns)
378 delta_swapin_delay_ns = (float) (swapin_delay_ns - process.last_swapin_delay_ns)
379
380 # make up some state data ...
381 if delta_cpu_ns > 0:
382 state = "R"
383 elif delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
384 state = "D"
385 else:
386 state = "S"
387
388 # retain the ns timing information into a CPUSample - that tries
389 # with the old-style to be a %age of CPU used in this time-slice.
390 if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
391# print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns)
392 cpuSample = CPUSample('null', delta_cpu_ns, 0.0,
393 delta_blkio_delay_ns,
394 delta_swapin_delay_ns)
395 process.samples.append(ProcessSample(time, state, cpuSample))
396
397 process.last_cpu_ns = cpu_ns
398 process.last_blkio_delay_ns = blkio_delay_ns
399 process.last_swapin_delay_ns = swapin_delay_ns
400 ltime = time
401
402 if len (timed_blocks) < 2:
403 return None
404
405 startTime = timed_blocks[0][0]
406 avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1)
407
408 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
409
410def _parse_proc_stat_log(file):
411 samples = []
412 ltimes = None
413 for time, lines in _parse_timed_blocks(file):
414 # skip emtpy lines
415 if not lines:
416 continue
417 # CPU times {user, nice, system, idle, io_wait, irq, softirq}
418 tokens = lines[0].split()
419 times = [ int(token) for token in tokens[1:] ]
420 if ltimes:
421 user = float((times[0] + times[1]) - (ltimes[0] + ltimes[1]))
422 system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6]))
423 idle = float(times[3] - ltimes[3])
424 iowait = float(times[4] - ltimes[4])
425
426 aSum = max(user + system + idle + iowait, 1)
427 samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) )
428
429 ltimes = times
430 # skip the rest of statistics lines
431 return samples
432
433def _parse_proc_disk_stat_log(file, numCpu):
434 """
435 Parse file for disk stats, but only look at the whole device, eg. sda,
436 not sda1, sda2 etc. The format of relevant lines should be:
437 {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq}
438 """
439 disk_regex_re = re.compile ('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$')
440
441 # this gets called an awful lot.
442 def is_relevant_line(linetokens):
443 if len(linetokens) != 14:
444 return False
445 disk = linetokens[2]
446 return disk_regex_re.match(disk)
447
448 disk_stat_samples = []
449
450 for time, lines in _parse_timed_blocks(file):
451 sample = DiskStatSample(time)
452 relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) if is_relevant_line(linetokens)]
453
454 for tokens in relevant_tokens:
455 disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12])
456 sample.add_diskdata([rsect, wsect, use])
457
458 disk_stat_samples.append(sample)
459
460 disk_stats = []
461 for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]):
462 interval = sample1.time - sample2.time
463 if interval == 0:
464 interval = 1
465 sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ]
466 readTput = sums[0] / 2.0 * 100.0 / interval
467 writeTput = sums[1] / 2.0 * 100.0 / interval
468 util = float( sums[2] ) / 10 / interval / numCpu
469 util = max(0.0, min(1.0, util))
470 disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util))
471
472 return disk_stats
473
474def _parse_proc_meminfo_log(file):
475 """
476 Parse file for global memory statistics.
477 The format of relevant lines should be: ^key: value( unit)?
478 """
479 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
480
481 mem_stats = []
482 meminfo_re = re.compile(r'([^ \t:]+):\s*(\d+).*')
483
484 for time, lines in _parse_timed_blocks(file):
485 sample = MemSample(time)
486
487 for line in lines:
488 match = meminfo_re.match(line)
489 if not match:
490 raise ParseError("Invalid meminfo line \"%s\"" % match.groups(0))
491 sample.add_value(match.group(1), int(match.group(2)))
492
493 if sample.valid():
494 mem_stats.append(sample)
495
496 return mem_stats
497
498# if we boot the kernel with: initcall_debug printk.time=1 we can
499# get all manner of interesting data from the dmesg output
500# We turn this into a pseudo-process tree: each event is
501# characterised by a
502# we don't try to detect a "kernel finished" state - since the kernel
503# continues to do interesting things after init is called.
504#
505# sample input:
506# [ 0.000000] ACPI: FACP 3f4fc000 000F4 (v04 INTEL Napa 00000001 MSFT 01000013)
507# ...
508# [ 0.039993] calling migration_init+0x0/0x6b @ 1
509# [ 0.039993] initcall migration_init+0x0/0x6b returned 1 after 0 usecs
510def _parse_dmesg(writer, file):
511 timestamp_re = re.compile ("^\[\s*(\d+\.\d+)\s*]\s+(.*)$")
512 split_re = re.compile ("^(\S+)\s+([\S\+_-]+) (.*)$")
513 processMap = {}
514 idx = 0
515 inc = 1.0 / 1000000
516 kernel = Process(writer, idx, "k-boot", 0, 0.1)
517 processMap['k-boot'] = kernel
518 base_ts = False
519 max_ts = 0
520 for line in file.read().decode('utf-8').split('\n'):
521 t = timestamp_re.match (line)
522 if t is None:
523# print "duff timestamp " + line
524 continue
525
526 time_ms = float (t.group(1)) * 1000
527 # looks like we may have a huge diff after the clock
528 # has been set up. This could lead to huge graph:
529 # so huge we will be killed by the OOM.
530 # So instead of using the plain timestamp we will
531 # use a delta to first one and skip the first one
532 # for convenience
533 if max_ts == 0 and not base_ts and time_ms > 1000:
534 base_ts = time_ms
535 continue
536 max_ts = max(time_ms, max_ts)
537 if base_ts:
538# print "fscked clock: used %f instead of %f" % (time_ms - base_ts, time_ms)
539 time_ms -= base_ts
540 m = split_re.match (t.group(2))
541
542 if m is None:
543 continue
544# print "match: '%s'" % (m.group(1))
545 type = m.group(1)
546 func = m.group(2)
547 rest = m.group(3)
548
549 if t.group(2).startswith ('Write protecting the') or \
550 t.group(2).startswith ('Freeing unused kernel memory'):
551 kernel.duration = time_ms / 10
552 continue
553
554# print "foo: '%s' '%s' '%s'" % (type, func, rest)
555 if type == "calling":
556 ppid = kernel.pid
557 p = re.match ("\@ (\d+)", rest)
558 if p is not None:
559 ppid = float (p.group(1)) // 1000
560# print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms)
561 name = func.split ('+', 1) [0]
562 idx += inc
563 processMap[func] = Process(writer, ppid + idx, name, ppid, time_ms / 10)
564 elif type == "initcall":
565# print "finished: '%s' at '%s'" % (func, time_ms)
566 if func in processMap:
567 process = processMap[func]
568 process.duration = (time_ms / 10) - process.start_time
569 else:
570 print("corrupted init call for %s" % (func))
571
572 elif type == "async_waiting" or type == "async_continuing":
573 continue # ignore
574
575 return processMap.values()
576
577#
578# Parse binary pacct accounting file output if we have one
579# cf. /usr/include/linux/acct.h
580#
581def _parse_pacct(writer, file):
582 # read LE int32
583 def _read_le_int32(file):
584 byts = file.read(4)
585 return (ord(byts[0])) | (ord(byts[1]) << 8) | \
586 (ord(byts[2]) << 16) | (ord(byts[3]) << 24)
587
588 parent_map = {}
589 parent_map[0] = 0
590 while file.read(1) != "": # ignore flags
591 ver = file.read(1)
592 if ord(ver) < 3:
593 print("Invalid version 0x%x" % (ord(ver)))
594 return None
595
596 file.seek (14, 1) # user, group etc.
597 pid = _read_le_int32 (file)
598 ppid = _read_le_int32 (file)
599# print "Parent of %d is %d" % (pid, ppid)
600 parent_map[pid] = ppid
601 file.seek (4 + 4 + 16, 1) # timings
602 file.seek (16, 1) # acct_comm
603 return parent_map
604
605def _parse_paternity_log(writer, file):
606 parent_map = {}
607 parent_map[0] = 0
608 for line in file.read().decode('utf-8').split('\n'):
609 if not line:
610 continue
611 elems = line.split(' ') # <Child> <Parent>
612 if len (elems) >= 2:
613# print "paternity of %d is %d" % (int(elems[0]), int(elems[1]))
614 parent_map[int(elems[0])] = int(elems[1])
615 else:
616 print("Odd paternity line '%s'" % (line))
617 return parent_map
618
619def _parse_cmdline_log(writer, file):
620 cmdLines = {}
621 for block in file.read().decode('utf-8').split('\n\n'):
622 lines = block.split('\n')
623 if len (lines) >= 3:
624# print "Lines '%s'" % (lines[0])
625 pid = int (lines[0])
626 values = {}
627 values['exe'] = lines[1].lstrip(':')
628 args = lines[2].lstrip(':').split('\0')
629 args.pop()
630 values['args'] = args
631 cmdLines[pid] = values
632 return cmdLines
633
634def get_num_cpus(headers):
635 """Get the number of CPUs from the system.cpu header property. As the
636 CPU utilization graphs are relative, the number of CPUs currently makes
637 no difference."""
638 if headers is None:
639 return 1
640 if headers.get("system.cpu.num"):
641 return max (int (headers.get("system.cpu.num")), 1)
642 cpu_model = headers.get("system.cpu")
643 if cpu_model is None:
644 return 1
645 mat = re.match(".*\\((\\d+)\\)", cpu_model)
646 if mat is None:
647 return 1
648 return max (int(mat.group(1)), 1)
649
650def _do_parse(writer, state, filename, file):
651 writer.info("parsing '%s'" % filename)
652 t1 = clock()
653 paths = filename.split("/")
654 task = paths[-1]
655 pn = paths[-2]
656 start = None
657 end = None
658 for line in file:
659 if line.startswith("Started:"):
660 start = int(float(line.split()[-1]))
661 elif line.startswith("Ended:"):
662 end = int(float(line.split()[-1]))
663 if start and end:
664 state.add_process(pn + ":" + task, start, end)
665 t2 = clock()
666 writer.info(" %s seconds" % str(t2-t1))
667 return state
668
669def parse_file(writer, state, filename):
670 if state.filename is None:
671 state.filename = filename
672 basename = os.path.basename(filename)
673 with open(filename, "rb") as file:
674 return _do_parse(writer, state, filename, file)
675
676def parse_paths(writer, state, paths):
677 for path in paths:
678 if state.filename is None:
679 state.filename = path
680 root, extension = os.path.splitext(path)
681 if not(os.path.exists(path)):
682 writer.warn("warning: path '%s' does not exist, ignoring." % path)
683 continue
684 #state.filename = path
685 if os.path.isdir(path):
686 files = sorted([os.path.join(path, f) for f in os.listdir(path)])
687 state = parse_paths(writer, state, files)
688 elif extension in [".tar", ".tgz", ".gz"]:
689 if extension == ".gz":
690 root, extension = os.path.splitext(root)
691 if extension != ".tar":
692 writer.warn("warning: can only handle zipped tar files, not zipped '%s'-files; ignoring" % extension)
693 continue
694 tf = None
695 try:
696 writer.status("parsing '%s'" % path)
697 tf = tarfile.open(path, 'r:*')
698 for name in tf.getnames():
699 state = _do_parse(writer, state, name, tf.extractfile(name))
700 except tarfile.ReadError as error:
701 raise ParseError("error: could not read tarfile '%s': %s." % (path, error))
702 finally:
703 if tf != None:
704 tf.close()
705 else:
706 state = parse_file(writer, state, path)
707 return state
708
709def split_res(res, options):
710 """ Split the res into n pieces """
711 res_list = []
712 if options.num > 1:
713 s_list = sorted(res.start.keys())
714 frag_size = len(s_list) / float(options.num)
715 # Need the top value
716 if frag_size > int(frag_size):
717 frag_size = int(frag_size + 1)
718 else:
719 frag_size = int(frag_size)
720
721 start = 0
722 end = frag_size
723 while start < end:
724 state = Trace(None, [], None)
725 if options.full_time:
726 state.min = min(res.start.keys())
727 state.max = max(res.end.keys())
728 for i in range(start, end):
729 # Add this line for reference
730 #state.add_process(pn + ":" + task, start, end)
731 for p in res.start[s_list[i]]:
732 state.add_process(p, s_list[i], res.processes[p][1])
733 start = end
734 end = end + frag_size
735 if end > len(s_list):
736 end = len(s_list)
737 res_list.append(state)
738 else:
739 res_list.append(res)
740 return res_list
diff --git a/scripts/pybootchartgui/pybootchartgui/process_tree.py b/scripts/pybootchartgui/pybootchartgui/process_tree.py
new file mode 100644
index 0000000000..cf88110b1c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/process_tree.py
@@ -0,0 +1,292 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16class ProcessTree:
17 """ProcessTree encapsulates a process tree. The tree is built from log files
18 retrieved during the boot process. When building the process tree, it is
19 pruned and merged in order to be able to visualize it in a comprehensible
20 manner.
21
22 The following pruning techniques are used:
23
24 * idle processes that keep running during the last process sample
25 (which is a heuristic for a background processes) are removed,
26 * short-lived processes (i.e. processes that only live for the
27 duration of two samples or less) are removed,
28 * the processes used by the boot logger are removed,
29 * exploders (i.e. processes that are known to spawn huge meaningless
30 process subtrees) have their subtrees merged together,
31 * siblings (i.e. processes with the same command line living
32 concurrently -- thread heuristic) are merged together,
33 * process runs (unary trees with processes sharing the command line)
34 are merged together.
35
36 """
37 LOGGER_PROC = 'bootchart-colle'
38 EXPLODER_PROCESSES = set(['hwup'])
39
40 def __init__(self, writer, kernel, psstats, sample_period,
41 monitoredApp, prune, idle, taskstats,
42 accurate_parentage, for_testing = False):
43 self.writer = writer
44 self.process_tree = []
45 self.taskstats = taskstats
46 if psstats is None:
47 process_list = kernel
48 elif kernel is None:
49 process_list = psstats.process_map.values()
50 else:
51 process_list = list(kernel) + list(psstats.process_map.values())
52 self.process_list = sorted(process_list, key = lambda p: p.pid)
53 self.sample_period = sample_period
54
55 self.build()
56 if not accurate_parentage:
57 self.update_ppids_for_daemons(self.process_list)
58
59 self.start_time = self.get_start_time(self.process_tree)
60 self.end_time = self.get_end_time(self.process_tree)
61 self.duration = self.end_time - self.start_time
62 self.idle = idle
63
64 if for_testing:
65 return
66
67 removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False)
68 writer.status("merged %i logger processes" % removed)
69
70 if prune:
71 p_processes = self.prune(self.process_tree, None)
72 p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES)
73 p_threads = self.merge_siblings(self.process_tree)
74 p_runs = self.merge_runs(self.process_tree)
75 writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % (p_processes, p_exploders, p_threads, p_runs))
76
77 self.sort(self.process_tree)
78
79 self.start_time = self.get_start_time(self.process_tree)
80 self.end_time = self.get_end_time(self.process_tree)
81 self.duration = self.end_time - self.start_time
82
83 self.num_proc = self.num_nodes(self.process_tree)
84
85 def build(self):
86 """Build the process tree from the list of top samples."""
87 self.process_tree = []
88 for proc in self.process_list:
89 if not proc.parent:
90 self.process_tree.append(proc)
91 else:
92 proc.parent.child_list.append(proc)
93
94 def sort(self, process_subtree):
95 """Sort process tree."""
96 for p in process_subtree:
97 p.child_list.sort(key = lambda p: p.pid)
98 self.sort(p.child_list)
99
100 def num_nodes(self, process_list):
101 "Counts the number of nodes in the specified process tree."""
102 nodes = 0
103 for proc in process_list:
104 nodes = nodes + self.num_nodes(proc.child_list)
105 return nodes + len(process_list)
106
107 def get_start_time(self, process_subtree):
108 """Returns the start time of the process subtree. This is the start
109 time of the earliest process.
110
111 """
112 if not process_subtree:
113 return 100000000
114 return min( [min(proc.start_time, self.get_start_time(proc.child_list)) for proc in process_subtree] )
115
116 def get_end_time(self, process_subtree):
117 """Returns the end time of the process subtree. This is the end time
118 of the last collected sample.
119
120 """
121 if not process_subtree:
122 return -100000000
123 return max( [max(proc.start_time + proc.duration, self.get_end_time(proc.child_list)) for proc in process_subtree] )
124
125 def get_max_pid(self, process_subtree):
126 """Returns the max PID found in the process tree."""
127 if not process_subtree:
128 return -100000000
129 return max( [max(proc.pid, self.get_max_pid(proc.child_list)) for proc in process_subtree] )
130
131 def update_ppids_for_daemons(self, process_list):
132 """Fedora hack: when loading the system services from rc, runuser(1)
133 is used. This sets the PPID of all daemons to 1, skewing
134 the process tree. Try to detect this and set the PPID of
135 these processes the PID of rc.
136
137 """
138 rcstartpid = -1
139 rcendpid = -1
140 rcproc = None
141 for p in process_list:
142 if p.cmd == "rc" and p.ppid // 1000 == 1:
143 rcproc = p
144 rcstartpid = p.pid
145 rcendpid = self.get_max_pid(p.child_list)
146 if rcstartpid != -1 and rcendpid != -1:
147 for p in process_list:
148 if p.pid > rcstartpid and p.pid < rcendpid and p.ppid // 1000 == 1:
149 p.ppid = rcstartpid
150 p.parent = rcproc
151 for p in process_list:
152 p.child_list = []
153 self.build()
154
155 def prune(self, process_subtree, parent):
156 """Prunes the process tree by removing idle processes and processes
157 that only live for the duration of a single top sample. Sibling
158 processes with the same command line (i.e. threads) are merged
159 together. This filters out sleepy background processes, short-lived
160 processes and bootcharts' analysis tools.
161 """
162 def is_idle_background_process_without_children(p):
163 process_end = p.start_time + p.duration
164 return not p.active and \
165 process_end >= self.start_time + self.duration and \
166 p.start_time > self.start_time and \
167 p.duration > 0.9 * self.duration and \
168 self.num_nodes(p.child_list) == 0
169
170 num_removed = 0
171 idx = 0
172 while idx < len(process_subtree):
173 p = process_subtree[idx]
174 if parent != None or len(p.child_list) == 0:
175
176 prune = False
177 if is_idle_background_process_without_children(p):
178 prune = True
179 elif p.duration <= 2 * self.sample_period:
180 # short-lived process
181 prune = True
182
183 if prune:
184 process_subtree.pop(idx)
185 for c in p.child_list:
186 process_subtree.insert(idx, c)
187 num_removed += 1
188 continue
189 else:
190 num_removed += self.prune(p.child_list, p)
191 else:
192 num_removed += self.prune(p.child_list, p)
193 idx += 1
194
195 return num_removed
196
197 def merge_logger(self, process_subtree, logger_proc, monitored_app, app_tree):
198 """Merges the logger's process subtree. The logger will typically
199 spawn lots of sleep and cat processes, thus polluting the
200 process tree.
201
202 """
203 num_removed = 0
204 for p in process_subtree:
205 is_app_tree = app_tree
206 if logger_proc == p.cmd and not app_tree:
207 is_app_tree = True
208 num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree)
209 # don't remove the logger itself
210 continue
211
212 if app_tree and monitored_app != None and monitored_app == p.cmd:
213 is_app_tree = False
214
215 if is_app_tree:
216 for child in p.child_list:
217 self.merge_processes(p, child)
218 num_removed += 1
219 p.child_list = []
220 else:
221 num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree)
222 return num_removed
223
224 def merge_exploders(self, process_subtree, processes):
225 """Merges specific process subtrees (used for processes which usually
226 spawn huge meaningless process trees).
227
228 """
229 num_removed = 0
230 for p in process_subtree:
231 if processes in processes and len(p.child_list) > 0:
232 subtreemap = self.getProcessMap(p.child_list)
233 for child in subtreemap.values():
234 self.merge_processes(p, child)
235 num_removed += len(subtreemap)
236 p.child_list = []
237 p.cmd += " (+)"
238 else:
239 num_removed += self.merge_exploders(p.child_list, processes)
240 return num_removed
241
242 def merge_siblings(self, process_subtree):
243 """Merges thread processes. Sibling processes with the same command
244 line are merged together.
245
246 """
247 num_removed = 0
248 idx = 0
249 while idx < len(process_subtree)-1:
250 p = process_subtree[idx]
251 nextp = process_subtree[idx+1]
252 if nextp.cmd == p.cmd:
253 process_subtree.pop(idx+1)
254 idx -= 1
255 num_removed += 1
256 p.child_list.extend(nextp.child_list)
257 self.merge_processes(p, nextp)
258 num_removed += self.merge_siblings(p.child_list)
259 idx += 1
260 if len(process_subtree) > 0:
261 p = process_subtree[-1]
262 num_removed += self.merge_siblings(p.child_list)
263 return num_removed
264
265 def merge_runs(self, process_subtree):
266 """Merges process runs. Single child processes which share the same
267 command line with the parent are merged.
268
269 """
270 num_removed = 0
271 idx = 0
272 while idx < len(process_subtree):
273 p = process_subtree[idx]
274 if len(p.child_list) == 1 and p.child_list[0].cmd == p.cmd:
275 child = p.child_list[0]
276 p.child_list = list(child.child_list)
277 self.merge_processes(p, child)
278 num_removed += 1
279 continue
280 num_removed += self.merge_runs(p.child_list)
281 idx += 1
282 return num_removed
283
284 def merge_processes(self, p1, p2):
285 """Merges two process' samples."""
286 p1.samples.extend(p2.samples)
287 p1.samples.sort( key = lambda p: p.time )
288 p1time = p1.start_time
289 p2time = p2.start_time
290 p1.start_time = min(p1time, p2time)
291 pendtime = max(p1time + p1.duration, p2time + p2.duration)
292 p1.duration = pendtime - p1.start_time
diff --git a/scripts/pybootchartgui/pybootchartgui/samples.py b/scripts/pybootchartgui/pybootchartgui/samples.py
new file mode 100644
index 0000000000..015d743aa0
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/samples.py
@@ -0,0 +1,151 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17class DiskStatSample:
18 def __init__(self, time):
19 self.time = time
20 self.diskdata = [0, 0, 0]
21 def add_diskdata(self, new_diskdata):
22 self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ]
23
24class CPUSample:
25 def __init__(self, time, user, sys, io = 0.0, swap = 0.0):
26 self.time = time
27 self.user = user
28 self.sys = sys
29 self.io = io
30 self.swap = swap
31
32 @property
33 def cpu(self):
34 return self.user + self.sys
35
36 def __str__(self):
37 return str(self.time) + "\t" + str(self.user) + "\t" + \
38 str(self.sys) + "\t" + str(self.io) + "\t" + str (self.swap)
39
40class MemSample:
41 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
42
43 def __init__(self, time):
44 self.time = time
45 self.records = {}
46
47 def add_value(self, name, value):
48 if name in MemSample.used_values:
49 self.records[name] = value
50
51 def valid(self):
52 keys = self.records.keys()
53 # discard incomplete samples
54 return [v for v in MemSample.used_values if v not in keys] == []
55
56class ProcessSample:
57 def __init__(self, time, state, cpu_sample):
58 self.time = time
59 self.state = state
60 self.cpu_sample = cpu_sample
61
62 def __str__(self):
63 return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample)
64
65class ProcessStats:
66 def __init__(self, writer, process_map, sample_count, sample_period, start_time, end_time):
67 self.process_map = process_map
68 self.sample_count = sample_count
69 self.sample_period = sample_period
70 self.start_time = start_time
71 self.end_time = end_time
72 writer.info ("%d samples, avg. sample length %f" % (self.sample_count, self.sample_period))
73 writer.info ("process list size: %d" % len (self.process_map.values()))
74
75class Process:
76 def __init__(self, writer, pid, cmd, ppid, start_time):
77 self.writer = writer
78 self.pid = pid
79 self.cmd = cmd
80 self.exe = cmd
81 self.args = []
82 self.ppid = ppid
83 self.start_time = start_time
84 self.duration = 0
85 self.samples = []
86 self.parent = None
87 self.child_list = []
88
89 self.active = None
90 self.last_user_cpu_time = None
91 self.last_sys_cpu_time = None
92
93 self.last_cpu_ns = 0
94 self.last_blkio_delay_ns = 0
95 self.last_swapin_delay_ns = 0
96
97 # split this process' run - triggered by a name change
98 def split(self, writer, pid, cmd, ppid, start_time):
99 split = Process (writer, pid, cmd, ppid, start_time)
100
101 split.last_cpu_ns = self.last_cpu_ns
102 split.last_blkio_delay_ns = self.last_blkio_delay_ns
103 split.last_swapin_delay_ns = self.last_swapin_delay_ns
104
105 return split
106
107 def __str__(self):
108 return " ".join([str(self.pid), self.cmd, str(self.ppid), '[ ' + str(len(self.samples)) + ' samples ]' ])
109
110 def calc_stats(self, samplePeriod):
111 if self.samples:
112 firstSample = self.samples[0]
113 lastSample = self.samples[-1]
114 self.start_time = min(firstSample.time, self.start_time)
115 self.duration = lastSample.time - self.start_time + samplePeriod
116
117 activeCount = sum( [1 for sample in self.samples if sample.cpu_sample and sample.cpu_sample.sys + sample.cpu_sample.user + sample.cpu_sample.io > 0.0] )
118 activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] )
119 self.active = (activeCount>2)
120
121 def calc_load(self, userCpu, sysCpu, interval):
122 userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval
123 sysCpuLoad = float(sysCpu - self.last_sys_cpu_time) / interval
124 cpuLoad = userCpuLoad + sysCpuLoad
125 # normalize
126 if cpuLoad > 1.0:
127 userCpuLoad = userCpuLoad / cpuLoad
128 sysCpuLoad = sysCpuLoad / cpuLoad
129 return (userCpuLoad, sysCpuLoad)
130
131 def set_parent(self, processMap):
132 if self.ppid != None:
133 self.parent = processMap.get (self.ppid)
134 if self.parent == None and self.pid // 1000 > 1 and \
135 not (self.ppid == 2000 or self.pid == 2000): # kernel threads: ppid=2
136 self.writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \
137 % (self.pid,self.cmd,self.ppid))
138
139 def get_end_time(self):
140 return self.start_time + self.duration
141
142class DiskSample:
143 def __init__(self, time, read, write, util):
144 self.time = time
145 self.read = read
146 self.write = write
147 self.util = util
148 self.tput = read + write
149
150 def __str__(self):
151 return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)])
diff --git a/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py b/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py
new file mode 100644
index 0000000000..00fb3bf797
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py
@@ -0,0 +1,105 @@
1import sys, os, re, struct, operator, math
2from collections import defaultdict
3import unittest
4
5sys.path.insert(0, os.getcwd())
6
7import pybootchartgui.parsing as parsing
8import pybootchartgui.main as main
9
10debug = False
11
12def floatEq(f1, f2):
13 return math.fabs(f1-f2) < 0.00001
14
15bootchart_dir = os.path.join(os.path.dirname(sys.argv[0]), '../../examples/1/')
16parser = main._mk_options_parser()
17options, args = parser.parse_args(['--q', bootchart_dir])
18writer = main._mk_writer(options)
19
20class TestBCParser(unittest.TestCase):
21
22 def setUp(self):
23 self.name = "My first unittest"
24 self.rootdir = bootchart_dir
25
26 def mk_fname(self,f):
27 return os.path.join(self.rootdir, f)
28
29 def testParseHeader(self):
30 trace = parsing.Trace(writer, args, options)
31 state = parsing.parse_file(writer, trace, self.mk_fname('header'))
32 self.assertEqual(6, len(state.headers))
33 self.assertEqual(2, parsing.get_num_cpus(state.headers))
34
35 def test_parseTimedBlocks(self):
36 trace = parsing.Trace(writer, args, options)
37 state = parsing.parse_file(writer, trace, self.mk_fname('proc_diskstats.log'))
38 self.assertEqual(141, len(state.disk_stats))
39
40 def testParseProcPsLog(self):
41 trace = parsing.Trace(writer, args, options)
42 state = parsing.parse_file(writer, trace, self.mk_fname('proc_ps.log'))
43 samples = state.ps_stats
44 processes = samples.process_map
45 sorted_processes = [processes[k] for k in sorted(processes.keys())]
46
47 ps_data = open(self.mk_fname('extract2.proc_ps.log'))
48 for index, line in enumerate(ps_data):
49 tokens = line.split();
50 process = sorted_processes[index]
51 if debug:
52 print(tokens[0:4])
53 print(process.pid / 1000, process.cmd, process.ppid, len(process.samples))
54 print('-------------------')
55
56 self.assertEqual(tokens[0], str(process.pid // 1000))
57 self.assertEqual(tokens[1], str(process.cmd))
58 self.assertEqual(tokens[2], str(process.ppid // 1000))
59 self.assertEqual(tokens[3], str(len(process.samples)))
60 ps_data.close()
61
62 def testparseProcDiskStatLog(self):
63 trace = parsing.Trace(writer, args, options)
64 state_with_headers = parsing.parse_file(writer, trace, self.mk_fname('header'))
65 state_with_headers.headers['system.cpu'] = 'xxx (2)'
66 samples = parsing.parse_file(writer, state_with_headers, self.mk_fname('proc_diskstats.log')).disk_stats
67 self.assertEqual(141, len(samples))
68
69 diskstats_data = open(self.mk_fname('extract.proc_diskstats.log'))
70 for index, line in enumerate(diskstats_data):
71 tokens = line.split('\t')
72 sample = samples[index]
73 if debug:
74 print(line.rstrip())
75 print(sample)
76 print('-------------------')
77
78 self.assertEqual(tokens[0], str(sample.time))
79 self.assert_(floatEq(float(tokens[1]), sample.read))
80 self.assert_(floatEq(float(tokens[2]), sample.write))
81 self.assert_(floatEq(float(tokens[3]), sample.util))
82 diskstats_data.close()
83
84 def testparseProcStatLog(self):
85 trace = parsing.Trace(writer, args, options)
86 samples = parsing.parse_file(writer, trace, self.mk_fname('proc_stat.log')).cpu_stats
87 self.assertEqual(141, len(samples))
88
89 stat_data = open(self.mk_fname('extract.proc_stat.log'))
90 for index, line in enumerate(stat_data):
91 tokens = line.split('\t')
92 sample = samples[index]
93 if debug:
94 print(line.rstrip())
95 print(sample)
96 print('-------------------')
97 self.assert_(floatEq(float(tokens[0]), sample.time))
98 self.assert_(floatEq(float(tokens[1]), sample.user))
99 self.assert_(floatEq(float(tokens[2]), sample.sys))
100 self.assert_(floatEq(float(tokens[3]), sample.io))
101 stat_data.close()
102
103if __name__ == '__main__':
104 unittest.main()
105
diff --git a/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py b/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py
new file mode 100644
index 0000000000..6f46a1c03d
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py
@@ -0,0 +1,92 @@
1import sys
2import os
3import unittest
4
5sys.path.insert(0, os.getcwd())
6
7import pybootchartgui.parsing as parsing
8import pybootchartgui.process_tree as process_tree
9import pybootchartgui.main as main
10
11if sys.version_info >= (3, 0):
12 long = int
13
14class TestProcessTree(unittest.TestCase):
15
16 def setUp(self):
17 self.name = "Process tree unittest"
18 self.rootdir = os.path.join(os.path.dirname(sys.argv[0]), '../../examples/1/')
19
20 parser = main._mk_options_parser()
21 options, args = parser.parse_args(['--q', self.rootdir])
22 writer = main._mk_writer(options)
23 trace = parsing.Trace(writer, args, options)
24
25 parsing.parse_file(writer, trace, self.mk_fname('proc_ps.log'))
26 trace.compile(writer)
27 self.processtree = process_tree.ProcessTree(writer, None, trace.ps_stats, \
28 trace.ps_stats.sample_period, None, options.prune, None, None, False, for_testing = True)
29
30 def mk_fname(self,f):
31 return os.path.join(self.rootdir, f)
32
33 def flatten(self, process_tree):
34 flattened = []
35 for p in process_tree:
36 flattened.append(p)
37 flattened.extend(self.flatten(p.child_list))
38 return flattened
39
40 def checkAgainstJavaExtract(self, filename, process_tree):
41 test_data = open(filename)
42 for expected, actual in zip(test_data, self.flatten(process_tree)):
43 tokens = expected.split('\t')
44 self.assertEqual(int(tokens[0]), actual.pid // 1000)
45 self.assertEqual(tokens[1], actual.cmd)
46 self.assertEqual(long(tokens[2]), 10 * actual.start_time)
47 self.assert_(long(tokens[3]) - 10 * actual.duration < 5, "duration")
48 self.assertEqual(int(tokens[4]), len(actual.child_list))
49 self.assertEqual(int(tokens[5]), len(actual.samples))
50 test_data.close()
51
52 def testBuild(self):
53 process_tree = self.processtree.process_tree
54 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.1.log'), process_tree)
55
56 def testMergeLogger(self):
57 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
58 process_tree = self.processtree.process_tree
59 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.2.log'), process_tree)
60
61 def testPrune(self):
62 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
63 self.processtree.prune(self.processtree.process_tree, None)
64 process_tree = self.processtree.process_tree
65 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3b.log'), process_tree)
66
67 def testMergeExploders(self):
68 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
69 self.processtree.prune(self.processtree.process_tree, None)
70 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
71 process_tree = self.processtree.process_tree
72 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3c.log'), process_tree)
73
74 def testMergeSiblings(self):
75 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
76 self.processtree.prune(self.processtree.process_tree, None)
77 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
78 self.processtree.merge_siblings(self.processtree.process_tree)
79 process_tree = self.processtree.process_tree
80 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3d.log'), process_tree)
81
82 def testMergeRuns(self):
83 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
84 self.processtree.prune(self.processtree.process_tree, None)
85 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
86 self.processtree.merge_siblings(self.processtree.process_tree)
87 self.processtree.merge_runs(self.processtree.process_tree)
88 process_tree = self.processtree.process_tree
89 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3e.log'), process_tree)
90
91if __name__ == '__main__':
92 unittest.main()