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