From c7a90e3949e0136265c893090ec8f9a300592b13 2013-07-18 06:57:40 From: Pankaj Pandey Date: 2013-07-18 06:57:40 Subject: [PATCH] Prevent qtconsole frontend freeze on lots of output. The output from the kernel is now clipped to last `buffer_size` before displaying and a timer is used to flush the pending output text instead of attempting to display text on every stream output from kernel. The timer interval is adjusted based on actual time taken to append a screenful of text to widget. This throttles the widget repaints and avoids choking the Qt event loop leaving time to handle other Qt events. Test cases: In [1]: for i in xrange(1000000): print i In [2]: range(100000) Without this commit the first input causes the qtconsole frontend to freeze, not responding to `Ctrl+C`. --- diff --git a/IPython/qt/console/console_widget.py b/IPython/qt/console/console_widget.py index a29bfb9..87cd3a4 100644 --- a/IPython/qt/console/console_widget.py +++ b/IPython/qt/console/console_widget.py @@ -9,6 +9,7 @@ import os.path import re import sys from textwrap import dedent +import time from unicodedata import category import webbrowser @@ -291,6 +292,21 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): self._reading_callback = None self._tab_width = 8 + # List of strings pending to be appended as plain text in the widget. + # The text is not immediately inserted when available to not + # choke the Qt event loop with paint events for the widget in + # case of lots of output from kernel. + self._pending_insert_text = [] + + # Timer to flush the pending stream messages. The interval is adjusted + # later based on actual time taken for flushing a screen (buffer_size) + # of output text. + self._pending_text_flush_interval = QtCore.QTimer(self._control) + self._pending_text_flush_interval.setInterval(100) + self._pending_text_flush_interval.setSingleShot(True) + self._pending_text_flush_interval.timeout.connect( + self._flush_pending_stream) + # Set a monospaced font. self.reset_font() @@ -877,8 +893,11 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): # Determine where to insert the content. cursor = self._control.textCursor() if before_prompt and (self._reading or not self._executing): + self._flush_pending_stream() cursor.setPosition(self._append_before_prompt_pos) else: + if insert != self._insert_plain_text: + self._flush_pending_stream() cursor.movePosition(QtGui.QTextCursor.End) start_pos = cursor.position() @@ -1459,6 +1478,20 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): return False + def _flush_pending_stream(self): + """ Flush out pending text into the widget. """ + text = self._pending_insert_text + self._pending_insert_text = [] + buffer_size = self._control.document().maximumBlockCount() + if buffer_size > 0: + text = self._get_last_lines_from_list(text, buffer_size) + text = ''.join(text) + t = time.time() + self._insert_plain_text(self._get_end_cursor(), text, flush=True) + # Set the flush interval to equal the maximum time to update text. + self._pending_text_flush_interval.setInterval(max(100, + (time.time()-t)*1000)) + def _format_as_columns(self, items, separator=' '): """ Transform a list of strings into a single string with columns. @@ -1540,6 +1573,43 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): else: return None + def _get_last_lines(self, text, num_lines, return_count=False): + """ Return last specified number of lines of text (like `tail -n`). + If return_count is True, returns a tuple of clipped text and the + number of lines in the clipped text. + """ + pos = len(text) + if pos < num_lines: + if return_count: + return text, text.count('\n') if return_count else text + else: + return text + i = 0 + while i < num_lines: + pos = text.rfind('\n', None, pos) + if pos == -1: + pos = None + break + i += 1 + if return_count: + return text[pos:], i + else: + return text[pos:] + + def _get_last_lines_from_list(self, text_list, num_lines): + """ Return the list of text clipped to last specified lines. + """ + ret = [] + lines_pending = num_lines + for text in reversed(text_list): + text, lines_added = self._get_last_lines(text, lines_pending, + return_count=True) + ret.append(text) + lines_pending -= lines_added + if lines_pending <= 0: + break + return ret[::-1] + def _get_prompt_cursor(self): """ Convenience method that returns a cursor for the prompt position. """ @@ -1644,10 +1714,29 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): cursor.endEditBlock() return text - def _insert_plain_text(self, cursor, text): + def _insert_plain_text(self, cursor, text, flush=False): """ Inserts plain text using the specified cursor, processing ANSI codes if enabled. """ + # maximumBlockCount() can be different from self.buffer_size in + # case input prompt is active. + buffer_size = self._control.document().maximumBlockCount() + + if self._executing and not flush and \ + self._pending_text_flush_interval.isActive(): + self._pending_insert_text.append(text) + if buffer_size > 0: + self._pending_insert_text = self._get_last_lines_from_list( + self._pending_insert_text, buffer_size) + return + + if self._executing and not self._pending_text_flush_interval.isActive(): + self._pending_text_flush_interval.start() + + # Clip the text to last `buffer_size` lines. + if buffer_size > 0: + text = self._get_last_lines(text, buffer_size) + cursor.beginEditBlock() if self.ansi_codes: for substring in self._ansi_processor.split_string(text):