- Manage a two level tree of processes:
[iotop.git] / iotop / ui.py
blob94b4fb0330803502e539642b9af39c859998884b
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
11 from iotop.data import find_uids, TaskStatsNetlink, ProcessList
12 from iotop.version import VERSION
15 # Utility functions for the UI
18 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
20 def human_bandwidth(size, duration):
21 bw = size and float(size) / duration
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 res = '%.2f %s/s' % ((float(bw) / base), UNITS[i])
30 return res
32 def human_stats(stats, duration):
33 # Keep in sync with TaskStatsNetlink.members_offsets and
34 # IOTopUI.get_data(self)
35 def delay2percent(delay): # delay in ns, duration in s
36 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
37 io_delay = delay2percent(stats.blkio_delay_total)
38 swapin_delay = delay2percent(stats.swapin_delay_total)
39 read_bytes = human_bandwidth(stats.read_bytes, duration)
40 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
41 written_bytes = max(0, written_bytes)
42 write_bytes = human_bandwidth(written_bytes, duration)
43 return io_delay, swapin_delay, read_bytes, write_bytes
46 # The UI
49 class IOTopUI(object):
50 # key, reverse
51 sorting_keys = [
52 (lambda p: p.pid, False),
53 (lambda p: p.ioprio_sort_key(), False),
54 (lambda p: p.get_user(), False),
55 (lambda p: p.stats_delta.read_bytes, True),
56 (lambda p: p.stats_delta.write_bytes -
57 p.stats_delta.cancelled_write_bytes, True),
58 (lambda p: p.stats_delta.swapin_delay_total, True),
59 # The default sorting (by I/O % time) should show processes doing
60 # only writes, without waiting on them
61 (lambda p: p.stats_delta.blkio_delay_total or
62 int(not(not(p.stats_delta.read_bytes or
63 p.stats_delta.write_bytes))), True),
64 (lambda p: p.get_cmdline(), False),
67 def __init__(self, win, process_list, options):
68 self.process_list = process_list
69 self.options = options
70 self.sorting_key = 6
71 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
72 if not self.options.batch:
73 self.win = win
74 self.resize()
75 try:
76 curses.use_default_colors()
77 curses.start_color()
78 curses.curs_set(0)
79 except curses.error:
80 # This call can fail with misconfigured terminals, for example
81 # TERM=xterm-color. This is harmless
82 pass
84 def resize(self):
85 self.height, self.width = self.win.getmaxyx()
87 def run(self):
88 iterations = 0
89 poll = select.poll()
90 if not self.options.batch:
91 poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
92 while self.options.iterations is None or \
93 iterations < self.options.iterations:
94 total = self.process_list.refresh_processes()
95 total_read, total_write = total
96 self.refresh_display(total_read, total_write,
97 self.process_list.duration)
98 if self.options.iterations is not None:
99 iterations += 1
100 if iterations >= self.options.iterations:
101 break
103 try:
104 events = poll.poll(self.options.delay_seconds * 1000.0)
105 except select.error, e:
106 if e.args and e.args[0] == errno.EINTR:
107 events = 0
108 else:
109 raise
110 if not self.options.batch:
111 self.resize()
112 if events:
113 key = self.win.getch()
114 self.handle_key(key)
116 def reverse_sorting(self):
117 self.sorting_reverse = not self.sorting_reverse
119 def adjust_sorting_key(self, delta):
120 orig_sorting_key = self.sorting_key
121 self.sorting_key += delta
122 self.sorting_key = max(0, self.sorting_key)
123 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
124 if orig_sorting_key != self.sorting_key:
125 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
127 def handle_key(self, key):
128 def toggle_only_io():
129 self.options.only ^= True
130 def toggle_processes():
131 self.options.processes ^= True
132 self.process_list.refresh_processes()
133 key_bindings = {
134 ord('q'):
135 lambda: sys.exit(0),
136 ord('Q'):
137 lambda: sys.exit(0),
138 ord('r'):
139 lambda: self.reverse_sorting(),
140 ord('R'):
141 lambda: self.reverse_sorting(),
142 ord('o'):
143 toggle_only_io,
144 ord('O'):
145 toggle_only_io,
146 ord('p'):
147 toggle_processes,
148 ord('P'):
149 toggle_processes,
150 curses.KEY_LEFT:
151 lambda: self.adjust_sorting_key(-1),
152 curses.KEY_RIGHT:
153 lambda: self.adjust_sorting_key(1),
154 curses.KEY_HOME:
155 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
156 curses.KEY_END:
157 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
160 action = key_bindings.get(key, lambda: None)
161 action()
163 def get_data(self):
164 def format(p):
165 stats = human_stats(p.stats_delta, self.process_list.duration)
166 io_delay, swapin_delay, read_bytes, write_bytes = stats
167 line = '%5d %4s %-8s %11s %11s %7s %7s ' % (
168 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
169 write_bytes, swapin_delay, io_delay)
170 line += p.get_cmdline()
171 if not self.options.batch:
172 line = line[:self.width - 1]
173 return line
175 def should_format(p):
176 return not self.options.only or p.did_some_io()
178 processes = filter(should_format, self.process_list.processes.values())
179 key = IOTopUI.sorting_keys[self.sorting_key][0]
180 processes.sort(key=key, reverse=self.sorting_reverse)
181 if not self.options.batch:
182 del processes[self.height - 2:]
183 return map(format, processes)
185 def refresh_display(self, total_read, total_write, duration):
186 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
187 human_bandwidth(total_read, duration),
188 human_bandwidth(total_write, duration))
189 if self.options.processes:
190 pid = ' PID'
191 else:
192 pid = ' TID'
193 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
194 ' SWAPIN', ' IO', ' COMMAND']
195 lines = self.get_data()
196 if self.options.batch:
197 print summary
198 print ''.join(titles)
199 for l in lines:
200 print l
201 sys.stdout.flush()
202 else:
203 self.win.erase()
204 self.win.addstr(summary)
205 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
206 for i in xrange(len(titles)):
207 attr = curses.A_REVERSE
208 title = titles[i]
209 if i == self.sorting_key:
210 attr |= curses.A_BOLD
211 title += self.sorting_reverse and '>' or '<'
212 self.win.addstr(title, attr)
213 for i in xrange(len(lines)):
214 try:
215 self.win.addstr(i + 2, 0, lines[i].encode('utf-8'))
216 except curses.error:
217 exc_type, value, traceback = sys.exc_info()
218 value = '%s win:%s i:%d line:%s' % \
219 (value, self.win.getmaxyx(), i, lines[i])
220 raise exc_type, value.encode('string_escape'), traceback
221 self.win.refresh()
223 def run_iotop_window(win, options):
224 taskstats_connection = TaskStatsNetlink(options)
225 process_list = ProcessList(taskstats_connection, options)
226 ui = IOTopUI(win, process_list, options)
227 ui.run()
229 def run_iotop(options):
230 if options.batch:
231 return run_iotop_window(None, options)
232 else:
233 return curses.wrapper(run_iotop_window, options)
236 # Profiling
239 def _profile(continuation):
240 prof_file = 'iotop.prof'
241 try:
242 import cProfile
243 import pstats
244 print 'Profiling using cProfile'
245 cProfile.runctx('continuation()', globals(), locals(), prof_file)
246 stats = pstats.Stats(prof_file)
247 except ImportError:
248 import hotshot
249 import hotshot.stats
250 prof = hotshot.Profile(prof_file, lineevents=1)
251 print 'Profiling using hotshot'
252 prof.runcall(continuation)
253 prof.close()
254 stats = hotshot.stats.load(prof_file)
255 stats.strip_dirs()
256 stats.sort_stats('time', 'calls')
257 stats.print_stats(50)
258 stats.print_callees(50)
259 os.remove(prof_file)
262 # Main program
265 USAGE = '''%s [OPTIONS]
267 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
268 period. SWAPIN and IO are the percentages of time the thread spent respectively
269 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
270 which the thread is running (set using the ionice command).
271 Controls: left and right arrows to change the sorting column, r to invert the
272 sorting order, o to toggle the --only option, p to toggle the --processes
273 option, q to quit, any other key to force a refresh.''' % sys.argv[0]
275 def main():
276 locale.setlocale(locale.LC_ALL, '')
277 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
278 parser.add_option('-o', '--only', action='store_true',
279 dest='only', default=False,
280 help='only show processes or threads actually doing I/O')
281 parser.add_option('-b', '--batch', action='store_true', dest='batch',
282 help='non-interactive mode')
283 parser.add_option('-n', '--iter', type='int', dest='iterations',
284 metavar='NUM',
285 help='number of iterations before ending [infinite]')
286 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
287 help='delay between iterations [1 second]',
288 metavar='SEC', default=1)
289 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
290 help='processes/threads to monitor [all]', metavar='PID')
291 parser.add_option('-u', '--user', type='str', dest='users', action='append',
292 help='users to monitor [all]', metavar='USER')
293 parser.add_option('-P', '--processes', action='store_true',
294 dest='processes', default=False,
295 help='only show processes, not all threads')
296 parser.add_option('--profile', action='store_true', dest='profile',
297 default=False, help=optparse.SUPPRESS_HELP)
299 options, args = parser.parse_args()
300 if args:
301 parser.error('Unexpected arguments: ' + ' '.join(args))
302 find_uids(options)
303 options.pids = options.pids or []
305 main_loop = lambda: run_iotop(options)
307 if options.profile:
308 def safe_main_loop():
309 try:
310 main_loop()
311 except:
312 pass
313 _profile(safe_main_loop)
314 else:
315 main_loop()