Show stats since iotop started, not since 'a' was pressed.
[iotop.git] / iotop / ui.py
blobc2e44f2138923d71cf533de9c1ffb2e666c83484
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 def toggle_only_io():
284 self.options.only ^= True
285 def toggle_processes():
286 self.options.processes ^= True
287 self.process_list.clear()
288 self.process_list.refresh_processes()
289 def ionice():
290 try:
291 if self.options.processes:
292 pid = self.prompt_pid()
293 exec_unit = self.process_list.get_process(pid)
294 else:
295 tid = self.prompt_tid()
296 exec_unit = ThreadInfo(tid,
297 self.process_list.taskstats_connection)
298 ioprio_value = exec_unit.get_ioprio()
299 (ioprio_class, ioprio_data) = \
300 ioprio.to_class_and_data(ioprio_value)
301 ioprio_class = self.prompt_class(ioprio_class)
302 if ioprio_class == 'idle':
303 ioprio_data = 0
304 else:
305 ioprio_data = self.prompt_data(ioprio_data)
306 exec_unit.set_ioprio(ioprio_class, ioprio_data)
307 self.process_list.clear()
308 self.process_list.refresh_processes()
309 except IoprioSetError, e:
310 self.prompt_error('Error setting I/O priority: %s' % e.err)
311 except InvalidPid:
312 self.prompt_error('Invalid process id!')
313 except InvalidTid:
314 self.prompt_error('Invalid thread id!')
315 except InvalidIoprioData:
316 self.prompt_error('Invalid I/O priority data!')
317 except InvalidInt:
318 self.prompt_error('Invalid integer!')
319 except CancelInput:
320 self.prompt_clear()
321 else:
322 self.prompt_clear()
324 key_bindings = {
325 ord('q'):
326 lambda: sys.exit(0),
327 ord('Q'):
328 lambda: sys.exit(0),
329 ord('r'):
330 lambda: self.reverse_sorting(),
331 ord('R'):
332 lambda: self.reverse_sorting(),
333 ord('a'):
334 toggle_accumulated,
335 ord('A'):
336 toggle_accumulated,
337 ord('o'):
338 toggle_only_io,
339 ord('O'):
340 toggle_only_io,
341 ord('p'):
342 toggle_processes,
343 ord('P'):
344 toggle_processes,
345 ord('i'):
346 ionice,
347 ord('I'):
348 ionice,
349 curses.KEY_LEFT:
350 lambda: self.adjust_sorting_key(-1),
351 curses.KEY_RIGHT:
352 lambda: self.adjust_sorting_key(1),
353 curses.KEY_HOME:
354 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
355 curses.KEY_END:
356 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
359 action = key_bindings.get(key, lambda: None)
360 action()
362 def get_data(self):
363 def format(p):
364 stats = format_stats(self.options, p, self.process_list.duration)
365 io_delay, swapin_delay, read_bytes, write_bytes = stats
366 if Stats.has_blkio_delay_total:
367 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
368 else:
369 delay_stats = ' ?unavailable? '
370 line = '%5d %4s %-8s %11s %11s %s' % (
371 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
372 write_bytes, delay_stats)
373 cmdline = p.get_cmdline()
374 if not self.options.batch:
375 remaining_length = self.width - len(line)
376 if 2 < remaining_length < len(cmdline):
377 len1 = (remaining_length - 1) // 2
378 offset2 = -(remaining_length - len1 - 1)
379 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
380 line += cmdline
381 if not self.options.batch:
382 line = line[:self.width]
383 return line
385 def should_format(p):
386 return not self.options.only or \
387 p.did_some_io(self.options.accumulated)
389 processes = filter(should_format, self.process_list.processes.values())
390 key = IOTopUI.sorting_keys[self.sorting_key][0]
391 if self.options.accumulated:
392 stats_lambda = lambda p: p.stats_accum
393 else:
394 stats_lambda = lambda p: p.stats_delta
395 processes.sort(key=lambda p: key(p, stats_lambda(p)),
396 reverse=self.sorting_reverse)
397 if not self.options.batch:
398 del processes[self.height - 2:]
399 return map(format, processes)
401 def refresh_display(self, first_time, total_read, total_write, duration):
402 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
403 format_bandwidth(self.options, total_read, duration),
404 format_bandwidth(self.options, total_write, duration))
405 if self.options.processes:
406 pid = ' PID'
407 else:
408 pid = ' TID'
409 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
410 ' SWAPIN', ' IO', ' COMMAND']
411 lines = self.get_data()
412 if self.options.time:
413 titles = [' TIME'] + titles
414 current_time = time.strftime('%H:%M:%S ')
415 lines = [current_time + l for l in lines]
416 if self.options.batch:
417 if self.options.quiet <= 2:
418 print summary
419 if self.options.quiet <= int(first_time):
420 print ''.join(titles)
421 for l in lines:
422 print l.encode('utf-8')
423 sys.stdout.flush()
424 else:
425 self.win.erase()
426 self.win.addstr(summary[:self.width])
427 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
428 remaining_cols = self.width
429 for i in xrange(len(titles)):
430 attr = curses.A_REVERSE
431 title = titles[i]
432 if i == self.sorting_key:
433 title = title[1:]
434 if i == self.sorting_key:
435 attr |= curses.A_BOLD
436 title += self.sorting_reverse and '>' or '<'
437 title = title[:remaining_cols]
438 remaining_cols -= len(title)
439 self.win.addstr(title, attr)
440 if Stats.has_blkio_delay_total:
441 status_msg = None
442 else:
443 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
444 'cannot determine SWAPIN and IO %')
445 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
446 for i in xrange(num_lines):
447 try:
448 self.win.addstr(i + 2, 0, lines[i].encode('utf-8'))
449 except curses.error:
450 pass
451 if status_msg:
452 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
453 self.win.refresh()
455 def run_iotop_window(win, options):
456 taskstats_connection = TaskStatsNetlink(options)
457 process_list = ProcessList(taskstats_connection, options)
458 ui = IOTopUI(win, process_list, options)
459 ui.run()
461 def run_iotop(options):
462 if options.batch:
463 return run_iotop_window(None, options)
464 else:
465 return curses.wrapper(run_iotop_window, options)
468 # Profiling
471 def _profile(continuation):
472 prof_file = 'iotop.prof'
473 try:
474 import cProfile
475 import pstats
476 print 'Profiling using cProfile'
477 cProfile.runctx('continuation()', globals(), locals(), prof_file)
478 stats = pstats.Stats(prof_file)
479 except ImportError:
480 import hotshot
481 import hotshot.stats
482 prof = hotshot.Profile(prof_file, lineevents=1)
483 print 'Profiling using hotshot'
484 prof.runcall(continuation)
485 prof.close()
486 stats = hotshot.stats.load(prof_file)
487 stats.strip_dirs()
488 stats.sort_stats('time', 'calls')
489 stats.print_stats(50)
490 stats.print_callees(50)
491 os.remove(prof_file)
494 # Main program
497 USAGE = '''%s [OPTIONS]
499 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
500 period. SWAPIN and IO are the percentages of time the thread spent respectively
501 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
502 which the thread is running (set using the ionice command).
504 Controls: left and right arrows to change the sorting column, r to invert the
505 sorting order, o to toggle the --only option, p to toggle the --processes
506 option, a to toggle the --accumulated option, q to quit, any other key to force
507 a refresh.''' % sys.argv[0]
509 def main():
510 try:
511 locale.setlocale(locale.LC_ALL, '')
512 except locale.Error:
513 print 'unable to set locale, falling back to the default locale'
514 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
515 parser.add_option('-o', '--only', action='store_true',
516 dest='only', default=False,
517 help='only show processes or threads actually doing I/O')
518 parser.add_option('-b', '--batch', action='store_true', dest='batch',
519 help='non-interactive mode')
520 parser.add_option('-n', '--iter', type='int', dest='iterations',
521 metavar='NUM',
522 help='number of iterations before ending [infinite]')
523 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
524 help='delay between iterations [1 second]',
525 metavar='SEC', default=1)
526 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
527 help='processes/threads to monitor [all]', metavar='PID')
528 parser.add_option('-u', '--user', type='str', dest='users', action='append',
529 help='users to monitor [all]', metavar='USER')
530 parser.add_option('-P', '--processes', action='store_true',
531 dest='processes', default=False,
532 help='only show processes, not all threads')
533 parser.add_option('-a', '--accumulated', action='store_true',
534 dest='accumulated', default=False,
535 help='show accumulated I/O instead of bandwidth')
536 parser.add_option('-k', '--kilobytes', action='store_true',
537 dest='kilobytes', default=False,
538 help='use kilobytes instead of a human friendly unit')
539 parser.add_option('-t', '--time', action='store_true', dest='time',
540 help='add a timestamp on each line (implies --batch)')
541 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
542 help='suppress some lines of header (implies --batch)')
543 parser.add_option('--profile', action='store_true', dest='profile',
544 default=False, help=optparse.SUPPRESS_HELP)
546 options, args = parser.parse_args()
547 if args:
548 parser.error('Unexpected arguments: ' + ' '.join(args))
549 find_uids(options)
550 options.pids = options.pids or []
551 options.batch = options.batch or options.time or options.quiet
553 main_loop = lambda: run_iotop(options)
555 if options.profile:
556 def safe_main_loop():
557 try:
558 main_loop()
559 except:
560 pass
561 _profile(safe_main_loop)
562 else:
563 main_loop()