80 columns
[iotop.git] / iotop / ui.py
blobad22a05036dc5f10c0ad98ace085a277d9ccf011
1 import curses
2 import errno
3 import locale
4 import optparse
5 import os
6 import pwd
7 import select
8 import struct
9 import sys
10 import time
12 from iotop.data import find_uids, TaskStatsNetlink, ProcessList, Stats
13 from iotop.version import VERSION
16 # Utility functions for the UI
19 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
21 def human_size(size):
22 for i in xrange(len(UNITS) - 1, 0, -1):
23 base = 1 << (10 * i)
24 if 2 * base < size:
25 break
26 else:
27 i = 0
28 base = 1
29 return '%.2f %s' % ((float(size) / base), UNITS[i])
31 def format_size(options, bytes):
32 if options.kilobytes:
33 return '%.2f K' % (bytes / 1024.0)
34 return human_size(bytes)
36 def format_bandwidth(options, size, duration):
37 return format_size(options, size and float(size) / duration) + '/s'
39 def format_stats(options, process, duration):
40 # Keep in sync with TaskStatsNetlink.members_offsets and
41 # IOTopUI.get_data(self)
42 def delay2percent(delay): # delay in ns, duration in s
43 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
44 if options.accumulated:
45 stats = process.stats_accum
46 display_format = lambda size, duration: format_size(options, size)
47 duration = time.time() - process.stats_accum_timestamp
48 else:
49 stats = process.stats_delta
50 display_format = lambda size, duration: format_bandwidth(
51 options, size, duration)
52 io_delay = delay2percent(stats.blkio_delay_total)
53 swapin_delay = delay2percent(stats.swapin_delay_total)
54 read_bytes = display_format(stats.read_bytes, duration)
55 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
56 written_bytes = max(0, written_bytes)
57 write_bytes = display_format(written_bytes, duration)
58 return io_delay, swapin_delay, read_bytes, write_bytes
61 # The UI
64 class IOTopUI(object):
65 # key, reverse
66 sorting_keys = [
67 (lambda p, s: p.pid, False),
68 (lambda p, s: p.ioprio_sort_key(), False),
69 (lambda p, s: p.get_user(), False),
70 (lambda p, s: s.read_bytes, True),
71 (lambda p, s: s.write_bytes - s.cancelled_write_bytes, True),
72 (lambda p, s: s.swapin_delay_total, True),
73 # The default sorting (by I/O % time) should show processes doing
74 # only writes, without waiting on them
75 (lambda p, s: s.blkio_delay_total or
76 int(not(not(s.read_bytes or s.write_bytes))), True),
77 (lambda p, s: p.get_cmdline(), False),
80 def __init__(self, win, process_list, options):
81 self.process_list = process_list
82 self.options = options
83 self.sorting_key = 6
84 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
85 if not self.options.batch:
86 self.win = win
87 self.resize()
88 try:
89 curses.use_default_colors()
90 curses.start_color()
91 curses.curs_set(0)
92 except curses.error:
93 # This call can fail with misconfigured terminals, for example
94 # TERM=xterm-color. This is harmless
95 pass
97 def resize(self):
98 self.height, self.width = self.win.getmaxyx()
100 def run(self):
101 iterations = 0
102 poll = select.poll()
103 if not self.options.batch:
104 poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
105 while self.options.iterations is None or \
106 iterations < self.options.iterations:
107 total = self.process_list.refresh_processes()
108 total_read, total_write = total
109 self.refresh_display(iterations == 0, total_read, total_write,
110 self.process_list.duration)
111 if self.options.iterations is not None:
112 iterations += 1
113 if iterations >= self.options.iterations:
114 break
115 elif iterations == 0:
116 iterations = 1
118 try:
119 events = poll.poll(self.options.delay_seconds * 1000.0)
120 except select.error, e:
121 if e.args and e.args[0] == errno.EINTR:
122 events = 0
123 else:
124 raise
125 if not self.options.batch:
126 self.resize()
127 if events:
128 key = self.win.getch()
129 self.handle_key(key)
131 def reverse_sorting(self):
132 self.sorting_reverse = not self.sorting_reverse
134 def adjust_sorting_key(self, delta):
135 orig_sorting_key = self.sorting_key
136 self.sorting_key += delta
137 self.sorting_key = max(0, self.sorting_key)
138 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
139 if orig_sorting_key != self.sorting_key:
140 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
142 def handle_key(self, key):
143 def toggle_accumulated():
144 self.options.accumulated ^= True
145 self.process_list.clear()
146 def toggle_only_io():
147 self.options.only ^= True
148 def toggle_processes():
149 self.options.processes ^= True
150 self.process_list.clear()
151 self.process_list.refresh_processes()
152 key_bindings = {
153 ord('q'):
154 lambda: sys.exit(0),
155 ord('Q'):
156 lambda: sys.exit(0),
157 ord('r'):
158 lambda: self.reverse_sorting(),
159 ord('R'):
160 lambda: self.reverse_sorting(),
161 ord('a'):
162 toggle_accumulated,
163 ord('A'):
164 toggle_accumulated,
165 ord('o'):
166 toggle_only_io,
167 ord('O'):
168 toggle_only_io,
169 ord('p'):
170 toggle_processes,
171 ord('P'):
172 toggle_processes,
173 curses.KEY_LEFT:
174 lambda: self.adjust_sorting_key(-1),
175 curses.KEY_RIGHT:
176 lambda: self.adjust_sorting_key(1),
177 curses.KEY_HOME:
178 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
179 curses.KEY_END:
180 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
183 action = key_bindings.get(key, lambda: None)
184 action()
186 def get_data(self):
187 def format(p):
188 stats = format_stats(self.options, p, self.process_list.duration)
189 io_delay, swapin_delay, read_bytes, write_bytes = stats
190 line = '%5d %4s %-8s %11s %11s %7s %7s ' % (
191 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
192 write_bytes, swapin_delay, io_delay)
193 cmdline = p.get_cmdline()
194 if not self.options.batch:
195 remaining_length = self.width - len(line)
196 if 2 < remaining_length < len(cmdline):
197 len1 = (remaining_length - 1) // 2
198 offset2 = -(remaining_length - len1 - 1)
199 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
200 line += cmdline
201 if not self.options.batch:
202 line = line[:self.width]
203 return line
205 def should_format(p):
206 return not self.options.only or \
207 p.did_some_io(self.options.accumulated)
209 processes = filter(should_format, self.process_list.processes.values())
210 key = IOTopUI.sorting_keys[self.sorting_key][0]
211 if self.options.accumulated:
212 stats_lambda = lambda p: p.stats_accum
213 else:
214 stats_lambda = lambda p: p.stats_delta
215 processes.sort(key=lambda p: key(p, stats_lambda(p)),
216 reverse=self.sorting_reverse)
217 if not self.options.batch:
218 del processes[self.height - 2:]
219 return map(format, processes)
221 def refresh_display(self, first_time, total_read, total_write, duration):
222 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
223 format_bandwidth(self.options, total_read, duration),
224 format_bandwidth(self.options, total_write, duration))
225 if self.options.processes:
226 pid = ' PID'
227 else:
228 pid = ' TID'
229 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
230 ' SWAPIN', ' IO', ' COMMAND']
231 lines = self.get_data()
232 if self.options.time:
233 titles = [' TIME'] + titles
234 current_time = time.strftime('%H:%M:%S ')
235 lines = [current_time + l for l in lines]
236 if self.options.batch:
237 if self.options.quiet <= 2:
238 print summary
239 if self.options.quiet <= int(first_time):
240 print ''.join(titles)
241 for l in lines:
242 print l
243 sys.stdout.flush()
244 else:
245 self.win.erase()
246 self.win.addstr(summary[:self.width])
247 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
248 remaining_cols = self.width
249 for i in xrange(len(titles)):
250 attr = curses.A_REVERSE
251 title = titles[i]
252 if i == self.sorting_key:
253 title = title[1:]
254 if i == self.sorting_key:
255 attr |= curses.A_BOLD
256 title += self.sorting_reverse and '>' or '<'
257 title = title[:remaining_cols]
258 remaining_cols -= len(title)
259 self.win.addstr(title, attr)
260 if Stats.has_blkio_delay_total:
261 status_msg = None
262 else:
263 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
264 'cannot determine IO %')
265 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
266 for i in xrange(num_lines):
267 try:
268 self.win.insstr(i + 2, 0, lines[i].encode('utf-8'))
269 except curses.error:
270 exc_type, value, traceback = sys.exc_info()
271 value = '%s win:%s i:%d line:%s' % \
272 (value, self.win.getmaxyx(), i, lines[i])
273 value = str(value).encode('string_escape')
274 raise exc_type, value, traceback
275 if status_msg:
276 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
277 self.win.refresh()
279 def run_iotop_window(win, options):
280 taskstats_connection = TaskStatsNetlink(options)
281 process_list = ProcessList(taskstats_connection, options)
282 ui = IOTopUI(win, process_list, options)
283 ui.run()
285 def run_iotop(options):
286 if options.batch:
287 return run_iotop_window(None, options)
288 else:
289 return curses.wrapper(run_iotop_window, options)
292 # Profiling
295 def _profile(continuation):
296 prof_file = 'iotop.prof'
297 try:
298 import cProfile
299 import pstats
300 print 'Profiling using cProfile'
301 cProfile.runctx('continuation()', globals(), locals(), prof_file)
302 stats = pstats.Stats(prof_file)
303 except ImportError:
304 import hotshot
305 import hotshot.stats
306 prof = hotshot.Profile(prof_file, lineevents=1)
307 print 'Profiling using hotshot'
308 prof.runcall(continuation)
309 prof.close()
310 stats = hotshot.stats.load(prof_file)
311 stats.strip_dirs()
312 stats.sort_stats('time', 'calls')
313 stats.print_stats(50)
314 stats.print_callees(50)
315 os.remove(prof_file)
318 # Main program
321 USAGE = '''%s [OPTIONS]
323 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
324 period. SWAPIN and IO are the percentages of time the thread spent respectively
325 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
326 which the thread is running (set using the ionice command).
328 Controls: left and right arrows to change the sorting column, r to invert the
329 sorting order, o to toggle the --only option, p to toggle the --processes
330 option, a to toggle the --accumulated option, q to quit, any other key to force
331 a refresh.''' % sys.argv[0]
333 def main():
334 locale.setlocale(locale.LC_ALL, '')
335 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
336 parser.add_option('-o', '--only', action='store_true',
337 dest='only', default=False,
338 help='only show processes or threads actually doing I/O')
339 parser.add_option('-b', '--batch', action='store_true', dest='batch',
340 help='non-interactive mode')
341 parser.add_option('-n', '--iter', type='int', dest='iterations',
342 metavar='NUM',
343 help='number of iterations before ending [infinite]')
344 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
345 help='delay between iterations [1 second]',
346 metavar='SEC', default=1)
347 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
348 help='processes/threads to monitor [all]', metavar='PID')
349 parser.add_option('-u', '--user', type='str', dest='users', action='append',
350 help='users to monitor [all]', metavar='USER')
351 parser.add_option('-P', '--processes', action='store_true',
352 dest='processes', default=False,
353 help='only show processes, not all threads')
354 parser.add_option('-a', '--accumulated', action='store_true',
355 dest='accumulated', default=False,
356 help='show accumulated I/O instead of bandwidth')
357 parser.add_option('-k', '--kilobytes', action='store_true',
358 dest='kilobytes', default=False,
359 help='use kilobytes instead of a human friendly unit')
360 parser.add_option('-t', '--time', action='store_true', dest='time',
361 help='add a timestamp on each line (implies --batch)')
362 parser.add_option('-q', '--quiet', action='count', dest='quiet',
363 help='suppress some lines of header (implies --batch)')
364 parser.add_option('--profile', action='store_true', dest='profile',
365 default=False, help=optparse.SUPPRESS_HELP)
367 options, args = parser.parse_args()
368 if args:
369 parser.error('Unexpected arguments: ' + ' '.join(args))
370 find_uids(options)
371 options.pids = options.pids or []
372 options.batch = options.batch or options.time or options.quiet
374 main_loop = lambda: run_iotop(options)
376 if options.profile:
377 def safe_main_loop():
378 try:
379 main_loop()
380 except:
381 pass
382 _profile(safe_main_loop)
383 else:
384 main_loop()