Back to addstr because of: http://marc.info/?l=ncurses-bug&m=125233342917443&w=3
[iotop.git] / iotop / ui.py
blob32397fa917988d6f94a08a2bd6ecc0d8ced8a596
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 exc_type, value, traceback = sys.exc_info()
452 value = '%s win:%s i:%d line:%s' % \
453 (value, self.win.getmaxyx(), i, lines[i])
454 value = str(value).encode('string_escape')
455 raise exc_type, value, traceback
456 if status_msg:
457 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
458 self.win.refresh()
460 def run_iotop_window(win, options):
461 taskstats_connection = TaskStatsNetlink(options)
462 process_list = ProcessList(taskstats_connection, options)
463 ui = IOTopUI(win, process_list, options)
464 ui.run()
466 def run_iotop(options):
467 if options.batch:
468 return run_iotop_window(None, options)
469 else:
470 return curses.wrapper(run_iotop_window, options)
473 # Profiling
476 def _profile(continuation):
477 prof_file = 'iotop.prof'
478 try:
479 import cProfile
480 import pstats
481 print 'Profiling using cProfile'
482 cProfile.runctx('continuation()', globals(), locals(), prof_file)
483 stats = pstats.Stats(prof_file)
484 except ImportError:
485 import hotshot
486 import hotshot.stats
487 prof = hotshot.Profile(prof_file, lineevents=1)
488 print 'Profiling using hotshot'
489 prof.runcall(continuation)
490 prof.close()
491 stats = hotshot.stats.load(prof_file)
492 stats.strip_dirs()
493 stats.sort_stats('time', 'calls')
494 stats.print_stats(50)
495 stats.print_callees(50)
496 os.remove(prof_file)
499 # Main program
502 USAGE = '''%s [OPTIONS]
504 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
505 period. SWAPIN and IO are the percentages of time the thread spent respectively
506 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
507 which the thread is running (set using the ionice command).
509 Controls: left and right arrows to change the sorting column, r to invert the
510 sorting order, o to toggle the --only option, p to toggle the --processes
511 option, a to toggle the --accumulated option, q to quit, any other key to force
512 a refresh.''' % sys.argv[0]
514 def main():
515 try:
516 locale.setlocale(locale.LC_ALL, '')
517 except locale.Error:
518 print 'unable to set locale, falling back to the default locale'
519 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
520 parser.add_option('-o', '--only', action='store_true',
521 dest='only', default=False,
522 help='only show processes or threads actually doing I/O')
523 parser.add_option('-b', '--batch', action='store_true', dest='batch',
524 help='non-interactive mode')
525 parser.add_option('-n', '--iter', type='int', dest='iterations',
526 metavar='NUM',
527 help='number of iterations before ending [infinite]')
528 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
529 help='delay between iterations [1 second]',
530 metavar='SEC', default=1)
531 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
532 help='processes/threads to monitor [all]', metavar='PID')
533 parser.add_option('-u', '--user', type='str', dest='users', action='append',
534 help='users to monitor [all]', metavar='USER')
535 parser.add_option('-P', '--processes', action='store_true',
536 dest='processes', default=False,
537 help='only show processes, not all threads')
538 parser.add_option('-a', '--accumulated', action='store_true',
539 dest='accumulated', default=False,
540 help='show accumulated I/O instead of bandwidth')
541 parser.add_option('-k', '--kilobytes', action='store_true',
542 dest='kilobytes', default=False,
543 help='use kilobytes instead of a human friendly unit')
544 parser.add_option('-t', '--time', action='store_true', dest='time',
545 help='add a timestamp on each line (implies --batch)')
546 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
547 help='suppress some lines of header (implies --batch)')
548 parser.add_option('--profile', action='store_true', dest='profile',
549 default=False, help=optparse.SUPPRESS_HELP)
551 options, args = parser.parse_args()
552 if args:
553 parser.error('Unexpected arguments: ' + ' '.join(args))
554 find_uids(options)
555 options.pids = options.pids or []
556 options.batch = options.batch or options.time or options.quiet
558 main_loop = lambda: run_iotop(options)
560 if options.profile:
561 def safe_main_loop():
562 try:
563 main_loop()
564 except:
565 pass
566 _profile(safe_main_loop)
567 else:
568 main_loop()