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