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