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