parse X resource counts and show the changes in summary
[maemo-tools:sp-endurance.git] / postproc / endurance_report.py
1 #!/usr/bin/python
2 # This file is part of sp-endurance.
3 #
4 # Copyright (C) 2006-2009 by Nokia Corporation
5 #
6 # Contact: Eero Tamminen <eero.tamminen@nokia.com>
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License 
10 # version 2 as published by the Free Software Foundation. 
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20 # 02110-1301 USA
21 #
22 # CHANGES:
23 #
24 # 2006-01-02:
25 # - First version
26 # 2006-01-04:
27 # - Parses everything relevant from current endurance measurements data
28 # 2006-01-05:
29 # - Can now parse also syslog which is not pre-processed
30 # - Parses also DSME reports
31 # 2006-01-10:
32 # - Generate a graph of the memory usage increase
33 # 2006-01-12:
34 # - Parse also DSME reboot messages and Glib warnings
35 # - Use red text for processes with critical errors or triggering reboot
36 # - Show logging time
37 # 2006-01-20:
38 # - Show total amount of FDs for processes which changed and
39 #   warn if system free FD count is low
40 # - Improved error checking / messages
41 # - Show software release
42 # - show new processes
43 # 2006-01-23:
44 # - Show initial state errors
45 # - Show free memory below bg-kill limit as red
46 # 2006-01-24:
47 # - Link from overview to each test round
48 # 2006-01-25:
49 # - Update memory limits to new values from Leonid
50 # 2006-01-27:
51 # - Optionally parse SMAPS diff output and output into HTML
52 #   table of process memory usage changes
53 # - Nicer help output
54 # 2006-01-31:
55 # - List also exited processes
56 # - Fixes to SMAPS diff parsing
57 # - Parse and output also X resource usage differencies
58 # - Sort fd and memory changes according to totals and new commands
59 #   according to their names, right align changes and totals
60 # - Bold low/high memory values
61 # - Caption all tables
62 # 2006-02-03:
63 # - Fix maemo-launcher syslog message parsing
64 # 2006-03-01:
65 # - Update to parse output from proc2csv instead of meminfo, parsing
66 #   /proc/PID/status data for command names is not anymore needed
67 # 2006-03-14:
68 # - Update to new DSME log format and output also reasons (signals)
69 #   for system service restarts
70 # - Convert signal numbers to names
71 # 2006-05-03:
72 # - Change SMAPS diff parsing to parse the new (much changed) diff files
73 # - Fix regexps to parse additional text that is in some DSME lines
74 # 2006-05-04:
75 # - Add links to generated SMAPS HTML files
76 # 2006-06-05:
77 # - Add support for dynamic lowmem limits
78 # 2006-13-05:
79 # - Support Swap
80 # - Fix/clarify service crash and signal explanation texts
81 # 2006-07-06:
82 # - Fix bug in system free mem calculation introduced by swap support
83 # 2006-09-28:
84 # - Show the issues from syslog even if syslogs don't match
85 # 2006-10-18, from Tuukka:
86 # - Always print error message before failing exit
87 # - Make loadable as module, split parse_and_output function
88 # 2006-11-09:
89 # - Arguments are directories instead of file names
90 # - Syslog data is now parsed from file separate from the CSV file
91 # - Separated syslog parsing to syslog_parse.py
92 # 2006-11-14:
93 # - Output disk free changes (for '/' and '/tmp')
94 # - Show whole device /proc/sys/fs/file-nr changes
95 # - Output also X resource usage decreases
96 # - Save errors to separate HTML pages
97 # - Output statistics and summary of different error types
98 # - Output summary of memory/X resource/FD usage changes
99 # 2006-11-16:
100 # - minor updates for syslog_parse.py
101 # - Add process changed/total counts
102 #   - with started/exited processes side by side
103 #   - remove "sleep" from the lists
104 # - Color code different tables
105 # - Add links to DSME stats, syslog, CSV file...
106 # - Add change totals to all tables, not just errors
107 # 2006-11-22:
108 # - HTML comment summary of all statistics (even ones with value zero)
109 #   for maturity metrics
110 # - Parse process statistics from /proc/PID/status files
111 # - Generalize output_memory_graph_table() and add bars of memory
112 #   usage changes per process (to the overview section) for all processes
113 #   where RSS (maximum) usage changes, sorted according to RSS
114 # - Re-organize and fine-tune the output file to be more readable,
115 #   and contents with links etc
116 # - Remove "sleep" from *all* the lists
117 # 2006-11-28:
118 # - Fix bug in case none of the syslogs had errors
119 # 2006-12-05:
120 # - Fix (another) bug in case some syslog didn't have any errors
121 # 2006-12-14:
122 # - Add link to the previous round error page
123 # 2007-01-05:
124 # - Do bargraphs with tables instead of images, this way the report
125 #   works even when sent as email or attached to Bugzilla
126 # 2007-01-28:
127 # - Fix error message (gave Python exception)
128 # - Show FS usage, not free
129 # - Process memory usage graphs:
130 #   - sort in this order: name, first round in which process appears, pid
131 #   - differentiate processes by name+pid instead just by pid
132 #     (if device had rebooted and some other process got same pid,
133 #     earlier results were funny)
134 #   - show successive rounds without process with just one line
135 # 2007-02-07:
136 # - sp-smaps-visualize package scripts are way too slow,
137 #   added my own parsing of SMAPS Private_Dirty numbers to here
138 #   - Ignore values for memory mapped devices
139 # - Output process statuses in the processes RSS change list
140 # 2007-02-08:
141 # - cleanup SMAPS stuff
142 # 2007-02-15:
143 # - fix memory usage bars for the case when deny limit is crossed
144 # - In process memory bars:
145 #   - for rounds where the value doesn't change, replace the header
146 #     with: "Rounds X-Y"
147 #   - only if >0.2% memory change per round in RSS or Size, show the change
148 # 2007-02-16:
149 # - show busyness only if SleepAVG < 90%
150 # - have numbers in different columns in graph
151 # 2007-03-09:
152 # - check in how many rounds size increases before skipping process memory bars
153 # 2007-03-13:
154 # - Parse&show total amount of system private dirty code pages
155 # - Olev asked /dev/ files to be counted from SMAPS data too
156 #   in case somebody would leak e.g. dsptasks...
157 # 2007-04-12:
158 # - In process memory bars:
159 #   - Fix how threads are indentified for removal from memory usage graphs
160 #   - Show process if RSS changes in enough rounds (not just increases)
161 # 2007-04-16:
162 # - Cope with missing SMAPS data
163 # - Do not ignore any processes
164 # 2007-04-18:
165 # - List also changes in kernel threads and zombie processes
166 # - Handle usage.csv data with (incorrect) extra columns
167 # - Fix to new thread ignore code
168 # 2007-04-25:
169 # - Ignore extra threads in all resource usage lists
170 # - Add script version to reports (as HTML comment)
171 # 2007-05-03:
172 # - Sort resource usage tables according to changes, not total
173 # - Fix to get_pid_usage_diffs() 
174 # 2007-10-31:
175 # - Link list of open file descriptors and smaps.cap
176 # - Show differences in process thread counts
177 # 2007-11-05:
178 # - Include SwapCached to system free and report swap usage
179 #   separately in summary
180 # - Handle compressed smaps.cap files
181 # 2007-11-29:
182 # - App memory usage graphs show also SMAPS private memory usage
183 # 2008-04-30:
184 # - Show optional use-case step description
185 # 2008-08-21:
186 # - Kernels from v2.6.22 don't anymore provide PID/status:SleepAVG
187 #   -> remove support for it (it was fairly useless anyway)
188 # 2008-12-04:
189 # - Show difference in shared memory segments use
190 #   (subset of FDs with their own limits)
191 # 2009-04-01:
192 # - Parse swap usage from SMAPS and add 'Swap' column in process memory usage.
193 #   Swap is shown in the graphs as well, although it's often just a few pixels
194 #   in width.
195 # - Parse PSS from SMAPS and add 'PSS' column in per-process tables.
196 # - Include Slab Reclaimable in system free memory calculations. The kernel low
197 #   memory notification calculations take these into account as well.
198 # - Remove SwapCached from the free memory calculations, it is already included
199 #   in SwapFree.
200 # - Take swap into consideration when looking for changes in Dirty and Size.
201 #   This affects what processes are listed under the "Processes memory usage"
202 #   section.
203 # - Add legend for the "Processes memory usage" section graphs.
204 # - Add UTF-8 header in HTML, some X client names may contain UTF-8 characters.
205 # - Include process name when giving warning about missing SMAPS data.
206 # - Python Gzip module is slow, so use /bin/zcat and popen() instead if
207 #   available. Gives 2-3x speed up.
208 # - Use the psyco JIT compiler, if installed. Gives 2-3x speed up.
209 # 2009-04-22:
210 # - Add System Load graph to the resource usage overview, that shows the CPU
211 #   time distribution between system processes, user processes, I/O-wait, etc.
212 #   The graph is generated with information parsed from /proc/stat.
213 # - Add Process CPU Usage graph for each test rounds, that shows the CPU time
214 #   distribution between processes during that particular test round. The graph
215 #   is generated with information parsed from /proc/pid/stat files.
216 # - Use RSS and Size from SMAPS data if available, instead of the ones from
217 #   /proc/pid/status. This fixes cases where PSS > RSS, because the SMAPS data
218 #   also includes device mappings.
219 # - Add support for lzop compressed smaps and syslog files.
220 # 2009-05-13:
221 # - Introduce a new section Kernel Events, and add tables Virtual Memory
222 #   Subsystem and Low Level System Events. The former includes details about
223 #   page faults and swap, and the latter details about the number of interrupts
224 #   and context switches. Data is parsed from /proc/stat and /proc/vmstat.
225 #   The numbers are highlited in red if they exceed certain fixed thresholds.
226 # - Process CPU Usage graph: show summary about the processes that we did not
227 #   include in the graph.
228 # 2009-06-01:
229 # - System Load graph: fix division with zero with exactly identical data. This
230 #   happened if user manually made another copy of one of the snapshot
231 #   directories.
232 # 2009-10-15:
233 # - Take last three xresource values, not ones from fixed offset.
234 # 2009-10-26:
235 # - Adapt to proc2csv providing whole command line
236 # - Parse X client resource counts and show them in summary
237 # - Generalize and move compressed file logging, opening and error
238 #   handling to syslog_parse.py
239 # TODO:
240 # - Mark reboots more prominently also in report (<h1>):
241 #   - dsme/stats/32wd_to -> HW watchdog reboot
242 #   - dsme/stats/sw_rst -> SW watchdog reboots
243 #   - bootreason -> last boot
244 # - Proper option parsing + possibility to state between which
245 #   test runs to produce the summaries?
246 # - Show differences in slabinfo and vmstat numbers (pswpin/pswpout)?
247 """
248 NAME
249         <TOOL_NAME>
250
251 SYNOPSIS
252         <TOOL_NAME> <data directories>
253
254 DESCRIPTION
255
256 This script reads data files produced by the endurance measurement
257 tools.   The data is gathered from proc, X server, SMAPS, syslog etc.
258
259 By default all arguments are assumed to be names of directories
260 containing (at least):
261     - usage.csv   -- /proc/ info + X resource & disk usage in CSV format
262     - slabinfo    -- information about kernel caches, see slabinfo(5)
263     - stat        -- kernel/system statistics, see proc(5)
264     - syslog[.gz] -- [compressed] syslog contents
265
266 As an output, it produces an HTML page listing/highlighting differencies
267 between the CSV, SMAPS and syslog files for the following values:
268     - Graph of system free memory changes
269     - Memory usages for processes which private memory usage
270       changes (as reported by sp_smaps_snapshot)
271     - Number of file descriptors used by the (system) processes
272     - Number of logged errors (from syslog)
273 The errors in syslog are output to a separate file.
274         
275 EXAMPLES
276         <TOOL_NAME> usecase/ usecase2/ > report.html
277 """
278
279 import sys, os, re
280 import syslog_parse as syslog
281
282 # CSV field separator
283 SEPARATOR = ','
284
285 # these are HTML hex color values for different HTML tables
286 class Colors:
287     errors = "FDEEEE"
288     disk = "EEFDFD"
289     memory = "EEEEFD"
290     threads = "CFEFEF"
291     xres_mem = "FDEEFD"
292     xres_count = "EEDDEE"
293     fds = "FDFDEE"
294     shm = "EEEEEE"
295     kernel = "EFFDF0"
296
297 # color values for (Swap used, RAM used, memory free, oom-limit)
298 #      magenta, blue, light green, red
299 bar1colors = ("EE00FF", "3149BD", "ADE739", "DE2821")
300 # color values for (Swap, Dirty, PSS, RSS, Size)
301 #      magenta, red, orange, orangeish, yellow
302 bar2colors = (bar1colors[0], "DE2821", "E0673E", "EAB040", "FBE84A")
303 # color values for CPU load (system, user, user nice, iowait, idle)
304 #      red, blue, light blue, magenta, light green
305 bar3colors = (bar2colors[1], bar1colors[1], "4265FF", bar1colors[0], bar1colors[2])
306
307
308 # --------------------- SMAPS data parsing --------------------------
309
310 # address range, access rights, page offset, major:minor, inode, mmap()ed item
311 smaps_mmap = re.compile("^[-0-9a-f]+ ([-rwxps]+) [0-9a-f]+ [:0-9a-f]+ \d+ *(|[^ ].*)$")
312
313 # data from sp_smaps_snapshot
314 def parse_smaps(file):
315     """
316     Parse SMAPS and return (smaps, private_code):
317       'smaps'        : Per PID dict with keys: private_dirty, swap, pss, rss
318                        and size, which are sums of the SMAPS fields. All these
319                        fields are initialized to 0 for each PID.
320       'private_code' : Amount of Private Dirty mappings for code pages in whole
321                        system.
322     Everything is in kilobytes.
323     """
324     private_code = code = idx = 0
325     smaps = {}
326     while 1:
327         try:
328             line = file.readline()
329         except IOError, e:
330             syslog.parse_error(write, "ERROR: SMAPS file '%s': %s" % (file, e))
331             break
332         if not line:
333             break
334         idx += 1
335         line = line.strip()
336         if not line:
337             continue
338         #print line        #DEBUG
339         if line.startswith('='):
340             # ==> /proc/767/smaps <==
341             continue
342         if line.startswith('#'):
343             if line.find("#Pid: ") == 0:
344                 pid = line[6:]
345                 smaps[pid] = { 'private_dirty' : 0,
346                                'swap'          : 0,
347                                'pss'           : 0,
348                                'rss'           : 0,
349                                'size'          : 0 }
350             continue
351         if not pid:
352             # sanity check
353             sys.stderr.write("ERROR: Pid missing for SMAPS line %d:\n  %s\n" % (idx, line))
354             sys.exit(1)
355         if line.startswith("Private_Dirty:"):
356             amount = int(line[15:-2])
357             if code and amount:
358                 #print line
359                 #sys.stderr.write("dirty code: %s, %dkB\n" %(mmap, amount))
360                 private_code += amount
361             smaps[pid]['private_dirty'] += amount
362             #print "ADD"        #DEBUG
363             continue
364         if line.startswith("Swap:"):
365             smaps[pid]['swap'] += int(line[6:-2])
366             continue
367         if line.startswith("Pss:"):
368             smaps[pid]['pss'] += int(line[5:-2])
369             continue
370         if line.startswith("Rss:"):
371             smaps[pid]['rss'] += int(line[5:-2])
372             continue
373         if line.startswith("Size:"):
374             smaps[pid]['size'] += int(line[6:-2])
375             continue
376         match = smaps_mmap.search(line)
377         if match:
378             # bef45000-bef5a000 rwxp bef45000 00:00 0          [stack]
379             mmap = match.group(2)
380             # code memory map = executable (..x.) and file (/path/...)?
381             if match.group(1)[2] == 'x' and mmap and mmap[0] == '/':
382                 #debug_line = match.group(0)
383                 code = 1
384             else:
385                 code = 0
386             #print "MMAP"        #DEBUG
387             continue
388         # sanity check that mmap lines are not missed
389         if (line[0] >= '0' and line[0] <= '9') or (line[0] >= 'a' and line[0] <= 'f'):
390             sys.stderr.write("ERROR: SMAPS mmap line not matched:\n  %s\n" % line)
391             sys.exit(1)
392     return (smaps, private_code)
393
394
395 # --------------------- CSV parsing ---------------------------
396
397 def get_filesystem_usage(file):
398     """reads Filesystem,1k-blocks,Used,Available,Use%,Mountpoint fields
399     until empty line, returns hash of free space on interesting mountpoints
400     """
401     mounts = {}
402     # device root and tmpfs with fixed size
403     keep = {'/':1, '/tmp':1}
404     while 1:
405         line = file.readline().strip()
406         if not line:
407             break
408         fs,blocks,used,available,inuse,mount = line.split(',')
409         if mount not in keep:
410             continue
411         mounts[mount] = int(used)
412     return mounts
413
414
415 def get_xres_usage(file):
416     "reads X client resource usage, return command hash of total X mem usage"
417     xres_mem = {}
418     xres_count = {}
419     while 1:
420         line = file.readline().strip()
421         if not line:
422             break
423         
424         # last three columns are the most interesting ones
425         mem,pid,name = line.split(',')[-3:]
426         if pid[-1] != 'B' and mem[-1] == 'B':
427             mem = int(mem[:-1])
428         else:
429             sys.stderr.write("Error: X resource total memory value not followed by 'B':\n  %s\n" % line)
430             sys.exit(1)
431         # in KBs, check on clients taking > 1KB
432         if mem >= 1024:
433             xres_mem[name] = mem/1024
434         
435         count = 0
436         # resource base, counts of resources, their memory usages, PID, name
437         cols = line.split(',')
438         for i in range(1, len(cols) - 3):
439             if cols[i][-1] != 'B':
440                 count += int(cols[i])
441         xres_count[name] = count
442
443     return (xres_mem, xres_count)
444
445
446 def get_process_info(file, headers):
447     """returns all process information in a hash indexed by the process PID,
448     containing hash of information provided by the /proc/PID/status file
449     (proc entry field name works as the hash key)
450     """
451     kthreads = {}
452     processes = {}
453     fields = headers.strip().split(',')
454     fields[-1] = fields[-1].split(':')[0]        # remove ':' from last field
455     pididx = fields.index('Pid')
456     nameidx = fields.index('Name')
457     while 1:
458         line = file.readline().strip()
459         if not line:
460             break
461         item = {}
462         info = line.split(',')
463         # kernel threads & zombies don't have all the fields
464         if len(info) < len(fields):
465             kthreads[info[pididx]] = info[nameidx]
466             continue
467         elif len(info) > len(fields):
468             sys.stderr.write("WARNING: Process [%s] has extra column(s) in CSV data!\n" % info[pididx])
469         for idx in range(len(fields)):
470             if info[idx][-3:] == " kB":
471                 # convert memory values to integers
472                 item[fields[idx]] = int(info[idx][:-3])
473             else:
474                 item[fields[idx]] = info[idx]
475         processes[item['Pid']] = item
476     return processes, kthreads
477
478
479 def get_shm_counts(file):
480     """reads shared memory segment lines until empty line,
481     returns total count of segments and ones with <2 users"""
482
483     headers = file.readline().strip()
484     if ("size" not in headers) or ("nattch" not in headers):
485         sys.stderr.write("\nError: Shared memory segments list header '%s' missing 'nattch' or 'size' column\n" % headers)
486         sys.exit(1)
487     nattach_idx = headers.split(',').index("nattch")
488     #size_idx = headers.split(',').index("size")
489     
490     size = others = orphans = 0
491     while 1:
492         line = file.readline().strip()
493         if not line:
494             break
495         items = line.split(',')
496         # how many processes attaches to the segment
497         if int(items[nattach_idx]) > 1:
498             others += 1
499         else:
500             orphans += 1
501         # size in KB, rounded to next page
502         #size += (int(items[size_idx]) + 4095) / 1024
503     return {
504         #"Total of segment sizes (KB)": size,
505         "Normal segments (>1 attached processes)": others,
506         "Orphan segments (<=1 attached processes)": orphans
507     }
508
509
510 def get_commands_and_fd_counts(file):
511     """reads fdcount,pid,command lines until empty line,
512     returns pid hashes of command names and fd counts"""
513     commands = {}
514     fd_counts = {}
515     while 1:
516         line = file.readline().strip()
517         if not line:
518             break
519         pid,fds,name = line.split(',')
520         commands[pid] = os.path.basename(name.split(' ')[0])
521         fd_counts[pid] = int(fds)
522     return (commands,fd_counts)
523
524
525 def parse_proc_stat(file):
526     "Parses relevant data from /proc/stat"
527     stat = {}
528     # CPU: take everything except "steal" and "guest", which are some
529     # virtualization related counters, obviously not useful in our case.
530     cpu = re.compile("^cpu\s+(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)")
531     # Interrupts: take first column, it contains the sum of the individual
532     # interrupts.
533     intr = re.compile("^intr\s+(\d+)")
534     # Context switches.
535     ctxt = re.compile("^ctxt\s+(\d+)")
536     for line in file:
537         m = cpu.search(line)
538         if m:
539             stat['cpu'] = {}
540             stat['cpu']['user'],       \
541             stat['cpu']['user_nice'],  \
542             stat['cpu']['system'],     \
543             stat['cpu']['idle'],       \
544             stat['cpu']['iowait'],     \
545             stat['cpu']['irq'],        \
546             stat['cpu']['softirq']     \
547                 = [int(x) for x in m.groups()]
548             continue
549         m = intr.search(line)
550         if m:
551             stat['intr'] = int(m.group(1))
552             continue
553         m = ctxt.search(line)
554         if m:
555             stat['ctxt'] = int(m.group(1))
556             continue
557     return stat
558
559
560 def get_proc_pid_stat(file):
561     """
562     Parses relevant data from /proc/pid/stat entries, and returns dict with per
563     process information:
564                 pid : utime
565                 pid : stime
566     """
567     stat = {}
568     while 1:
569         line = file.readline().strip()
570         if not line:
571             break
572         pid = int(line.split(',')[0])
573         utime, stime = [int(x) for x in line.split(',')[13:15]]
574         stat[pid] = {}
575         stat[pid]['utime'] = utime
576         stat[pid]['stime'] = stime
577     return stat
578
579
580 def get_meminfo(data, headers, values):
581     "adds meminfo values to data"
582     headers = headers.split(',')
583     values = values.split(',')
584     mem = {}
585     for i in range(len(values)):
586         # remove 'kB'
587         mem[headers[i]] = int(values[i].split(" kB")[0])
588     total = mem['MemTotal']
589     free = mem['MemFree']
590     buffers = mem['Buffers']
591     cached = mem['Cached']
592     slab_reclaimable = mem['SReclaimable']
593     swaptotal = mem['SwapTotal']
594     swapfree = mem['SwapFree']
595
596     data['ram_total'] = total
597     data['ram_free'] = free + buffers + cached + slab_reclaimable
598     data['ram_used'] = data['ram_total'] - data['ram_free']
599     data['swap_total'] = swaptotal
600     data['swap_free'] = swapfree
601     data['swap_used'] = swaptotal - swapfree
602
603
604 def skip_to(file, header):
605     "reads the given file until first CSV column has given header"
606     l = len(header)
607     while 1:
608         line = file.readline()
609         if not line:
610             sys.stderr.write("\nError: premature file end, CSV header '%s' not found\n" % header)
611             sys.exit(2)
612         if line[:l] == header:
613             return line
614
615
616 def skip_to_next_header(file):
617     "reads the given file until we get first nonempty line"
618     while 1:
619         line = file.readline()
620         if not line:
621             sys.stderr.write("\nError: premature file end while scanning for CSV header\n")
622             sys.exit(2)
623         if line.strip():
624             return line.strip()
625
626
627 def parse_csv(file):
628     "Parses interesting information from the endurance measurement CSV file"
629     data = {}
630     
631     # Check that file is generated with correct script so that
632     # we can trust it's format and order of rows & fields:
633     # format: generator = <generator name> <version>
634     mygen = "syte-endurance-stats"
635     generator = file.readline().strip().split(' ')
636     if len(generator) < 3 or generator[2] != mygen:
637         sys.stderr.write("\nError: CSV file '%s' is not generated by '%s'!\n" % (filename, mygen))
638         sys.exit(1)
639     
640     # get the basic data
641     file.readline()
642     data['release'] = file.readline().strip()
643     data['datetime'] = file.readline().strip()
644     if data['release'][:2] != "SW" or data['datetime'][:4] != "date":
645         sys.stderr.write("\nError: CSV file '%s' is missing 'SW-version' or 'date' fields!\n" % filename)
646         sys.exit(1)
647
648     # total,free,buffers,cached
649     mem_header = skip_to(file, "MemTotal").strip()
650     mem_values = file.readline().strip()
651     get_meminfo(data, mem_header, mem_values)
652
653     # /proc/vmstat
654     # The header line ends with ':', so get rid of that.
655     keys = skip_to(file, "nr_free_pages").strip()[:-1].split(',')
656     try:
657         vals = [int(x) for x in file.readline().strip().split(',')]
658         data['/proc/vmstat'] = dict(zip(keys, vals))
659     except:
660         pass
661
662     # low memory limits
663     skip_to(file, "lowmem_")
664     mem = file.readline().split(',')
665     if len(mem) == 3:
666         data['limitlow'] = int(mem[0])
667         data['limithigh'] = int(mem[1])
668         data['limitdeny'] = int(mem[2])
669     else:
670         # not fatal as lowmem stuff is not in standard kernel
671         sys.stderr.write("\nWarning: CSV file '%s' lowmem limits are missing!\n" % filename)
672         data['limitlow'] = data['limithigh'] = data['limitdeny'] = 0
673
674     # get shared memory segment counts
675     skip_to(file, "Shared memory segments")
676     data['shm'] = get_shm_counts(file)
677
678     # get system free FDs
679     skip_to(file, "Allocated FDs")
680     fdused,fdfree,fdtotal = file.readline().split(',')
681     data['fdfree'] = (int(fdtotal) - int(fdused)) + int(fdfree)
682
683     # get the process FD usage
684     skip_to(file, "PID,FD count,Command")
685     data['commands'], data['fdcounts'] = get_commands_and_fd_counts(file)
686     
687     # get process statistics
688     headers = skip_to(file, "Name,State,")
689     data['processes'], data['kthreads'] = get_process_info(file, headers)
690
691     # check if we have /proc/pid/stat in the CSV file
692     headers = skip_to_next_header(file)
693     if headers.startswith("Process status:"):
694         data['/proc/pid/stat'] = get_proc_pid_stat(file)
695         skip_to(file, "res-base")
696     elif headers.startswith("res-base"):
697         pass
698     else:
699         sys.stderr.write("\nError: unexpected '%s' in CSV file\n" % headers)
700         sys.exit(2)
701
702     # get the X resource usage
703     data['xclient_mem'], data['xclient_count'] = get_xres_usage(file)
704     
705     # get the file system usage
706     skip_to(file, "Filesystem")
707     data['mounts'] = get_filesystem_usage(file)
708     
709     return data
710
711 # --------------------- HTML output ---------------------------
712
713 def get_pids_from_procs(processes, commands):
714     "return pid:name dictionary for given processes array"
715     pids = {}
716     for process in processes.values():
717         pid = process['Pid']
718         name = process['Name']
719         if name == "maemo-launcher":
720             # commands array takes the name from /proc/PID/cmdline
721             pids[pid] = commands[pid]
722         else:
723             pids[pid] = name
724     return pids
725         
726 def output_process_changes(pids1, pids2, titles, do_summary):
727     "outputs which commands are new and which gone in separate columns"
728     # ignore re-starts i.e. check only command names
729     gone = []
730     new_coms = []
731     new_pids = []
732     for pid in pids2:
733         if pid not in pids1:
734             new_coms.append("%s[%s]" % (pids2[pid], pid))
735     for pid in pids1:
736         if pid not in pids2:
737             gone.append("%s[%s]" % (pids1[pid], pid))
738     change = 0
739     if gone or new_coms or new_pids:
740         processes = len(pids2)
741         change = processes - len(pids1)
742         print "<p>%s: <b>%d</b>" % (titles[0], change)
743         print "<br>(now totaling %d)." % processes
744
745         print "<p><table border=1>"
746         print "<tr><th>%s</th><th>%s</th><tr>" % (titles[1], titles[2])
747         print "<tr><td>"
748         if gone:
749             print "<ul>"
750             gone.sort()
751             for name in gone:
752                 print "<li>%s" % name
753             print "</ul>"
754         print "</td><td>"
755         if new_coms or new_pids:
756             print "<ul>"
757             new_coms.sort()
758             for name in new_coms:
759                 print "<li>%s" % name
760             new_pids.sort()
761             for name in new_pids:
762                 print "<li>%s" % name
763             print "</ul>"
764         print "</td></tr></table>"
765     if do_summary:
766         print "<!--\n- %s: %+d\n-->" % (titles[0], change)
767
768
769 def output_diffs(diffs, title, colname, colamount, colors, do_summary):
770     "output diffs of data: { difference, total, name }"
771     total = 0
772     if diffs:
773         diffs.sort()
774         diffs.reverse()
775         print '\n<p><table border=1 bgcolor="#%s">' % colors
776         print "<caption><i>%s</i></caption>" % title
777         print "<tr><th>%s:</th><th>Change:</th><th>Total:</th></tr>" % colname
778         for data in diffs:
779             total += data[0]
780             print "<tr><td>%s</td><td align=right><b>%+d</b>%s</td><td align=right>%d%s</td></tr>" % (data[2], data[0], colamount, data[1], colamount)
781         print "<tr><td align=right><i>Total change =</i></td><td align=right><b>%+d%s</b></td><td>&nbsp;</td>" % (total, colamount)
782         print "</table>"
783     if do_summary:
784         print "<!--\n- %s change: %+d\n-->" % (title, total)
785     
786     
787 def get_usage_diffs(list1, list2):
788     """return list of (total, name, diff) change tuples for given items"""
789     diffs = []
790     for name,value2 in list2.items():
791         if name in list1:
792             value1 = list1[name]
793             if value2 != value1:
794                 # will be sorted according to first column
795                 diffs.append((value2 - value1, value2, name))
796     return diffs
797
798
799 def pid_is_main_thread(pid, commands, processes):
800     "return true if PID is the main thread, otherwise false"
801     # command list has better name than process list
802     process = processes[pid]
803     ppid = process['PPid']
804     name = commands[pid]
805     if ppid != '1' and ppid in commands and name == commands[ppid]:
806         # parent has same name as this process...
807         if ppid in processes and process['VmSize'] == processes[ppid]['VmSize']:
808             # and also size
809             # -> assume it's a thread which should be ignored
810             return 0
811     return 1
812
813
814 def get_pid_usage_diffs(commands, processes, values1, values2):
815     """return {diff, total, name} hash of differences in numbers between
816     two {pid:value} hashes, remove threads based on given 'processes' hash
817     and name the rest based on the given 'commands' hash"""
818     diffs = []
819     for pid in values2:
820         if pid in values1:
821             c1 = values1[pid]
822             c2 = values2[pid]
823             if c1 != c2:
824                 if pid not in processes or pid not in commands:
825                     sys.stderr.write("Warning: PID %s not in commands or processes\n" % pid)
826                     continue
827                 if not pid_is_main_thread(pid, commands, processes):
828                     continue
829                 name = commands[pid]
830                 # will be sorted according to first column (i.e. change)
831                 diffs.append((c2-c1, c2, "%s[%s]" % (name, pid)))
832     return diffs
833
834
835 def get_thread_count_diffs(commands, processes1, processes2):
836     """return { difference, total, name } hash where name is taken from
837     'commands', total is taken from 'processes2', and differences in
838     thread counts is between 'processes2'-'processes1' and all these
839     are matched by pids."""
840     diffs = []
841     for pid in commands:
842         if pid in processes2 and pid in processes1:
843             t1 = int(processes1[pid]['Threads'])
844             t2 = int(processes2[pid]['Threads'])
845             if t1 == t2:
846                 continue
847             name = commands[pid]
848             # will be sorted according to first column
849             diffs.append((t2-t1, t2, "%s[%s]" % (name, pid)))
850     return diffs
851
852
853 def output_errors(idx, run1, run2):
854     "write syslog errors to separate HTML file and return statistics"
855
856     title = "Errors for round %d" % idx
857     url = "%s/errors.html" % run2['basedir']
858     write = open(url, "w").write
859
860     # write the separate error report...
861     write("<html>\n<title>%s</title>\n<body>\n<h1>%s</h1>\n" % (title, title))
862     if 'errors' in run1:
863         errors1 = run1['errors']
864         path = run1['basedir']
865         if path[0] != '/':
866             # assume files are in the same hierachy
867             path = "../" + path.split('/')[-1]
868         path += "/errors.html"
869         write('<a href="%s">Errors for previous round</a>\n' % path)
870     else:
871         errors1 = {}
872     if 'errors' in run2:
873         errors2 = run2['errors']
874     else:
875         errors2 = {}
876     stat = syslog.output_errors(write, errors1, errors2)
877     write("<hr>\n")
878     syslog.explain_signals(write)
879     write("</body>\n</html>\n")
880
881     # ...and summary for the main page
882     for value in stat.values():
883         if value:
884             syslog.errors_summary(stat, url, Colors.errors)
885             break
886     return stat
887
888
889 def output_data_links(run):
890     "output links to all collected data"
891     basedir = run['basedir']
892     print "<h4>For more details on...</h4>"
893     print "<ul>"
894     if 'logfile' in run:
895         print '<li>log messages, see <a href="%s">syslog</a>' % run['logfile']
896     if os.path.exists("%s/smaps.html" % basedir):
897         print "<li>private memory usage of all processes, see"
898         print '<a href="%s/smaps.html">smaps overview</a>' % basedir
899     elif os.path.exists("%s/smaps.cap" % basedir):
900         print "<li>private memory usage of all processes, see"
901         print '<a href="%s/smaps.cap">smaps data</a>' % basedir
902     print "<li>process and device state details, see"
903     print '<a href="%s/usage.csv">collected CSV data</a> and' % basedir
904     print '<a href="%s/ifconfig">ifconfig output</a>' % basedir
905     print "<li>rest of /proc/ information; see "
906     if os.path.exists("%s/open-fds" % basedir):
907         print '<a href="%s/open-fds">open file descriptors</a>, ' % basedir
908     print '<a href="%s/interrupts">interrupts</a>, ' % basedir
909     print '<a href="%s/slabinfo">slabinfo</a> and' % basedir
910     print '<a href="%s/stat">stat</a> files' % basedir
911     print "</ul>"
912
913 def combine_dirty_and_swap(smaps):
914     "Combines private dirty and swap memory usage for each PID"
915     result = {}
916     for pid in smaps:
917         result[pid] = smaps[pid]['private_dirty'] + smaps[pid]['swap']
918     return result
919
920 def output_run_diffs(idx1, idx2, data, do_summary):
921     "outputs the differencies between two runs"
922
923     run1 = data[idx1]
924     run2 = data[idx2]
925     if run1['release'] != run2['release']:
926         syslog.parse_error(sys.stdout.write, "ERROR: release '%s' doesn't match previous round release '%s'!" % (run1['release'], run2['release']))
927         return None
928
929     # syslogged errors
930     if do_summary:
931         stat = None
932     else:
933         stat = output_errors(idx2, run1, run2)
934
935     # Create the following table (based on /proc/pid/stat):
936     #
937     #   Command[Pid]: system / user       CPU Usage:
938     #   app2[1234]:   ###########%%%%%%%  45%  (90s)
939     #   app1[987]:    ######%%%%%%%%%     44%  (88s)
940     #   app3[543]:    #%                   5%  (10s)
941     #
942     def process_cpu_usage():
943         CLK_TCK=100.0
944         if not '/proc/pid/stat' in run1 or not '/proc/pid/stat' in run2:
945             return
946         print "<h4>Process CPU usage</h4>"
947         cpusum1 = sum(run1['/proc/stat']['cpu'].itervalues())
948         cpusum2 = sum(run2['/proc/stat']['cpu'].itervalues())
949         if cpusum2 < cpusum1:
950             print "<p><i>System reboot detected, omitted.</i>"
951             return
952         elif cpusum2 == cpusum1:
953             # Two identical entries? Most likely user has manually copied the snapshot directories.
954             print "<p><i>Identical snapshots detected, omitted.</i>"
955             return
956         cpu_total_diff = float(cpusum2-cpusum1)
957         print "<p>Interval between rounds was %d seconds." % (cpu_total_diff/CLK_TCK)
958         if cpu_total_diff <= 0:
959             return
960         print "<p>"
961         diffs = []
962         for pid in iter(run2['/proc/pid/stat']):
963             stime1 = utime1 = 0
964             if pid in run1['/proc/pid/stat']:
965                 stime1 = run1['/proc/pid/stat'][pid]['stime']
966                 utime1 = run1['/proc/pid/stat'][pid]['utime']
967             stimediff  = run2['/proc/pid/stat'][pid]['stime']-stime1
968             utimediff  = run2['/proc/pid/stat'][pid]['utime']-utime1
969             if str(pid) in run2['kthreads']:
970                 name = "[" + run2['kthreads'][str(pid)] + "]"
971             else:
972                 name = run2['commands'][str(pid)]
973             diffs.append(("%s[%d]" % (name, pid), stimediff, utimediff))
974         # Other processes often eat significant amount of CPU, so lets show
975         # that to the user as well.
976         def total_sys(r):
977             return r['/proc/stat']['cpu']['system'] + r['/proc/stat']['cpu']['irq'] + r['/proc/stat']['cpu']['softirq']
978         def total_usr(r):
979             return r['/proc/stat']['cpu']['user'] + r['/proc/stat']['cpu']['user_nice']
980         UNACC = "<i>(Unaccounted CPU time)</i>"
981         diffs.append((UNACC,\
982                 total_sys(run2)-total_sys(run1)-sum([x[1] for x in diffs]),\
983                 total_usr(run2)-total_usr(run1)-sum([x[2] for x in diffs])))
984         # Dont include in the graph those processes that have used only a
985         # little CPU, but collect them and show some statistics.
986         THRESHOLD = max(1, 0.005*cpu_total_diff)
987         filtered_out = []
988         diffs2 = []
989         for x in diffs:
990             if x[1]+x[2] > THRESHOLD:
991                 diffs2.append(x)
992             elif x[1]+x[2] > 0:
993                 filtered_out.append(x)
994         diffs = diffs2
995         # Sort in descending order of CPU ticks used.
996         diffs.sort(lambda x,y: cmp(x[1]+x[2], y[1]+y[2]))
997         diffs.reverse()
998         if len(diffs)==0:
999             return
1000         # Scale the graphics to the largest CPU usage value.
1001         divisor = float(sum(diffs[0][1:3]))
1002         output_memory_graph_table(\
1003             ("Command[Pid]:", "<font color=%s>system</font> / <font color=%s>user</font>" % bar3colors[0:2], "CPU Usage:"),
1004             bar3colors[0:2],\
1005             [(x[0], (x[1]/divisor, x[2]/divisor),\
1006                 ["%.2f%% (%.2fs)" % (100*(x[1]+x[2])/cpu_total_diff, (x[1]+x[2])/CLK_TCK)]) for x in diffs]\
1007             + [("", (0,0), ["<i>%.2f%% (%.2fs)</i>" % (\
1008                     100*sum([x[1]+x[2] for x in diffs])/cpu_total_diff,\
1009                         sum([x[1]+x[2] for x in diffs])/CLK_TCK)\
1010                 ])])
1011         if filtered_out:
1012             print "<p><i>Note:</i> %d other processes also used some CPU, but "\
1013                   "did not exceed the threshold of 0.5%% CPU Usage (%.2f seconds).<br>"\
1014                   "They used %.2f seconds of CPU time in total." \
1015                   % (len(filtered_out), THRESHOLD/CLK_TCK, sum([x[1]+x[2] for x in filtered_out])/CLK_TCK)
1016         if UNACC in [x[0] for x in diffs]:
1017             print "<p><i>Unaccounted CPU time</i> stands for such CPU time that "\
1018                   "could not be attributed to any process.<br>"\
1019                   "These can be for example short living programs that "\
1020                   "started and exited during one round of the tests."
1021
1022     process_cpu_usage()
1023
1024     print "<h4>Resource usage changes</h4>"
1025
1026     # overall stats
1027     total_change = (run2['ram_free']+run2['swap_free']) - (run1['ram_free']+run1['swap_free'])
1028     ram_change = run2['ram_free'] - run1['ram_free']
1029     swap_change = run2['swap_free'] - run1['swap_free']
1030     fdfree_change = run2['fdfree'] - run1['fdfree']
1031     print "<p>System free memory change: <b>%+d</b> kB" % total_change
1032     if ram_change or swap_change:
1033         print "<br>(free RAM change: <b>%+d</b> kB, free swap change: <b>%+d</b> kB)" % (ram_change, swap_change)
1034     print "<br>System unused file descriptor change: <b>%+d</b>" % fdfree_change
1035     if run2['fdfree'] < 200:
1036         print "<br><font color=red>Less than 200 FDs are free in the system.</font>"
1037     elif run2['fdfree'] < 500:
1038         print "<br>(Less that 500 FDs are free in the system.)"
1039     if do_summary:
1040         print """
1041 <!--
1042 - System free memory change: %+d
1043 - System free RAM change: %+d
1044 - System free swap change: %+d
1045 - System free FD change: %+d
1046 -->""" % (total_change, ram_change, swap_change, fdfree_change)
1047         if 'private_code' in run1:
1048             dcode_change = run2['private_code'] - run1['private_code']
1049             if dcode_change:
1050                 print "<br>System private dirty code pages change: <b>%+d</b> kB" % dcode_change
1051
1052     # filesystem usage changes
1053     diffs = get_usage_diffs(run1['mounts'], run2['mounts'])
1054     output_diffs(diffs, "Filesystem usage", "Mount", " kB",
1055                 Colors.disk, do_summary)
1056
1057     # Combine Private dirty + swap into one table. The idea is to reduce the
1058     # amount of data included in the report (=less tables & smaller HTML file
1059     # size), and entries like -4 kB private dirty & +4 kB swap. Most of the
1060     # swapped pages will be private dirty anyways.
1061     if 'smaps' in run1:
1062         diffs = get_pid_usage_diffs(run2['commands'], run2['processes'],
1063                         combine_dirty_and_swap(run1['smaps']),
1064                         combine_dirty_and_swap(run2['smaps']))
1065         output_diffs(diffs,
1066                 "Process private and swap memory usages combined (according to SMAPS)",
1067                 "Command[Pid]", " kB", Colors.memory, do_summary)
1068     else:
1069         print "<p>No SMAPS data for process private memory usage available."
1070     
1071     # process X resource usage changes
1072     diffs = get_usage_diffs(run1['xclient_mem'], run2['xclient_mem'])
1073     output_diffs(diffs, "X resource memory usage", "X client", " kB",
1074                  Colors.xres_mem, do_summary)
1075     if do_summary:
1076         diffs = get_usage_diffs(run1['xclient_count'], run2['xclient_count'])
1077         output_diffs(diffs, "X resource count", "X client", "",
1078                      Colors.xres_count, do_summary)
1079     
1080     # FD count changes
1081     diffs = get_pid_usage_diffs(run2['commands'], run2['processes'],
1082                     run1['fdcounts'], run2['fdcounts'])
1083     output_diffs(diffs, "Process file descriptor count", "Command[Pid]", "",
1084                     Colors.fds, do_summary)
1085
1086     # shared memory segment count changes
1087     diffs = get_usage_diffs(run1['shm'], run2['shm'])
1088     output_diffs(diffs, "Shared memory segments", "Type", "",
1089                 Colors.shm, do_summary)
1090
1091     # Kernel statistics
1092     cpu_total_diff = float(sum(run2['/proc/stat']['cpu'].itervalues())-sum(run1['/proc/stat']['cpu'].itervalues()))
1093     if cpu_total_diff > 0:
1094         print "\n<h4>Kernel events</h4>"
1095
1096         def format_key(key, max):
1097             if key > max:
1098                 return "<font color=red>%.1f</font>" % key
1099             else:
1100                 return "%.1f" % key
1101
1102         # Kernel virtual memory subsystem statistics, /proc/vmstat
1103         pgmajfault = (run2['/proc/vmstat']['pgmajfault']-run1['/proc/vmstat']['pgmajfault'])/cpu_total_diff*3600
1104         pswpin     = (run2['/proc/vmstat']['pswpin']-run1['/proc/vmstat']['pswpin'])/cpu_total_diff*3600
1105         pswpout    = (run2['/proc/vmstat']['pswpout']-run1['/proc/vmstat']['pswpout'])/cpu_total_diff*3600
1106         diffs = []
1107         if pgmajfault > 0:
1108             diffs.append(("Major page faults per hour", format_key(pgmajfault, 1000)))
1109         if pswpin > 0:
1110             diffs.append(("Page swap ins per hour", format_key(pswpin, 10000)))
1111         if pswpout > 0:
1112             diffs.append(("Page swap outs per hour", format_key(pswpout, 1000)))
1113         if diffs:
1114             print '\n<p><table border=1 bgcolor=%s>' % Colors.kernel
1115             print "<caption><i>Virtual memory subsystem</i></caption>"
1116             print "<tr><th>Type:</th><th>Value:</th></tr>"
1117             for data in diffs:
1118                 print "<tr><td>%s</td><td align=right><b>%s</b></td></tr>" % data
1119             print "</table>"
1120
1121         # Interrupts and context switches.
1122         intr = (run2['/proc/stat']['intr']-run1['/proc/stat']['intr'])/cpu_total_diff
1123         ctxt = (run2['/proc/stat']['ctxt']-run1['/proc/stat']['ctxt'])/cpu_total_diff
1124         diffs = [
1125                  ("Interrupts per second", format_key(intr, 1e5/3600)),
1126                  ("Context switches per second", format_key(ctxt, 1e6/3600)),
1127                 ]
1128         print '\n<p><table border=1 bgcolor=%s>' % Colors.kernel
1129         print "<caption><i>Low level system events</i></caption>"
1130         print "<tr><th>Type:</th><th>Value:</th></tr>"
1131         for data in diffs:
1132             print "<tr><td>%s</td><td align=right><b>%s</b></td></tr>" % data
1133         print "</table>"
1134
1135
1136     print "\n<h4>Changes in processes</h4>"
1137
1138     # thread count changes
1139     diffs = get_thread_count_diffs(run2['commands'],
1140                     run1['processes'], run2['processes'])
1141     output_diffs(diffs, "Process thread count", "Command[Pid]", "",
1142                     Colors.threads, do_summary)
1143
1144     # new and closed processes
1145     titles = ("Change in number of processes",
1146               "Exited processes",
1147               "New processes")
1148     output_process_changes(
1149                 get_pids_from_procs(run1['processes'], run1['commands']),
1150                 get_pids_from_procs(run2['processes'], run2['commands']),
1151                 titles, do_summary)
1152
1153     # new and collected kthreads/zombies
1154     titles = ("Change in number of kernel threads and zombie processes",
1155               "Collected kthreads/zombies",
1156               "New kthreads/zombies")
1157     output_process_changes(run1['kthreads'], run2['kthreads'], titles, do_summary)
1158     return stat
1159
1160
1161 def output_initial_state(run):
1162     "show basic information about the test run"
1163     print "<p>%s" % run['release']
1164     print "<p>%s" % run['datetime']
1165     print "<p>Free system RAM: <b>%d</b> kB" % run['ram_free']
1166     print "<br>(free = free+cached+buffered+slab reclaimable)"
1167     if run['swap_total']:
1168         print "<p>Free system Swap: <b>%d</b> kB (out of <b>%d</b> kB)" % (run['swap_free'], run['swap_total'])
1169     if 'private_code' in run and run['private_code']:
1170         print "<p>Private dirty code pages: <b>%d</b> kB" % run['private_code']
1171         print "<br><i>(this means that system has incorrectly built shared libraries)</i>"
1172     output_errors(0, {}, run)
1173     print """
1174 <p>Errors from each additional test round are listed below, but for
1175 a summary of them, see <a href="#error-summary">all errors summary
1176 section</a>. Note that the same issues (related to system services)
1177 may appear under multiple error types.
1178 """ # "fool Jed syntax highlighter
1179     output_data_links(run)
1180     print "<hr>\n"
1181
1182
1183 # ------------------- output memory graphs -------------------------
1184
1185 def output_memory_graph_table(titles, colors, data):
1186     "outputs memory usage bars for given (name, (values), tex(t)) tupple array"
1187     width = 640 # total width of the graph bars
1188     print "<table><tr>"
1189     # column titles
1190     for title in titles:
1191         if title:
1192             print "<th>%s</th>" % title
1193         else:
1194             print "<th></th>"
1195     print "</tr>"
1196     for item in data:
1197         # row title
1198         print '<tr><td>%s</td>' % item[0]
1199         # graphical bar
1200         print "<td><table border=0 cellpadding=0 cellspacing=0><tr>"
1201         for idx in range(len(colors)):
1202             w = int(item[1][idx]*width)
1203             if w:
1204                 sys.stdout.write('<td bgcolor="%s" width=%d height=16></td>' % (colors[idx], w))
1205         print "</tr></table></td>"
1206         # texts at end
1207         for text in item[2]:
1208             if text:
1209                 print '<td align="right">%s</td>' % text
1210             else:
1211                 print "<td></td>"
1212         print "</tr>"
1213     print "</table>"
1214
1215
1216 def output_apps_memory_graphs(cases):
1217     "outputs memory graphs bars for the individual processes"
1218     # arrange per use-case data to be per pid
1219     smaps_available = 0
1220     rounds = 0
1221     data = {}
1222     # get names and pids
1223     for testcase in cases:
1224         commands = testcase['commands']
1225         processes = testcase['processes']
1226         for process in processes.values():
1227             pid = process['Pid']
1228             if pid not in commands:
1229                 sys.stderr.write("Debug: %s[%s] in status list but not in FD list\n" % (process['Name'], pid))
1230                 continue
1231             if not pid_is_main_thread(pid, commands, processes):
1232                 continue
1233             name = commands[pid]
1234             namepid = (name, pid)
1235             if namepid not in data:
1236                 data[namepid] = {}
1237             try:
1238                 process['SMAPS_PRIVATE_DIRTY'] = testcase['smaps'][pid]['private_dirty']
1239                 smaps_available = 1
1240             except KeyError:
1241                 if 'smaps' in testcase:
1242                     syslog.parse_error(sys.stdout.write, "WARNING: SMAPS data missing for %s[%s]" % namepid)
1243             try: process['SMAPS_SWAP'] = testcase['smaps'][pid]['swap']
1244             except KeyError: pass
1245             try: process['SMAPS_PSS'] = testcase['smaps'][pid]['pss']
1246             except KeyError: pass
1247             try: process['SMAPS_RSS'] = testcase['smaps'][pid]['rss']
1248             except KeyError: pass
1249             try: process['SMAPS_SIZE'] = testcase['smaps'][pid]['size']
1250             except KeyError: pass
1251             data[namepid][rounds] = process
1252         rounds += 1
1253
1254     # get largest size for any of the namepids, get largest rss
1255     # for sorting and ignore items which rss/size don't change
1256     #
1257     # Also filter out processes that get dirty pages swapped to disk:
1258     #
1259     #     initial state: Swap:0kB Dirty:100kB
1260     #     ...
1261     #     last round:    Swap:8kB Dirty:92kB
1262     #
1263     sizes = []
1264     largest_size = 0
1265     for namepid in data:
1266         changerounds = pidrounds = 0
1267         max_size = max_dirty = max_swap = 0
1268         min_size = min_dirty = min_swap = 512*1024
1269         for idx in range(rounds):
1270             if idx in data[namepid]:
1271                 try:    dirty = data[namepid][idx]['SMAPS_PRIVATE_DIRTY']
1272                 except: dirty = data[namepid][idx]['VmRSS']
1273                 try:    size  = data[namepid][idx]['SMAPS_SIZE']
1274                 except: size  = data[namepid][idx]['VmSize']
1275                 try:    swap  = data[namepid][idx]['SMAPS_SWAP']
1276                 except: swap  = 0
1277                 min_dirty = min(dirty, min_dirty)
1278                 max_dirty = max(dirty, max_dirty)
1279                 min_swap = min(swap, min_swap)
1280                 max_swap = max(swap, max_swap)
1281                 if size < min_size:
1282                     if pidrounds:
1283                         changerounds += 1
1284                     min_size = size
1285                 if size > max_size:
1286                     if pidrounds:
1287                         changerounds += 1
1288                     max_size = size
1289                 pidrounds += 1
1290         if pidrounds > 1:
1291             if max_dirty+min_swap:
1292                 swap_and_dirty_change = (float)((max_dirty+min_swap) - (min_dirty+max_swap)) / (max_dirty+min_swap) / pidrounds
1293             else:
1294                 if smaps_available:
1295                     syslog.parse_error(sys.stdout.write, "WARNING: no SMAPS dirty for %s[%s]. Disable swap and try again\n\t(SMAPS doesn't work properly with swap)" % namepid)
1296                 swap_and_dirty_change = 0
1297             size_change = (float)(max_size - min_size) / max_size / pidrounds
1298             # if >0.2% memory change per round in dirty or Size, or
1299             # size changes on more than half of the rounds, add to list
1300             if swap_and_dirty_change > 0.002 or size_change > 0.002 or 2*changerounds > pidrounds:
1301                 sizes.append((max_dirty,namepid))
1302         if max_size > largest_size:
1303             largest_size = max_size
1304     largest_size = float(largest_size)
1305     
1306     # first sort according to the dirty (or RSS) size
1307     sizes.sort()
1308     sizes.reverse()
1309     # then sort according to names
1310     orders = []
1311     for size in sizes:
1312         namepid = size[1]
1313         # sorting order is: name, first round for pid, pid
1314         orders.append((namepid[0], min(data[namepid].keys()), namepid[1]))
1315     del(sizes)
1316     orders.sort()
1317     
1318     # amount of memory in the device (float for calculations)
1319     print """
1320 <p>Only processes which VmSize and amount of private dirty memory
1321 changes during tests are listed.  If a process has same name and size
1322 as its parent, it's assumed to be a thread and ignored.
1323
1324 <p>Note: Memory is not any more accounted as dirty if it's swapped out
1325 (even when it's paged back in), this seems like a kernel (2.6.21) bug.
1326 RSS can decrease if device is just running low on memory because
1327 kernel can just discard unmodified/unused pages. Size tells amount of
1328 all virtual allocations and memory maps of a process, so it might not
1329 have any relation to real process memory usage. However, it can show
1330 leaks which cause process eventually to run out of (2GB) address space
1331 (e.g. if it's not collecting thread resources).
1332 """
1333
1334     # LEGEND
1335     print '<p><table><tr><th><th align="left">Legend:'
1336     if smaps_available: print """
1337 <tr><td bgcolor="%s" height="16" width="16"><td>Swap
1338 <tr><td bgcolor="%s" height="16" width="16"><td>Dirty
1339 <tr><td bgcolor="%s" height="16" width="16"><td>PSS: Proportional Set Size -- amount of resident memory, where each 4kB memory page is divided by the number of processes sharing it.
1340 """ % (bar2colors[0], bar2colors[1], bar2colors[2])
1341     print """
1342 <tr><td bgcolor="%s" height="16" width="16"><td>RSS: Resident Set Size
1343 <tr><td bgcolor="%s" height="16" width="16"><td>Size
1344 </table>
1345 """ % (bar2colors[3], bar2colors[4])
1346
1347     for order in orders:
1348         namepid = (order[0],order[2])
1349         process = data[namepid]
1350         print "<h4><i>%s [%s]</i></h4>" % namepid
1351         text = ''
1352         prev_idx = 0
1353         prev_text = ""
1354         columndata = []
1355         for idx in range(rounds):
1356             if idx in process:
1357                 item = process[idx]
1358
1359                 rss = item['VmRSS']
1360                 size = item['VmSize']
1361                 if smaps_available:
1362                     try: dirty = item['SMAPS_PRIVATE_DIRTY']
1363                     except: dirty = 0
1364                     try: swap = item['SMAPS_SWAP']
1365                     except: swap = 0
1366                     try: rss = item['SMAPS_RSS']
1367                     except: pass
1368                     try: pss = item['SMAPS_PSS']
1369                     except: pss = 0
1370                     try: size = item['SMAPS_SIZE']
1371                     except: pass
1372                     if rss < dirty:
1373                         syslog.parse_error(sys.stdout.write, "WARNING: %s[%s] RSS (%s) < SMAPS dirty (%s)" % (namepid + (rss, dirty)))
1374                         rss = dirty
1375                     if pss < dirty:
1376                         syslog.parse_error(sys.stdout.write, "WARNING: %s[%s] SMAPS PSS (%s) < SMAPS dirty (%s)" % (namepid + (pss, dirty)))
1377                     if rss < pss:
1378                         syslog.parse_error(sys.stdout.write, "WARNING: %s[%s] RSS (%s) < SMAPS PSS (%s)" % (namepid + (rss, pss)))
1379                     text = ["%skB" % swap, "%skB" % dirty, "%skB" % pss, "%skB" % rss, "%skB" % size]
1380                 else:
1381                     swap = 0
1382                     dirty = 0
1383                     pss = 0
1384                     text = ["", "", "", "%skB" % rss, "%skB" % size]
1385                 barwidth_swap  = swap/largest_size
1386                 barwidth_dirty = dirty/largest_size
1387                 barwidth_pss   = pss/largest_size
1388                 barwidth_rss   = rss/largest_size
1389                 barwidth_size  = size/largest_size
1390                 #  ___________________________________________________
1391                 # |      |    ____________________     |              |
1392                 # |      |   |                    |    |              |
1393                 # |      |   |  _______________   |    |              |
1394                 # |      |   | |               |  |    |              |
1395                 # |      |   | | Private Dirty |  |    |              |
1396                 # |      |   | |_______________|  |    |              |
1397                 # | SWAP |   |                    |    |              |
1398                 # |      |   |        PSS         |    |              |
1399                 # |      |   |____________________|    |              |
1400                 # |      |                             |              |
1401                 # |      |            RSS              |              |
1402                 # |______|_____________________________|              |
1403                 # |                                                   |
1404                 # |                   Size                            |
1405                 # |___________________________________________________|
1406                 #
1407                 sizes = (barwidth_swap,
1408                          barwidth_dirty,
1409                          barwidth_pss - barwidth_dirty,
1410                          barwidth_rss - barwidth_pss,
1411                          barwidth_size - barwidth_swap - barwidth_rss)
1412                 if idx:
1413                     if text == prev_text:
1414                         columndata.pop()
1415                         case = 'Rounds <a href="#round-%d">%02d</a> - <a href="#round-%d">%02d</a>:' % (prev_idx, prev_idx, idx, idx)
1416                     else:
1417                         case = 'Test round <a href="#round-%d">%02d</a>:' % (idx, idx)
1418                         prev_idx = idx
1419                 else:
1420                     case = '<a href="#initial-state">Initial state</a>:'
1421                     prev_idx = idx
1422                 prev_text = text
1423             else:
1424                 nan = ("N/A",)
1425                 if text == nan:
1426                     # previous one didn't have anything either
1427                     continue
1428                 sizes = (0,0,0,0,0)
1429                 text = nan
1430                 case = "---"
1431             columndata.append((case, sizes, text))
1432         titles = ['Test-case:', 'Graph', 'Swap:', 'Dirty:', 'PSS:', 'RSS:', 'Size:']
1433         if not smaps_available:
1434             titles[2] = "" #Swap
1435             titles[3] = "" #Dirty
1436             titles[4] = "" #PSS
1437         output_memory_graph_table(titles, bar2colors, columndata)
1438
1439
1440 def output_system_load_graphs(data):
1441     print '<p>System CPU time distribution during the execution of test cases.'
1442     print '<p>'
1443     prev = data[0]
1444     entries = []
1445     idx = 1
1446     reboots = []
1447     for testcase in data[1:]:
1448         case = '<a href="#round-%d">Test round %02d</a>:' % (idx, idx)
1449         if sum(testcase['/proc/stat']['cpu'].itervalues()) < sum(prev['/proc/stat']['cpu'].itervalues()):
1450             entries.append((case, (0,0,0,0,0), "-"))
1451             reboots.append(idx)
1452         elif sum(testcase['/proc/stat']['cpu'].itervalues()) == sum(prev['/proc/stat']['cpu'].itervalues()):
1453             # Two identical entries? Most likely user has manually copied the snapshot directories.
1454             entries.append((case, (0,0,0,0,0), "-"))
1455         else:
1456             diffs = {}
1457             for key in testcase['/proc/stat']['cpu'].keys():
1458                 diffs[key] = testcase['/proc/stat']['cpu'][key] - prev['/proc/stat']['cpu'][key]
1459             divisor = float(sum(diffs.values()))
1460             if divisor <= 0:
1461                 entries.append((case, (0,0,0,0,0), "-"))
1462             else:
1463                 for key in diffs.keys():
1464                     diffs[key] = diffs[key] / divisor
1465                 bars = (diffs['system'] + diffs['irq'] + diffs['softirq'], \
1466                         diffs['user'], \
1467                         diffs['user_nice'], \
1468                         diffs['iowait'], \
1469                         diffs['idle'])
1470                 entries.append((case, bars, ["%d%%" % int(100-100*diffs['idle'])]))
1471         idx += 1
1472         prev = testcase
1473     titles = ("Test-case:", "system load:", "CPU usage-%:")
1474     output_memory_graph_table(titles, bar3colors, entries)
1475     if reboots:
1476         text = '<p>Reboots occured during rounds:'
1477         for r in reboots:
1478             text += " %d," % r
1479         print text[:-1] + '.<br>'
1480     # Legend
1481     print '<table><tr><th><th align="left">Legend:'
1482     print '<tr><td bgcolor="%s" height=16 width=16><td>CPU time used by <i>system</i> tasks, including time spent in interrupt handling' % bar3colors[0]
1483     print '<tr><td bgcolor="%s" height=16 width=16><td>CPU time used by <i>user</i> tasks' % bar3colors[1]
1484     print '<tr><td bgcolor="%s" height=16 width=16><td>CPU time used by <i>user</i> tasks with <i>low priority</i> (nice)' % bar3colors[2]
1485     print '<tr><td bgcolor="%s" height=16 width=16><td>CPU time wasted waiting for I/O (idle)' % bar3colors[3]
1486     print '<tr><td bgcolor="%s" height=16 width=16><td>CPU time idle' % bar3colors[4]
1487     print '</table>'
1488
1489
1490 def output_system_memory_graphs(data):
1491     "outputs memory graphs bars for the system"
1492     idx = 0
1493     swaptext = None
1494     columndata = []
1495     # See whether swap was used during the tests. We need to know this in
1496     # advance in the next loop.
1497     for testcase in data:
1498         if testcase['swap_used']:
1499             swaptext = "swap used:"
1500             break
1501     for testcase in data:
1502         if not idx:
1503             case = '<a href="#initial-state">Initial state</a>:'
1504         else:
1505             case = '<a href="#round-%d">Test round %02d</a>:' % (idx, idx)
1506         idx += 1
1507
1508         # amount of memory in the device (float for calculations)
1509         mem_total = float(testcase['ram_total'] + testcase['swap_total'])
1510         # memory usage %-limit after which apps are bg-killed
1511         mem_low = testcase['limitlow']
1512         # memory usage %-limit after which apps refuse certain operations
1513         mem_high = testcase['limithigh']
1514         # memory usage %-limit after which kernel denies app allocs
1515         mem_deny = testcase['limitdeny']
1516         
1517         if mem_low + mem_high + mem_deny > 0:
1518             # convert percentages to real memory values
1519             mem_low = mem_total * mem_low / 100
1520             mem_high = mem_total * mem_high / 100
1521             mem_deny = mem_total * mem_deny / 100
1522         else:
1523             mem_low = mem_high = mem_deny = mem_total
1524             sys.stderr.write("Warning: low memory limits are zero -> disabling\n")
1525         mem_used = testcase['ram_used'] + testcase['swap_used']
1526         mem_free = testcase['ram_free'] + testcase['swap_free']
1527         # Graphics
1528         show_swap = testcase['swap_used']/mem_total
1529         show_ram  = testcase['ram_used']/mem_total
1530         if mem_used > mem_deny:
1531             show_deny = (mem_total - mem_used)/mem_total
1532             show_free = 0.0
1533         else:
1534             show_deny = 1.0 - mem_deny/mem_total
1535             show_free = 1.0 - show_swap - show_ram - show_deny
1536         bars = (show_swap, show_ram, show_free, show_deny)
1537         # Numbers
1538         def label():
1539             if mem_used > mem_high: return "<font color=red><b>%d</b></font>kB"
1540             if mem_used > mem_low:  return "<font color=blue><b>%d</b></font>kB"
1541             return "%dkB"
1542         memtext = None
1543         if swaptext: memtext = label() % testcase['swap_used']
1544         memtext = (memtext,) + (label() % testcase['ram_used'], "%dkB" % mem_free)
1545         # done!
1546         columndata.append((case, bars, memtext))
1547     titles = ("Test-case:", "memory usage graph:", swaptext, "RAM used:", "free:")
1548     output_memory_graph_table(titles, bar1colors, columndata)
1549     print '<table><tr><th><th align="left">Legend:'
1550     if testcase['swap_total']:
1551         print '<tr><td bgcolor="%s" height="16" width="16"><td>Swap used' % bar1colors[0]
1552     print """
1553 <tr><td bgcolor="%s" height="16" width="16"><td>RAM used in the device
1554 <tr><td bgcolor="%s" height="16" width="16"><td>RAM and swap freely usable in the device
1555 <tr><td bgcolor="%s" height="16" width="16"><td>If memory usage reaches this, application allocations fail and the allocating app is OOM-killed (&gt;= %d MB used)
1556 """ % (bar1colors[1], bar1colors[2], bar1colors[3], round(mem_deny/1024))
1557     print "</table>"
1558     if mem_low == mem_total:
1559         print "<p>(memory limits are not in effect)"
1560         return
1561     print """
1562 <p>Memory usage values which trigger background killing are marked with
1563 blue color (&gt;= <font color=blue><b>%d</b></font> MB used).<br>
1564 After bg-killing and memory low mark comes the memory high pressure mark
1565 at which point e.g.<br> Browser refuses to open new pages, these numbers
1566 are marked with red color (&gt;= <font color=red><b>%d</b></font> MB used).
1567 """ % (round(mem_low/1024), round(mem_high/1024))
1568
1569
1570 # ------------------- output all data -------------------------
1571
1572 def output_html_report(data):
1573     title = "Endurance measurements report"
1574     rounds = len(data)-1
1575     last = rounds
1576     first = 1
1577
1578     # X client names may contain UTF-8 characters, so add character encoding.
1579     print """<html>
1580 <head>
1581 <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
1582 <title>%s</title>
1583 </head>
1584 <body>
1585 <h1>%s</h1>
1586
1587 <!-- endurance_report.py v1.1.13 -->
1588
1589 <p><b>Contents:</b>
1590 <ul>
1591 <li><a href="#initial-state">Initial state</a>
1592 <li>Resource usage overview for the test rounds:
1593   <ul>
1594     <li><a href="#system-memory">System memory usage</a>
1595     <li><a href="#system-load">System load</a>
1596     <li><a href="#process-memory">Processes memory usage</a>
1597   </ul>
1598 <li>Resource usage changes for each of the test rounds:
1599   <ul>
1600 """ % (title, title)   #" fool Jed syntax highlighter
1601     for round in range(rounds):
1602         idx = round + 1
1603         if data[idx].has_key('description') and data[idx]['description']:
1604             desc = " (%s)" % data[idx]['description']
1605         else:
1606             desc = ""
1607         print '  <li><a href="#round-%d">Round %d</a>%s' % (idx, idx, desc)
1608     print """
1609   </ul>
1610 <li>Summary of changes between all the rounds after the initial one:
1611   <ul>
1612     <li><a href="#error-summary">Error summary</a>
1613     <li><a href="#resource-summary">Resource usage summary</a>
1614   </ul>
1615 </ul>
1616 <hr>
1617
1618 <a name="initial-state"></a>
1619 <h2>Initial state</h2>
1620 """
1621     output_initial_state(data[0])
1622
1623     print """
1624 <a name="system-memory"></a>
1625 <h2>Resource usage overview for the test rounds</h2>
1626 <h3>System memory usage</h3>
1627 """
1628     output_system_memory_graphs(data)
1629     print """
1630 <hr>
1631 <a name="system-load"></a>
1632 <h3>System load</h3>"""
1633     output_system_load_graphs(data)
1634     print """
1635 <hr>
1636 <a name="process-memory"></a>
1637 <h3>Processes memory usage</h3>
1638 """ # "fool Jed syntax highlighter
1639     output_apps_memory_graphs(data)
1640     if last - first > 1:
1641         summary = "resource-summary"
1642     else:
1643         summary = "round-%d" % last
1644     print"""
1645 <hr>
1646 <h2>Resource usage changes for the test rounds</h2>
1647 <p>Details of resource changes are listed below, but for a summary,
1648 see <a href="#%s">resource changes summary section</a>.
1649 """ % summary # "fool Jed syntax highlighter
1650
1651     err_stats = {}
1652     for idx in range(rounds):
1653         if idx:
1654             title = "Test round %d differences from round %d" % (idx+1, idx)
1655         else:
1656             title = "Test round 1 differences from initial state"
1657         print
1658         print '<a name="round-%d"></a>' % (idx+1)
1659         print "<h3>%s</h3>" % title
1660         print "<p>%s" % data[idx+1]['datetime']
1661         stat = output_run_diffs(idx, idx+1, data, 0)
1662         if stat:
1663             syslog.errors_add(err_stats, stat)
1664         output_data_links(data[idx+1])
1665         print "\n<hr>"
1666     
1667     print """
1668 <a name="error-summary"></a>
1669 <h2>Summary of changes between test rounds %d - %d</h2>
1670 <h3>Error summary</h3>""" % (first, last)
1671     syslog.errors_summary(err_stats, "", Colors.errors)
1672     print "<!-- summary for automatic parsing:"
1673     syslog.use_html = 0
1674     syslog.errors_summary(err_stats)
1675     print """-->
1676
1677 <hr>
1678 <a name="resource-summary"></a>
1679 <h3>Resource usage summary</h3>
1680 <p><font color="red">NOTE</font>: Process specific resource usage
1681 changes are shown only for processes which exist in both of the
1682 compared rounds!
1683 """
1684     output_run_diffs(first, last, data, 1)
1685
1686     print "\n</body></html>"
1687
1688
1689 # ------------------- go through all files -------------------------
1690
1691 def parse_syte_stats(dirs):
1692     """parses given CSV files into a data structure"""
1693     data = []
1694     for dirname in dirs:
1695
1696         # get basic information
1697         file, filename = syslog.open_compressed("%s/usage.csv" % dirname, syslog.FATAL)
1698         items = parse_csv(file)
1699         if not items:
1700             syslog.error_exit("CSV parsing failed")
1701
1702         # filename without the extension
1703         items['basedir'] = dirname
1704
1705         filename = "%s/step.txt" % dirname
1706         if os.path.exists(filename):
1707             # use-case step description
1708             items['description'] = open(filename).read().strip()
1709
1710         file, filename = syslog.open_compressed("%s/smaps.cap" % dirname)
1711         if file:
1712             # get system SMAPS memory usage data
1713             items['smaps'], items['private_code'] = parse_smaps(file)
1714             if not items['smaps']:
1715                 syslog.error_exit("SMAPS data parsing failed")
1716
1717         file, filename = syslog.open_compressed("%s/syslog" % dirname)
1718         if file:
1719             # get the crashes and other errors
1720             items['logfile'] = filename
1721             items['errors'] = syslog.parse_syslog(sys.stdout.write, file)
1722
1723         file, filename = syslog.open_compressed("%s/stat" % dirname)
1724         if file:
1725             items['/proc/stat'] = parse_proc_stat(file)
1726             if not items['/proc/stat']:
1727                 syslog.error_exit("/proc/stat parsing failed")
1728
1729         data.append(items)
1730     return data
1731
1732
1733 if __name__ == "__main__":
1734     if len(sys.argv) < 3:
1735         msg = __doc__.replace("<TOOL_NAME>", sys.argv[0].split('/')[-1])
1736         syslog.error_exit(msg)
1737     # Use psyco if available. Gives 2-3x speed up.
1738     try:
1739         import psyco
1740         psyco.full()
1741     except ImportError:
1742         pass
1743     stats = parse_syte_stats(sys.argv[1:])
1744     output_html_report(stats)