13 from iotop
.data
import find_uids
, TaskStatsNetlink
, ProcessList
, Stats
14 from iotop
.data
import ThreadInfo
15 from iotop
.version
import VERSION
17 from ioprio
import IoprioSetError
20 # Utility functions for the UI
23 UNITS
= ['B', 'K', 'M', 'G', 'T', 'P', 'E']
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
):
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
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
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
80 class IOTopUI(object):
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
100 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
101 if not self
.options
.batch
:
105 curses
.use_default_colors()
109 # This call can fail with misconfigured terminals, for example
110 # TERM=xterm-color. This is harmless
114 self
.height
, self
.width
= self
.win
.getmaxyx()
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:
129 if iterations
>= self
.options
.iterations
:
131 elif iterations
== 0:
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
:
141 if not self
.options
.batch
:
144 key
= self
.win
.getch()
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
)
166 inp
= self
.win
.getstr(1, len(prompt
))
169 if inp
not in (None, ''):
175 def prompt_int(self
, prompt
, default
= None, empty_is_cancel
= True):
176 inp
= self
.prompt_str(prompt
, default
, empty_is_cancel
)
182 def prompt_pid(self
):
184 return self
.prompt_int('PID to ionice: ')
190 def prompt_tid(self
):
192 return self
.prompt_int('TID to ionice: ')
198 def prompt_data(self
, ioprio_data
):
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)
204 inp
= self
.prompt_int('I/O priority data (0-7): ', None, False)
206 raise InvalidIoprioData()
207 if inp
< 0 or inp
> 7:
208 raise InvalidIoprioData()
211 def prompt_set(self
, prompt
, display_list
, ret_list
, selected
):
213 selected
= ret_list
.index(selected
)
216 set_len
= len(display_list
) - 1
218 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
219 self
.win
.insstr(1, 0, prompt
, curses
.A_BOLD
)
221 for i
, item
in enumerate(display_list
):
222 display
= ' %s ' % item
224 attr
= curses
.A_REVERSE
226 attr
= curses
.A_NORMAL
227 self
.win
.insstr(1, offset
, display
, attr
)
228 offset
+= len(display
)
230 key
= self
.win
.getch()
231 if key
in (curses
.KEY_LEFT
, ord('l')) and selected
> 0:
234 elif key
in (curses
.KEY_RIGHT
, ord('r')) and selected
< set_len
:
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')):
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:
249 inp
= self
.prompt_set(prompt
, classes_prompt
, classes_ret
, ioprio_class
)
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
)
258 def prompt_clear(self
):
259 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
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()
274 if self
.options
.processes
:
275 pid
= self
.prompt_pid()
276 exec_unit
= self
.process_list
.get_process(pid
)
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':
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
)
295 self
.prompt_error('Invalid process id!')
297 self
.prompt_error('Invalid thread id!')
298 except InvalidIoprioData
:
299 self
.prompt_error('Invalid I/O priority data!')
301 self
.prompt_error('Invalid integer!')
313 lambda: self
.reverse_sorting(),
315 lambda: self
.reverse_sorting(),
333 lambda: self
.adjust_sorting_key(-1),
335 lambda: self
.adjust_sorting_key(1),
337 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
339 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
342 action
= key_bindings
.get(key
, lambda: None)
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
)
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
:]
364 if not self
.options
.batch
:
365 line
= line
[:self
.width
]
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
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
:
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:
402 if self
.options
.quiet
<= int(first_time
):
403 print ''.join(titles
)
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
415 if i
== self
.sorting_key
:
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
:
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
):
431 self
.win
.insstr(i
+ 2, 0, lines
[i
].encode('utf-8'))
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
439 self
.win
.insstr(self
.height
- 1, 0, status_msg
, curses
.A_BOLD
)
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
)
448 def run_iotop(options
):
450 return run_iotop_window(None, options
)
452 return curses
.wrapper(run_iotop_window
, options
)
458 def _profile(continuation
):
459 prof_file
= 'iotop.prof'
463 print 'Profiling using cProfile'
464 cProfile
.runctx('continuation()', globals(), locals(), prof_file
)
465 stats
= pstats
.Stats(prof_file
)
469 prof
= hotshot
.Profile(prof_file
, lineevents
=1)
470 print 'Profiling using hotshot'
471 prof
.runcall(continuation
)
473 stats
= hotshot
.stats
.load(prof_file
)
475 stats
.sort_stats('time', 'calls')
476 stats
.print_stats(50)
477 stats
.print_callees(50)
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]
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',
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()
532 parser
.error('Unexpected arguments: ' + ' '.join(args
))
534 options
.pids
= options
.pids
or []
535 options
.batch
= options
.batch
or options
.time
or options
.quiet
537 main_loop
= lambda: run_iotop(options
)
540 def safe_main_loop():
545 _profile(safe_main_loop
)