Added the -k, --kilobytes option
[iotop.git] / iotop / ui.py
blob9744d505c8da363929b5180eec1f8a60e7f936f9
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
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: p.pid, False),
68 (lambda p: p.ioprio_sort_key(), False),
69 (lambda p: p.get_user(), False),
70 (lambda p: p.stats_delta.read_bytes, True),
71 (lambda p: p.stats_delta.write_bytes -
72 p.stats_delta.cancelled_write_bytes, True),
73 (lambda p: p.stats_delta.swapin_delay_total, True),
74 # The default sorting (by I/O % time) should show processes doing
75 # only writes, without waiting on them
76 (lambda p: p.stats_delta.blkio_delay_total or
77 int(not(not(p.stats_delta.read_bytes or
78 p.stats_delta.write_bytes))), True),
79 (lambda p: p.get_cmdline(), False),
82 def __init__(self, win, process_list, options):
83 self.process_list = process_list
84 self.options = options
85 self.sorting_key = 6
86 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
87 if not self.options.batch:
88 self.win = win
89 self.resize()
90 try:
91 curses.use_default_colors()
92 curses.start_color()
93 curses.curs_set(0)
94 except curses.error:
95 # This call can fail with misconfigured terminals, for example
96 # TERM=xterm-color. This is harmless
97 pass
99 def resize(self):
100 self.height, self.width = self.win.getmaxyx()
102 def run(self):
103 iterations = 0
104 poll = select.poll()
105 if not self.options.batch:
106 poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
107 while self.options.iterations is None or \
108 iterations < self.options.iterations:
109 total = self.process_list.refresh_processes()
110 total_read, total_write = total
111 self.refresh_display(total_read, total_write,
112 self.process_list.duration)
113 if self.options.iterations is not None:
114 iterations += 1
115 if iterations >= self.options.iterations:
116 break
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 line += p.get_cmdline()
194 if not self.options.batch:
195 line = line[:self.width - 1]
196 return line
198 def should_format(p):
199 return not self.options.only or p.did_some_io()
201 processes = filter(should_format, self.process_list.processes.values())
202 key = IOTopUI.sorting_keys[self.sorting_key][0]
203 processes.sort(key=key, reverse=self.sorting_reverse)
204 if not self.options.batch:
205 del processes[self.height - 2:]
206 return map(format, processes)
208 def refresh_display(self, total_read, total_write, duration):
209 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
210 format_bandwidth(self.options, total_read, duration),
211 format_bandwidth(self.options, total_write, duration))
212 if self.options.processes:
213 pid = ' PID'
214 else:
215 pid = ' TID'
216 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
217 ' SWAPIN', ' IO', ' COMMAND']
218 lines = self.get_data()
219 if self.options.batch:
220 print summary
221 print ''.join(titles)
222 for l in lines:
223 print l
224 sys.stdout.flush()
225 else:
226 self.win.erase()
227 self.win.addstr(summary)
228 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
229 for i in xrange(len(titles)):
230 attr = curses.A_REVERSE
231 title = titles[i]
232 if i == self.sorting_key:
233 title = title[1:]
234 if i == self.sorting_key:
235 attr |= curses.A_BOLD
236 title += self.sorting_reverse and '>' or '<'
237 self.win.addstr(title, attr)
238 for i in xrange(len(lines)):
239 try:
240 self.win.addstr(i + 2, 0, lines[i].encode('utf-8'))
241 except curses.error:
242 exc_type, value, traceback = sys.exc_info()
243 value = '%s win:%s i:%d line:%s' % \
244 (value, self.win.getmaxyx(), i, lines[i])
245 value = str(value).encode('string_escape')
246 raise exc_type, value, traceback
247 self.win.refresh()
249 def run_iotop_window(win, options):
250 taskstats_connection = TaskStatsNetlink(options)
251 process_list = ProcessList(taskstats_connection, options)
252 ui = IOTopUI(win, process_list, options)
253 ui.run()
255 def run_iotop(options):
256 if options.batch:
257 return run_iotop_window(None, options)
258 else:
259 return curses.wrapper(run_iotop_window, options)
262 # Profiling
265 def _profile(continuation):
266 prof_file = 'iotop.prof'
267 try:
268 import cProfile
269 import pstats
270 print 'Profiling using cProfile'
271 cProfile.runctx('continuation()', globals(), locals(), prof_file)
272 stats = pstats.Stats(prof_file)
273 except ImportError:
274 import hotshot
275 import hotshot.stats
276 prof = hotshot.Profile(prof_file, lineevents=1)
277 print 'Profiling using hotshot'
278 prof.runcall(continuation)
279 prof.close()
280 stats = hotshot.stats.load(prof_file)
281 stats.strip_dirs()
282 stats.sort_stats('time', 'calls')
283 stats.print_stats(50)
284 stats.print_callees(50)
285 os.remove(prof_file)
288 # Main program
291 USAGE = '''%s [OPTIONS]
293 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
294 period. SWAPIN and IO are the percentages of time the thread spent respectively
295 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
296 which the thread is running (set using the ionice command).
298 Controls: left and right arrows to change the sorting column, r to invert the
299 sorting order, o to toggle the --only option, p to toggle the --processes
300 option, a to toggle the --accumulated option, q to quit, any other key to force a refresh.''' % sys.argv[0]
302 def main():
303 locale.setlocale(locale.LC_ALL, '')
304 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
305 parser.add_option('-o', '--only', action='store_true',
306 dest='only', default=False,
307 help='only show processes or threads actually doing I/O')
308 parser.add_option('-b', '--batch', action='store_true', dest='batch',
309 help='non-interactive mode')
310 parser.add_option('-n', '--iter', type='int', dest='iterations',
311 metavar='NUM',
312 help='number of iterations before ending [infinite]')
313 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
314 help='delay between iterations [1 second]',
315 metavar='SEC', default=1)
316 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
317 help='processes/threads to monitor [all]', metavar='PID')
318 parser.add_option('-u', '--user', type='str', dest='users', action='append',
319 help='users to monitor [all]', metavar='USER')
320 parser.add_option('-P', '--processes', action='store_true',
321 dest='processes', default=False,
322 help='only show processes, not all threads')
323 parser.add_option('-a', '--accumulated', action='store_true',
324 dest='accumulated', default=False,
325 help='show accumulated I/O instead of bandwidth')
326 parser.add_option('-k', '--kilobytes', action='store_true',
327 dest='kilobytes', default=False,
328 help='use kilobytes instead of a human friendly unit')
329 parser.add_option('--profile', action='store_true', dest='profile',
330 default=False, help=optparse.SUPPRESS_HELP)
332 options, args = parser.parse_args()
333 if args:
334 parser.error('Unexpected arguments: ' + ' '.join(args))
335 find_uids(options)
336 options.pids = options.pids or []
338 main_loop = lambda: run_iotop(options)
340 if options.profile:
341 def safe_main_loop():
342 try:
343 main_loop()
344 except:
345 pass
346 _profile(safe_main_loop)
347 else:
348 main_loop()