Explain that iotop now requires root.
[iotop.git] / iotop / ui.py
blobe8da128079a8964f8f18bd8157c967a6c0d1dc0a
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).rjust(14),
402 format_bandwidth(self.options, total_write, duration).rjust(14))
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 try:
462 if options.batch:
463 return run_iotop_window(None, options)
464 else:
465 return curses.wrapper(run_iotop_window, options)
466 except OSError, e:
467 if e.errno == errno.EPERM:
468 print >> sys.stderr, e
469 print >> sys.stderr, ('iotop requires root or the NET_ADMIN '
470 'capability.')
471 sys.exit(1)
472 else:
473 raise
476 # Profiling
479 def _profile(continuation):
480 prof_file = 'iotop.prof'
481 try:
482 import cProfile
483 import pstats
484 print 'Profiling using cProfile'
485 cProfile.runctx('continuation()', globals(), locals(), prof_file)
486 stats = pstats.Stats(prof_file)
487 except ImportError:
488 import hotshot
489 import hotshot.stats
490 prof = hotshot.Profile(prof_file, lineevents=1)
491 print 'Profiling using hotshot'
492 prof.runcall(continuation)
493 prof.close()
494 stats = hotshot.stats.load(prof_file)
495 stats.strip_dirs()
496 stats.sort_stats('time', 'calls')
497 stats.print_stats(50)
498 stats.print_callees(50)
499 os.remove(prof_file)
502 # Main program
505 USAGE = '''%s [OPTIONS]
507 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
508 period. SWAPIN and IO are the percentages of time the thread spent respectively
509 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
510 which the thread is running (set using the ionice command).
512 Controls: left and right arrows to change the sorting column, r to invert the
513 sorting order, o to toggle the --only option, p to toggle the --processes
514 option, a to toggle the --accumulated option, q to quit, any other key to force
515 a refresh.''' % sys.argv[0]
517 def main():
518 try:
519 locale.setlocale(locale.LC_ALL, '')
520 except locale.Error:
521 print 'unable to set locale, falling back to the default locale'
522 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
523 parser.add_option('-o', '--only', action='store_true',
524 dest='only', default=False,
525 help='only show processes or threads actually doing I/O')
526 parser.add_option('-b', '--batch', action='store_true', dest='batch',
527 help='non-interactive mode')
528 parser.add_option('-n', '--iter', type='int', dest='iterations',
529 metavar='NUM',
530 help='number of iterations before ending [infinite]')
531 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
532 help='delay between iterations [1 second]',
533 metavar='SEC', default=1)
534 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
535 help='processes/threads to monitor [all]', metavar='PID')
536 parser.add_option('-u', '--user', type='str', dest='users', action='append',
537 help='users to monitor [all]', metavar='USER')
538 parser.add_option('-P', '--processes', action='store_true',
539 dest='processes', default=False,
540 help='only show processes, not all threads')
541 parser.add_option('-a', '--accumulated', action='store_true',
542 dest='accumulated', default=False,
543 help='show accumulated I/O instead of bandwidth')
544 parser.add_option('-k', '--kilobytes', action='store_true',
545 dest='kilobytes', default=False,
546 help='use kilobytes instead of a human friendly unit')
547 parser.add_option('-t', '--time', action='store_true', dest='time',
548 help='add a timestamp on each line (implies --batch)')
549 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
550 help='suppress some lines of header (implies --batch)')
551 parser.add_option('--profile', action='store_true', dest='profile',
552 default=False, help=optparse.SUPPRESS_HELP)
554 options, args = parser.parse_args()
555 if args:
556 parser.error('Unexpected arguments: ' + ' '.join(args))
557 find_uids(options)
558 options.pids = options.pids or []
559 options.batch = options.batch or options.time or options.quiet
561 main_loop = lambda: run_iotop(options)
563 if options.profile:
564 def safe_main_loop():
565 try:
566 main_loop()
567 except:
568 pass
569 _profile(safe_main_loop)
570 else:
571 main_loop()