From 0db8bf7d461e1d618ae7efb2f00dc2d99ddf7ca3 Mon Sep 17 00:00:00 2001 From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13" Date: Wed, 15 Dec 2010 15:40:43 +0000 Subject: [PATCH] HUGE performance improvement on refresh. fixed #1855 --- src/core.py | 8 ++- src/room.py | 17 +++-- src/tabs.py | 42 +++++++----- src/text_buffer.py | 38 ++++++----- src/windows.py | 165 ++++++++++++++++++++++++++------------------- 5 files changed, 161 insertions(+), 109 deletions(-) diff --git a/src/core.py b/src/core.py index ac0d709e..3fcfabfb 100644 --- a/src/core.py +++ b/src/core.py @@ -40,6 +40,7 @@ log = logging.getLogger(__name__) import multiuserchat as muc import tabs +import windows from connection import connection from config import config @@ -83,6 +84,8 @@ class Core(object): self.stdscr = curses.initscr() self.init_curses(self.stdscr) self.xmpp = xmpp + self.information_buffer = TextBuffer() + self.information_win_size = 0 # Todo, get this from config default_tab = tabs.InfoTab(self, "Info") if self.xmpp.anon\ else tabs.RosterInfoTab(self) default_tab.on_gain_focus() @@ -90,8 +93,7 @@ class Core(object): # a unique buffer used to store global informations # that are displayed in almost all tabs, in an # information window. - self.information_buffer = TextBuffer() - self.information_win_size = 2 # Todo, get this from config + self.resize_timer = None self.previous_tab_nb = 0 self.own_nick = config.get('own_nick', self.xmpp.boundjid.bare) @@ -548,7 +550,6 @@ class Core(object): Resize the whole screen """ with resize_lock: - # self.resize_timer = None for tab in self.tabs: tab.resize() self.refresh_window() @@ -1225,6 +1226,7 @@ class Core(object): Displays an informational message in the "Info" room window """ self.information_buffer.add_message(msg, nickname=typ) + # TODO: refresh only the correct window in the current tab self.refresh_window() def command_quit(self, arg): diff --git a/src/room.py b/src/room.py index 62ece926..2fe7a188 100644 --- a/src/room.py +++ b/src/room.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with Poezio. If not, see . -from text_buffer import TextBuffer +from text_buffer import TextBuffer, MESSAGE_NB_LIMIT from datetime import datetime from random import randrange from config import config @@ -25,8 +25,6 @@ import common import theme class Room(TextBuffer): - """ - """ def __init__(self, name, nick): TextBuffer.__init__(self) self.name = name @@ -118,6 +116,13 @@ class Room(TextBuffer): if time: # History messages are colored to be distinguished color = theme.COLOR_INFORMATION_TEXT time = time if time is not None else datetime.now() - if self.pos: # avoid scrolling of one line when one line is received - self.pos += 1 - self.messages.append(Message(txt, time, nickname, user, color, colorized)) + message = Message(txt, time, nickname, user, color, colorized) + while len(self.messages) > MESSAGE_NB_LIMIT: + self.messages.pop(0) + self.messages.append(message) + for window in self.windows: # make the associated windows + # build the lines from the new message + nb = window.build_new_message(message) + if window.pos != 0: + window.scroll_up(nb) + diff --git a/src/tabs.py b/src/tabs.py index f227e2fc..9f78a708 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -199,6 +199,7 @@ class InfoTab(Tab): self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) self.text_win.resize(self.height-2, self.width, 0, 0, self.core.stdscr) + self.text_win.rebuild_everything(self._room) self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) def refresh(self, tabs, informations, _): @@ -308,10 +309,12 @@ class MucTab(ChatTab): ChatTab.__init__(self, core, room) self.topic_win = windows.Topic() self.text_win = windows.TextWin() + room.add_window(self.text_win) self.v_separator = windows.VerticalSeparator() self.user_win = windows.UserList() self.info_header = windows.MucInfoWin() self.info_win = windows.TextWin() + self.core.information_buffer.add_window(self.info_win) self.tab_win = windows.GlobalInfoBar() self.input = windows.MessageInput() self.ignores = [] # set of Users @@ -485,10 +488,11 @@ class MucTab(ChatTab): text_width = (self.width//10)*9 self.topic_win.resize(1, self.width, 0, 0, self.core.stdscr) self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) + self.text_win.rebuild_everything(self._room) self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10), self.core.stdscr) self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, self.core.stdscr) self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr, self.core.information_buffer) self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) @@ -499,9 +503,9 @@ class MucTab(ChatTab): self.text_win.refresh(self._room) self.v_separator.refresh() self.user_win.refresh(self._room.users) - self.info_header.refresh(self._room) - self.info_win.refresh(informations) + self.info_header.refresh(self._room, self.text_win) self.tab_win.refresh(tabs, tabs[0]) + self.info_win.refresh(informations) self.input.refresh() def on_input(self, key): @@ -549,10 +553,10 @@ class MucTab(ChatTab): curses.curs_set(1) def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) + self.text_win.scroll_up(self.text_win.height-1) def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) + self.text_win.scroll_down(self.text_win.height-1) def on_info_win_size_changed(self): text_width = (self.width//10)*9 @@ -573,8 +577,10 @@ class PrivateTab(ChatTab): def __init__(self, core, room): ChatTab.__init__(self, core, room) self.text_win = windows.TextWin() + room.add_window(self.text_win) self.info_header = windows.PrivateInfoWin() self.info_win = windows.TextWin() + self.core.information_buffer.add_window(self.info_win) self.tab_win = windows.GlobalInfoBar() self.input = windows.MessageInput() # keys @@ -601,8 +607,9 @@ class PrivateTab(ChatTab): def resize(self): Tab.resize(self) self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.text_win.rebuild_everything(self._room) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr, self.core.information_buffer) self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) @@ -610,7 +617,7 @@ class PrivateTab(ChatTab): if not self.visible: return self.text_win.refresh(self._room) - self.info_header.refresh(self._room) + self.info_header.refresh(self._room, self.text_win) self.info_win.refresh(informations) self.tab_win.refresh(tabs, tabs[0]) self.input.refresh() @@ -644,15 +651,15 @@ class PrivateTab(ChatTab): curses.curs_set(1) def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) + self.text_win.scroll_up(self.text_win.height-1) def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) + self.text_win.scroll_down(self.text_win.height-1) def on_info_win_size_changed(self): self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr, None) def get_room(self): return self._room @@ -673,6 +680,7 @@ class RosterInfoTab(Tab): self.v_separator = windows.VerticalSeparator() self.tab_win = windows.GlobalInfoBar() self.info_win = windows.TextWin() + self.core.information_buffer.add_window(self.info_win) self.roster_win = windows.RosterWin() self.contact_info_win = windows.ContactInfoWin() self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") @@ -696,7 +704,7 @@ class RosterInfoTab(Tab): info_width = self.width-roster_width-1 self.v_separator.resize(self.height-2, 1, 0, roster_width, self.core.stdscr) self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.info_win.resize(self.height-2, info_width, 0, roster_width+1, self.core.stdscr) + self.info_win.resize(self.height-2, info_width, 0, roster_width+1, self.core.stdscr, self.core.information_buffer) self.roster_win.resize(self.height-2-3, roster_width, 0, 0, self.core.stdscr) self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, self.core.stdscr) self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) @@ -706,13 +714,14 @@ class RosterInfoTab(Tab): if isinstance(self.input, windows.CommandInput) and\ not self.input.help_message: self.complete_commands(self.input) - + def refresh(self, tabs, informations, roster): if not self.visible: return self.v_separator.refresh() self.roster_win.refresh(roster) self.contact_info_win.refresh(self.roster_win.get_selected_row()) + # self.core.global_information_win.refresh(informations) self.info_win.refresh(informations) self.tab_win.refresh(tabs, tabs[0]) self.input.refresh() @@ -840,9 +849,11 @@ class ConversationTab(ChatTab): self.color_state = theme.COLOR_TAB_NORMAL self._name = jid # a conversation tab is linked to one specific full jid OR bare jid self.text_win = windows.TextWin() + text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() self.info_header = windows.ConversationInfoWin() self.info_win = windows.TextWin() + self.core.information_buffer.add_window(self.info_win) self.tab_win = windows.GlobalInfoBar() self.input = windows.MessageInput() # keys @@ -869,9 +880,10 @@ class ConversationTab(ChatTab): def resize(self): Tab.resize(self) self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 1, 0, self.core.stdscr) + self.text_win.rebuild_everything(self._room) self.upper_bar.resize(1, self.width, 0, 0, self.core.stdscr) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr, self.core.information_buffer) self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) @@ -914,10 +926,10 @@ class ConversationTab(ChatTab): curses.curs_set(1) def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) + self.text_win.scroll_up(self.text_win.height-1) def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) + self.text_win.scroll_down(self.text_win.height-1) def on_info_win_size_changed(self): self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) diff --git a/src/text_buffer.py b/src/text_buffer.py index 43bf008f..32c6f725 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -18,10 +18,15 @@ Define the TextBuffer class """ +import logging +log = logging.getLogger(__name__) + from message import Message from datetime import datetime import theme +MESSAGE_NB_LIMIT = 16384 + class TextBuffer(object): """ This class just keep trace of messages, in a list with various @@ -29,15 +34,28 @@ class TextBuffer(object): """ def __init__(self): self.messages = [] # Message objects - self.pos = 0 + self.windows = [] # we keep track of one or more windows + # so we can pass the new messages to them, as they are added, so + # they (the windows) can built the lines from the new message + + def add_window(self, win): + self.windows.append(win) def add_message(self, txt, time=None, nickname=None, colorized=False): color = theme.COLOR_NORMAL_TEXT user = None time = time or datetime.now() - if self.pos: # avoid scrolling of one line when one line is received - self.pos += 1 - self.messages.append(Message(txt, time, nickname, user, color, colorized)) + # if self.pos: # avoid scrolling of one line when one line is received + # self.pos += 1 + msg = Message(txt, time, nickname, user, color, colorized) + self.messages.append(msg) + while len(self.messages) > MESSAGE_NB_LIMIT: + self.messages.pop(0) + for window in self.windows: # make the associated windows + # build the lines from the new message + nb = window.build_new_message(msg) + if window.pos != 0: + window.scroll_up(nb) def remove_line_separator(self): """ @@ -52,15 +70,3 @@ class TextBuffer(object): """ if None not in self.messages: self.messages.append(None) - - def scroll_up(self, dist=14): - # The pos can grow a lot over the top of the number of - # available lines, it will be fixed on the next refresh of the - # screen anyway - self.pos += dist - - def scroll_down(self, dist=14): - self.pos -= dist - if self.pos <= 0: - self.pos = 0 - diff --git a/src/windows.py b/src/windows.py index 81f81f60..13383536 100644 --- a/src/windows.py +++ b/src/windows.py @@ -49,6 +49,8 @@ import theme g_lock = Lock() +LINES_NB_LIMIT = 16384 + class Win(object): def __init__(self): pass @@ -186,14 +188,14 @@ class InfoWin(Win): def __init__(self): Win.__init__(self) - def print_scroll_position(self, text_buffer): + def print_scroll_position(self, window): """ Print, link in Weechat, a -PLUS(n)- where n is the number of available lines to scroll down """ - if text_buffer.pos > 0: - plus = ' -PLUS(%s)-' % text_buffer.pos + if window.pos > 0: + plus = ' -PLUS(%s)-' % window.pos self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD) class PrivateInfoWin(InfoWin): @@ -341,14 +343,15 @@ class MucInfoWin(InfoWin): def resize(self, height, width, y, x, stdscr): self._resize(height, width, y, x, stdscr) - def refresh(self, room): + def refresh(self, room, window=None): with g_lock: self._win.erase() self.write_room_name(room) self.write_own_nick(room) self.write_disconnected(room) self.write_role(room) - self.print_scroll_position(room) + if window: + self.print_scroll_position(window) self.finish_line(theme.COLOR_INFORMATION_BAR) self._refresh() @@ -397,64 +400,81 @@ class MucInfoWin(InfoWin): class TextWin(Win): def __init__(self): Win.__init__(self) + self.pos = 0 + self.built_lines = [] # Each new message is built and kept here. + # on resize, we rebuild all the messages - def build_lines_from_messages(self, messages): + def scroll_up(self, dist=14): + # The pos can grow a lot over the top of the number of + # available lines, it will be fixed on the next refresh of the + # screen anyway + self.pos += dist + + def scroll_down(self, dist=14): + self.pos -= dist + if self.pos <= 0: + self.pos = 0 + + def build_new_message(self, message): """ - From all the existing messages in the window, create the that will - be displayed on the screen + Take one message, build it and add it to the list + Return the number of lines that are built for the given + message. """ - lines = [] - for message in messages: - if message == None: # line separator - lines.append(None) - continue - txt = message.txt - if not txt: - continue + if message == None: # line separator + self.built_lines.append(None) + return 0 + txt = message.txt + if not txt: + return 0 # length of the time - offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1]) - if message.nickname and len(message.nickname) >= 30: - nick = message.nickname[:30]+'…' + offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1]) + if message.nickname and len(message.nickname) >= 30: + nick = message.nickname[:30]+'…' + else: + nick = message.nickname + if nick: + offset += len(nick) + 2 # + nick + spaces length + first = True + this_line_was_broken_by_space = False + nb = 0 + while txt != '': + if txt[:self.width-offset].find('\n') != -1: + limit = txt[:self.width-offset].find('\n') else: - nick = message.nickname - if nick: - offset += len(nick) + 2 # + nick + spaces length - first = True - this_line_was_broken_by_space = False - while txt != '': - if txt[:self.width-offset].find('\n') != -1: - limit = txt[:self.width-offset].find('\n') - else: - # break between words if possible - if len(txt) >= self.width-offset: - limit = txt[:self.width-offset].rfind(' ') - this_line_was_broken_by_space = True - if limit <= 0: - limit = self.width-offset - this_line_was_broken_by_space = False - else: - limit = self.width-offset-1 + # break between words if possible + if len(txt) >= self.width-offset: + limit = txt[:self.width-offset].rfind(' ') + this_line_was_broken_by_space = True + if limit <= 0: + limit = self.width-offset this_line_was_broken_by_space = False - color = message.user.color if message.user else None - if not first: - nick = None - time = None else: - time = message.time - l = Line(nick, color, - time, - txt[:limit], message.color, - offset, - message.colorized) - lines.append(l) - if this_line_was_broken_by_space: - txt = txt[limit+1:] # jump the space at the start of the line - else: - txt = txt[limit:] - if txt.startswith('\n'): - txt = txt[1:] - first = False - return lines + limit = self.width-offset-1 + this_line_was_broken_by_space = False + color = message.user.color if message.user else None + if not first: + nick = None + time = None + else: + time = message.time + l = Line(nick, color, + time, + txt[:limit], message.color, + offset, + message.colorized) + self.built_lines.append(l) + nb += 1 + if this_line_was_broken_by_space: + txt = txt[limit+1:] # jump the space at the start of the line + else: + txt = txt[limit:] + if txt.startswith('\n'): + txt = txt[1:] + first = False + while len(self.built_lines) > LINES_NB_LIMIT: + self.built_lines.pop(0) + return nb def refresh(self, room): """ @@ -465,13 +485,14 @@ class TextWin(Win): return with g_lock: self._win.erase() - lines = self.build_lines_from_messages(room.messages) - if room.pos + self.height > len(lines): - room.pos = len(lines) - self.height - if room.pos < 0: - room.pos = 0 - if room.pos != 0: - lines = lines[-self.height-room.pos:-room.pos] + # lines = self.build_lines_from_messages(room.messages) + lines = self.built_lines + if self.pos + self.height > len(lines): + self.pos = len(lines) - self.height + if self.pos < 0: + self.pos = 0 + if self.pos != 0: + lines = lines[-self.height-self.pos:-self.pos] else: lines = lines[-self.height:] y = 0 @@ -562,8 +583,15 @@ class TextWin(Win): self.addnstr(theme.CHAR_TIME_RIGHT, curses.color_pair(theme.COLOR_TIME_LIMITER)) self.addstr(' ') - def resize(self, height, width, y, x, stdscr): + def resize(self, height, width, y, x, stdscr, room=None): self._resize(height, width, y, x, stdscr) + if room: + self.rebuild_everything(room) + + def rebuild_everything(self, room): + self.built_lines = [] + for message in room.messages: + self.build_new_message(message) class HelpText(Win): """ @@ -824,7 +852,6 @@ class Input(Win): begin = self.text.split()[-1].lower() else: begin = '' - log.debug('BEGIN: [%s]\n' % begin) hit_list = [] # list of matching nicks for word in word_list: if word.lower().startswith(begin): @@ -895,15 +922,15 @@ class Input(Win): def do_command(self, key, reset=True): if key in self.key_func: return self.key_func[key]() - if not key or len(key) > 1: - return False # ignore non-handled keyboard shortcuts + # if not key or len(key) > 1: + # return False # ignore non-handled keyboard shortcuts self.reset_completion() self.text = self.text[:self.pos+self.line_pos]+key+self.text[self.pos+self.line_pos:] (y, x) = self._win.getyx() if x == self.width-1: - self.line_pos += 1 + self.line_pos += len(key) else: - self.pos += 1 + self.pos += len(key) if reset: self.rewrite_text() return True