When printing the time, print it also in the summary
[iotop.git] / iotop / ui.py
blob39b6fef33648a3812a1224eb85e4f489f197f3d8
1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 # See the COPYING file for license information.
17 # Copyright (c) 2007 Guillaume Chazarain <guichaz@gmail.com>
19 import curses
20 import errno
21 import locale
22 import math
23 import optparse
24 import os
25 import select
26 import sys
27 import time
29 from iotop.data import find_uids, TaskStatsNetlink, ProcessList, Stats
30 from iotop.data import ThreadInfo
31 from iotop.version import VERSION
32 import ioprio
33 from ioprio import IoprioSetError
36 # Utility functions for the UI
39 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
41 def human_size(size):
42 if size > 0:
43 sign = ''
44 elif size < 0:
45 sign = '-'
46 size = -size
47 else:
48 return '0.00 B'
50 expo = int(math.log(size / 2, 2) / 10)
51 return '%s%.2f %s' % (sign, (float(size) / (1 << (10 * expo))), UNITS[expo])
53 def format_size(options, bytes):
54 if options.kilobytes:
55 return '%.2f K' % (bytes / 1024.0)
56 return human_size(bytes)
58 def format_bandwidth(options, size, duration):
59 return format_size(options, size and float(size) / duration) + '/s'
61 def format_stats(options, process, duration):
62 # Keep in sync with TaskStatsNetlink.members_offsets and
63 # IOTopUI.get_data(self)
64 def delay2percent(delay): # delay in ns, duration in s
65 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
66 if options.accumulated:
67 stats = process.stats_accum
68 display_format = lambda size, duration: format_size(options, size)
69 duration = time.time() - process.stats_accum_timestamp
70 else:
71 stats = process.stats_delta
72 display_format = lambda size, duration: format_bandwidth(
73 options, size, duration)
74 io_delay = delay2percent(stats.blkio_delay_total)
75 swapin_delay = delay2percent(stats.swapin_delay_total)
76 read_bytes = display_format(stats.read_bytes, duration)
77 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
78 written_bytes = max(0, written_bytes)
79 write_bytes = display_format(written_bytes, duration)
80 return io_delay, swapin_delay, read_bytes, write_bytes
83 # UI Exceptions
86 class CancelInput(Exception): pass
87 class InvalidInt(Exception): pass
88 class InvalidPid(Exception): pass
89 class InvalidTid(Exception): pass
90 class InvalidIoprioData(Exception): pass
93 # The UI
96 class IOTopUI(object):
97 # key, reverse
98 sorting_keys = [
99 (lambda p, s: p.pid, False),
100 (lambda p, s: p.ioprio_sort_key(), False),
101 (lambda p, s: p.get_user(), False),
102 (lambda p, s: s.read_bytes, True),
103 (lambda p, s: s.write_bytes - s.cancelled_write_bytes, True),
104 (lambda p, s: s.swapin_delay_total, True),
105 # The default sorting (by I/O % time) should show processes doing
106 # only writes, without waiting on them
107 (lambda p, s: s.blkio_delay_total or
108 int(not(not(s.read_bytes or s.write_bytes))), True),
109 (lambda p, s: p.get_cmdline(), False),
112 def __init__(self, win, process_list, options):
113 self.process_list = process_list
114 self.options = options
115 self.sorting_key = 6
116 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
117 if not self.options.batch:
118 self.win = win
119 self.resize()
120 try:
121 curses.use_default_colors()
122 curses.start_color()
123 curses.curs_set(0)
124 except curses.error:
125 # This call can fail with misconfigured terminals, for example
126 # TERM=xterm-color. This is harmless
127 pass
129 def resize(self):
130 self.height, self.width = self.win.getmaxyx()
132 def run(self):
133 iterations = 0
134 poll = select.poll()
135 if not self.options.batch:
136 poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
137 while self.options.iterations is None or \
138 iterations < self.options.iterations:
139 total = self.process_list.refresh_processes()
140 total_read, total_write = total
141 self.refresh_display(iterations == 0, total_read, total_write,
142 self.process_list.duration)
143 if self.options.iterations is not None:
144 iterations += 1
145 if iterations >= self.options.iterations:
146 break
147 elif iterations == 0:
148 iterations = 1
150 try:
151 events = poll.poll(self.options.delay_seconds * 1000.0)
152 except select.error, e:
153 if e.args and e.args[0] == errno.EINTR:
154 events = 0
155 else:
156 raise
157 if not self.options.batch:
158 self.resize()
159 if events:
160 key = self.win.getch()
161 self.handle_key(key)
163 def reverse_sorting(self):
164 self.sorting_reverse = not self.sorting_reverse
166 def adjust_sorting_key(self, delta):
167 orig_sorting_key = self.sorting_key
168 self.sorting_key += delta
169 self.sorting_key = max(0, self.sorting_key)
170 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
171 if orig_sorting_key != self.sorting_key:
172 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
174 # I wonder if switching to urwid for the display would be better here
176 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
177 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
178 self.win.addstr(1, 0, prompt, curses.A_BOLD)
179 self.win.refresh()
180 curses.echo()
181 curses.curs_set(1)
182 inp = self.win.getstr(1, len(prompt))
183 curses.curs_set(0)
184 curses.noecho()
185 if inp not in (None, ''):
186 return inp
187 if empty_is_cancel:
188 raise CancelInput()
189 return default
191 def prompt_int(self, prompt, default = None, empty_is_cancel = True):
192 inp = self.prompt_str(prompt, default, empty_is_cancel)
193 try:
194 return int(inp)
195 except ValueError:
196 raise InvalidInt()
198 def prompt_pid(self):
199 try:
200 return self.prompt_int('PID to ionice: ')
201 except InvalidInt:
202 raise InvalidPid()
203 except CancelInput:
204 raise
206 def prompt_tid(self):
207 try:
208 return self.prompt_int('TID to ionice: ')
209 except InvalidInt:
210 raise InvalidTid()
211 except CancelInput:
212 raise
214 def prompt_data(self, ioprio_data):
215 try:
216 if ioprio_data is not None:
217 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
218 % ioprio_data, ioprio_data, False)
219 else:
220 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
221 except InvalidInt:
222 raise InvalidIoprioData()
223 if inp < 0 or inp > 7:
224 raise InvalidIoprioData()
225 return inp
227 def prompt_set(self, prompt, display_list, ret_list, selected):
228 try:
229 selected = ret_list.index(selected)
230 except ValueError:
231 selected = -1
232 set_len = len(display_list) - 1
233 while True:
234 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
235 self.win.insstr(1, 0, prompt, curses.A_BOLD)
236 offset = len(prompt)
237 for i, item in enumerate(display_list):
238 display = ' %s ' % item
239 if i is selected:
240 attr = curses.A_REVERSE
241 else:
242 attr = curses.A_NORMAL
243 self.win.insstr(1, offset, display, attr)
244 offset += len(display)
245 while True:
246 key = self.win.getch()
247 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
248 selected -= 1
249 break
250 elif key in (curses.KEY_RIGHT, ord('r')) and selected < set_len:
251 selected += 1
252 break
253 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
254 return ret_list[selected]
255 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
256 curses.KEY_EXIT, ord('q'), ord('Q')):
257 raise CancelInput()
259 def prompt_class(self, ioprio_class=None):
260 prompt = 'I/O priority class: '
261 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
262 classes_ret = ['rt', 'be', 'idle']
263 if ioprio_class is None:
264 ioprio_class = 2
265 inp = self.prompt_set(prompt, classes_prompt, classes_ret, ioprio_class)
266 return inp
268 def prompt_error(self, error = 'Error!'):
269 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
270 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
271 self.win.refresh()
272 time.sleep(1)
274 def prompt_clear(self):
275 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
276 self.win.refresh()
278 def handle_key(self, key):
279 def toggle_accumulated():
280 self.options.accumulated ^= True
281 def toggle_only_io():
282 self.options.only ^= True
283 def toggle_processes():
284 self.options.processes ^= True
285 self.process_list.clear()
286 self.process_list.refresh_processes()
287 def ionice():
288 try:
289 if self.options.processes:
290 pid = self.prompt_pid()
291 exec_unit = self.process_list.get_process(pid)
292 else:
293 tid = self.prompt_tid()
294 exec_unit = ThreadInfo(tid,
295 self.process_list.taskstats_connection)
296 ioprio_value = exec_unit.get_ioprio()
297 (ioprio_class, ioprio_data) = \
298 ioprio.to_class_and_data(ioprio_value)
299 ioprio_class = self.prompt_class(ioprio_class)
300 if ioprio_class == 'idle':
301 ioprio_data = 0
302 else:
303 ioprio_data = self.prompt_data(ioprio_data)
304 exec_unit.set_ioprio(ioprio_class, ioprio_data)
305 self.process_list.clear()
306 self.process_list.refresh_processes()
307 except IoprioSetError, e:
308 self.prompt_error('Error setting I/O priority: %s' % e.err)
309 except InvalidPid:
310 self.prompt_error('Invalid process id!')
311 except InvalidTid:
312 self.prompt_error('Invalid thread id!')
313 except InvalidIoprioData:
314 self.prompt_error('Invalid I/O priority data!')
315 except InvalidInt:
316 self.prompt_error('Invalid integer!')
317 except CancelInput:
318 self.prompt_clear()
319 else:
320 self.prompt_clear()
322 key_bindings = {
323 ord('q'):
324 lambda: sys.exit(0),
325 ord('Q'):
326 lambda: sys.exit(0),
327 ord('r'):
328 lambda: self.reverse_sorting(),
329 ord('R'):
330 lambda: self.reverse_sorting(),
331 ord('a'):
332 toggle_accumulated,
333 ord('A'):
334 toggle_accumulated,
335 ord('o'):
336 toggle_only_io,
337 ord('O'):
338 toggle_only_io,
339 ord('p'):
340 toggle_processes,
341 ord('P'):
342 toggle_processes,
343 ord('i'):
344 ionice,
345 ord('I'):
346 ionice,
347 curses.KEY_LEFT:
348 lambda: self.adjust_sorting_key(-1),
349 curses.KEY_RIGHT:
350 lambda: self.adjust_sorting_key(1),
351 curses.KEY_HOME:
352 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
353 curses.KEY_END:
354 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
357 action = key_bindings.get(key, lambda: None)
358 action()
360 def get_data(self):
361 def format(p):
362 stats = format_stats(self.options, p, self.process_list.duration)
363 io_delay, swapin_delay, read_bytes, write_bytes = stats
364 if Stats.has_blkio_delay_total:
365 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
366 else:
367 delay_stats = ' ?unavailable? '
368 line = '%5d %4s %-8s %11s %11s %s' % (
369 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
370 write_bytes, delay_stats)
371 cmdline = p.get_cmdline()
372 if not self.options.batch:
373 remaining_length = self.width - len(line)
374 if 2 < remaining_length < len(cmdline):
375 len1 = (remaining_length - 1) // 2
376 offset2 = -(remaining_length - len1 - 1)
377 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
378 line += cmdline
379 if not self.options.batch:
380 line = line[:self.width]
381 return line
383 def should_format(p):
384 return not self.options.only or \
385 p.did_some_io(self.options.accumulated)
387 processes = filter(should_format, self.process_list.processes.values())
388 key = IOTopUI.sorting_keys[self.sorting_key][0]
389 if self.options.accumulated:
390 stats_lambda = lambda p: p.stats_accum
391 else:
392 stats_lambda = lambda p: p.stats_delta
393 processes.sort(key=lambda p: key(p, stats_lambda(p)),
394 reverse=self.sorting_reverse)
395 if not self.options.batch:
396 del processes[self.height - 2:]
397 return map(format, processes)
399 def refresh_display(self, first_time, total_read, total_write, duration):
400 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
401 format_bandwidth(self.options, total_read, duration),
402 format_bandwidth(self.options, total_write, duration))
403 if self.options.processes:
404 pid = ' PID'
405 else:
406 pid = ' TID'
407 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
408 ' SWAPIN', ' IO', ' COMMAND']
409 lines = self.get_data()
410 if self.options.time:
411 titles = [' TIME'] + titles
412 current_time = time.strftime('%H:%M:%S ')
413 lines = [current_time + l for l in lines]
414 summary = current_time + summary
415 if self.options.batch:
416 if self.options.quiet <= 2:
417 print summary
418 if self.options.quiet <= int(first_time):
419 print ''.join(titles)
420 for l in lines:
421 print l.encode('utf-8')
422 sys.stdout.flush()
423 else:
424 self.win.erase()
425 self.win.addstr(summary[:self.width])
426 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
427 remaining_cols = self.width
428 for i in xrange(len(titles)):
429 attr = curses.A_REVERSE
430 title = titles[i]
431 if i == self.sorting_key:
432 title = title[1:]
433 if i == self.sorting_key:
434 attr |= curses.A_BOLD
435 title += self.sorting_reverse and '>' or '<'
436 title = title[:remaining_cols]
437 remaining_cols -= len(title)
438 self.win.addstr(title, attr)
439 if Stats.has_blkio_delay_total:
440 status_msg = None
441 else:
442 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
443 'cannot determine SWAPIN and IO %')
444 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
445 for i in xrange(num_lines):
446 try:
447 self.win.addstr(i + 2, 0, lines[i].encode('utf-8'))
448 except curses.error:
449 pass
450 if status_msg:
451 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
452 self.win.refresh()
454 def run_iotop_window(win, options):
455 taskstats_connection = TaskStatsNetlink(options)
456 process_list = ProcessList(taskstats_connection, options)
457 ui = IOTopUI(win, process_list, options)
458 ui.run()
460 def run_iotop(options):
461 if options.batch:
462 return run_iotop_window(None, options)
463 else:
464 return curses.wrapper(run_iotop_window, options)
467 # Profiling
470 def _profile(continuation):
471 prof_file = 'iotop.prof'
472 try:
473 import cProfile
474 import pstats
475 print 'Profiling using cProfile'
476 cProfile.runctx('continuation()', globals(), locals(), prof_file)
477 stats = pstats.Stats(prof_file)
478 except ImportError:
479 import hotshot
480 import hotshot.stats
481 prof = hotshot.Profile(prof_file, lineevents=1)
482 print 'Profiling using hotshot'
483 prof.runcall(continuation)
484 prof.close()
485 stats = hotshot.stats.load(prof_file)
486 stats.strip_dirs()
487 stats.sort_stats('time', 'calls')
488 stats.print_stats(50)
489 stats.print_callees(50)
490 os.remove(prof_file)
493 # Main program
496 USAGE = '''%s [OPTIONS]
498 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
499 period. SWAPIN and IO are the percentages of time the thread spent respectively
500 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
501 which the thread is running (set using the ionice command).
503 Controls: left and right arrows to change the sorting column, r to invert the
504 sorting order, o to toggle the --only option, p to toggle the --processes
505 option, a to toggle the --accumulated option, q to quit, any other key to force
506 a refresh.''' % sys.argv[0]
508 def main():
509 try:
510 locale.setlocale(locale.LC_ALL, '')
511 except locale.Error:
512 print 'unable to set locale, falling back to the default locale'
513 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
514 parser.add_option('-o', '--only', action='store_true',
515 dest='only', default=False,
516 help='only show processes or threads actually doing I/O')
517 parser.add_option('-b', '--batch', action='store_true', dest='batch',
518 help='non-interactive mode')
519 parser.add_option('-n', '--iter', type='int', dest='iterations',
520 metavar='NUM',
521 help='number of iterations before ending [infinite]')
522 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
523 help='delay between iterations [1 second]',
524 metavar='SEC', default=1)
525 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
526 help='processes/threads to monitor [all]', metavar='PID')
527 parser.add_option('-u', '--user', type='str', dest='users', action='append',
528 help='users to monitor [all]', metavar='USER')
529 parser.add_option('-P', '--processes', action='store_true',
530 dest='processes', default=False,
531 help='only show processes, not all threads')
532 parser.add_option('-a', '--accumulated', action='store_true',
533 dest='accumulated', default=False,
534 help='show accumulated I/O instead of bandwidth')
535 parser.add_option('-k', '--kilobytes', action='store_true',
536 dest='kilobytes', default=False,
537 help='use kilobytes instead of a human friendly unit')
538 parser.add_option('-t', '--time', action='store_true', dest='time',
539 help='add a timestamp on each line (implies --batch)')
540 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
541 help='suppress some lines of header (implies --batch)')
542 parser.add_option('--profile', action='store_true', dest='profile',
543 default=False, help=optparse.SUPPRESS_HELP)
545 options, args = parser.parse_args()
546 if args:
547 parser.error('Unexpected arguments: ' + ' '.join(args))
548 find_uids(options)
549 options.pids = options.pids or []
550 options.batch = options.batch or options.time or options.quiet
552 main_loop = lambda: run_iotop(options)
554 if options.profile:
555 def safe_main_loop():
556 try:
557 main_loop()
558 except:
559 pass
560 _profile(safe_main_loop)
561 else:
562 main_loop()