summaryrefslogtreecommitdiffstats
path: root/scripts/pybootchartgui
diff options
context:
space:
mode:
authorRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
commit8c22ff0d8b70d9b12f0487ef696a7e915b9e3173 (patch)
treeefdc32587159d0050a69009bdf2330a531727d95 /scripts/pybootchartgui
parentd412d2747595c1cc4a5e3ca975e3adc31b2f7891 (diff)
downloadpoky-8c22ff0d8b70d9b12f0487ef696a7e915b9e3173.tar.gz
The poky repository master branch is no longer being updated.
You can either: a) switch to individual clones of bitbake, openembedded-core, meta-yocto and yocto-docs b) use the new bitbake-setup You can find information about either approach in our documentation: https://docs.yoctoproject.org/ Note that "poky" the distro setting is still available in meta-yocto as before and we continue to use and maintain that. Long live Poky! Some further information on the background of this change can be found in: https://lists.openembedded.org/g/openembedded-architecture/message/2179 Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
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.py1165
-rw-r--r--scripts/pybootchartgui/pybootchartgui/gui.py348
l---------scripts/pybootchartgui/pybootchartgui/main.py1
-rw-r--r--scripts/pybootchartgui/pybootchartgui/main.py.in183
-rw-r--r--scripts/pybootchartgui/pybootchartgui/parsing.py872
-rw-r--r--scripts/pybootchartgui/pybootchartgui/process_tree.py292
-rw-r--r--scripts/pybootchartgui/pybootchartgui/samples.py213
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/parser_test.py105
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py92
17 files changed, 0 insertions, 3935 deletions
diff --git a/scripts/pybootchartgui/AUTHORS b/scripts/pybootchartgui/AUTHORS
deleted file mode 100644
index 672b7e9520..0000000000
--- a/scripts/pybootchartgui/AUTHORS
+++ /dev/null
@@ -1,11 +0,0 @@
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
deleted file mode 100644
index ed87acf948..0000000000
--- a/scripts/pybootchartgui/COPYING
+++ /dev/null
@@ -1,340 +0,0 @@
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
deleted file mode 100644
index c65e1315f1..0000000000
--- a/scripts/pybootchartgui/MAINTAINERS
+++ /dev/null
@@ -1,3 +0,0 @@
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
deleted file mode 100644
index 7c5b2fc3a1..0000000000
--- a/scripts/pybootchartgui/NEWS
+++ /dev/null
@@ -1,204 +0,0 @@
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
deleted file mode 100644
index 8642e64679..0000000000
--- a/scripts/pybootchartgui/README.pybootchart
+++ /dev/null
@@ -1,37 +0,0 @@
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
deleted file mode 100755
index 1c4062b42c..0000000000
--- a/scripts/pybootchartgui/pybootchartgui.py
+++ /dev/null
@@ -1,23 +0,0 @@
1#!/usr/bin/env python3
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
deleted file mode 100644
index e69de29bb2..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/__init__.py
+++ /dev/null
diff --git a/scripts/pybootchartgui/pybootchartgui/batch.py b/scripts/pybootchartgui/pybootchartgui/batch.py
deleted file mode 100644
index 05c714e95e..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/batch.py
+++ /dev/null
@@ -1,46 +0,0 @@
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
deleted file mode 100644
index 4d76ce6e8e..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/draw.py
+++ /dev/null
@@ -1,1165 +0,0 @@
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
22import functools
23from operator import itemgetter
24
25class RenderOptions:
26
27 def __init__(self, app_options):
28 # should we render a cumulative CPU time chart
29 self.cumulative = True
30 self.charts = True
31 self.kernel_only = False
32 self.app_options = app_options
33
34 def proc_tree (self, trace):
35 if self.kernel_only:
36 return trace.kernel_tree
37 else:
38 return trace.proc_tree
39
40# Process tree background color.
41BACK_COLOR = (1.0, 1.0, 1.0, 1.0)
42
43WHITE = (1.0, 1.0, 1.0, 1.0)
44# Process tree border color.
45BORDER_COLOR = (0.63, 0.63, 0.63, 1.0)
46# Second tick line color.
47TICK_COLOR = (0.92, 0.92, 0.92, 1.0)
48# 5-second tick line color.
49TICK_COLOR_BOLD = (0.86, 0.86, 0.86, 1.0)
50# Annotation colour
51ANNOTATION_COLOR = (0.63, 0.0, 0.0, 0.5)
52# Text color.
53TEXT_COLOR = (0.0, 0.0, 0.0, 1.0)
54
55# Font family
56FONT_NAME = "Bitstream Vera Sans"
57# Title text font.
58TITLE_FONT_SIZE = 18
59# Default text font.
60TEXT_FONT_SIZE = 12
61# Axis label font.
62AXIS_FONT_SIZE = 11
63# Legend font.
64LEGEND_FONT_SIZE = 12
65
66# CPU load chart color.
67CPU_COLOR = (0.40, 0.55, 0.70, 1.0)
68# IO wait chart color.
69IO_COLOR = (0.76, 0.48, 0.48, 0.5)
70# Disk throughput color.
71DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0)
72
73BYTES_RECEIVED_COLOR = (0.0, 0.0, 1.0, 1.0)
74BYTES_TRANSMITTED_COLOR = (1.0, 0.0, 0.0, 1.0)
75BYTES_RECEIVE_DIFF_COLOR = (0.0, 0.0, 1.0, 0.3)
76BYTES_TRANSMIT_DIFF_COLOR = (1.0, 0.0, 0.0, 0.3)
77# CPU load chart color.
78FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0)
79# Mem cached color
80MEM_CACHED_COLOR = CPU_COLOR
81# Mem used color
82MEM_USED_COLOR = IO_COLOR
83# Buffers color
84MEM_BUFFERS_COLOR = (0.4, 0.4, 0.4, 0.3)
85# Swap color
86MEM_SWAP_COLOR = DISK_TPUT_COLOR
87
88# avg10 CPU pressure color
89CPU_PRESSURE_AVG10_COLOR = (0.0, 0.0, 0.0, 1.0)
90# delta total CPU pressure color
91CPU_PRESSURE_TOTAL_COLOR = CPU_COLOR
92# avg10 IO pressure color
93IO_PRESSURE_AVG10_COLOR = (0.0, 0.0, 0.0, 1.0)
94# delta total IO pressure color
95IO_PRESSURE_TOTAL_COLOR = IO_COLOR
96# avg10 memory pressure color
97MEM_PRESSURE_AVG10_COLOR = (0.0, 0.0, 0.0, 1.0)
98# delta total memory pressure color
99MEM_PRESSURE_TOTAL_COLOR = DISK_TPUT_COLOR
100
101
102
103
104# Process border color.
105PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0)
106# Waiting process color.
107PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5)
108# Running process color.
109PROC_COLOR_R = CPU_COLOR
110# Sleeping process color.
111PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0)
112# Stopped process color.
113PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0)
114# Zombie process color.
115PROC_COLOR_Z = (0.71, 0.71, 0.71, 1.0)
116# Dead process color.
117PROC_COLOR_X = (0.71, 0.71, 0.71, 0.125)
118# Paging process color.
119PROC_COLOR_W = (0.71, 0.71, 0.71, 0.125)
120
121# Process label color.
122PROC_TEXT_COLOR = (0.19, 0.19, 0.19, 1.0)
123# Process label font.
124PROC_TEXT_FONT_SIZE = 12
125
126# Signature color.
127SIG_COLOR = (0.0, 0.0, 0.0, 0.3125)
128# Signature font.
129SIG_FONT_SIZE = 14
130# Signature text.
131SIGNATURE = "http://github.com/mmeeks/bootchart"
132
133# Process dependency line color.
134DEP_COLOR = (0.75, 0.75, 0.75, 1.0)
135# Process dependency line stroke.
136DEP_STROKE = 1.0
137
138# Process description date format.
139DESC_TIME_FORMAT = "mm:ss.SSS"
140
141# Cumulative coloring bits
142HSV_MAX_MOD = 31
143HSV_STEP = 7
144
145# Configure task color
146TASK_COLOR_CONFIGURE = (1.0, 1.0, 0.00, 1.0)
147# Compile task color.
148TASK_COLOR_COMPILE = (0.0, 1.00, 0.00, 1.0)
149# Install task color
150TASK_COLOR_INSTALL = (1.0, 0.00, 1.00, 1.0)
151# Sysroot task color
152TASK_COLOR_SYSROOT = (0.0, 0.00, 1.00, 1.0)
153# Package task color
154TASK_COLOR_PACKAGE = (0.0, 1.00, 1.00, 1.0)
155# Package Write RPM/DEB/IPK task color
156TASK_COLOR_PACKAGE_WRITE = (0.0, 0.50, 0.50, 1.0)
157
158# Distinct colors used for different disk volumnes.
159# If we have more volumns, colors get re-used.
160VOLUME_COLORS = [
161 (1.0, 1.0, 0.00, 1.0),
162 (0.0, 1.00, 0.00, 1.0),
163 (1.0, 0.00, 1.00, 1.0),
164 (0.0, 0.00, 1.00, 1.0),
165 (0.0, 1.00, 1.00, 1.0),
166]
167
168# Process states
169STATE_UNDEFINED = 0
170STATE_RUNNING = 1
171STATE_SLEEPING = 2
172STATE_WAITING = 3
173STATE_STOPPED = 4
174STATE_ZOMBIE = 5
175
176STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, \
177 PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W]
178
179# CumulativeStats Types
180STAT_TYPE_CPU = 0
181STAT_TYPE_IO = 1
182
183# Convert ps process state to an int
184def get_proc_state(flag):
185 return "RSDTZXW".find(flag) + 1
186
187def draw_text(ctx, text, color, x, y):
188 ctx.set_source_rgba(*color)
189 ctx.move_to(x, y)
190 ctx.show_text(text)
191
192def draw_fill_rect(ctx, color, rect):
193 ctx.set_source_rgba(*color)
194 ctx.rectangle(*rect)
195 ctx.fill()
196
197def draw_rect(ctx, color, rect):
198 ctx.set_source_rgba(*color)
199 ctx.rectangle(*rect)
200 ctx.stroke()
201
202def draw_legend_box(ctx, label, fill_color, x, y, s):
203 draw_fill_rect(ctx, fill_color, (x, y - s, s, s))
204 draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s))
205 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
206
207def draw_legend_line(ctx, label, fill_color, x, y, s):
208 draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3))
209 ctx.arc(x + (s + 1)/2.0, y - (s - 3)/2.0, 2.5, 0, 2.0 * math.pi)
210 ctx.fill()
211 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
212
213def draw_label_in_box(ctx, color, label, x, y, w, maxx):
214 label_w = ctx.text_extents(label)[2]
215 label_x = x + w / 2 - label_w / 2
216 if label_w + 10 > w:
217 label_x = x + w + 5
218 if label_x + label_w > maxx:
219 label_x = x - label_w - 5
220 draw_text(ctx, label, color, label_x, y)
221
222def draw_sec_labels(ctx, options, rect, sec_w, nsecs):
223 ctx.set_font_size(AXIS_FONT_SIZE)
224 prev_x = 0
225 for i in range(0, rect[2] + 1, sec_w):
226 if ((i / sec_w) % nsecs == 0) :
227 if options.app_options.as_minutes :
228 label = "%.1f" % (i / sec_w / 60.0)
229 else :
230 label = "%d" % (i / sec_w)
231 label_w = ctx.text_extents(label)[2]
232 x = rect[0] + i - label_w/2
233 if x >= prev_x:
234 draw_text(ctx, label, TEXT_COLOR, x, rect[1] - 2)
235 prev_x = x + label_w
236
237def draw_box_ticks(ctx, rect, sec_w):
238 draw_rect(ctx, BORDER_COLOR, tuple(rect))
239
240 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
241
242 for i in range(sec_w, rect[2] + 1, sec_w):
243 if ((i / sec_w) % 10 == 0) :
244 ctx.set_line_width(1.5)
245 elif sec_w < 5 :
246 continue
247 else :
248 ctx.set_line_width(1.0)
249 if ((i / sec_w) % 30 == 0) :
250 ctx.set_source_rgba(*TICK_COLOR_BOLD)
251 else :
252 ctx.set_source_rgba(*TICK_COLOR)
253 ctx.move_to(rect[0] + i, rect[1] + 1)
254 ctx.line_to(rect[0] + i, rect[1] + rect[3] - 1)
255 ctx.stroke()
256 ctx.set_line_width(1.0)
257
258 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
259
260def draw_annotations(ctx, proc_tree, times, rect):
261 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
262 ctx.set_source_rgba(*ANNOTATION_COLOR)
263 ctx.set_dash([4, 4])
264
265 for time in times:
266 if time is not None:
267 x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration)
268
269 ctx.move_to(rect[0] + x, rect[1] + 1)
270 ctx.line_to(rect[0] + x, rect[1] + rect[3] - 1)
271 ctx.stroke()
272
273 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
274 ctx.set_dash([])
275
276def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range):
277 ctx.set_line_width(0.5)
278 x_shift = proc_tree.start_time
279
280 def transform_point_coords(point, x_base, y_base, \
281 xscale, yscale, x_trans, y_trans):
282 x = (point[0] - x_base) * xscale + x_trans
283 y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3]
284 return x, y
285
286 max_x = max (x for (x, y) in data)
287 max_y = max (y for (x, y) in data)
288 # avoid divide by zero
289 if max_y == 0:
290 max_y = 1.0
291 if (max_x - x_shift):
292 xscale = float (chart_bounds[2]) / (max_x - x_shift)
293 else:
294 xscale = float (chart_bounds[2])
295 # If data_range is given, scale the chart so that the value range in
296 # data_range matches the chart bounds exactly.
297 # Otherwise, scale so that the actual data matches the chart bounds.
298 if data_range and (data_range[1] - data_range[0]):
299 yscale = float(chart_bounds[3]) / (data_range[1] - data_range[0])
300 ybase = data_range[0]
301 else:
302 yscale = float(chart_bounds[3]) / max_y
303 ybase = 0
304
305 first = transform_point_coords (data[0], x_shift, ybase, xscale, yscale, \
306 chart_bounds[0], chart_bounds[1])
307 last = transform_point_coords (data[-1], x_shift, ybase, xscale, yscale, \
308 chart_bounds[0], chart_bounds[1])
309
310 ctx.set_source_rgba(*color)
311 ctx.move_to(*first)
312 for point in data:
313 x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \
314 chart_bounds[0], chart_bounds[1])
315 ctx.line_to(x, y)
316 if fill:
317 ctx.stroke_preserve()
318 ctx.line_to(last[0], chart_bounds[1]+chart_bounds[3])
319 ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3])
320 ctx.line_to(first[0], first[1])
321 ctx.fill()
322 else:
323 ctx.stroke()
324 ctx.set_line_width(1.0)
325
326bar_h = 55
327meminfo_bar_h = 2 * bar_h
328header_h = 60
329# offsets
330off_x, off_y = 220, 10
331sec_w_base = 1 # the width of a second
332proc_h = 16 # the height of a process
333leg_s = 10
334MIN_IMG_W = 800
335CUML_HEIGHT = 2000 # Increased value to accommodate CPU and I/O Graphs
336OPTIONS = None
337
338def extents(options, xscale, trace):
339 start = min(trace.start.keys())
340 end = start
341
342 processes = 0
343 for proc in trace.processes:
344 if not options.app_options.show_all and \
345 trace.processes[proc][1] - trace.processes[proc][0] < options.app_options.mintime:
346 continue
347
348 if trace.processes[proc][1] > end:
349 end = trace.processes[proc][1]
350 processes += 1
351
352 if trace.min is not None and trace.max is not None:
353 start = trace.min
354 end = trace.max
355
356 w = int ((end - start) * sec_w_base * xscale) + 2 * off_x
357 h = proc_h * processes + header_h + 2 * off_y
358
359 if options.charts:
360 if trace.cpu_stats:
361 h += 30 + bar_h
362 if trace.disk_stats:
363 h += 30 + bar_h
364 if trace.cpu_pressure:
365 h += 30 + bar_h
366 if trace.io_pressure:
367 h += 30 + bar_h
368 if trace.mem_pressure:
369 h += 30 + bar_h
370 if trace.monitor_disk:
371 h += 30 + bar_h
372 if trace.mem_stats:
373 h += meminfo_bar_h
374 if trace.net_stats:
375 h += (30 + bar_h) * len(trace.net_stats)
376
377 # Allow for width of process legend and offset
378 if w < (720 + off_x):
379 w = 720 + off_x
380
381 return (w, h)
382
383def clip_visible(clip, rect):
384 xmax = max (clip[0], rect[0])
385 ymax = max (clip[1], rect[1])
386 xmin = min (clip[0] + clip[2], rect[0] + rect[2])
387 ymin = min (clip[1] + clip[3], rect[1] + rect[3])
388 return (xmin > xmax and ymin > ymax)
389
390def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w):
391 proc_tree = options.proc_tree(trace)
392
393 # render bar legend
394 if trace.cpu_stats:
395 ctx.set_font_size(LEGEND_FONT_SIZE)
396
397 draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s)
398 draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s)
399
400 # render I/O wait
401 chart_rect = (off_x, curr_y+30, w, bar_h)
402 if clip_visible (clip, chart_rect):
403 draw_box_ticks (ctx, chart_rect, sec_w)
404 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
405 draw_chart (ctx, IO_COLOR, True, chart_rect, \
406 [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \
407 proc_tree, None)
408 # render CPU load
409 draw_chart (ctx, CPU_COLOR, True, chart_rect, \
410 [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \
411 proc_tree, None)
412
413 curr_y = curr_y + 30 + bar_h
414
415 # render second chart
416 if trace.disk_stats:
417 draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s)
418 draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s)
419
420 # render I/O utilization
421 chart_rect = (off_x, curr_y+30, w, bar_h)
422 if clip_visible (clip, chart_rect):
423 draw_box_ticks (ctx, chart_rect, sec_w)
424 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
425 draw_chart (ctx, IO_COLOR, True, chart_rect, \
426 [(sample.time, sample.util) for sample in trace.disk_stats], \
427 proc_tree, None)
428
429 # render disk throughput
430 max_sample = max (trace.disk_stats, key = lambda s: s.tput)
431 if clip_visible (clip, chart_rect):
432 draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, \
433 [(sample.time, sample.tput) for sample in trace.disk_stats], \
434 proc_tree, None)
435
436 pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration)
437
438 shift_x, shift_y = -20, 20
439 if (pos_x < off_x + 245):
440 shift_x, shift_y = 5, 40
441
442 label = "%dMB/s" % round ((max_sample.tput) / 1024.0)
443 draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y)
444
445 curr_y = curr_y + 30 + bar_h
446
447 if trace.net_stats:
448 for iface, samples in trace.net_stats.items():
449 max_received_sample = max(samples, key=lambda s: s.received_bytes)
450 max_transmitted_sample = max(samples, key=lambda s: s.transmitted_bytes)
451 max_receive_diff_sample = max(samples, key=lambda s: s.receive_diff)
452 max_transmit_diff_sample = max(samples, key=lambda s: s.transmit_diff)
453
454 draw_text(ctx, "Iface: %s" % (iface), TEXT_COLOR, off_x, curr_y+20)
455 draw_legend_line(ctx, "Bytes received (max %d)" % (max_received_sample.received_bytes),
456 BYTES_RECEIVED_COLOR, off_x+150, curr_y+20, leg_s)
457 draw_legend_line(ctx, "Bytes transmitted (max %d)" % (max_transmitted_sample.transmitted_bytes),
458 BYTES_TRANSMITTED_COLOR, off_x+400, curr_y+20, leg_s)
459 draw_legend_box(ctx, "Bytes receive diff (max %d)" % (max_receive_diff_sample.receive_diff),
460 BYTES_RECEIVE_DIFF_COLOR, off_x+650, curr_y+20, leg_s)
461 draw_legend_box(ctx, "Bytes transmit diff (max %d)" % (max_transmit_diff_sample.transmit_diff),
462 BYTES_TRANSMIT_DIFF_COLOR, off_x+900, curr_y+20, leg_s)
463
464
465 chart_rect = (off_x, curr_y + 30, w, bar_h)
466 if clip_visible(clip, chart_rect):
467 draw_box_ticks(ctx, chart_rect, sec_w)
468 draw_annotations(ctx, proc_tree, trace.times, chart_rect)
469
470 if clip_visible (clip, chart_rect):
471 draw_chart (ctx, BYTES_RECEIVED_COLOR, False, chart_rect, \
472 [(sample.time, sample.received_bytes) for sample in samples], \
473 proc_tree, None)
474
475 draw_chart (ctx, BYTES_TRANSMITTED_COLOR, False, chart_rect, \
476 [(sample.time, sample.transmitted_bytes) for sample in samples], \
477 proc_tree, None)
478
479 if clip_visible (clip, chart_rect):
480 draw_chart (ctx, BYTES_RECEIVE_DIFF_COLOR, True, chart_rect, \
481 [(sample.time, sample.receive_diff) for sample in samples], \
482 proc_tree, None)
483
484 draw_chart (ctx, BYTES_TRANSMIT_DIFF_COLOR, True, chart_rect, \
485 [(sample.time, sample.transmit_diff) for sample in samples], \
486 proc_tree, None)
487
488 curr_y = curr_y + 30 + bar_h
489
490 # render CPU pressure chart
491 if trace.cpu_pressure:
492 max_sample_avg = max (trace.cpu_pressure, key = lambda s: s.avg10)
493 max_sample_total = max (trace.cpu_pressure, key = lambda s: s.deltaTotal)
494 draw_legend_line(ctx, "avg10 CPU Pressure (max %d%%)" % (max_sample_avg.avg10), CPU_PRESSURE_AVG10_COLOR, off_x, curr_y+20, leg_s)
495 draw_legend_box(ctx, "delta total CPU Pressure (max %d)" % (max_sample_total.deltaTotal), CPU_PRESSURE_TOTAL_COLOR, off_x + 240, curr_y+20, leg_s)
496
497 # render delta total cpu
498 chart_rect = (off_x, curr_y+30, w, bar_h)
499 if clip_visible (clip, chart_rect):
500 draw_box_ticks (ctx, chart_rect, sec_w)
501 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
502 draw_chart (ctx, CPU_PRESSURE_TOTAL_COLOR, True, chart_rect, \
503 [(sample.time, sample.deltaTotal) for sample in trace.cpu_pressure], \
504 proc_tree, None)
505
506 # render avg10 cpu
507 if clip_visible (clip, chart_rect):
508 draw_chart (ctx, CPU_PRESSURE_AVG10_COLOR, False, chart_rect, \
509 [(sample.time, sample.avg10) for sample in trace.cpu_pressure], \
510 proc_tree, None)
511
512 pos_x = off_x + ((max_sample_avg.time - proc_tree.start_time) * w / proc_tree.duration)
513
514 shift_x, shift_y = -20, 20
515 if (pos_x < off_x + 245):
516 shift_x, shift_y = 5, 40
517
518
519 label = "%d%%" % (max_sample_avg.avg10)
520 draw_text (ctx, label, CPU_PRESSURE_AVG10_COLOR, pos_x + shift_x, curr_y + shift_y)
521
522 curr_y = curr_y + 30 + bar_h
523
524 # render I/O pressure chart
525 if trace.io_pressure:
526 max_sample_avg = max (trace.io_pressure, key = lambda s: s.avg10)
527 max_sample_total = max (trace.io_pressure, key = lambda s: s.deltaTotal)
528 draw_legend_line(ctx, "avg10 I/O Pressure (max %d%%)" % (max_sample_avg.avg10), IO_PRESSURE_AVG10_COLOR, off_x, curr_y+20, leg_s)
529 draw_legend_box(ctx, "delta total I/O Pressure (max %d)" % (max_sample_total.deltaTotal), IO_PRESSURE_TOTAL_COLOR, off_x + 240, curr_y+20, leg_s)
530
531 # render delta total io
532 chart_rect = (off_x, curr_y+30, w, bar_h)
533 if clip_visible (clip, chart_rect):
534 draw_box_ticks (ctx, chart_rect, sec_w)
535 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
536 draw_chart (ctx, IO_PRESSURE_TOTAL_COLOR, True, chart_rect, \
537 [(sample.time, sample.deltaTotal) for sample in trace.io_pressure], \
538 proc_tree, None)
539
540 # render avg10 io
541 if clip_visible (clip, chart_rect):
542 draw_chart (ctx, IO_PRESSURE_AVG10_COLOR, False, chart_rect, \
543 [(sample.time, sample.avg10) for sample in trace.io_pressure], \
544 proc_tree, None)
545
546 pos_x = off_x + ((max_sample_avg.time - proc_tree.start_time) * w / proc_tree.duration)
547
548 shift_x, shift_y = -20, 20
549 if (pos_x < off_x + 245):
550 shift_x, shift_y = 5, 40
551
552
553 label = "%d%%" % (max_sample_avg.avg10)
554 draw_text (ctx, label, IO_PRESSURE_AVG10_COLOR, pos_x + shift_x, curr_y + shift_y)
555
556 curr_y = curr_y + 30 + bar_h
557
558 # render MEM pressure chart
559 if trace.mem_pressure:
560 max_sample_avg = max (trace.mem_pressure, key = lambda s: s.avg10)
561 max_sample_total = max (trace.mem_pressure, key = lambda s: s.deltaTotal)
562 draw_legend_line(ctx, "avg10 MEM Pressure (max %d%%)" % (max_sample_avg.avg10), MEM_PRESSURE_AVG10_COLOR, off_x, curr_y+20, leg_s)
563 draw_legend_box(ctx, "delta total MEM Pressure (max %d)" % (max_sample_total.deltaTotal), MEM_PRESSURE_TOTAL_COLOR, off_x + 240, curr_y+20, leg_s)
564
565 # render delta total mem
566 chart_rect = (off_x, curr_y+30, w, bar_h)
567 if clip_visible (clip, chart_rect):
568 draw_box_ticks (ctx, chart_rect, sec_w)
569 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
570 draw_chart (ctx, MEM_PRESSURE_TOTAL_COLOR, True, chart_rect, \
571 [(sample.time, sample.deltaTotal) for sample in trace.mem_pressure], \
572 proc_tree, None)
573
574 # render avg10 mem
575 if clip_visible (clip, chart_rect):
576 draw_chart (ctx, MEM_PRESSURE_AVG10_COLOR, False, chart_rect, \
577 [(sample.time, sample.avg10) for sample in trace.mem_pressure], \
578 proc_tree, None)
579
580 pos_x = off_x + ((max_sample_avg.time - proc_tree.start_time) * w / proc_tree.duration)
581
582 shift_x, shift_y = -20, 20
583 if (pos_x < off_x + 245):
584 shift_x, shift_y = 5, 40
585
586
587 label = "%d%%" % (max_sample_avg.avg10)
588 draw_text (ctx, label, MEM_PRESSURE_AVG10_COLOR, pos_x + shift_x, curr_y + shift_y)
589
590 curr_y = curr_y + 30 + bar_h
591
592 # render disk space usage
593 #
594 # Draws the amount of disk space used on each volume relative to the
595 # lowest recorded amount. The graphs for each volume are stacked above
596 # each other so that total disk usage is visible.
597 if trace.monitor_disk:
598 ctx.set_font_size(LEGEND_FONT_SIZE)
599 # Determine set of volumes for which we have
600 # information and the minimal amount of used disk
601 # space for each. Currently samples are allowed to
602 # not have a values for all volumes; drawing could be
603 # made more efficient if that wasn't the case.
604 volumes = set()
605 min_used = {}
606 for sample in trace.monitor_disk:
607 for volume, used in sample.records.items():
608 volumes.add(volume)
609 if volume not in min_used or min_used[volume] > used:
610 min_used[volume] = used
611 volumes = sorted(list(volumes))
612 disk_scale = 0
613 for i, volume in enumerate(volumes):
614 volume_scale = max([sample.records[volume] - min_used[volume]
615 for sample in trace.monitor_disk
616 if volume in sample.records])
617 # Does not take length of volume name into account, but fixed offset
618 # works okay in practice.
619 draw_legend_box(ctx, '%s (max: %u MiB)' % (volume, volume_scale / 1024 / 1024),
620 VOLUME_COLORS[i % len(VOLUME_COLORS)],
621 off_x + i * 250, curr_y+20, leg_s)
622 disk_scale += volume_scale
623
624 # render used amount of disk space
625 chart_rect = (off_x, curr_y+30, w, bar_h)
626 if clip_visible (clip, chart_rect):
627 draw_box_ticks (ctx, chart_rect, sec_w)
628 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
629 for i in range(len(volumes), 0, -1):
630 draw_chart (ctx, VOLUME_COLORS[(i - 1) % len(VOLUME_COLORS)], True, chart_rect, \
631 [(sample.time,
632 # Sum up used space of all volumes including the current one
633 # so that the graphs appear as stacked on top of each other.
634 functools.reduce(lambda x,y: x+y,
635 [sample.records[volume] - min_used[volume]
636 for volume in volumes[0:i]
637 if volume in sample.records],
638 0))
639 for sample in trace.monitor_disk], \
640 proc_tree, [0, disk_scale])
641
642 curr_y = curr_y + 30 + bar_h
643
644 # render mem usage
645 chart_rect = (off_x, curr_y+30, w, meminfo_bar_h)
646 mem_stats = trace.mem_stats
647 if mem_stats and clip_visible (clip, chart_rect):
648 mem_scale = max(sample.buffers for sample in mem_stats)
649 draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, off_x, curr_y+20, leg_s)
650 draw_legend_box(ctx, "Used", MEM_USED_COLOR, off_x + 240, curr_y+20, leg_s)
651 draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, off_x + 360, curr_y+20, leg_s)
652 draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.swap)/1024 for sample in mem_stats]), \
653 MEM_SWAP_COLOR, off_x + 480, curr_y+20, leg_s)
654 draw_box_ticks(ctx, chart_rect, sec_w)
655 draw_annotations(ctx, proc_tree, trace.times, chart_rect)
656 draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \
657 [(sample.time, sample.buffers) for sample in trace.mem_stats], \
658 proc_tree, [0, mem_scale])
659 draw_chart(ctx, MEM_USED_COLOR, True, chart_rect, \
660 [(sample.time, sample.used) for sample in mem_stats], \
661 proc_tree, [0, mem_scale])
662 draw_chart(ctx, MEM_CACHED_COLOR, True, chart_rect, \
663 [(sample.time, sample.cached) for sample in mem_stats], \
664 proc_tree, [0, mem_scale])
665 draw_chart(ctx, MEM_SWAP_COLOR, False, chart_rect, \
666 [(sample.time, float(sample.swap)) for sample in mem_stats], \
667 proc_tree, None)
668
669 curr_y = curr_y + meminfo_bar_h
670
671 return curr_y
672
673def render_processes_chart(ctx, options, trace, curr_y, width, h, sec_w):
674 chart_rect = [off_x, curr_y+header_h, width, h - curr_y - 1 * off_y - header_h ]
675
676 draw_legend_box (ctx, "Configure", \
677 TASK_COLOR_CONFIGURE, off_x , curr_y + 45, leg_s)
678 draw_legend_box (ctx, "Compile", \
679 TASK_COLOR_COMPILE, off_x+120, curr_y + 45, leg_s)
680 draw_legend_box (ctx, "Install", \
681 TASK_COLOR_INSTALL, off_x+240, curr_y + 45, leg_s)
682 draw_legend_box (ctx, "Populate Sysroot", \
683 TASK_COLOR_SYSROOT, off_x+360, curr_y + 45, leg_s)
684 draw_legend_box (ctx, "Package", \
685 TASK_COLOR_PACKAGE, off_x+480, curr_y + 45, leg_s)
686 draw_legend_box (ctx, "Package Write", \
687 TASK_COLOR_PACKAGE_WRITE, off_x+600, curr_y + 45, leg_s)
688
689 ctx.set_font_size(PROC_TEXT_FONT_SIZE)
690
691 draw_box_ticks(ctx, chart_rect, sec_w)
692 draw_sec_labels(ctx, options, chart_rect, sec_w, 30)
693
694 y = curr_y+header_h
695
696 offset = trace.min or min(trace.start.keys())
697 for start in sorted(trace.start.keys()):
698 for process in sorted(trace.start[start]):
699 elapsed_time = trace.processes[process][1] - start
700 if not options.app_options.show_all and \
701 elapsed_time < options.app_options.mintime:
702 continue
703 task = process.split(":")[1]
704
705 #print(process)
706 #print(trace.processes[process][1])
707 #print(s)
708
709 x = chart_rect[0] + (start - offset) * sec_w
710 w = elapsed_time * sec_w
711
712 def set_alfa(color, alfa):
713 clist = list(color)
714 clist[-1] = alfa
715 return tuple(clist)
716
717 #print("proc at %s %s %s %s" % (x, y, w, proc_h))
718 col = None
719 if task == "do_compile":
720 col = TASK_COLOR_COMPILE
721 elif "do_compile" in task:
722 col = set_alfa(TASK_COLOR_COMPILE, 0.25)
723 elif task == "do_configure":
724 col = TASK_COLOR_CONFIGURE
725 elif "do_configure" in task:
726 col = set_alfa(TASK_COLOR_CONFIGURE, 0.25)
727 elif task == "do_install":
728 col = TASK_COLOR_INSTALL
729 elif task == "do_populate_sysroot":
730 col = TASK_COLOR_SYSROOT
731 elif task == "do_package":
732 col = TASK_COLOR_PACKAGE
733 elif task == "do_package_write_rpm" or \
734 task == "do_package_write_deb" or \
735 task == "do_package_write_ipk":
736 col = TASK_COLOR_PACKAGE_WRITE
737 else:
738 col = WHITE
739
740 if col:
741 draw_fill_rect(ctx, col, (x, y, w, proc_h))
742 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
743
744 # Show elapsed time for each task
745 process = "%ds %s" % (elapsed_time, process)
746 draw_label_in_box(ctx, PROC_TEXT_COLOR, process, x, y + proc_h - 4, w, width)
747
748 y = y + proc_h
749
750 return curr_y
751
752#
753# Render the chart.
754#
755def render(ctx, options, xscale, trace):
756 (w, h) = extents (options, xscale, trace)
757 global OPTIONS
758 OPTIONS = options.app_options
759
760 # x, y, w, h
761 clip = ctx.clip_extents()
762
763 sec_w = int (xscale * sec_w_base)
764 ctx.set_line_width(1.0)
765 ctx.select_font_face(FONT_NAME)
766 draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h))
767 w -= 2*off_x
768 curr_y = off_y;
769
770 if options.charts:
771 curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h, sec_w)
772
773 curr_y = render_processes_chart (ctx, options, trace, curr_y, w, h, sec_w)
774
775 return
776
777 proc_tree = options.proc_tree (trace)
778
779 # draw the title and headers
780 if proc_tree.idle:
781 duration = proc_tree.idle
782 else:
783 duration = proc_tree.duration
784
785 if not options.kernel_only:
786 curr_y = draw_header (ctx, trace.headers, duration)
787 else:
788 curr_y = off_y;
789
790 # draw process boxes
791 proc_height = h
792 if proc_tree.taskstats and options.cumulative:
793 proc_height -= CUML_HEIGHT
794
795 draw_process_bar_chart(ctx, clip, options, proc_tree, trace.times,
796 curr_y, w, proc_height, sec_w)
797
798 curr_y = proc_height
799 ctx.set_font_size(SIG_FONT_SIZE)
800 draw_text(ctx, SIGNATURE, SIG_COLOR, off_x + 5, proc_height - 8)
801
802 # draw a cumulative CPU-time-per-process graph
803 if proc_tree.taskstats and options.cumulative:
804 cuml_rect = (off_x, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2)
805 if clip_visible (clip, cuml_rect):
806 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_CPU)
807
808 # draw a cumulative I/O-time-per-process graph
809 if proc_tree.taskstats and options.cumulative:
810 cuml_rect = (off_x, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2)
811 if clip_visible (clip, cuml_rect):
812 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_IO)
813
814def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h, sec_w):
815 header_size = 0
816 if not options.kernel_only:
817 draw_legend_box (ctx, "Running (%cpu)",
818 PROC_COLOR_R, off_x , curr_y + 45, leg_s)
819 draw_legend_box (ctx, "Unint.sleep (I/O)",
820 PROC_COLOR_D, off_x+120, curr_y + 45, leg_s)
821 draw_legend_box (ctx, "Sleeping",
822 PROC_COLOR_S, off_x+240, curr_y + 45, leg_s)
823 draw_legend_box (ctx, "Zombie",
824 PROC_COLOR_Z, off_x+360, curr_y + 45, leg_s)
825 header_size = 45
826
827 chart_rect = [off_x, curr_y + header_size + 15,
828 w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h]
829 ctx.set_font_size (PROC_TEXT_FONT_SIZE)
830
831 draw_box_ticks (ctx, chart_rect, sec_w)
832 if sec_w > 100:
833 nsec = 1
834 else:
835 nsec = 5
836 draw_sec_labels (ctx, options, chart_rect, sec_w, nsec)
837 draw_annotations (ctx, proc_tree, times, chart_rect)
838
839 y = curr_y + 60
840 for root in proc_tree.process_tree:
841 draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect, clip)
842 y = y + proc_h * proc_tree.num_nodes([root])
843
844
845def draw_header (ctx, headers, duration):
846 toshow = [
847 ('system.uname', 'uname', lambda s: s),
848 ('system.release', 'release', lambda s: s),
849 ('system.cpu', 'CPU', lambda s: re.sub(r'model name\s*:\s*', '', s, 1)),
850 ('system.kernel.options', 'kernel options', lambda s: s),
851 ]
852
853 header_y = ctx.font_extents()[2] + 10
854 ctx.set_font_size(TITLE_FONT_SIZE)
855 draw_text(ctx, headers['title'], TEXT_COLOR, off_x, header_y)
856 ctx.set_font_size(TEXT_FONT_SIZE)
857
858 for (headerkey, headertitle, mangle) in toshow:
859 header_y += ctx.font_extents()[2]
860 if headerkey in headers:
861 value = headers.get(headerkey)
862 else:
863 value = ""
864 txt = headertitle + ': ' + mangle(value)
865 draw_text(ctx, txt, TEXT_COLOR, off_x, header_y)
866
867 dur = duration / 100.0
868 txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60))
869 if headers.get('system.maxpid') is not None:
870 txt = txt + ' max pid: %s' % (headers.get('system.maxpid'))
871
872 header_y += ctx.font_extents()[2]
873 draw_text (ctx, txt, TEXT_COLOR, off_x, header_y)
874
875 return header_y
876
877def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) :
878 x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration)
879 w = ((proc.duration) * rect[2] / proc_tree.duration)
880
881 draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip)
882 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
883 ipid = int(proc.pid)
884 if not OPTIONS.show_all:
885 cmdString = proc.cmd
886 else:
887 cmdString = ''
888 if (OPTIONS.show_pid or OPTIONS.show_all) and ipid != 0:
889 cmdString = cmdString + " [" + str(ipid // 1000) + "]"
890 if OPTIONS.show_all:
891 if proc.args:
892 cmdString = cmdString + " '" + "' '".join(proc.args) + "'"
893 else:
894 cmdString = cmdString + " " + proc.exe
895
896 draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, rect[0] + rect[2])
897
898 next_y = y + proc_h
899 for child in proc.child_list:
900 if next_y > clip[1] + clip[3]:
901 break
902 child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect, clip)
903 draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h)
904 next_y = next_y + proc_h * proc_tree.num_nodes([child])
905
906 return x, y
907
908
909def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip):
910
911 if y > clip[1] + clip[3] or y + proc_h + 2 < clip[1]:
912 return
913
914 draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h))
915
916 last_tx = -1
917 for sample in proc.samples :
918 tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration))
919
920 # samples are sorted chronologically
921 if tx < clip[0]:
922 continue
923 if tx > clip[0] + clip[2]:
924 break
925
926 tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration))
927 if last_tx != -1 and abs(last_tx - tx) <= tw:
928 tw -= last_tx - tx
929 tx = last_tx
930 tw = max (tw, 1) # nice to see at least something
931
932 last_tx = tx + tw
933 state = get_proc_state( sample.state )
934
935 color = STATE_COLORS[state]
936 if state == STATE_RUNNING:
937 alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0)
938 color = tuple(list(PROC_COLOR_R[0:3]) + [alpha])
939# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha)
940 elif state == STATE_SLEEPING:
941 continue
942
943 draw_fill_rect(ctx, color, (tx, y, tw, proc_h))
944
945def draw_process_connecting_lines(ctx, px, py, x, y, proc_h):
946 ctx.set_source_rgba(*DEP_COLOR)
947 ctx.set_dash([2, 2])
948 if abs(px - x) < 3:
949 dep_off_x = 3
950 dep_off_y = proc_h / 4
951 ctx.move_to(x, y + proc_h / 2)
952 ctx.line_to(px - dep_off_x, y + proc_h / 2)
953 ctx.line_to(px - dep_off_x, py - dep_off_y)
954 ctx.line_to(px, py - dep_off_y)
955 else:
956 ctx.move_to(x, y + proc_h / 2)
957 ctx.line_to(px, y + proc_h / 2)
958 ctx.line_to(px, py)
959 ctx.stroke()
960 ctx.set_dash([])
961
962# elide the bootchart collector - it is quite distorting
963def elide_bootchart(proc):
964 return proc.cmd == 'bootchartd' or proc.cmd == 'bootchart-colle'
965
966class CumlSample:
967 def __init__(self, proc):
968 self.cmd = proc.cmd
969 self.samples = []
970 self.merge_samples (proc)
971 self.color = None
972
973 def merge_samples(self, proc):
974 self.samples.extend (proc.samples)
975 self.samples.sort (key = lambda p: p.time)
976
977 def next(self):
978 global palette_idx
979 palette_idx += HSV_STEP
980 return palette_idx
981
982 def get_color(self):
983 if self.color is None:
984 i = self.next() % HSV_MAX_MOD
985 h = 0.0
986 if i != 0:
987 h = (1.0 * i) / HSV_MAX_MOD
988 s = 0.5
989 v = 1.0
990 c = colorsys.hsv_to_rgb (h, s, v)
991 self.color = (c[0], c[1], c[2], 1.0)
992 return self.color
993
994
995def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type):
996 global palette_idx
997 palette_idx = 0
998
999 time_hash = {}
1000 total_time = 0.0
1001 m_proc_list = {}
1002
1003 if stat_type is STAT_TYPE_CPU:
1004 sample_value = 'cpu'
1005 else:
1006 sample_value = 'io'
1007 for proc in proc_tree.process_list:
1008 if elide_bootchart(proc):
1009 continue
1010
1011 for sample in proc.samples:
1012 total_time += getattr(sample.cpu_sample, sample_value)
1013 if not sample.time in time_hash:
1014 time_hash[sample.time] = 1
1015
1016 # merge pids with the same cmd
1017 if not proc.cmd in m_proc_list:
1018 m_proc_list[proc.cmd] = CumlSample (proc)
1019 continue
1020 s = m_proc_list[proc.cmd]
1021 s.merge_samples (proc)
1022
1023 # all the sample times
1024 times = sorted(time_hash)
1025 if len (times) < 2:
1026 print("degenerate boot chart")
1027 return
1028
1029 pix_per_ns = chart_bounds[3] / total_time
1030# print "total time: %g pix-per-ns %g" % (total_time, pix_per_ns)
1031
1032 # FIXME: we have duplicates in the process list too [!] - why !?
1033
1034 # Render bottom up, left to right
1035 below = {}
1036 for time in times:
1037 below[time] = chart_bounds[1] + chart_bounds[3]
1038
1039 # same colors each time we render
1040 random.seed (0)
1041
1042 ctx.set_line_width(1)
1043
1044 legends = []
1045 labels = []
1046
1047 # render each pid in order
1048 for cs in m_proc_list.values():
1049 row = {}
1050 cuml = 0.0
1051
1052 # print "pid : %s -> %g samples %d" % (proc.cmd, cuml, len (cs.samples))
1053 for sample in cs.samples:
1054 cuml += getattr(sample.cpu_sample, sample_value)
1055 row[sample.time] = cuml
1056
1057 process_total_time = cuml
1058
1059 # hide really tiny processes
1060 if cuml * pix_per_ns <= 2:
1061 continue
1062
1063 last_time = times[0]
1064 y = last_below = below[last_time]
1065 last_cuml = cuml = 0.0
1066
1067 ctx.set_source_rgba(*cs.get_color())
1068 for time in times:
1069 render_seg = False
1070
1071 # did the underlying trend increase ?
1072 if below[time] != last_below:
1073 last_below = below[last_time]
1074 last_cuml = cuml
1075 render_seg = True
1076
1077 # did we move up a pixel increase ?
1078 if time in row:
1079 nc = round (row[time] * pix_per_ns)
1080 if nc != cuml:
1081 last_cuml = cuml
1082 cuml = nc
1083 render_seg = True
1084
1085# if last_cuml > cuml:
1086# assert fail ... - un-sorted process samples
1087
1088 # draw the trailing rectangle from the last time to
1089 # before now, at the height of the last segment.
1090 if render_seg:
1091 w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration) + 1
1092 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
1093 ctx.rectangle (x, below[last_time] - last_cuml, w, last_cuml)
1094 ctx.fill()
1095# ctx.stroke()
1096 last_time = time
1097 y = below [time] - cuml
1098
1099 row[time] = y
1100
1101 # render the last segment
1102 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
1103 y = below[last_time] - cuml
1104 ctx.rectangle (x, y, chart_bounds[2] - x, cuml)
1105 ctx.fill()
1106# ctx.stroke()
1107
1108 # render legend if it will fit
1109 if cuml > 8:
1110 label = cs.cmd
1111 extnts = ctx.text_extents(label)
1112 label_w = extnts[2]
1113 label_h = extnts[3]
1114# print "Text extents %g by %g" % (label_w, label_h)
1115 labels.append((label,
1116 chart_bounds[0] + chart_bounds[2] - label_w - off_x * 2,
1117 y + (cuml + label_h) / 2))
1118 if cs in legends:
1119 print("ARGH - duplicate process in list !")
1120
1121 legends.append ((cs, process_total_time))
1122
1123 below = row
1124
1125 # render grid-lines over the top
1126 draw_box_ticks(ctx, chart_bounds, sec_w)
1127
1128 # render labels
1129 for l in labels:
1130 draw_text(ctx, l[0], TEXT_COLOR, l[1], l[2])
1131
1132 # Render legends
1133 font_height = 20
1134 label_width = 300
1135 LEGENDS_PER_COL = 15
1136 LEGENDS_TOTAL = 45
1137 ctx.set_font_size (TITLE_FONT_SIZE)
1138 dur_secs = duration / 100
1139 cpu_secs = total_time / 1000000000
1140
1141 # misleading - with multiple CPUs ...
1142# idle = ((dur_secs - cpu_secs) / dur_secs) * 100.0
1143 if stat_type is STAT_TYPE_CPU:
1144 label = "Cumulative CPU usage, by process; total CPU: " \
1145 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
1146 else:
1147 label = "Cumulative I/O usage, by process; total I/O: " \
1148 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
1149
1150 draw_text(ctx, label, TEXT_COLOR, chart_bounds[0] + off_x,
1151 chart_bounds[1] + font_height)
1152
1153 i = 0
1154 legends = sorted(legends, key=itemgetter(1), reverse=True)
1155 ctx.set_font_size(TEXT_FONT_SIZE)
1156 for t in legends:
1157 cs = t[0]
1158 time = t[1]
1159 x = chart_bounds[0] + off_x + int (i/LEGENDS_PER_COL) * label_width
1160 y = chart_bounds[1] + font_height * ((i % LEGENDS_PER_COL) + 2)
1161 str = "%s - %.0f(ms) (%2.2f%%)" % (cs.cmd, time/1000000, (time/total_time) * 100.0)
1162 draw_legend_box(ctx, str, cs.color, x, y, leg_s)
1163 i = i + 1
1164 if i >= LEGENDS_TOTAL:
1165 break
diff --git a/scripts/pybootchartgui/pybootchartgui/gui.py b/scripts/pybootchartgui/pybootchartgui/gui.py
deleted file mode 100644
index e1fe915563..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/gui.py
+++ /dev/null
@@ -1,348 +0,0 @@
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 gi
17gi.require_version('Gtk', '3.0')
18from gi.repository import Gtk as gtk
19from gi.repository import Gtk
20from gi.repository import Gdk
21from gi.repository import GObject as gobject
22from gi.repository import GObject
23
24from . import draw
25from .draw import RenderOptions
26
27class PyBootchartWidget(gtk.DrawingArea, gtk.Scrollable):
28 __gsignals__ = {
29 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, Gdk.Event)),
30 'position-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)),
31 'set-scroll-adjustments' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment))
32 }
33
34 hadjustment = GObject.property(type=Gtk.Adjustment,
35 default=Gtk.Adjustment(),
36 flags=GObject.PARAM_READWRITE)
37 hscroll_policy = GObject.property(type=Gtk.ScrollablePolicy,
38 default=Gtk.ScrollablePolicy.MINIMUM,
39 flags=GObject.PARAM_READWRITE)
40 vadjustment = GObject.property(type=Gtk.Adjustment,
41 default=Gtk.Adjustment(),
42 flags=GObject.PARAM_READWRITE)
43 vscroll_policy = GObject.property(type=Gtk.ScrollablePolicy,
44 default=Gtk.ScrollablePolicy.MINIMUM,
45 flags=GObject.PARAM_READWRITE)
46
47 def __init__(self, trace, options, xscale):
48 gtk.DrawingArea.__init__(self)
49
50 self.trace = trace
51 self.options = options
52
53 self.set_can_focus(True)
54
55 self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK)
56 self.connect("button-press-event", self.on_area_button_press)
57 self.connect("button-release-event", self.on_area_button_release)
58 self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK)
59 self.connect("motion-notify-event", self.on_area_motion_notify)
60 self.connect("scroll-event", self.on_area_scroll_event)
61 self.connect('key-press-event', self.on_key_press_event)
62
63 self.connect("size-allocate", self.on_allocation_size_changed)
64 self.connect("position-changed", self.on_position_changed)
65
66 self.connect("draw", self.on_draw)
67
68 self.zoom_ratio = 1.0
69 self.xscale = xscale
70 self.x, self.y = 0.0, 0.0
71
72 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
73 self.our_width, self.our_height = self.chart_width, self.chart_height
74
75 self.hadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
76 self.vadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
77 self.vadj.connect('value-changed', self.on_adjustments_changed)
78 self.hadj.connect('value-changed', self.on_adjustments_changed)
79
80 def bound_vals(self):
81 self.x = max(0, self.x)
82 self.y = max(0, self.y)
83 self.x = min(self.chart_width - self.our_width, self.x)
84 self.y = min(self.chart_height - self.our_height, self.y)
85
86 def on_draw(self, darea, cr):
87 # set a clip region
88 #cr.rectangle(
89 # self.x, self.y,
90 # self.chart_width, self.chart_height
91 #)
92 #cr.clip()
93 cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
94 cr.paint()
95 cr.scale(self.zoom_ratio, self.zoom_ratio)
96 cr.translate(-self.x, -self.y)
97 draw.render(cr, self.options, self.xscale, self.trace)
98
99 def position_changed(self):
100 self.emit("position-changed", self.x, self.y)
101
102 ZOOM_INCREMENT = 1.25
103
104 def zoom_image (self, zoom_ratio):
105 self.zoom_ratio = zoom_ratio
106 self._set_scroll_adjustments()
107 self.queue_draw()
108
109 def zoom_to_rect (self, rect):
110 zoom_ratio = float(rect.width)/float(self.chart_width)
111 self.zoom_image(zoom_ratio)
112 self.x = 0
113 self.position_changed()
114
115 def set_xscale(self, xscale):
116 old_mid_x = self.x + self.hadj.page_size / 2
117 self.xscale = xscale
118 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
119 new_x = old_mid_x
120 self.zoom_image (self.zoom_ratio)
121
122 def on_expand(self, action):
123 self.set_xscale (int(self.xscale * 1.5 + 0.5))
124
125 def on_contract(self, action):
126 self.set_xscale (max(int(self.xscale / 1.5), 1))
127
128 def on_zoom_in(self, action):
129 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
130
131 def on_zoom_out(self, action):
132 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
133
134 def on_zoom_fit(self, action):
135 self.zoom_to_rect(self.get_allocation())
136
137 def on_zoom_100(self, action):
138 self.zoom_image(1.0)
139 self.set_xscale(1.0)
140
141 def show_toggled(self, button):
142 self.options.app_options.show_all = button.get_property ('active')
143 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
144 self._set_scroll_adjustments()
145 self.queue_draw()
146
147 POS_INCREMENT = 100
148
149 def on_key_press_event(self, widget, event):
150 if event.keyval == Gdk.keyval_from_name("Left"):
151 self.x -= self.POS_INCREMENT/self.zoom_ratio
152 elif event.keyval == Gdk.keyval_from_name("Right"):
153 self.x += self.POS_INCREMENT/self.zoom_ratio
154 elif event.keyval == Gdk.keyval_from_name("Up"):
155 self.y -= self.POS_INCREMENT/self.zoom_ratio
156 elif event.keyval == Gdk.keyval_from_name("Down"):
157 self.y += self.POS_INCREMENT/self.zoom_ratio
158 else:
159 return False
160 self.bound_vals()
161 self.queue_draw()
162 self.position_changed()
163 return True
164
165 def on_area_button_press(self, area, event):
166 if event.button == 2 or event.button == 1:
167 window = self.get_window()
168 window.set_cursor(Gdk.Cursor(Gdk.CursorType.FLEUR))
169 self.prevmousex = event.x
170 self.prevmousey = event.y
171 if event.type not in (Gdk.EventType.BUTTON_PRESS, Gdk.EventType.BUTTON_RELEASE):
172 return False
173 return False
174
175 def on_area_button_release(self, area, event):
176 if event.button == 2 or event.button == 1:
177 window = self.get_window()
178 window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW))
179 self.prevmousex = None
180 self.prevmousey = None
181 return True
182 return False
183
184 def on_area_scroll_event(self, area, event):
185 if event.state & Gdk.CONTROL_MASK:
186 if event.direction == Gdk.SCROLL_UP:
187 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
188 return True
189 if event.direction == Gdk.SCROLL_DOWN:
190 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
191 return True
192 return False
193
194 def on_area_motion_notify(self, area, event):
195 state = event.state
196 if state & Gdk.ModifierType.BUTTON2_MASK or state & Gdk.ModifierType.BUTTON1_MASK:
197 x, y = int(event.x), int(event.y)
198 # pan the image
199 self.x += (self.prevmousex - x)/self.zoom_ratio
200 self.y += (self.prevmousey - y)/self.zoom_ratio
201 self.bound_vals()
202 self.queue_draw()
203 self.prevmousex = x
204 self.prevmousey = y
205 self.position_changed()
206 return True
207
208 def on_allocation_size_changed(self, widget, allocation):
209 self.hadj.page_size = allocation.width
210 self.hadj.page_increment = allocation.width * 0.9
211 self.vadj.page_size = allocation.height
212 self.vadj.page_increment = allocation.height * 0.9
213 self.our_width = allocation.width
214 if self.chart_width < self.our_width:
215 self.our_width = self.chart_width
216 self.our_height = allocation.height
217 if self.chart_height < self.our_height:
218 self.our_height = self.chart_height
219 self._set_scroll_adjustments()
220
221 def _set_adj_upper(self, adj, upper):
222
223 if adj.get_upper() != upper:
224 adj.set_upper(upper)
225
226 def _set_scroll_adjustments(self):
227 self._set_adj_upper (self.hadj, self.zoom_ratio * (self.chart_width - self.our_width))
228 self._set_adj_upper (self.vadj, self.zoom_ratio * (self.chart_height - self.our_height))
229
230 def on_adjustments_changed(self, adj):
231 self.x = self.hadj.get_value() / self.zoom_ratio
232 self.y = self.vadj.get_value() / self.zoom_ratio
233 self.queue_draw()
234
235 def on_position_changed(self, widget, x, y):
236 self.hadj.set_value(x * self.zoom_ratio)
237 #self.hadj.value_changed()
238 self.vadj.set_value(y * self.zoom_ratio)
239
240class PyBootchartShell(gtk.VBox):
241 ui = '''
242 <ui>
243 <toolbar name="ToolBar">
244 <toolitem action="Expand"/>
245 <toolitem action="Contract"/>
246 <separator/>
247 <toolitem action="ZoomIn"/>
248 <toolitem action="ZoomOut"/>
249 <toolitem action="ZoomFit"/>
250 <toolitem action="Zoom100"/>
251 </toolbar>
252 </ui>
253 '''
254 def __init__(self, window, trace, options, xscale):
255 gtk.VBox.__init__(self)
256
257 self.widget2 = PyBootchartWidget(trace, options, xscale)
258
259 # Create a UIManager instance
260 uimanager = self.uimanager = gtk.UIManager()
261
262 # Add the accelerator group to the toplevel window
263 accelgroup = uimanager.get_accel_group()
264 window.add_accel_group(accelgroup)
265
266 # Create an ActionGroup
267 actiongroup = gtk.ActionGroup('Actions')
268 self.actiongroup = actiongroup
269
270 # Create actions
271 actiongroup.add_actions((
272 ('Expand', gtk.STOCK_ADD, None, None, None, self.widget2.on_expand),
273 ('Contract', gtk.STOCK_REMOVE, None, None, None, self.widget2.on_contract),
274 ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget2.on_zoom_in),
275 ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget2.on_zoom_out),
276 ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget2.on_zoom_fit),
277 ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget2.on_zoom_100),
278 ))
279
280 # Add the actiongroup to the uimanager
281 uimanager.insert_action_group(actiongroup, 0)
282
283 # Add a UI description
284 uimanager.add_ui_from_string(self.ui)
285
286 # Scrolled window
287 scrolled = gtk.ScrolledWindow(self.widget2.hadj, self.widget2.vadj)
288 scrolled.add(self.widget2)
289
290 #scrolled.set_hadjustment()
291 #scrolled.set_vadjustment(self.widget2.vadj)
292 scrolled.set_policy(gtk.PolicyType.ALWAYS, gtk.PolicyType.ALWAYS)
293
294 # toolbar / h-box
295 hbox = gtk.HBox(False, 8)
296
297 # Create a Toolbar
298 toolbar = uimanager.get_widget('/ToolBar')
299 hbox.pack_start(toolbar, True, True, 0)
300
301 if not options.kernel_only:
302 # Misc. options
303 button = gtk.CheckButton("Show more")
304 button.connect ('toggled', self.widget2.show_toggled)
305 button.set_active(options.app_options.show_all)
306 hbox.pack_start (button, False, True, 0)
307
308 self.pack_start(hbox, False, True, 0)
309 self.pack_start(scrolled, True, True, 0)
310 self.show_all()
311
312 def grab_focus(self, window):
313 window.set_focus(self.widget2)
314
315
316class PyBootchartWindow(gtk.Window):
317
318 def __init__(self, trace, app_options):
319 gtk.Window.__init__(self)
320
321 window = self
322 window.set_title("Bootchart %s" % trace.filename)
323 window.set_default_size(750, 550)
324
325 tab_page = gtk.Notebook()
326 tab_page.show()
327 window.add(tab_page)
328
329 full_opts = RenderOptions(app_options)
330 full_tree = PyBootchartShell(window, trace, full_opts, 1.0)
331 tab_page.append_page (full_tree, gtk.Label("Full tree"))
332
333 if trace.kernel is not None and len (trace.kernel) > 2:
334 kernel_opts = RenderOptions(app_options)
335 kernel_opts.cumulative = False
336 kernel_opts.charts = False
337 kernel_opts.kernel_only = True
338 kernel_tree = PyBootchartShell(window, trace, kernel_opts, 5.0)
339 tab_page.append_page (kernel_tree, gtk.Label("Kernel boot"))
340
341 full_tree.grab_focus(self)
342 self.show()
343
344
345def show(trace, options):
346 win = PyBootchartWindow(trace, options)
347 win.connect('destroy', gtk.main_quit)
348 gtk.main()
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py b/scripts/pybootchartgui/pybootchartgui/main.py
deleted file mode 120000
index b45ae0a3d2..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/main.py
+++ /dev/null
@@ -1 +0,0 @@
1main.py.in \ No newline at end of file
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py.in b/scripts/pybootchartgui/pybootchartgui/main.py.in
deleted file mode 100644
index a954b125da..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/main.py.in
+++ /dev/null
@@ -1,183 +0,0 @@
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
19import sys
20import os
21import optparse
22
23from . import parsing
24from . import batch
25
26def _mk_options_parser():
27 """Make an options parser."""
28 usage = "%prog [options] /path/to/tmp/buildstats/<recipe-machine>/<BUILDNAME>/"
29 version = "%prog v1.0.0"
30 parser = optparse.OptionParser(usage, version=version)
31 parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False,
32 help="start in active mode")
33 parser.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"],
34 help="image format (png, svg, pdf); default format png")
35 parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None,
36 help="output path (file or directory) where charts are stored")
37 parser.add_option("-s", "--split", dest="num", type=int, default=1,
38 help="split the output chart into <NUM> charts, only works with \"-o PATH\"")
39 parser.add_option("-m", "--mintime", dest="mintime", type=int, default=8,
40 help="only tasks longer than this time will be displayed")
41 parser.add_option("-M", "--minutes", action="store_true", dest="as_minutes", default=False,
42 help="display time in minutes instead of seconds")
43# parser.add_option("-n", "--no-prune", action="store_false", dest="prune", default=True,
44# help="do not prune the process tree")
45 parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False,
46 help="suppress informational messages")
47# parser.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False,
48# help="only display the boot time of the boot in text format (stdout)")
49 parser.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False,
50 help="suppress all messages except errors")
51 parser.add_option("--verbose", action="store_true", dest="verbose", default=False,
52 help="print all messages")
53# parser.add_option("--profile", action="store_true", dest="profile", default=False,
54# help="profile rendering of chart (only useful when in batch mode indicated by -f)")
55# parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False,
56# help="show process ids in the bootchart as 'processname [pid]'")
57 parser.add_option("--show-all", action="store_true", dest="show_all", default=False,
58 help="show all processes in the chart")
59# parser.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None,
60# help="crop chart when idle after PROCESS is started")
61# parser.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None,
62# help="annotate position where PROCESS is started; can be specified multiple times. " +
63# "To create a single annotation when any one of a set of processes is started, use commas to separate the names")
64# parser.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None,
65# help="filename to write annotation points to")
66 parser.add_option("-T", "--full-time", action="store_true", dest="full_time", default=False,
67 help="display the full time regardless of which processes are currently shown")
68 return parser
69
70class Writer:
71 def __init__(self, write, options):
72 self.write = write
73 self.options = options
74
75 def error(self, msg):
76 self.write(msg)
77
78 def warn(self, msg):
79 if not self.options.quiet:
80 self.write(msg)
81
82 def info(self, msg):
83 if self.options.verbose:
84 self.write(msg)
85
86 def status(self, msg):
87 if not self.options.quiet:
88 self.write(msg)
89
90def _mk_writer(options):
91 def write(s):
92 print(s)
93 return Writer(write, options)
94
95def _get_filename(path):
96 """Construct a usable filename for outputs"""
97 dname = "."
98 fname = "bootchart"
99 if path != None:
100 if os.path.isdir(path):
101 dname = path
102 else:
103 fname = path
104 return os.path.join(dname, fname)
105
106def main(argv=None):
107 try:
108 if argv is None:
109 argv = sys.argv[1:]
110
111 parser = _mk_options_parser()
112 options, args = parser.parse_args(argv)
113
114 # Default values for disabled options
115 options.prune = True
116 options.boottime = False
117 options.profile = False
118 options.show_pid = False
119 options.crop_after = None
120 options.annotate = None
121 options.annotate_file = None
122
123 writer = _mk_writer(options)
124
125 if len(args) == 0:
126 print("No path given, trying /var/log/bootchart.tgz")
127 args = [ "/var/log/bootchart.tgz" ]
128
129 res = parsing.Trace(writer, args, options)
130
131 if options.interactive or options.output == None:
132 from . import gui
133 gui.show(res, options)
134 elif options.boottime:
135 import math
136 proc_tree = res.proc_tree
137 if proc_tree.idle:
138 duration = proc_tree.idle
139 else:
140 duration = proc_tree.duration
141 dur = duration / 100.0
142 print('%02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)))
143 else:
144 if options.annotate_file:
145 f = open (options.annotate_file, "w")
146 try:
147 for time in res[4]:
148 if time is not None:
149 # output as ms
150 f.write(time * 10)
151 finally:
152 f.close()
153 filename = _get_filename(options.output)
154 res_list = parsing.split_res(res, options)
155 n = 1
156 width = len(str(len(res_list)))
157 s = "_%%0%dd." % width
158 for r in res_list:
159 if len(res_list) == 1:
160 f = filename + "." + options.format
161 else:
162 f = filename + s % n + options.format
163 n = n + 1
164 def render():
165 batch.render(writer, r, options, f)
166 if options.profile:
167 import cProfile
168 import pstats
169 profile = '%s.prof' % os.path.splitext(filename)[0]
170 cProfile.runctx('render()', globals(), locals(), profile)
171 p = pstats.Stats(profile)
172 p.strip_dirs().sort_stats('time').print_stats(20)
173 else:
174 render()
175
176 return 0
177 except parsing.ParseError as ex:
178 print(("Parse error: %s" % ex))
179 return 2
180
181
182if __name__ == '__main__':
183 sys.exit(main())
diff --git a/scripts/pybootchartgui/pybootchartgui/parsing.py b/scripts/pybootchartgui/pybootchartgui/parsing.py
deleted file mode 100644
index 72a54c6ba5..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/parsing.py
+++ /dev/null
@@ -1,872 +0,0 @@
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 os
17import string
18import re
19import sys
20import tarfile
21import time
22from collections import defaultdict
23from functools import reduce
24
25from .samples import *
26from .process_tree import ProcessTree
27
28if sys.version_info >= (3, 0):
29 long = int
30
31# Parsing produces as its end result a 'Trace'
32
33class Trace:
34 def __init__(self, writer, paths, options):
35 self.processes = {}
36 self.start = {}
37 self.end = {}
38 self.min = None
39 self.max = None
40 self.headers = None
41 self.disk_stats = []
42 self.ps_stats = None
43 self.taskstats = None
44 self.cpu_stats = []
45 self.cmdline = None
46 self.kernel = None
47 self.kernel_tree = None
48 self.filename = None
49 self.parent_map = None
50 self.mem_stats = []
51 self.net_stats = []
52 self.monitor_disk = None
53 self.cpu_pressure = []
54 self.io_pressure = []
55 self.mem_pressure = []
56 self.times = [] # Always empty, but expected by draw.py when drawing system charts.
57
58 if len(paths):
59 parse_paths (writer, self, paths)
60 if not self.valid():
61 raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths))
62
63 if options.full_time:
64 self.min = min(self.start.keys())
65 self.max = max(self.end.keys())
66
67
68 # Rendering system charts depends on start and end
69 # time. Provide them where the original drawing code expects
70 # them, i.e. in proc_tree.
71 class BitbakeProcessTree:
72 def __init__(self, start_time, end_time):
73 self.start_time = start_time
74 self.end_time = end_time
75 self.duration = self.end_time - self.start_time
76 self.proc_tree = BitbakeProcessTree(min(self.start.keys()),
77 max(self.end.keys()))
78
79
80 return
81
82 # Turn that parsed information into something more useful
83 # link processes into a tree of pointers, calculate statistics
84 self.compile(writer)
85
86 # Crop the chart to the end of the first idle period after the given
87 # process
88 if options.crop_after:
89 idle = self.crop (writer, options.crop_after)
90 else:
91 idle = None
92
93 # Annotate other times as the first start point of given process lists
94 self.times = [ idle ]
95 if options.annotate:
96 for procnames in options.annotate:
97 names = [x[:15] for x in procnames.split(",")]
98 for proc in self.ps_stats.process_map.values():
99 if proc.cmd in names:
100 self.times.append(proc.start_time)
101 break
102 else:
103 self.times.append(None)
104
105 self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats,
106 self.ps_stats.sample_period,
107 self.headers.get("profile.process"),
108 options.prune, idle, self.taskstats,
109 self.parent_map is not None)
110
111 if self.kernel is not None:
112 self.kernel_tree = ProcessTree(writer, self.kernel, None, 0,
113 self.headers.get("profile.process"),
114 False, None, None, True)
115
116 def valid(self):
117 return len(self.processes) != 0
118 return self.headers != None and self.disk_stats != None and \
119 self.ps_stats != None and self.cpu_stats != None
120
121 def add_process(self, process, start, end):
122 self.processes[process] = [start, end]
123 if start not in self.start:
124 self.start[start] = []
125 if process not in self.start[start]:
126 self.start[start].append(process)
127 if end not in self.end:
128 self.end[end] = []
129 if process not in self.end[end]:
130 self.end[end].append(process)
131
132 def compile(self, writer):
133
134 def find_parent_id_for(pid):
135 if pid == 0:
136 return 0
137 ppid = self.parent_map.get(pid)
138 if ppid:
139 # many of these double forks are so short lived
140 # that we have no samples, or process info for them
141 # so climb the parent hierarcy to find one
142 if int (ppid * 1000) not in self.ps_stats.process_map:
143# print "Pid '%d' short lived with no process" % ppid
144 ppid = find_parent_id_for (ppid)
145# else:
146# print "Pid '%d' has an entry" % ppid
147 else:
148# print "Pid '%d' missing from pid map" % pid
149 return 0
150 return ppid
151
152 # merge in the cmdline data
153 if self.cmdline is not None:
154 for proc in self.ps_stats.process_map.values():
155 rpid = int (proc.pid // 1000)
156 if rpid in self.cmdline:
157 cmd = self.cmdline[rpid]
158 proc.exe = cmd['exe']
159 proc.args = cmd['args']
160# else:
161# print "proc %d '%s' not in cmdline" % (rpid, proc.exe)
162
163 # re-parent any stray orphans if we can
164 if self.parent_map is not None:
165 for process in self.ps_stats.process_map.values():
166 ppid = find_parent_id_for (int(process.pid // 1000))
167 if ppid:
168 process.ppid = ppid * 1000
169
170 # stitch the tree together with pointers
171 for process in self.ps_stats.process_map.values():
172 process.set_parent (self.ps_stats.process_map)
173
174 # count on fingers variously
175 for process in self.ps_stats.process_map.values():
176 process.calc_stats (self.ps_stats.sample_period)
177
178 def crop(self, writer, crop_after):
179
180 def is_idle_at(util, start, j):
181 k = j + 1
182 while k < len(util) and util[k][0] < start + 300:
183 k += 1
184 k = min(k, len(util)-1)
185
186 if util[j][1] >= 0.25:
187 return False
188
189 avgload = sum(u[1] for u in util[j:k+1]) / (k-j+1)
190 if avgload < 0.25:
191 return True
192 else:
193 return False
194 def is_idle(util, start):
195 for j in range(0, len(util)):
196 if util[j][0] < start:
197 continue
198 return is_idle_at(util, start, j)
199 else:
200 return False
201
202 names = [x[:15] for x in crop_after.split(",")]
203 for proc in self.ps_stats.process_map.values():
204 if proc.cmd in names or proc.exe in names:
205 writer.info("selected proc '%s' from list (start %d)"
206 % (proc.cmd, proc.start_time))
207 break
208 if proc is None:
209 writer.warn("no selected crop proc '%s' in list" % crop_after)
210
211
212 cpu_util = [(sample.time, sample.user + sample.sys + sample.io) for sample in self.cpu_stats]
213 disk_util = [(sample.time, sample.util) for sample in self.disk_stats]
214
215 idle = None
216 for i in range(0, len(cpu_util)):
217 if cpu_util[i][0] < proc.start_time:
218 continue
219 if is_idle_at(cpu_util, cpu_util[i][0], i) \
220 and is_idle(disk_util, cpu_util[i][0]):
221 idle = cpu_util[i][0]
222 break
223
224 if idle is None:
225 writer.warn ("not idle after proc '%s'" % crop_after)
226 return None
227
228 crop_at = idle + 300
229 writer.info ("cropping at time %d" % crop_at)
230 while len (self.cpu_stats) \
231 and self.cpu_stats[-1].time > crop_at:
232 self.cpu_stats.pop()
233 while len (self.disk_stats) \
234 and self.disk_stats[-1].time > crop_at:
235 self.disk_stats.pop()
236
237 self.ps_stats.end_time = crop_at
238
239 cropped_map = {}
240 for key, value in self.ps_stats.process_map.items():
241 if (value.start_time <= crop_at):
242 cropped_map[key] = value
243
244 for proc in cropped_map.values():
245 proc.duration = min (proc.duration, crop_at - proc.start_time)
246 while len (proc.samples) \
247 and proc.samples[-1].time > crop_at:
248 proc.samples.pop()
249
250 self.ps_stats.process_map = cropped_map
251
252 return idle
253
254
255
256class ParseError(Exception):
257 """Represents errors during parse of the bootchart."""
258 def __init__(self, value):
259 self.value = value
260
261 def __str__(self):
262 return self.value
263
264def _parse_headers(file):
265 """Parses the headers of the bootchart."""
266 def parse(acc, line):
267 (headers, last) = acc
268 if '=' in line:
269 last, value = map (lambda x: x.strip(), line.split('=', 1))
270 else:
271 value = line.strip()
272 headers[last] += value
273 return headers, last
274 return reduce(parse, file.read().split('\n'), (defaultdict(str),''))[0]
275
276def _parse_timed_blocks(file):
277 """Parses (ie., splits) a file into so-called timed-blocks. A
278 timed-block consists of a timestamp on a line by itself followed
279 by zero or more lines of data for that point in time."""
280 def parse(block):
281 lines = block.split('\n')
282 if not lines:
283 raise ParseError('expected a timed-block consisting a timestamp followed by data lines')
284 try:
285 return (int(lines[0]), lines[1:])
286 except ValueError:
287 raise ParseError("expected a timed-block, but timestamp '%s' is not an integer" % lines[0])
288 blocks = file.read().split('\n\n')
289 return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')]
290
291def _parse_proc_ps_log(writer, file):
292 """
293 * See proc(5) for details.
294 *
295 * {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime,
296 * cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack,
297 * kstkesp, kstkeip}
298 """
299 processMap = {}
300 ltime = 0
301 timed_blocks = _parse_timed_blocks(file)
302 for time, lines in timed_blocks:
303 for line in lines:
304 if not line: continue
305 tokens = line.split(' ')
306 if len(tokens) < 21:
307 continue
308
309 offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0]
310 pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset])
311 userCpu, sysCpu, stime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset])
312
313 # magic fixed point-ness ...
314 pid *= 1000
315 ppid *= 1000
316 if pid in processMap:
317 process = processMap[pid]
318 process.cmd = cmd.strip('()') # why rename after latest name??
319 else:
320 process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime))
321 processMap[pid] = process
322
323 if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None:
324 userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime))
325 cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0)
326 process.samples.append(ProcessSample(time, state, cpuSample))
327
328 process.last_user_cpu_time = userCpu
329 process.last_sys_cpu_time = sysCpu
330 ltime = time
331
332 if len (timed_blocks) < 2:
333 return None
334
335 startTime = timed_blocks[0][0]
336 avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1)
337
338 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
339
340def _parse_taskstats_log(writer, file):
341 """
342 * See bootchart-collector.c for details.
343 *
344 * { pid, ppid, comm, cpu_run_real_total, blkio_delay_total, swapin_delay_total }
345 *
346 """
347 processMap = {}
348 pidRewrites = {}
349 ltime = None
350 timed_blocks = _parse_timed_blocks(file)
351 for time, lines in timed_blocks:
352 # we have no 'stime' from taskstats, so prep 'init'
353 if ltime is None:
354 process = Process(writer, 1, '[init]', 0, 0)
355 processMap[1000] = process
356 ltime = time
357# continue
358 for line in lines:
359 if not line: continue
360 tokens = line.split(' ')
361 if len(tokens) != 6:
362 continue
363
364 opid, ppid, cmd = int(tokens[0]), int(tokens[1]), tokens[2]
365 cpu_ns, blkio_delay_ns, swapin_delay_ns = long(tokens[-3]), long(tokens[-2]), long(tokens[-1]),
366
367 # make space for trees of pids
368 opid *= 1000
369 ppid *= 1000
370
371 # when the process name changes, we re-write the pid.
372 if opid in pidRewrites:
373 pid = pidRewrites[opid]
374 else:
375 pid = opid
376
377 cmd = cmd.strip('(').strip(')')
378 if pid in processMap:
379 process = processMap[pid]
380 if process.cmd != cmd:
381 pid += 1
382 pidRewrites[opid] = pid
383# print "process mutation ! '%s' vs '%s' pid %s -> pid %s\n" % (process.cmd, cmd, opid, pid)
384 process = process.split (writer, pid, cmd, ppid, time)
385 processMap[pid] = process
386 else:
387 process.cmd = cmd;
388 else:
389 process = Process(writer, pid, cmd, ppid, time)
390 processMap[pid] = process
391
392 delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns)
393 delta_blkio_delay_ns = (float) (blkio_delay_ns - process.last_blkio_delay_ns)
394 delta_swapin_delay_ns = (float) (swapin_delay_ns - process.last_swapin_delay_ns)
395
396 # make up some state data ...
397 if delta_cpu_ns > 0:
398 state = "R"
399 elif delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
400 state = "D"
401 else:
402 state = "S"
403
404 # retain the ns timing information into a CPUSample - that tries
405 # with the old-style to be a %age of CPU used in this time-slice.
406 if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
407# print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns)
408 cpuSample = CPUSample('null', delta_cpu_ns, 0.0,
409 delta_blkio_delay_ns,
410 delta_swapin_delay_ns)
411 process.samples.append(ProcessSample(time, state, cpuSample))
412
413 process.last_cpu_ns = cpu_ns
414 process.last_blkio_delay_ns = blkio_delay_ns
415 process.last_swapin_delay_ns = swapin_delay_ns
416 ltime = time
417
418 if len (timed_blocks) < 2:
419 return None
420
421 startTime = timed_blocks[0][0]
422 avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1)
423
424 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
425
426def _parse_proc_stat_log(file):
427 samples = []
428 ltimes = None
429 for time, lines in _parse_timed_blocks(file):
430 # skip emtpy lines
431 if not lines:
432 continue
433 # CPU times {user, nice, system, idle, io_wait, irq, softirq}
434 tokens = lines[0].split()
435 times = [ int(token) for token in tokens[1:] ]
436 if ltimes:
437 user = float((times[0] + times[1]) - (ltimes[0] + ltimes[1]))
438 system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6]))
439 idle = float(times[3] - ltimes[3])
440 iowait = float(times[4] - ltimes[4])
441
442 aSum = max(user + system + idle + iowait, 1)
443 samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) )
444
445 ltimes = times
446 # skip the rest of statistics lines
447 return samples
448
449def _parse_reduced_log(file, sample_class):
450 samples = []
451 for time, lines in _parse_timed_blocks(file):
452 samples.append(sample_class(time, *[float(x) for x in lines[0].split()]))
453 return samples
454
455def _parse_proc_disk_stat_log(file):
456 """
457 Parse file for disk stats, but only look at the whole device, eg. sda,
458 not sda1, sda2 etc. The format of relevant lines should be:
459 {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq}
460 """
461 disk_regex_re = re.compile (r'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$')
462
463 # this gets called an awful lot.
464 def is_relevant_line(linetokens):
465 if len(linetokens) != 14:
466 return False
467 disk = linetokens[2]
468 return disk_regex_re.match(disk)
469
470 disk_stat_samples = []
471
472 for time, lines in _parse_timed_blocks(file):
473 sample = DiskStatSample(time)
474 relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) if is_relevant_line(linetokens)]
475
476 for tokens in relevant_tokens:
477 disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12])
478 sample.add_diskdata([rsect, wsect, use])
479
480 disk_stat_samples.append(sample)
481
482 disk_stats = []
483 for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]):
484 interval = sample1.time - sample2.time
485 if interval == 0:
486 interval = 1
487 sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ]
488 readTput = sums[0] / 2.0 * 100.0 / interval
489 writeTput = sums[1] / 2.0 * 100.0 / interval
490 util = float( sums[2] ) / 10 / interval
491 util = max(0.0, min(1.0, util))
492 disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util))
493
494 return disk_stats
495
496def _parse_reduced_proc_meminfo_log(file):
497 """
498 Parse file for global memory statistics with
499 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree' values
500 (in that order) directly stored on one line.
501 """
502 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
503
504 mem_stats = []
505 for time, lines in _parse_timed_blocks(file):
506 sample = MemSample(time)
507 for name, value in zip(used_values, lines[0].split()):
508 sample.add_value(name, int(value))
509
510 if sample.valid():
511 mem_stats.append(DrawMemSample(sample))
512
513 return mem_stats
514
515def _parse_proc_meminfo_log(file):
516 """
517 Parse file for global memory statistics.
518 The format of relevant lines should be: ^key: value( unit)?
519 """
520 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
521
522 mem_stats = []
523 meminfo_re = re.compile(r'([^ \t:]+):\s*(\d+).*')
524
525 for time, lines in _parse_timed_blocks(file):
526 sample = MemSample(time)
527
528 for line in lines:
529 match = meminfo_re.match(line)
530 if not match:
531 raise ParseError("Invalid meminfo line \"%s\"" % line)
532 sample.add_value(match.group(1), int(match.group(2)))
533
534 if sample.valid():
535 mem_stats.append(DrawMemSample(sample))
536
537 return mem_stats
538
539def _parse_monitor_disk_log(file):
540 """
541 Parse file with information about amount of diskspace used.
542 The format of relevant lines should be: ^volume path: number-of-bytes?
543 """
544 disk_stats = []
545 diskinfo_re = re.compile(r'^(.+):\s*(\d+)$')
546
547 for time, lines in _parse_timed_blocks(file):
548 sample = DiskSpaceSample(time)
549
550 for line in lines:
551 match = diskinfo_re.match(line)
552 if not match:
553 raise ParseError("Invalid monitor_disk line \"%s\"" % line)
554 sample.add_value(match.group(1), int(match.group(2)))
555
556 if sample.valid():
557 disk_stats.append(sample)
558
559 return disk_stats
560
561
562def _parse_reduced_net_log(file):
563 net_stats = {}
564 for time, lines in _parse_timed_blocks(file):
565
566 for line in lines:
567 parts = line.split()
568 iface = parts[0][:-1]
569 if iface not in net_stats:
570 net_stats[iface] = [NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))]
571 else:
572 net_stats[iface].append(NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4])))
573 return net_stats
574
575
576def _parse_pressure_logs(file, filename):
577 """
578 Parse file for "some" pressure with 'avg10', 'avg60' 'avg300' and delta total values
579 (in that order) directly stored on one line for both CPU and IO, based on filename.
580 """
581 pressure_stats = []
582 if filename == "cpu.log":
583 SamplingClass = CPUPressureSample
584 elif filename == "memory.log":
585 SamplingClass = MemPressureSample
586 else:
587 SamplingClass = IOPressureSample
588 for time, lines in _parse_timed_blocks(file):
589 for line in lines:
590 if not line: continue
591 tokens = line.split()
592 avg10 = float(tokens[0])
593 avg60 = float(tokens[1])
594 avg300 = float(tokens[2])
595 delta = float(tokens[3])
596 pressure_stats.append(SamplingClass(time, avg10, avg60, avg300, delta))
597
598 return pressure_stats
599
600# if we boot the kernel with: initcall_debug printk.time=1 we can
601# get all manner of interesting data from the dmesg output
602# We turn this into a pseudo-process tree: each event is
603# characterised by a
604# we don't try to detect a "kernel finished" state - since the kernel
605# continues to do interesting things after init is called.
606#
607# sample input:
608# [ 0.000000] ACPI: FACP 3f4fc000 000F4 (v04 INTEL Napa 00000001 MSFT 01000013)
609# ...
610# [ 0.039993] calling migration_init+0x0/0x6b @ 1
611# [ 0.039993] initcall migration_init+0x0/0x6b returned 1 after 0 usecs
612def _parse_dmesg(writer, file):
613 timestamp_re = re.compile (r"^\[\s*(\d+\.\d+)\s*]\s+(.*)$")
614 split_re = re.compile (r"^(\S+)\s+([\S\+_-]+) (.*)$")
615 processMap = {}
616 idx = 0
617 inc = 1.0 / 1000000
618 kernel = Process(writer, idx, "k-boot", 0, 0.1)
619 processMap['k-boot'] = kernel
620 base_ts = False
621 max_ts = 0
622 for line in file.read().split('\n'):
623 t = timestamp_re.match (line)
624 if t is None:
625# print "duff timestamp " + line
626 continue
627
628 time_ms = float (t.group(1)) * 1000
629 # looks like we may have a huge diff after the clock
630 # has been set up. This could lead to huge graph:
631 # so huge we will be killed by the OOM.
632 # So instead of using the plain timestamp we will
633 # use a delta to first one and skip the first one
634 # for convenience
635 if max_ts == 0 and not base_ts and time_ms > 1000:
636 base_ts = time_ms
637 continue
638 max_ts = max(time_ms, max_ts)
639 if base_ts:
640# print "fscked clock: used %f instead of %f" % (time_ms - base_ts, time_ms)
641 time_ms -= base_ts
642 m = split_re.match (t.group(2))
643
644 if m is None:
645 continue
646# print "match: '%s'" % (m.group(1))
647 type = m.group(1)
648 func = m.group(2)
649 rest = m.group(3)
650
651 if t.group(2).startswith ('Write protecting the') or \
652 t.group(2).startswith ('Freeing unused kernel memory'):
653 kernel.duration = time_ms / 10
654 continue
655
656# print "foo: '%s' '%s' '%s'" % (type, func, rest)
657 if type == "calling":
658 ppid = kernel.pid
659 p = re.match (r"\@ (\d+)", rest)
660 if p is not None:
661 ppid = float (p.group(1)) // 1000
662# print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms)
663 name = func.split ('+', 1) [0]
664 idx += inc
665 processMap[func] = Process(writer, ppid + idx, name, ppid, time_ms / 10)
666 elif type == "initcall":
667# print "finished: '%s' at '%s'" % (func, time_ms)
668 if func in processMap:
669 process = processMap[func]
670 process.duration = (time_ms / 10) - process.start_time
671 else:
672 print("corrupted init call for %s" % (func))
673
674 elif type == "async_waiting" or type == "async_continuing":
675 continue # ignore
676
677 return processMap.values()
678
679#
680# Parse binary pacct accounting file output if we have one
681# cf. /usr/include/linux/acct.h
682#
683def _parse_pacct(writer, file):
684 # read LE int32
685 def _read_le_int32(file):
686 byts = file.read(4)
687 return (ord(byts[0])) | (ord(byts[1]) << 8) | \
688 (ord(byts[2]) << 16) | (ord(byts[3]) << 24)
689
690 parent_map = {}
691 parent_map[0] = 0
692 while file.read(1) != "": # ignore flags
693 ver = file.read(1)
694 if ord(ver) < 3:
695 print("Invalid version 0x%x" % (ord(ver)))
696 return None
697
698 file.seek (14, 1) # user, group etc.
699 pid = _read_le_int32 (file)
700 ppid = _read_le_int32 (file)
701# print "Parent of %d is %d" % (pid, ppid)
702 parent_map[pid] = ppid
703 file.seek (4 + 4 + 16, 1) # timings
704 file.seek (16, 1) # acct_comm
705 return parent_map
706
707def _parse_paternity_log(writer, file):
708 parent_map = {}
709 parent_map[0] = 0
710 for line in file.read().split('\n'):
711 if not line:
712 continue
713 elems = line.split(' ') # <Child> <Parent>
714 if len (elems) >= 2:
715# print "paternity of %d is %d" % (int(elems[0]), int(elems[1]))
716 parent_map[int(elems[0])] = int(elems[1])
717 else:
718 print("Odd paternity line '%s'" % (line))
719 return parent_map
720
721def _parse_cmdline_log(writer, file):
722 cmdLines = {}
723 for block in file.read().split('\n\n'):
724 lines = block.split('\n')
725 if len (lines) >= 3:
726# print "Lines '%s'" % (lines[0])
727 pid = int (lines[0])
728 values = {}
729 values['exe'] = lines[1].lstrip(':')
730 args = lines[2].lstrip(':').split('\0')
731 args.pop()
732 values['args'] = args
733 cmdLines[pid] = values
734 return cmdLines
735
736def _parse_bitbake_buildstats(writer, state, filename, file):
737 paths = filename.split("/")
738 task = paths[-1]
739 pn = paths[-2]
740 start = None
741 end = None
742 for line in file:
743 if line.startswith("Started:"):
744 start = int(float(line.split()[-1]))
745 elif line.startswith("Ended:"):
746 end = int(float(line.split()[-1]))
747 if start and end:
748 state.add_process(pn + ":" + task, start, end)
749
750def get_num_cpus(headers):
751 """Get the number of CPUs from the system.cpu header property. As the
752 CPU utilization graphs are relative, the number of CPUs currently makes
753 no difference."""
754 if headers is None:
755 return 1
756 if headers.get("system.cpu.num"):
757 return max (int (headers.get("system.cpu.num")), 1)
758 cpu_model = headers.get("system.cpu")
759 if cpu_model is None:
760 return 1
761 mat = re.match(r".*\\((\\d+)\\)", cpu_model)
762 if mat is None:
763 return 1
764 return max (int(mat.group(1)), 1)
765
766def _do_parse(writer, state, filename, file):
767 writer.info("parsing '%s'" % filename)
768 t1 = time.process_time()
769 name = os.path.basename(filename)
770 if name == "proc_diskstats.log":
771 state.disk_stats = _parse_proc_disk_stat_log(file)
772 elif name == "reduced_proc_diskstats.log":
773 state.disk_stats = _parse_reduced_log(file, DiskSample)
774 elif name == "proc_stat.log":
775 state.cpu_stats = _parse_proc_stat_log(file)
776 elif name == "reduced_proc_stat.log":
777 state.cpu_stats = _parse_reduced_log(file, CPUSample)
778 elif name == "proc_meminfo.log":
779 state.mem_stats = _parse_proc_meminfo_log(file)
780 elif name == "reduced_proc_meminfo.log":
781 state.mem_stats = _parse_reduced_proc_meminfo_log(file)
782 elif name == "cmdline2.log":
783 state.cmdline = _parse_cmdline_log(writer, file)
784 elif name == "monitor_disk.log":
785 state.monitor_disk = _parse_monitor_disk_log(file)
786 elif name == "reduced_proc_net.log":
787 state.net_stats = _parse_reduced_net_log(file)
788 #pressure logs are in a subdirectory
789 elif name == "cpu.log":
790 state.cpu_pressure = _parse_pressure_logs(file, name)
791 elif name == "io.log":
792 state.io_pressure = _parse_pressure_logs(file, name)
793 elif name == "memory.log":
794 state.mem_pressure = _parse_pressure_logs(file, name)
795 elif not filename.endswith('.log'):
796 _parse_bitbake_buildstats(writer, state, filename, file)
797 t2 = time.process_time()
798 writer.info(" %s seconds" % str(t2-t1))
799 return state
800
801def parse_file(writer, state, filename):
802 if state.filename is None:
803 state.filename = filename
804 basename = os.path.basename(filename)
805 with open(filename, "r") as file:
806 return _do_parse(writer, state, filename, file)
807
808def parse_paths(writer, state, paths):
809 for path in paths:
810 if state.filename is None:
811 state.filename = path
812 root, extension = os.path.splitext(path)
813 if not(os.path.exists(path)):
814 writer.warn("warning: path '%s' does not exist, ignoring." % path)
815 continue
816 #state.filename = path
817 if os.path.isdir(path):
818 files = sorted([os.path.join(path, f) for f in os.listdir(path)])
819 state = parse_paths(writer, state, files)
820 elif extension in [".tar", ".tgz", ".gz"]:
821 if extension == ".gz":
822 root, extension = os.path.splitext(root)
823 if extension != ".tar":
824 writer.warn("warning: can only handle zipped tar files, not zipped '%s'-files; ignoring" % extension)
825 continue
826 tf = None
827 try:
828 writer.status("parsing '%s'" % path)
829 tf = tarfile.open(path, 'r:*')
830 for name in tf.getnames():
831 state = _do_parse(writer, state, name, tf.extractfile(name))
832 except tarfile.ReadError as error:
833 raise ParseError("error: could not read tarfile '%s': %s." % (path, error))
834 finally:
835 if tf != None:
836 tf.close()
837 else:
838 state = parse_file(writer, state, path)
839 return state
840
841def split_res(res, options):
842 """ Split the res into n pieces """
843 res_list = []
844 if options.num > 1:
845 s_list = sorted(res.start.keys())
846 frag_size = len(s_list) / float(options.num)
847 # Need the top value
848 if frag_size > int(frag_size):
849 frag_size = int(frag_size + 1)
850 else:
851 frag_size = int(frag_size)
852
853 start = 0
854 end = frag_size
855 while start < end:
856 state = Trace(None, [], None)
857 if options.full_time:
858 state.min = min(res.start.keys())
859 state.max = max(res.end.keys())
860 for i in range(start, end):
861 # Add this line for reference
862 #state.add_process(pn + ":" + task, start, end)
863 for p in res.start[s_list[i]]:
864 state.add_process(p, s_list[i], res.processes[p][1])
865 start = end
866 end = end + frag_size
867 if end > len(s_list):
868 end = len(s_list)
869 res_list.append(state)
870 else:
871 res_list.append(res)
872 return res_list
diff --git a/scripts/pybootchartgui/pybootchartgui/process_tree.py b/scripts/pybootchartgui/pybootchartgui/process_tree.py
deleted file mode 100644
index cf88110b1c..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/process_tree.py
+++ /dev/null
@@ -1,292 +0,0 @@
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
deleted file mode 100644
index 7c92d2ce6a..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/samples.py
+++ /dev/null
@@ -1,213 +0,0 @@
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
40
41class NetSample:
42 def __init__(self, time, iface, received_bytes, transmitted_bytes, receive_diff, transmit_diff):
43 self.time = time
44 self.iface = iface
45 self.received_bytes = received_bytes
46 self.transmitted_bytes = transmitted_bytes
47 self.receive_diff = receive_diff
48 self.transmit_diff = transmit_diff
49
50class CPUPressureSample:
51 def __init__(self, time, avg10, avg60, avg300, deltaTotal):
52 self.time = time
53 self.avg10 = avg10
54 self.avg60 = avg60
55 self.avg300 = avg300
56 self.deltaTotal = deltaTotal
57
58class IOPressureSample:
59 def __init__(self, time, avg10, avg60, avg300, deltaTotal):
60 self.time = time
61 self.avg10 = avg10
62 self.avg60 = avg60
63 self.avg300 = avg300
64 self.deltaTotal = deltaTotal
65
66class MemPressureSample:
67 def __init__(self, time, avg10, avg60, avg300, deltaTotal):
68 self.time = time
69 self.avg10 = avg10
70 self.avg60 = avg60
71 self.avg300 = avg300
72 self.deltaTotal = deltaTotal
73
74
75class MemSample:
76 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
77
78 def __init__(self, time):
79 self.time = time
80 self.records = {}
81
82 def add_value(self, name, value):
83 if name in MemSample.used_values:
84 self.records[name] = value
85
86 def valid(self):
87 keys = self.records.keys()
88 # discard incomplete samples
89 return [v for v in MemSample.used_values if v not in keys] == []
90
91class DrawMemSample:
92 """
93 Condensed version of a MemSample with exactly the values used by the drawing code.
94 Initialized either from a valid MemSample or
95 a tuple/list of buffer/used/cached/swap values.
96 """
97 def __init__(self, mem_sample):
98 self.time = mem_sample.time
99 if isinstance(mem_sample, MemSample):
100 self.buffers = mem_sample.records['MemTotal'] - mem_sample.records['MemFree']
101 self.used = mem_sample.records['MemTotal'] - mem_sample.records['MemFree'] - mem_sample.records['Buffers']
102 self.cached = mem_sample.records['Cached']
103 self.swap = mem_sample.records['SwapTotal'] - mem_sample.records['SwapFree']
104 else:
105 self.buffers, self.used, self.cached, self.swap = mem_sample
106
107class DiskSpaceSample:
108 def __init__(self, time):
109 self.time = time
110 self.records = {}
111
112 def add_value(self, name, value):
113 self.records[name] = value
114
115 def valid(self):
116 return bool(self.records)
117
118class ProcessSample:
119 def __init__(self, time, state, cpu_sample):
120 self.time = time
121 self.state = state
122 self.cpu_sample = cpu_sample
123
124 def __str__(self):
125 return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample)
126
127class ProcessStats:
128 def __init__(self, writer, process_map, sample_count, sample_period, start_time, end_time):
129 self.process_map = process_map
130 self.sample_count = sample_count
131 self.sample_period = sample_period
132 self.start_time = start_time
133 self.end_time = end_time
134 writer.info ("%d samples, avg. sample length %f" % (self.sample_count, self.sample_period))
135 writer.info ("process list size: %d" % len (self.process_map.values()))
136
137class Process:
138 def __init__(self, writer, pid, cmd, ppid, start_time):
139 self.writer = writer
140 self.pid = pid
141 self.cmd = cmd
142 self.exe = cmd
143 self.args = []
144 self.ppid = ppid
145 self.start_time = start_time
146 self.duration = 0
147 self.samples = []
148 self.parent = None
149 self.child_list = []
150
151 self.active = None
152 self.last_user_cpu_time = None
153 self.last_sys_cpu_time = None
154
155 self.last_cpu_ns = 0
156 self.last_blkio_delay_ns = 0
157 self.last_swapin_delay_ns = 0
158
159 # split this process' run - triggered by a name change
160 def split(self, writer, pid, cmd, ppid, start_time):
161 split = Process (writer, pid, cmd, ppid, start_time)
162
163 split.last_cpu_ns = self.last_cpu_ns
164 split.last_blkio_delay_ns = self.last_blkio_delay_ns
165 split.last_swapin_delay_ns = self.last_swapin_delay_ns
166
167 return split
168
169 def __str__(self):
170 return " ".join([str(self.pid), self.cmd, str(self.ppid), '[ ' + str(len(self.samples)) + ' samples ]' ])
171
172 def calc_stats(self, samplePeriod):
173 if self.samples:
174 firstSample = self.samples[0]
175 lastSample = self.samples[-1]
176 self.start_time = min(firstSample.time, self.start_time)
177 self.duration = lastSample.time - self.start_time + samplePeriod
178
179 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] )
180 activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] )
181 self.active = (activeCount>2)
182
183 def calc_load(self, userCpu, sysCpu, interval):
184 userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval
185 sysCpuLoad = float(sysCpu - self.last_sys_cpu_time) / interval
186 cpuLoad = userCpuLoad + sysCpuLoad
187 # normalize
188 if cpuLoad > 1.0:
189 userCpuLoad = userCpuLoad / cpuLoad
190 sysCpuLoad = sysCpuLoad / cpuLoad
191 return (userCpuLoad, sysCpuLoad)
192
193 def set_parent(self, processMap):
194 if self.ppid != None:
195 self.parent = processMap.get (self.ppid)
196 if self.parent == None and self.pid // 1000 > 1 and \
197 not (self.ppid == 2000 or self.pid == 2000): # kernel threads: ppid=2
198 self.writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \
199 % (self.pid,self.cmd,self.ppid))
200
201 def get_end_time(self):
202 return self.start_time + self.duration
203
204class DiskSample:
205 def __init__(self, time, read, write, util):
206 self.time = time
207 self.read = read
208 self.write = write
209 self.util = util
210 self.tput = read + write
211
212 def __str__(self):
213 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
deleted file mode 100644
index 00fb3bf797..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py
+++ /dev/null
@@ -1,105 +0,0 @@
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
deleted file mode 100644
index 6f46a1c03d..0000000000
--- a/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py
+++ /dev/null
@@ -1,92 +0,0 @@
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()