Use monotonic time to calculate durations
[iotop.git] / iotop / ui.py
blobd4145592ec68b2b8f1688e90b987d64142063b58
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15 # See the COPYING file for license information.
17 # Copyright (c) 2007 Guillaume Chazarain <guichaz@gmail.com>
19 # Allow printing with same syntax in Python 2/3
20 from __future__ import print_function
22 import curses
23 import errno
24 import locale
25 import math
26 import optparse
27 import os
28 import select
29 import signal
30 import sys
31 import time
32 from collections import OrderedDict
34 from iotop.data import find_uids, TaskStatsNetlink, ProcessList, Stats
35 from iotop.data import ThreadInfo
36 from iotop.version import VERSION
37 from iotop import ioprio
38 from iotop.ioprio import IoprioSetError
41 # Utility functions for the UI
44 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
47 def human_size(size):
48 if size > 0:
49 sign = ''
50 elif size < 0:
51 sign = '-'
52 size = -size
53 else:
54 return '0.00 B'
56 expo = int(math.log(size / 2, 2) / 10)
57 return '%s%.2f %s' % (
58 sign, (float(size) / (1 << (10 * expo))), UNITS[expo])
61 def format_size(options, bytes):
62 if options.kilobytes:
63 return '%.2f K' % (bytes / 1024.0)
64 return human_size(bytes)
67 def format_bandwidth(options, size, duration):
68 return format_size(options, size and float(size) / duration) + '/s'
71 def format_stats(options, process, duration):
72 # Keep in sync with TaskStatsNetlink.members_offsets and
73 # IOTopUI.get_data(self)
74 def delay2percent(delay): # delay in ns, duration in s
75 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
76 if options.accumulated:
77 stats = process.stats_accum
78 display_format = lambda size, duration: format_size(options, size)
79 duration = time.monotonic() - process.stats_accum_timestamp
80 else:
81 stats = process.stats_delta
82 display_format = lambda size, duration: format_bandwidth(
83 options, size, duration)
84 io_delay = delay2percent(stats.blkio_delay_total)
85 swapin_delay = delay2percent(stats.swapin_delay_total)
86 read_bytes = display_format(stats.read_bytes, duration)
87 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
88 written_bytes = max(0, written_bytes)
89 write_bytes = display_format(written_bytes, duration)
90 return io_delay, swapin_delay, read_bytes, write_bytes
93 def get_max_pid_width():
94 try:
95 return len(open('/proc/sys/kernel/pid_max').read().strip())
96 except Exception as e:
97 print(e)
98 # Reasonable default in case something fails
99 return 5
101 MAX_PID_WIDTH = get_max_pid_width()
104 # UI Exceptions
108 class CancelInput(Exception):
109 pass
112 class InvalidInt(Exception):
113 pass
116 class InvalidPid(Exception):
117 pass
120 class InvalidTid(Exception):
121 pass
124 class InvalidIoprioData(Exception):
125 pass
128 # The UI
132 class IOTopUI(object):
133 # key, reverse
134 sorting_keys = [
135 (lambda p, s: p.pid, False),
136 (lambda p, s: p.ioprio_sort_key(), False),
137 (lambda p, s: p.get_user(), False),
138 (lambda p, s: s.read_bytes, True),
139 (lambda p, s: s.write_bytes - s.cancelled_write_bytes, True),
140 (lambda p, s: s.swapin_delay_total, True),
141 # The default sorting (by I/O % time) should show processes doing
142 # only writes, without waiting on them
143 (lambda p, s: s.blkio_delay_total or
144 int(not(not(s.read_bytes or s.write_bytes))), True),
145 (lambda p, s: p.get_cmdline(), False),
148 def __init__(self, win, process_list, options):
149 self.process_list = process_list
150 self.options = options
151 self.sorting_key = 6
152 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
153 if not self.options.batch:
154 self.win = win
155 self.resize()
156 try:
157 curses.use_default_colors()
158 curses.start_color()
159 curses.curs_set(0)
160 except curses.error:
161 # This call can fail with misconfigured terminals, for example
162 # TERM=xterm-color. This is harmless
163 pass
165 def resize(self):
166 self.height, self.width = self.win.getmaxyx()
168 def run(self):
169 iterations = 0
170 poll = select.poll()
171 if not self.options.batch:
172 poll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI)
173 while self.options.iterations is None or \
174 iterations < self.options.iterations:
175 total, current = self.process_list.refresh_processes()
176 self.refresh_display(iterations == 0, total, current,
177 self.process_list.duration)
178 if self.options.iterations is not None:
179 iterations += 1
180 if iterations >= self.options.iterations:
181 break
182 elif iterations == 0:
183 iterations = 1
185 try:
186 events = poll.poll(self.options.delay_seconds * 1000.0)
187 except select.error as e:
188 if e.args and e.args[0] == errno.EINTR:
189 events = []
190 else:
191 raise
192 for (fd, event) in events:
193 if event & (select.POLLERR | select.POLLHUP):
194 sys.exit(1)
195 if not self.options.batch:
196 self.resize()
197 if events:
198 key = self.win.getch()
199 self.handle_key(key)
201 def reverse_sorting(self):
202 self.sorting_reverse = not self.sorting_reverse
204 def adjust_sorting_key(self, delta):
205 orig_sorting_key = self.sorting_key
206 self.sorting_key = self.get_sorting_key(delta)
207 if orig_sorting_key != self.sorting_key:
208 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
210 def get_sorting_key(self, delta):
211 new_sorting_key = self.sorting_key
212 new_sorting_key += delta
213 new_sorting_key = max(0, new_sorting_key)
214 new_sorting_key = min(len(IOTopUI.sorting_keys) - 1, new_sorting_key)
215 return new_sorting_key
217 # I wonder if switching to urwid for the display would be better here
219 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
220 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
221 self.win.addstr(1, 0, prompt, curses.A_BOLD)
222 self.win.refresh()
223 curses.echo()
224 curses.curs_set(1)
225 inp = self.win.getstr(1, len(prompt))
226 curses.curs_set(0)
227 curses.noecho()
228 if inp not in (None, ''):
229 return inp
230 if empty_is_cancel:
231 raise CancelInput()
232 return default
234 def prompt_int(self, prompt, default=None, empty_is_cancel=True):
235 inp = self.prompt_str(prompt, default, empty_is_cancel)
236 try:
237 return int(inp)
238 except ValueError:
239 raise InvalidInt()
241 def prompt_pid(self):
242 try:
243 return self.prompt_int('PID to ionice: ')
244 except InvalidInt:
245 raise InvalidPid()
246 except CancelInput:
247 raise
249 def prompt_tid(self):
250 try:
251 return self.prompt_int('TID to ionice: ')
252 except InvalidInt:
253 raise InvalidTid()
254 except CancelInput:
255 raise
257 def prompt_data(self, ioprio_data):
258 try:
259 if ioprio_data is not None:
260 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
261 % ioprio_data, ioprio_data, False)
262 else:
263 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
264 except InvalidInt:
265 raise InvalidIoprioData()
266 if inp < 0 or inp > 7:
267 raise InvalidIoprioData()
268 return inp
270 def prompt_set(self, prompt, display_list, ret_list, selected):
271 try:
272 selected = ret_list.index(selected)
273 except ValueError:
274 selected = -1
275 set_len = len(display_list) - 1
276 while True:
277 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
278 self.win.insstr(1, 0, prompt, curses.A_BOLD)
279 offset = len(prompt)
280 for i, item in enumerate(display_list):
281 display = ' %s ' % item
282 if i is selected:
283 attr = curses.A_REVERSE
284 else:
285 attr = curses.A_NORMAL
286 self.win.insstr(1, offset, display, attr)
287 offset += len(display)
288 while True:
289 key = self.win.getch()
290 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
291 selected -= 1
292 break
293 elif (key in (curses.KEY_RIGHT, ord('r')) and
294 selected < set_len):
295 selected += 1
296 break
297 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
298 return ret_list[selected]
299 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
300 curses.KEY_EXIT, ord('q'), ord('Q')):
301 raise CancelInput()
303 def prompt_class(self, ioprio_class=None):
304 prompt = 'I/O priority class: '
305 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
306 classes_ret = ['rt', 'be', 'idle']
307 if ioprio_class is None:
308 ioprio_class = 2
309 inp = self.prompt_set(prompt, classes_prompt,
310 classes_ret, ioprio_class)
311 return inp
313 def prompt_error(self, error='Error!'):
314 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
315 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
316 self.win.refresh()
317 time.sleep(1)
319 def prompt_clear(self):
320 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
321 self.win.refresh()
323 def handle_key(self, key):
324 def toggle_accumulated():
325 self.options.accumulated ^= True
327 def toggle_only_io():
328 self.options.only ^= True
330 def toggle_processes():
331 self.options.processes ^= True
332 self.process_list.clear()
333 self.process_list.refresh_processes()
335 def ionice():
336 try:
337 if self.options.processes:
338 pid = self.prompt_pid()
339 exec_unit = self.process_list.get_process(pid)
340 else:
341 tid = self.prompt_tid()
342 exec_unit = ThreadInfo(tid,
343 self.process_list.taskstats_connection)
344 ioprio_value = exec_unit.get_ioprio()
345 (ioprio_class, ioprio_data) = \
346 ioprio.to_class_and_data(ioprio_value)
347 ioprio_class = self.prompt_class(ioprio_class)
348 if ioprio_class == 'idle':
349 ioprio_data = 0
350 else:
351 ioprio_data = self.prompt_data(ioprio_data)
352 exec_unit.set_ioprio(ioprio_class, ioprio_data)
353 self.process_list.clear()
354 self.process_list.refresh_processes()
355 except IoprioSetError as e:
356 self.prompt_error('Error setting I/O priority: %s' % e.err)
357 except InvalidPid:
358 self.prompt_error('Invalid process id!')
359 except InvalidTid:
360 self.prompt_error('Invalid thread id!')
361 except InvalidIoprioData:
362 self.prompt_error('Invalid I/O priority data!')
363 except InvalidInt:
364 self.prompt_error('Invalid integer!')
365 except CancelInput:
366 self.prompt_clear()
367 else:
368 self.prompt_clear()
370 key_bindings = {
371 ord('q'):
372 lambda: sys.exit(0),
373 ord('Q'):
374 lambda: sys.exit(0),
375 ord('r'):
376 lambda: self.reverse_sorting(),
377 ord('R'):
378 lambda: self.reverse_sorting(),
379 ord('a'):
380 toggle_accumulated,
381 ord('A'):
382 toggle_accumulated,
383 ord('o'):
384 toggle_only_io,
385 ord('O'):
386 toggle_only_io,
387 ord('p'):
388 toggle_processes,
389 ord('P'):
390 toggle_processes,
391 ord('i'):
392 ionice,
393 ord('I'):
394 ionice,
395 curses.KEY_LEFT:
396 lambda: self.adjust_sorting_key(-1),
397 curses.KEY_RIGHT:
398 lambda: self.adjust_sorting_key(1),
399 curses.KEY_HOME:
400 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
401 curses.KEY_END:
402 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
405 action = key_bindings.get(key, lambda: None)
406 action()
408 def get_data(self):
409 def format(p):
410 stats = format_stats(self.options, p, self.process_list.duration)
411 io_delay, swapin_delay, read_bytes, write_bytes = stats
412 if Stats.has_blkio_delay_total:
413 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
414 else:
415 delay_stats = ' ?unavailable? '
416 pid_format = '%%%dd' % MAX_PID_WIDTH
417 line = (pid_format + ' %4s %-8s %11s %11s %s') % (
418 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
419 write_bytes, delay_stats)
420 cmdline = p.get_cmdline()
421 if not self.options.batch:
422 remaining_length = self.width - len(line)
423 if 2 < remaining_length < len(cmdline):
424 len1 = (remaining_length - 1) // 2
425 offset2 = -(remaining_length - len1 - 1)
426 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
427 line += cmdline
428 if not self.options.batch:
429 line = line[:self.width]
430 return line
432 def should_format(p):
433 return not self.options.only or \
434 p.did_some_io(self.options.accumulated)
436 processes = list(filter(should_format,
437 self.process_list.processes.values()))
438 key = IOTopUI.sorting_keys[self.sorting_key][0]
439 if self.options.accumulated:
440 stats_lambda = lambda p: p.stats_accum
441 else:
442 stats_lambda = lambda p: p.stats_delta
443 processes.sort(key=lambda p: key(p, stats_lambda(p)),
444 reverse=self.sorting_reverse)
445 return list(map(format, processes))
447 def refresh_display(self, first_time, total, current, duration):
448 summary = [
449 'Total DISK READ: %s | Total DISK WRITE: %s' % (
450 format_bandwidth(self.options, total[0], duration).rjust(14),
451 format_bandwidth(self.options, total[1], duration).rjust(14)),
452 'Current DISK READ: %s | Current DISK WRITE: %s' % (
453 format_bandwidth(self.options, current[0], duration).rjust(14),
454 format_bandwidth(self.options, current[1], duration).rjust(14))
457 pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
458 if self.options.processes:
459 pid += 'PID'
460 else:
461 pid += 'TID'
462 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
463 ' SWAPIN', ' IO', ' COMMAND']
464 lines = self.get_data()
465 if self.options.time:
466 titles = [' TIME'] + titles
467 current_time = time.strftime('%H:%M:%S ')
468 lines = [current_time + l for l in lines]
469 summary = [current_time + s for s in summary]
470 if self.options.batch:
471 if self.options.quiet <= 2:
472 for s in summary:
473 print(s)
474 if self.options.quiet <= int(first_time):
475 print(''.join(titles))
476 for l in lines:
477 print(l)
478 sys.stdout.flush()
479 else:
480 self.win.erase()
482 if Stats.has_blkio_delay_total:
483 status_msg = None
484 else:
485 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
486 'cannot determine SWAPIN and IO %')
488 help_lines = []
489 help_attrs = []
490 if self.options.help:
491 prev = self.get_sorting_key(-1)
492 next = self.get_sorting_key(1)
493 help = OrderedDict([
494 ('keys', ''),
495 ('any', 'refresh'),
496 ('q', 'quit'),
497 ('i', 'ionice'),
498 ('o', 'all' if self.options.only else 'active'),
499 ('p', 'threads' if self.options.processes else 'procs'),
500 ('a', 'bandwidth' if self.options.accumulated else 'accum'),
501 ('sort', ''),
502 ('r', 'asc' if self.sorting_reverse else 'desc'),
503 ('left', titles[prev].strip()),
504 ('right', titles[next].strip()),
505 ('home', titles[0].strip()),
506 ('end', titles[-1].strip()),
508 help_line = -1
509 for key, help in help.items():
510 if help:
511 help_item = [' ', key, ': ', help]
512 help_attr = [0, 0 if key == 'any' else curses.A_UNDERLINE, 0, 0]
513 else:
514 help_item = [' ', key, ':']
515 help_attr = [0, 0, 0]
516 if not help_lines or not help or len(''.join(help_lines[help_line]) + ''.join(help_item)) > self.width:
517 help_lines.append(help_item)
518 help_attrs.append(help_attr)
519 help_line += 1
520 else:
521 help_lines[help_line] += help_item
522 help_attrs[help_line] += help_attr
524 len_summary = len(summary)
525 len_titles = int(bool(titles))
526 len_status_msg = int(bool(status_msg))
527 len_help = len(help_lines)
528 max_lines = self.height - len_summary - len_titles - len_status_msg - len_help
529 if max_lines < 5:
530 titles = []
531 len_titles = 0
532 if max_lines < 6:
533 summary = []
534 len_summary = 0
535 if max_lines < 7:
536 status_msg = None
537 len_status_msg = 0
538 if max_lines < 8:
539 help_lines = []
540 help_attrs = []
541 len_help = 0
542 max_lines = self.height - len_summary - len_titles - len_status_msg - len_help
543 num_lines = min(len(lines), max_lines)
545 for i, s in enumerate(summary):
546 self.win.addstr(i, 0, s[:self.width])
547 if titles:
548 self.win.hline(len_summary, 0, ord(' ') | curses.A_REVERSE, self.width)
549 pos = 0
550 remaining_cols = self.width
551 for i in range(len(titles)):
552 attr = curses.A_REVERSE
553 title = titles[i]
554 if i == self.sorting_key:
555 title = title[1:]
556 if i == self.sorting_key:
557 attr |= curses.A_BOLD
558 title += self.sorting_reverse and '>' or '<'
559 title = title[:remaining_cols]
560 remaining_cols -= len(title)
561 if title:
562 self.win.addstr(len_summary, pos, title, attr)
563 pos += len(title)
564 for i in range(num_lines):
565 try:
566 def print_line(line):
567 self.win.addstr(i + len_summary + len_titles, 0, line)
568 try:
569 print_line(lines[i])
570 except UnicodeEncodeError:
571 # Python2: 'ascii' codec can't encode character ...
572 # https://bugs.debian.org/708252
573 print_line(lines[i].encode('utf-8'))
574 except curses.error:
575 pass
576 for ln in range(len_help):
577 line = self.height - len_status_msg - len_help + ln
578 self.win.hline(line, 0, ord(' ') | curses.A_REVERSE, self.width)
579 pos = 0
580 for i in range(len(help_lines[ln])):
581 self.win.insstr(line, pos, help_lines[ln][i], curses.A_REVERSE | help_attrs[ln][i])
582 pos += len(help_lines[ln][i])
583 if status_msg:
584 self.win.insstr(self.height - 1, 0, status_msg,
585 curses.A_BOLD)
586 self.win.refresh()
589 def run_iotop_window(win, options):
590 if options.batch:
591 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
592 else:
593 def clean_exit(*args, **kwargs):
594 sys.exit(0)
595 signal.signal(signal.SIGINT, clean_exit)
596 signal.signal(signal.SIGTERM, clean_exit)
597 taskstats_connection = TaskStatsNetlink(options)
598 process_list = ProcessList(taskstats_connection, options)
599 ui = IOTopUI(win, process_list, options)
600 ui.run()
603 def run_iotop(options):
604 try:
605 if options.batch:
606 return run_iotop_window(None, options)
607 else:
608 return curses.wrapper(run_iotop_window, options)
609 except OSError as e:
610 if e.errno == errno.EPERM:
611 print(e, file=sys.stderr)
612 print('''
613 The Linux kernel interfaces that iotop relies on now require root privileges
614 or the NET_ADMIN capability. This change occurred because a security issue
615 (CVE-2011-2494) was found that allows leakage of sensitive data across user
616 boundaries. If you require the ability to run iotop as a non-root user, please
617 configure sudo to allow you to run iotop as root.
619 Please do not file bugs on iotop about this.''', file=sys.stderr)
620 sys.exit(1)
621 else:
622 raise
625 # Profiling
629 def _profile(continuation):
630 prof_file = 'iotop.prof'
631 try:
632 import cProfile
633 import pstats
634 print('Profiling using cProfile')
635 cProfile.runctx('continuation()', globals(), locals(), prof_file)
636 stats = pstats.Stats(prof_file)
637 except ImportError:
638 import hotshot
639 import hotshot.stats
640 prof = hotshot.Profile(prof_file, lineevents=1)
641 print('Profiling using hotshot')
642 prof.runcall(continuation)
643 prof.close()
644 stats = hotshot.stats.load(prof_file)
645 stats.strip_dirs()
646 stats.sort_stats('time', 'calls')
647 stats.print_stats(50)
648 stats.print_callees(50)
649 os.remove(prof_file)
652 # Main program
655 USAGE = '''%s [OPTIONS]
657 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
658 period. SWAPIN and IO are the percentages of time the thread spent respectively
659 while swapping in and waiting on I/O more generally. PRIO is the I/O priority
660 at which the thread is running (set using the ionice command).
662 Controls: left and right arrows to change the sorting column, r to invert the
663 sorting order, o to toggle the --only option, p to toggle the --processes
664 option, a to toggle the --accumulated option, i to change I/O priority, q to
665 quit, any other key to force a refresh.''' % sys.argv[0]
668 def main():
669 try:
670 locale.setlocale(locale.LC_ALL, '')
671 except locale.Error:
672 print('unable to set locale, falling back to the default locale')
673 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
674 parser.add_option('-o', '--only', action='store_true',
675 dest='only', default=False,
676 help='only show processes or threads actually doing I/O')
677 parser.add_option('-b', '--batch', action='store_true', dest='batch',
678 help='non-interactive mode')
679 parser.add_option('-n', '--iter', type='int', dest='iterations',
680 metavar='NUM',
681 help='number of iterations before ending [infinite]')
682 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
683 help='delay between iterations [1 second]',
684 metavar='SEC', default=1)
685 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
686 help='processes/threads to monitor [all]', metavar='PID')
687 parser.add_option('-u', '--user', type='str', dest='users',
688 action='append', help='users to monitor [all]',
689 metavar='USER')
690 parser.add_option('-P', '--processes', action='store_true',
691 dest='processes', default=False,
692 help='only show processes, not all threads')
693 parser.add_option('-a', '--accumulated', action='store_true',
694 dest='accumulated', default=False,
695 help='show accumulated I/O instead of bandwidth')
696 parser.add_option('-k', '--kilobytes', action='store_true',
697 dest='kilobytes', default=False,
698 help='use kilobytes instead of a human friendly unit')
699 parser.add_option('-t', '--time', action='store_true', dest='time',
700 help='add a timestamp on each line (implies --batch)')
701 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
702 help='suppress some lines of header (implies --batch)')
703 parser.add_option('--profile', action='store_true', dest='profile',
704 default=False, help=optparse.SUPPRESS_HELP)
705 parser.add_option('--no-help', action='store_false', dest='help', default=True,
706 help='suppress listing of shortcuts')
708 options, args = parser.parse_args()
709 if args:
710 parser.error('Unexpected arguments: ' + ' '.join(args))
711 find_uids(options)
712 options.pids = options.pids or []
713 options.batch = options.batch or options.time or options.quiet
715 main_loop = lambda: run_iotop(options)
717 if options.profile:
718 def safe_main_loop():
719 try:
720 main_loop()
721 except Exception:
722 pass
723 _profile(safe_main_loop)
724 else:
725 main_loop()