1184ba2eb2e144da5767e7ed90f76b708237b579
[iotop.git] / iotop / ui.py
blob1184ba2eb2e144da5767e7ed90f76b708237b579
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, actual = self.process_list.refresh_processes()
154 self.refresh_display(iterations == 0, total, actual,
155 self.process_list.duration)
156 if self.options.iterations is not None:
157 iterations += 1
158 if iterations >= self.options.iterations:
159 break
160 elif iterations == 0:
161 iterations = 1
163 try:
164 events = poll.poll(self.options.delay_seconds * 1000.0)
165 except select.error as e:
166 if e.args and e.args[0] == errno.EINTR:
167 events = 0
168 else:
169 raise
170 if not self.options.batch:
171 self.resize()
172 if events:
173 key = self.win.getch()
174 self.handle_key(key)
176 def reverse_sorting(self):
177 self.sorting_reverse = not self.sorting_reverse
179 def adjust_sorting_key(self, delta):
180 orig_sorting_key = self.sorting_key
181 self.sorting_key += delta
182 self.sorting_key = max(0, self.sorting_key)
183 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
184 if orig_sorting_key != self.sorting_key:
185 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
187 # I wonder if switching to urwid for the display would be better here
189 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
190 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
191 self.win.addstr(1, 0, prompt, curses.A_BOLD)
192 self.win.refresh()
193 curses.echo()
194 curses.curs_set(1)
195 inp = self.win.getstr(1, len(prompt))
196 curses.curs_set(0)
197 curses.noecho()
198 if inp not in (None, ''):
199 return inp
200 if empty_is_cancel:
201 raise CancelInput()
202 return default
204 def prompt_int(self, prompt, default = None, empty_is_cancel = True):
205 inp = self.prompt_str(prompt, default, empty_is_cancel)
206 try:
207 return int(inp)
208 except ValueError:
209 raise InvalidInt()
211 def prompt_pid(self):
212 try:
213 return self.prompt_int('PID to ionice: ')
214 except InvalidInt:
215 raise InvalidPid()
216 except CancelInput:
217 raise
219 def prompt_tid(self):
220 try:
221 return self.prompt_int('TID to ionice: ')
222 except InvalidInt:
223 raise InvalidTid()
224 except CancelInput:
225 raise
227 def prompt_data(self, ioprio_data):
228 try:
229 if ioprio_data is not None:
230 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
231 % ioprio_data, ioprio_data, False)
232 else:
233 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
234 except InvalidInt:
235 raise InvalidIoprioData()
236 if inp < 0 or inp > 7:
237 raise InvalidIoprioData()
238 return inp
240 def prompt_set(self, prompt, display_list, ret_list, selected):
241 try:
242 selected = ret_list.index(selected)
243 except ValueError:
244 selected = -1
245 set_len = len(display_list) - 1
246 while True:
247 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
248 self.win.insstr(1, 0, prompt, curses.A_BOLD)
249 offset = len(prompt)
250 for i, item in enumerate(display_list):
251 display = ' %s ' % item
252 if i is selected:
253 attr = curses.A_REVERSE
254 else:
255 attr = curses.A_NORMAL
256 self.win.insstr(1, offset, display, attr)
257 offset += len(display)
258 while True:
259 key = self.win.getch()
260 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
261 selected -= 1
262 break
263 elif key in (curses.KEY_RIGHT, ord('r')) and selected < set_len:
264 selected += 1
265 break
266 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
267 return ret_list[selected]
268 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
269 curses.KEY_EXIT, ord('q'), ord('Q')):
270 raise CancelInput()
272 def prompt_class(self, ioprio_class=None):
273 prompt = 'I/O priority class: '
274 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
275 classes_ret = ['rt', 'be', 'idle']
276 if ioprio_class is None:
277 ioprio_class = 2
278 inp = self.prompt_set(prompt, classes_prompt, classes_ret, ioprio_class)
279 return inp
281 def prompt_error(self, error = 'Error!'):
282 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
283 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
284 self.win.refresh()
285 time.sleep(1)
287 def prompt_clear(self):
288 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
289 self.win.refresh()
291 def handle_key(self, key):
292 def toggle_accumulated():
293 self.options.accumulated ^= True
294 def toggle_only_io():
295 self.options.only ^= True
296 def toggle_processes():
297 self.options.processes ^= True
298 self.process_list.clear()
299 self.process_list.refresh_processes()
300 def ionice():
301 try:
302 if self.options.processes:
303 pid = self.prompt_pid()
304 exec_unit = self.process_list.get_process(pid)
305 else:
306 tid = self.prompt_tid()
307 exec_unit = ThreadInfo(tid,
308 self.process_list.taskstats_connection)
309 ioprio_value = exec_unit.get_ioprio()
310 (ioprio_class, ioprio_data) = \
311 ioprio.to_class_and_data(ioprio_value)
312 ioprio_class = self.prompt_class(ioprio_class)
313 if ioprio_class == 'idle':
314 ioprio_data = 0
315 else:
316 ioprio_data = self.prompt_data(ioprio_data)
317 exec_unit.set_ioprio(ioprio_class, ioprio_data)
318 self.process_list.clear()
319 self.process_list.refresh_processes()
320 except IoprioSetError as e:
321 self.prompt_error('Error setting I/O priority: %s' % e.err)
322 except InvalidPid:
323 self.prompt_error('Invalid process id!')
324 except InvalidTid:
325 self.prompt_error('Invalid thread id!')
326 except InvalidIoprioData:
327 self.prompt_error('Invalid I/O priority data!')
328 except InvalidInt:
329 self.prompt_error('Invalid integer!')
330 except CancelInput:
331 self.prompt_clear()
332 else:
333 self.prompt_clear()
335 key_bindings = {
336 ord('q'):
337 lambda: sys.exit(0),
338 ord('Q'):
339 lambda: sys.exit(0),
340 ord('r'):
341 lambda: self.reverse_sorting(),
342 ord('R'):
343 lambda: self.reverse_sorting(),
344 ord('a'):
345 toggle_accumulated,
346 ord('A'):
347 toggle_accumulated,
348 ord('o'):
349 toggle_only_io,
350 ord('O'):
351 toggle_only_io,
352 ord('p'):
353 toggle_processes,
354 ord('P'):
355 toggle_processes,
356 ord('i'):
357 ionice,
358 ord('I'):
359 ionice,
360 curses.KEY_LEFT:
361 lambda: self.adjust_sorting_key(-1),
362 curses.KEY_RIGHT:
363 lambda: self.adjust_sorting_key(1),
364 curses.KEY_HOME:
365 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
366 curses.KEY_END:
367 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
370 action = key_bindings.get(key, lambda: None)
371 action()
373 def get_data(self):
374 def format(p):
375 stats = format_stats(self.options, p, self.process_list.duration)
376 io_delay, swapin_delay, read_bytes, write_bytes = stats
377 if Stats.has_blkio_delay_total:
378 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
379 else:
380 delay_stats = ' ?unavailable? '
381 pid_format = '%%%dd' % MAX_PID_WIDTH
382 line = (pid_format + ' %4s %-8s %11s %11s %s') % (
383 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
384 write_bytes, delay_stats)
385 cmdline = p.get_cmdline()
386 if not self.options.batch:
387 remaining_length = self.width - len(line)
388 if 2 < remaining_length < len(cmdline):
389 len1 = (remaining_length - 1) // 2
390 offset2 = -(remaining_length - len1 - 1)
391 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
392 line += cmdline
393 if not self.options.batch:
394 line = line[:self.width]
395 return line
397 def should_format(p):
398 return not self.options.only or \
399 p.did_some_io(self.options.accumulated)
401 processes = list(filter(should_format,
402 self.process_list.processes.values()))
403 key = IOTopUI.sorting_keys[self.sorting_key][0]
404 if self.options.accumulated:
405 stats_lambda = lambda p: p.stats_accum
406 else:
407 stats_lambda = lambda p: p.stats_delta
408 processes.sort(key=lambda p: key(p, stats_lambda(p)),
409 reverse=self.sorting_reverse)
410 if not self.options.batch:
411 del processes[self.height - 2:]
412 return list(map(format, processes))
414 def refresh_display(self, first_time, total, actual, duration):
415 summary = [
416 'Total DISK READ : %s | Total DISK WRITE : %s' % (
417 format_bandwidth(self.options, total[0], duration).rjust(14),
418 format_bandwidth(self.options, total[1], duration).rjust(14)),
419 'Actual DISK READ: %s | Actual DISK WRITE: %s' % (
420 format_bandwidth(self.options, actual[0], duration).rjust(14),
421 format_bandwidth(self.options, actual[1], duration).rjust(14))
424 pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
425 if self.options.processes:
426 pid += 'PID'
427 else:
428 pid += 'TID'
429 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
430 ' SWAPIN', ' IO', ' COMMAND']
431 lines = self.get_data()
432 if self.options.time:
433 titles = [' TIME'] + titles
434 current_time = time.strftime('%H:%M:%S ')
435 lines = [current_time + l for l in lines]
436 summary = [current_time + s for s in summary]
437 if self.options.batch:
438 if self.options.quiet <= 2:
439 for s in summary:
440 print(s)
441 if self.options.quiet <= int(first_time):
442 print(''.join(titles))
443 for l in lines:
444 print(l)
445 sys.stdout.flush()
446 else:
447 self.win.erase()
448 for i, s in enumerate(summary):
449 self.win.addstr(i, 0, s[:self.width])
450 self.win.hline(len(summary), 0, ord(' ') | curses.A_REVERSE, self.width)
451 remaining_cols = self.width
452 for i in range(len(titles)):
453 attr = curses.A_REVERSE
454 title = titles[i]
455 if i == self.sorting_key:
456 title = title[1:]
457 if i == self.sorting_key:
458 attr |= curses.A_BOLD
459 title += self.sorting_reverse and '>' or '<'
460 title = title[:remaining_cols]
461 remaining_cols -= len(title)
462 self.win.addstr(title, attr)
463 if Stats.has_blkio_delay_total:
464 status_msg = None
465 else:
466 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
467 'cannot determine SWAPIN and IO %')
468 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
469 for i in range(num_lines):
470 try:
471 self.win.addstr(i + len(summary) + 1, 0, lines[i])
472 except curses.error:
473 pass
474 if status_msg:
475 self.win.insstr(self.height - len(summary), 0, status_msg, curses.A_BOLD)
476 self.win.refresh()
478 def run_iotop_window(win, options):
479 if options.batch:
480 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
481 taskstats_connection = TaskStatsNetlink(options)
482 process_list = ProcessList(taskstats_connection, options)
483 ui = IOTopUI(win, process_list, options)
484 ui.run()
486 def run_iotop(options):
487 try:
488 if options.batch:
489 return run_iotop_window(None, options)
490 else:
491 return curses.wrapper(run_iotop_window, options)
492 except OSError as e:
493 if e.errno == errno.EPERM:
494 print(e, file=sys.stderr)
495 print('''
496 The Linux kernel interfaces that iotop relies on now require root priviliges
497 or the NET_ADMIN capability. This change occured because a security issue
498 (CVE-2011-2494) was found that allows leakage of sensitive data across user
499 boundaries. If you require the ability to run iotop as a non-root user, please
500 configure sudo to allow you to run iotop as root.
502 Please do not file bugs on iotop about this.''', file=sys.stderr)
503 sys.exit(1)
504 else:
505 raise
508 # Profiling
511 def _profile(continuation):
512 prof_file = 'iotop.prof'
513 try:
514 import cProfile
515 import pstats
516 print('Profiling using cProfile')
517 cProfile.runctx('continuation()', globals(), locals(), prof_file)
518 stats = pstats.Stats(prof_file)
519 except ImportError:
520 import hotshot
521 import hotshot.stats
522 prof = hotshot.Profile(prof_file, lineevents=1)
523 print('Profiling using hotshot')
524 prof.runcall(continuation)
525 prof.close()
526 stats = hotshot.stats.load(prof_file)
527 stats.strip_dirs()
528 stats.sort_stats('time', 'calls')
529 stats.print_stats(50)
530 stats.print_callees(50)
531 os.remove(prof_file)
534 # Main program
537 USAGE = '''%s [OPTIONS]
539 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
540 period. SWAPIN and IO are the percentages of time the thread spent respectively
541 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
542 which the thread is running (set using the ionice command).
544 Controls: left and right arrows to change the sorting column, r to invert the
545 sorting order, o to toggle the --only option, p to toggle the --processes
546 option, a to toggle the --accumulated option, i to change I/O priority, q to
547 quit, any other key to force a refresh.''' % sys.argv[0]
549 def main():
550 try:
551 locale.setlocale(locale.LC_ALL, '')
552 except locale.Error:
553 print('unable to set locale, falling back to the default locale')
554 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
555 parser.add_option('-o', '--only', action='store_true',
556 dest='only', default=False,
557 help='only show processes or threads actually doing I/O')
558 parser.add_option('-b', '--batch', action='store_true', dest='batch',
559 help='non-interactive mode')
560 parser.add_option('-n', '--iter', type='int', dest='iterations',
561 metavar='NUM',
562 help='number of iterations before ending [infinite]')
563 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
564 help='delay between iterations [1 second]',
565 metavar='SEC', default=1)
566 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
567 help='processes/threads to monitor [all]', metavar='PID')
568 parser.add_option('-u', '--user', type='str', dest='users', action='append',
569 help='users to monitor [all]', metavar='USER')
570 parser.add_option('-P', '--processes', action='store_true',
571 dest='processes', default=False,
572 help='only show processes, not all threads')
573 parser.add_option('-a', '--accumulated', action='store_true',
574 dest='accumulated', default=False,
575 help='show accumulated I/O instead of bandwidth')
576 parser.add_option('-k', '--kilobytes', action='store_true',
577 dest='kilobytes', default=False,
578 help='use kilobytes instead of a human friendly unit')
579 parser.add_option('-t', '--time', action='store_true', dest='time',
580 help='add a timestamp on each line (implies --batch)')
581 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
582 help='suppress some lines of header (implies --batch)')
583 parser.add_option('--profile', action='store_true', dest='profile',
584 default=False, help=optparse.SUPPRESS_HELP)
586 options, args = parser.parse_args()
587 if args:
588 parser.error('Unexpected arguments: ' + ' '.join(args))
589 find_uids(options)
590 options.pids = options.pids or []
591 options.batch = options.batch or options.time or options.quiet
593 main_loop = lambda: run_iotop(options)
595 if options.profile:
596 def safe_main_loop():
597 try:
598 main_loop()
599 except:
600 pass
601 _profile(safe_main_loop)
602 else:
603 main_loop()