Unsolved Performance of QTreeView vs QPlainTextEdit for chat log/history
-
Hello,
I'm maintaining a Qt 4.8 chat application running on 20 year old hardware. As you can imagine, performance is not great, even with a low number of a couple of hundred messages in the chat history/log. The current implementation is keeping the chat history in a QPlainTextEdit widget. The application uses both html encoding in the message text, and a syntax highlighter to highlight important words.The idea now is to migrate to a QTreeView for displaying the chat history. This is to have the days be collapsable to navigate the history faster. There is alo the hope that we may be able to cache more of the costly text parsing. CPU is the limiting factor, there is "plenty" of memory. My initial testing with a custom delegate extended from QStyledItemDelegate, and a custom model extending QAbstractModel shows about a threefold increase in CPU utilization during testing with a spam bot. The old is showing about 10-15% in the old version and 35-45% CPU in my new version. This is bad when the user is expected to have multiple chats running along side a heavy main application.
Also, the delegate does not word wrap correctly, but that's not as important right now.
Have I done anything wrong, or terribly inefficient? Is the size hint and paint method of my delegate all wrong? Should I use a map to fake the tree rather than actual items?
Thanks in advance for any help or suggestions!
,
Code for the model:
Note that the actual message object is much more complex, but I've removed most of that conversion logic as it is probably irrelevant.MessageModel.h:
class MessageModel : public QAbstractItemModel { Q_OBJECT public: explicit MessageModel(QObject *parent = 0); ~MessageModel(); QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &index) const; int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; bool setData(const QModelIndex &index, const QVariant &value, int role); public slots: bool insertMessage(const SampleMessageObj* message); private: MessageItem* m_root; MessageItem* item(const QModelIndex &index) const; QModelIndex itemIndex(MessageItem* item) const; MessageItem* findOrInsert(MessageItem* parent, MessageItem::Type type, QDateTime datetime); MessageItem* insert(MessageItem* parent, MessageItem::Type type, QDateTime datetime, int childNumber = -1); };
MessageModel.cpp:
MessageModel::MessageModel(QObject *parent) : QAbstractItemModel(parent) { m_root = new MessageItem(MessageItem::Root, QDateTime::currentDateTimeUtc()); } MessageModel::~MessageModel() { delete m_root; } QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const { if( !hasIndex(row, column, parent) ) return QModelIndex(); MessageItem* parentItem = item(parent); if( !parentItem ) return QModelIndex(); MessageItem* childItem = parentItem->child(row); if( !childItem ) return QModelIndex(); return createIndex(row, column, childItem); } QModelIndex MessageModel::parent(const QModelIndex &index) const { if( !index.isValid() ) return QModelIndex(); MessageItem* childItem = item(index); if( childItem == m_root || !childItem ) return QModelIndex(); MessageItem* parentItem = childItem->parent(); if( parentItem == m_root || !parentItem ) return QModelIndex(); return createIndex(parentItem->childNumber(), 0, parentItem); } int MessageModel::rowCount(const QModelIndex &parent) const { if(parent.column() > 0) return 0; MessageItem* parentItem = item(parent); return parentItem ? parentItem->childCount() : 0; } int MessageModel::columnCount(const QModelIndex &parent) const { return item(parent)->columnCount(); } QVariant MessageModel::data(const QModelIndex &index, int role) const { if( role != Qt::DisplayRole || !index.isValid() ) return QVariant(); MessageItem* message = item(index); return message->data(index.column()); } bool MessageModel::setData(const QModelIndex &index, const QVariant &value, int role) { if( role != Qt::DisplayRole && role != Qt::EditRole ) return false; MessageItem* message = item(index); if( !message ) return false; bool success = message->setData(index.column(), value); if( !success ) return false; emit dataChanged(index, index); return true; } bool MessageModel::insertMessage(const SampleMessageObj *message) { QDateTime datetime = QDateTime::fromMSecsSinceEpoch(message->timestamp).toUTC(); QDateTime midnight(datetime.date(), QTime(), Qt::UTC); MessageItem* dateItem = findOrInsert(m_root, MessageItem::Date, midnight); if( !dateItem ) return false; MessageItem* messageItem = findOrInsert(dateItem, MessageItem::Message, datetime); if( !messageItem ) return false; messageItem->setData(MessageItem::Sender, message->sender); messageItem->setData(MessageItem::Label, "NEW"); QString text = QString::fromUtf8(message->text.c_str()); text.replace(QChar('\n'), QString("<br>")); messageItem->setData(MessageItem::Text, text); return true; } MessageItem *MessageModel::item(const QModelIndex &index) const { if(!index.isValid()) return m_root; MessageItem* item = static_cast<MessageItem*>(index.internalPointer()); return item ? item : m_root; } QModelIndex MessageModel::itemIndex(MessageItem *item) const { return item ? createIndex(item->childNumber(), 0, item) : QModelIndex(); } MessageItem *MessageModel::findOrInsert(MessageItem *parent, MessageItem::Type type, QDateTime datetime) { if(!parent) return 0; for(int i = parent->childCount(); i > 0; i--) { MessageItem* child = parent->child(i-1); if( !child || child->type() != type ) return 0; if( datetime == child->datetime() ) { return child; } else if( datetime > child->datetime() ) { return insert(parent, type, datetime, i); } } return insert(parent, type, datetime); } MessageItem *MessageModel::insert(MessageItem *parent, MessageItem::Type type, QDateTime datetime, int childNumber) { MessageItem* newChild = new MessageItem(type, datetime, parent); QModelIndex parentIndex = itemIndex(parent); if( !parentIndex.isValid() ) return 0; int row = childNumber < 0 ? parent->childCount() : childNumber; beginInsertRows(parentIndex, row, row); bool success = parent->insertChild(row, newChild); endInsertRows(); return success ? newChild : 0; }
Code for the item:
Note that the tree has only three levels: root, dates and messages.MessageItem.h:
class MessageItem { public: enum Type { Root, Date, Message }; enum Column { Timestamp, Label, Sender, Text, COLUMN_COUNT }; explicit MessageItem(const Type type, const QDateTime &datetime, MessageItem* parent = 0); ~MessageItem(); Type type(); void setType(Type type); MessageItem* parent(); void setParent(MessageItem* parent); int childNumber() const; MessageItem* child(int number); int childCount() const; bool insertChild(int positon, MessageItem* child); QDateTime datetime(); bool setDatetime(const QDateTime& datetime); QVariant data(int column); bool setData(int column, const QVariant value); int columnCount() const; private: bool validIndex(int index, int size); Type m_type; MessageItem* m_parent; QVector<MessageItem*> m_children; QVector<QVariant> m_data; QDateTime m_datetime; };
MessageItem.cpp:
MessageItem::MessageItem(const MessageItem::Type type, const QDateTime &datetime, MessageItem *parent) : m_type(type) , m_parent(parent) { m_data = QVector<QVariant>(columnCount()); setDatetime(datetime); } MessageItem::~MessageItem() { qDeleteAll(m_children); } MessageItem::Type MessageItem::type() { return m_type; } void MessageItem::setType(MessageItem::Type type) { m_type = type; } MessageItem *MessageItem::parent() { return m_parent; } void MessageItem::setParent(MessageItem *parent) { if(parent) m_parent = parent; } int MessageItem::childNumber() const { return m_parent ? m_parent->m_children.indexOf(const_cast<MessageItem*>(this)) : 0; } MessageItem *MessageItem::child(int number) { return validIndex(number, childCount()) ? m_children[number] : 0; } int MessageItem::childCount() const { return m_children.size(); } bool MessageItem::insertChild(int positon, MessageItem *child) { if(!child) return false; if( !validIndex(positon, childCount()) ) { positon = m_children.size(); } child->setParent(this); m_children.insert(positon, child); return true; } QDateTime MessageItem::datetime() { return m_datetime; } bool MessageItem::setDatetime(const QDateTime &datetime) { m_datetime = datetime; QString timestamp; switch (m_type) { case Date: timestamp = m_datetime.date().toString(Qt::ISODate); break; case Message: timestamp = m_datetime.time().toString(Qt::ISODate); break; default: timestamp = m_datetime.toString(Qt::ISODate); break; } timestamp = "<font color=\"blue\">" + timestamp + "</font>"; m_data[Timestamp] = timestamp; return true; } QVariant MessageItem::data(int column) { return validIndex(column, columnCount()) ? m_data[column] : QVariant(); } bool MessageItem::setData(int column, const QVariant value) { if( !validIndex(column, columnCount()) ) return false; if(column == Timestamp) return setDatetime(value.toDateTime()); m_data[column] = value; return true; } int MessageItem::columnCount() const { return COLUMN_COUNT; } bool MessageItem::validIndex(int index, int size) { return ( index >= 0 && index < size ); }
Code for the delegate:
Note that delegate has to support html, highlighter and word wrap.MessageDelegate.h:
class MessageDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit MessageDelegate(QObject* parent = 0); protected: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: QTextDocument* m_document; QSyntaxHighlighter* m_highlighter; };
MessageDelegate.cpp:
MessageDelegate::MessageDelegate(QObject *parent) : QStyledItemDelegate(parent) { m_document = new QTextDocument(this); // m_highlighter = new QSyntaxHighlighter(m_document); // using a custom subclass } void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItemV4 options = option; initStyleOption(&options, index); painter->save(); m_document->setDefaultFont(options.font); m_document->setTextWidth(options.rect.width()); m_document->setHtml(options.text); options.text = ""; options.widget->style()->drawControl( QStyle::CE_ItemViewItem, &option, painter ); painter->translate( options.rect.left(), options.rect.top() ); QRect clip( 0, 0, options.rect.width(), options.rect.height() ); painter->setClipRect(clip); QAbstractTextDocumentLayout::PaintContext ctx; ctx.clip = clip; QPalette::ColorRole role = options.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text; ctx.palette.setColor( QPalette::Text, option.palette.color(QPalette::Active, role) ); m_document->documentLayout()->draw( painter, ctx ); painter->restore(); } QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItemV4 options = option; initStyleOption(&options, index); m_document->setDefaultFont(options.font); m_document->setTextWidth(options.rect.width()); m_document->setHtml(options.text); return QSize(m_document->idealWidth(), m_document->size().height()); }
-
Hi,
Why are you re-initializing the option on every call of paint ?
-
To be honest, the "manual" painting parts of Qt is still a bit of a mystery to me. It came from this question over at Stack Overflow, and it didn't work properly if I removed or fiddled too much with it.
-
What performance do you have if your remove your custom delegate ?
-
Hmm, it seems to be around 20-30%. Not great, but significantly better. Of course, now the messages are displaying the html tags as text and become very long. I imagine this could potentially hide an even better performance improvement as the longer strings are more complex to calculate, even for the default delegate? If that is the case, the paint and size hint methods, not the model, is probably the biggest performace drain.