bfec6d8cd0b883ad7ec72a650611835df8114605
[iotop.git] / iotop / ui.py
blobbfec6d8cd0b883ad7ec72a650611835df8114605
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 signal
27 import sys
28 import time
30 from iotop.data import find_uids, TaskStatsNetlink, ProcessList, Stats
31 from iotop.data import ThreadInfo
32 from iotop.version import VERSION
33 import ioprio
34 from ioprio import IoprioSetError
37 # Utility functions for the UI
40 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
42 def human_size(size):
43 if size > 0:
44 sign = ''
45 elif size < 0:
46 sign = '-'
47 size = -size
48 else:
49 return '0.00 B'
51 expo = int(math.log(size / 2, 2) / 10)
52 return '%s%.2f %s' % (sign, (float(size) / (1 << (10 * expo))), UNITS[expo])
54 def format_size(options, bytes):
55 if options.kilobytes:
56 return '%.2f K' % (bytes / 1024.0)
57 return human_size(bytes)
59 def format_bandwidth(options, size, duration):
60 return format_size(options, size and float(size) / duration) + '/s'
62 def format_stats(options, process, duration):
63 # Keep in sync with TaskStatsNetlink.members_offsets and
64 # IOTopUI.get_data(self)
65 def delay2percent(delay): # delay in ns, duration in s
66 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
67 if options.accumulated:
68 stats = process.stats_accum
69 display_format = lambda size, duration: format_size(options, size)
70 duration = time.time() - process.stats_accum_timestamp
71 else:
72 stats = process.stats_delta
73 display_format = lambda size, duration: format_bandwidth(
74 options, size, duration)
75 io_delay = delay2percent(stats.blkio_delay_total)
76 swapin_delay = delay2percent(stats.swapin_delay_total)
77 read_bytes = display_format(stats.read_bytes, duration)
78 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
79 written_bytes = max(0, written_bytes)
80 write_bytes = display_format(written_bytes, duration)
81 return io_delay, swapin_delay, read_bytes, write_bytes
83 def get_max_pid_width():
84 try:
85 return len(file('/proc/sys/kernel/pid_max').read().strip())
86 except Exception, e:
87 print e
88 # Reasonable default in case something fails
89 return 5
91 MAX_PID_WIDTH = get_max_pid_width()
94 # UI Exceptions
97 class CancelInput(Exception): pass
98 class InvalidInt(Exception): pass
99 class InvalidPid(Exception): pass
100 class InvalidTid(Exception): pass
101 class InvalidIoprioData(Exception): pass
104 # The UI
107 class IOTopUI(object):
108 # key, reverse
109 sorting_keys = [
110 (lambda p, s: p.pid, False),
111 (lambda p, s: p.ioprio_sort_key(), False),
112 (lambda p, s: p.get_user(), False),
113 (lambda p, s: s.read_bytes, True),
114 (lambda p, s: s.write_bytes - s.cancelled_write_bytes, True),
115 (lambda p, s: s.swapin_delay_total, True),
116 # The default sorting (by I/O % time) should show processes doing
117 # only writes, without waiting on them
118 (lambda p, s: s.blkio_delay_total or
119 int(not(not(s.read_bytes or s.write_bytes))), True),
120 (lambda p, s: p.get_cmdline(), False),
123 def __init__(self, win, process_list, options):
124 self.process_list = process_list
125 self.options = options
126 self.sorting_key = 6
127 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
128 if not self.options.batch:
129 self.win = win
130 self.resize()
131 try:
132 curses.use_default_colors()
133 curses.start_color()
134 curses.curs_set(0)
135 except curses.error:
136 # This call can fail with misconfigured terminals, for example
137 # TERM=xterm-color. This is harmless
138 pass
140 def resize(self):
141 self.height, self.width = self.win.getmaxyx()
143 def run(self):
144 iterations = 0
145 poll = select.poll()
146 if not self.options.batch:
147 poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
148 while self.options.iterations is None or \
149 iterations < self.options.iterations:
150 total = self.process_list.refresh_processes()
151 total_read, total_write = total
152 self.refresh_display(iterations == 0, total_read, total_write,
153 self.process_list.duration)
154 if self.options.iterations is not None:
155 iterations += 1
156 if iterations >= self.options.iterations:
157 break
158 elif iterations == 0:
159 iterations = 1
161 try:
162 events = poll.poll(self.options.delay_seconds * 1000.0)
163 except select.error, e:
164 if e.args and e.args[0] == errno.EINTR:
165 events = 0
166 else:
167 raise
168 if not self.options.batch:
169 self.resize()
170 if events:
171 key = self.win.getch()
172 self.handle_key(key)
174 def reverse_sorting(self):
175 self.sorting_reverse = not self.sorting_reverse
177 def adjust_sorting_key(self, delta):
178 orig_sorting_key = self.sorting_key
179 self.sorting_key += delta
180 self.sorting_key = max(0, self.sorting_key)
181 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
182 if orig_sorting_key != self.sorting_key:
183 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
185 # I wonder if switching to urwid for the display would be better here
187 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
188 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
189 self.win.addstr(1, 0, prompt, curses.A_BOLD)
190 self.win.refresh()
191 curses.echo()
192 curses.curs_set(1)
193 inp = self.win.getstr(1, len(prompt))
194 curses.curs_set(0)
195 curses.noecho()
196 if inp not in (None, ''):
197 return inp
198 if empty_is_cancel:
199 raise CancelInput()
200 return default
202 def prompt_int(self, prompt, default = None, empty_is_cancel = True):
203 inp = self.prompt_str(prompt, default, empty_is_cancel)
204 try:
205 return int(inp)
206 except ValueError:
207 raise InvalidInt()
209 def prompt_pid(self):
210 try:
211 return self.prompt_int('PID to ionice: ')
212 except InvalidInt:
213 raise InvalidPid()
214 except CancelInput:
215 raise
217 def prompt_tid(self):
218 try:
219 return self.prompt_int('TID to ionice: ')
220 except InvalidInt:
221 raise InvalidTid()
222 except CancelInput:
223 raise
225 def prompt_data(self, ioprio_data):
226 try:
227 if ioprio_data is not None:
228 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
229 % ioprio_data, ioprio_data, False)
230 else:
231 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
232 except InvalidInt:
233 raise InvalidIoprioData()
234 if inp < 0 or inp > 7:
235 raise InvalidIoprioData()
236 return inp
238 def prompt_set(self, prompt, display_list, ret_list, selected):
239 try:
240 selected = ret_list.index(selected)
241 except ValueError:
242 selected = -1
243 set_len = len(display_list) - 1
244 while True:
245 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
246 self.win.insstr(1, 0, prompt, curses.A_BOLD)
247 offset = len(prompt)
248 for i, item in enumerate(display_list):
249 display = ' %s ' % item
250 if i is selected:
251 attr = curses.A_REVERSE
252 else:
253 attr = curses.A_NORMAL
254 self.win.insstr(1, offset, display, attr)
255 offset += len(display)
256 while True:
257 key = self.win.getch()
258 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
259 selected -= 1
260 break
261 elif key in (curses.KEY_RIGHT, ord('r')) and selected < set_len:
262 selected += 1
263 break
264 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
265 return ret_list[selected]
266 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
267 curses.KEY_EXIT, ord('q'), ord('Q')):
268 raise CancelInput()
270 def prompt_class(self, ioprio_class=None):
271 prompt = 'I/O priority class: '
272 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
273 classes_ret = ['rt', 'be', 'idle']
274 if ioprio_class is None:
275 ioprio_class = 2
276 inp = self.prompt_set(prompt, classes_prompt, classes_ret, ioprio_class)
277 return inp
279 def prompt_error(self, error = 'Error!'):
280 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
281 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
282 self.win.refresh()
283 time.sleep(1)
285 def prompt_clear(self):
286 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
287 self.win.refresh()
289 def handle_key(self, key):
290 def toggle_accumulated():
291 self.options.accumulated ^= True
292 def toggle_only_io():
293 self.options.only ^= True
294 def toggle_processes():
295 self.options.processes ^= True
296 self.process_list.clear()
297 self.process_list.refresh_processes()
298 def ionice():
299 try:
300 if self.options.processes:
301 pid = self.prompt_pid()
302 exec_unit = self.process_list.get_process(pid)
303 else:
304 tid = self.prompt_tid()
305 exec_unit = ThreadInfo(tid,
306 self.process_list.taskstats_connection)
307 ioprio_value = exec_unit.get_ioprio()
308 (ioprio_class, ioprio_data) = \
309 ioprio.to_class_and_data(ioprio_value)
310 ioprio_class = self.prompt_class(ioprio_class)
311 if ioprio_class == 'idle':
312 ioprio_data = 0
313 else:
314 ioprio_data = self.prompt_data(ioprio_data)
315 exec_unit.set_ioprio(ioprio_class, ioprio_data)
316 self.process_list.clear()
317 self.process_list.refresh_processes()
318 except IoprioSetError, e:
319 self.prompt_error('Error setting I/O priority: %s' % e.err)
320 except InvalidPid:
321 self.prompt_error('Invalid process id!')
322 except InvalidTid:
323 self.prompt_error('Invalid thread id!')
324 except InvalidIoprioData:
325 self.prompt_error('Invalid I/O priority data!')
326 except InvalidInt:
327 self.prompt_error('Invalid integer!')
328 except CancelInput:
329 self.prompt_clear()
330 else:
331 self.prompt_clear()
333 key_bindings = {
334 ord('q'):
335 lambda: sys.exit(0),
336 ord('Q'):
337 lambda: sys.exit(0),
338 ord('r'):
339 lambda: self.reverse_sorting(),
340 ord('R'):
341 lambda: self.reverse_sorting(),
342 ord('a'):
343 toggle_accumulated,
344 ord('A'):
345 toggle_accumulated,
346 ord('o'):
347 toggle_only_io,
348 ord('O'):
349 toggle_only_io,
350 ord('p'):
351 toggle_processes,
352 ord('P'):
353 toggle_processes,
354 ord('i'):
355 ionice,
356 ord('I'):
357 ionice,
358 curses.KEY_LEFT:
359 lambda: self.adjust_sorting_key(-1),
360 curses.KEY_RIGHT:
361 lambda: self.adjust_sorting_key(1),
362 curses.KEY_HOME:
363 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
364 curses.KEY_END:
365 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
368 action = key_bindings.get(key, lambda: None)
369 action()
371 def get_data(self):
372 def format(p):
373 stats = format_stats(self.options, p, self.process_list.duration)
374 io_delay, swapin_delay, read_bytes, write_bytes = stats
375 if Stats.has_blkio_delay_total:
376 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
377 else:
378 delay_stats = ' ?unavailable? '
379 pid_format = '%%%dd' % MAX_PID_WIDTH
380 line = (pid_format + ' %4s %-8s %11s %11s %s') % (
381 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
382 write_bytes, delay_stats)
383 cmdline = p.get_cmdline()
384 if not self.options.batch:
385 remaining_length = self.width - len(line)
386 if 2 < remaining_length < len(cmdline):
387 len1 = (remaining_length - 1) // 2
388 offset2 = -(remaining_length - len1 - 1)
389 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
390 line += cmdline
391 if not self.options.batch:
392 line = line[:self.width]
393 return line
395 def should_format(p):
396 return not self.options.only or \
397 p.did_some_io(self.options.accumulated)
399 processes = filter(should_format, self.process_list.processes.values())
400 key = IOTopUI.sorting_keys[self.sorting_key][0]
401 if self.options.accumulated:
402 stats_lambda = lambda p: p.stats_accum
403 else:
404 stats_lambda = lambda p: p.stats_delta
405 processes.sort(key=lambda p: key(p, stats_lambda(p)),
406 reverse=self.sorting_reverse)
407 if not self.options.batch:
408 del processes[self.height - 2:]
409 return map(format, processes)
411 def refresh_display(self, first_time, total_read, total_write, duration):
412 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
413 format_bandwidth(self.options, total_read, duration).rjust(14),
414 format_bandwidth(self.options, total_write, duration).rjust(14))
416 pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
417 if self.options.processes:
418 pid += 'PID'
419 else:
420 pid += 'TID'
421 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
422 ' SWAPIN', ' IO', ' COMMAND']
423 lines = self.get_data()
424 if self.options.time:
425 titles = [' TIME'] + titles
426 current_time = time.strftime('%H:%M:%S ')
427 lines = [current_time + l for l in lines]
428 summary = current_time + summary
429 if self.options.batch:
430 if self.options.quiet <= 2:
431 print summary
432 if self.options.quiet <= int(first_time):
433 print ''.join(titles)
434 for l in lines:
435 print l.encode('utf-8')
436 sys.stdout.flush()
437 else:
438 self.win.erase()
439 self.win.addstr(summary[:self.width])
440 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
441 remaining_cols = self.width
442 for i in xrange(len(titles)):
443 attr = curses.A_REVERSE
444 title = titles[i]
445 if i == self.sorting_key:
446 title = title[1:]
447 if i == self.sorting_key:
448 attr |= curses.A_BOLD
449 title += self.sorting_reverse and '>' or '<'
450 title = title[:remaining_cols]
451 remaining_cols -= len(title)
452 self.win.addstr(title, attr)
453 if Stats.has_blkio_delay_total:
454 status_msg = None
455 else:
456 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
457 'cannot determine SWAPIN and IO %')
458 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
459 for i in xrange(num_lines):
460 try:
461 self.win.addstr(i + 2, 0, lines[i].encode('utf-8'))
462 except curses.error:
463 pass
464 if status_msg:
465 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
466 self.win.refresh()
468 def run_iotop_window(win, options):
469 if options.batch:
470 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
471 taskstats_connection = TaskStatsNetlink(options)
472 process_list = ProcessList(taskstats_connection, options)
473 ui = IOTopUI(win, process_list, options)
474 ui.run()
476 def run_iotop(options):
477 try:
478 if options.batch:
479 return run_iotop_window(None, options)
480 else:
481 return curses.wrapper(run_iotop_window, options)
482 except OSError, e:
483 if e.errno == errno.EPERM:
484 print >> sys.stderr, e
485 print >> sys.stderr, ('''
486 The Linux kernel interfaces that iotop relies on now require root priviliges
487 or the NET_ADMIN capability. This change occured because a security issue
488 (CVE-2011-2494) was found that allows leakage of sensitive data across user
489 boundaries. If you require the ability to run iotop as a non-root user, please
490 configure sudo to allow you to run iotop as root.
492 Please do not file bugs on iotop about this.''')
493 sys.exit(1)
494 else:
495 raise
498 # Profiling
501 def _profile(continuation):
502 prof_file = 'iotop.prof'
503 try:
504 import cProfile
505 import pstats
506 print 'Profiling using cProfile'
507 cProfile.runctx('continuation()', globals(), locals(), prof_file)
508 stats = pstats.Stats(prof_file)
509 except ImportError:
510 import hotshot
511 import hotshot.stats
512 prof = hotshot.Profile(prof_file, lineevents=1)
513 print 'Profiling using hotshot'
514 prof.runcall(continuation)
515 prof.close()
516 stats = hotshot.stats.load(prof_file)
517 stats.strip_dirs()
518 stats.sort_stats('time', 'calls')
519 stats.print_stats(50)
520 stats.print_callees(50)
521 os.remove(prof_file)
524 # Main program
527 USAGE = '''%s [OPTIONS]
529 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
530 period. SWAPIN and IO are the percentages of time the thread spent respectively
531 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
532 which the thread is running (set using the ionice command).
534 Controls: left and right arrows to change the sorting column, r to invert the
535 sorting order, o to toggle the --only option, p to toggle the --processes
536 option, a to toggle the --accumulated option, q to quit, any other key to force
537 a refresh.''' % sys.argv[0]
539 def main():
540 try:
541 locale.setlocale(locale.LC_ALL, '')
542 except locale.Error:
543 print 'unable to set locale, falling back to the default locale'
544 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
545 parser.add_option('-o', '--only', action='store_true',
546 dest='only', default=False,
547 help='only show processes or threads actually doing I/O')
548 parser.add_option('-b', '--batch', action='store_true', dest='batch',
549 help='non-interactive mode')
550 parser.add_option('-n', '--iter', type='int', dest='iterations',
551 metavar='NUM',
552 help='number of iterations before ending [infinite]')
553 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
554 help='delay between iterations [1 second]',
555 metavar='SEC', default=1)
556 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
557 help='processes/threads to monitor [all]', metavar='PID')
558 parser.add_option('-u', '--user', type='str', dest='users', action='append',
559 help='users to monitor [all]', metavar='USER')
560 parser.add_option('-P', '--processes', action='store_true',
561 dest='processes', default=False,
562 help='only show processes, not all threads')
563 parser.add_option('-a', '--accumulated', action='store_true',
564 dest='accumulated', default=False,
565 help='show accumulated I/O instead of bandwidth')
566 parser.add_option('-k', '--kilobytes', action='store_true',
567 dest='kilobytes', default=False,
568 help='use kilobytes instead of a human friendly unit')
569 parser.add_option('-t', '--time', action='store_true', dest='time',
570 help='add a timestamp on each line (implies --batch)')
571 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
572 help='suppress some lines of header (implies --batch)')
573 parser.add_option('--profile', action='store_true', dest='profile',
574 default=False, help=optparse.SUPPRESS_HELP)
576 options, args = parser.parse_args()
577 if args:
578 parser.error('Unexpected arguments: ' + ' '.join(args))
579 find_uids(options)
580 options.pids = options.pids or []
581 options.batch = options.batch or options.time or options.quiet
583 main_loop = lambda: run_iotop(options)
585 if options.profile:
586 def safe_main_loop():
587 try:
588 main_loop()
589 except:
590 pass
591 _profile(safe_main_loop)
592 else:
593 main_loop()