Skip to content

Commit

Permalink
use cache for layout since QTextLayout::setFormats is slow
Browse files Browse the repository at this point in the history
QTextLayout::setFormats is really slow so this patch introduces a cache
named HighlightedLine which will only call QTextLayout::setFormats if
necessary
  • Loading branch information
lievenhey committed Dec 5, 2023
1 parent 1f1691c commit 5445957
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 114 deletions.
202 changes: 112 additions & 90 deletions src/models/highlightedtext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "formattingutils.h"

#if KFSyntaxHighlighting_FOUND
// highlighter using KSyntaxHighlighting
class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlighter
{
public:
Expand All @@ -37,18 +38,11 @@ class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlight
}
~HighlightingImplementation() override = default;

virtual QVector<QTextLayout::FormatRange> format(const QStringList& text)
virtual QVector<QTextLayout::FormatRange> format(const QString& text)
{
m_formats.clear();
m_offset = 0;

KSyntaxHighlighting::State state;
for (const auto& line : text) {
state = highlightLine(line, state);

// KSyntaxHighlighting uses line offsets but QTextLayout uses global offsets
m_offset += line.size();
}
highlightLine(text, {});

return m_formats;
}
Expand Down Expand Up @@ -84,15 +78,16 @@ class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlight
{
QTextCharFormat textCharFormat;
textCharFormat.setForeground(format.textColor(theme()));
m_formats.push_back({m_offset + offset, length, textCharFormat});
textCharFormat.setFontWeight(format.isBold(theme()) ? QFont::Bold : QFont::Normal);
m_formats.push_back({offset, length, textCharFormat});
}

private:
KSyntaxHighlighting::Repository* m_repository;
QVector<QTextLayout::FormatRange> m_formats;
int m_offset = 0;
};
#else
// stub incase KSyntaxHighlighting is not available
class HighlightingImplementation
{
public:
Expand Down Expand Up @@ -122,7 +117,7 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
}
~AnsiHighlightingImplementation() override = default;

QVector<QTextLayout::FormatRange> format(const QStringList& text) final
QVector<QTextLayout::FormatRange> format(const QString& text) final
{
QVector<QTextLayout::FormatRange> formats;

Expand All @@ -133,41 +128,39 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
constexpr int resetColorSequenceLength = 4;
constexpr int colorCodeLength = 2;

for (const auto& line : text) {
auto lastToken = line.cbegin();
int lineOffset = 0;
for (auto escapeIt = std::find(line.cbegin(), line.cend(), Util::escapeChar); escapeIt != line.cend();
escapeIt = std::find(escapeIt, line.cend(), Util::escapeChar)) {

lineOffset += std::distance(lastToken, escapeIt);
Q_ASSERT(*(escapeIt + 1) == QLatin1Char('['));

// escapeIt + 2 points to the first color code character
auto color = QStringView {escapeIt + 2, colorCodeLength};
bool ok = false;
const uint8_t colorCode = color.toUInt(&ok);
if (ok) {
// only support the 8 default colors
Q_ASSERT(colorCode >= 30 && colorCode <= 37);

format.start = offset + lineOffset;
const auto colorRole = static_cast<KColorScheme::ForegroundRole>(colorCode - 30);
format.format.setForeground(m_colorScheme.foreground(colorRole));

std::advance(escapeIt, setColorSequenceLength);
} else {
// make sure we have a reset sequence
Q_ASSERT(color == QStringLiteral("0m"));
format.length = offset + lineOffset - format.start;
if (format.length) {
formats.push_back(format);
}

std::advance(escapeIt, resetColorSequenceLength);
auto lastToken = text.begin();
for (auto escapeIt = std::find(text.cbegin(), text.cend(), Util::escapeChar); escapeIt != text.cend();
escapeIt = std::find(escapeIt, text.cend(), Util::escapeChar)) {

Q_ASSERT(*(escapeIt + 1) == QLatin1Char('['));

offset += std::distance(lastToken, escapeIt);

// escapeIt + 2 points to the first color code character
auto color = QStringView {escapeIt + 2, colorCodeLength};
bool ok = false;
const uint8_t colorCode = color.toUInt(&ok);
if (ok) {
// only support the 8 default colors
Q_ASSERT(colorCode >= 30 && colorCode <= 37);

format.start = offset;
const auto colorRole = static_cast<KColorScheme::ForegroundRole>(colorCode - 30);
format.format.setForeground(m_colorScheme.foreground(colorRole));

std::advance(escapeIt, setColorSequenceLength);
} else {
// make sure we have a reset sequence
Q_ASSERT(color == QStringLiteral("0m"));
format.length = offset - format.start;
if (format.length) {
formats.push_back(format);
}
lastToken = escapeIt;

std::advance(escapeIt, resetColorSequenceLength);
}
offset += lineOffset + std::distance(lastToken, line.cend());

lastToken = escapeIt;
}

return formats;
Expand All @@ -188,15 +181,67 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
KColorScheme m_colorScheme;
};

// QTextLayout is slow, this class acts as a cache that only creates and fills the QTextLayout on demand
class HighlightedLine
{
public:
HighlightedLine(HighlightingImplementation* highlighter, const QString& text)
: m_highlighter(highlighter)
, m_text(text)
, m_layout(nullptr)
{
}

~HighlightedLine() = default;

HighlightedLine(HighlightedLine&&) = default;

QTextLayout* layout()
{
if (!m_layout) {
doLayout();
}
return m_layout.get();
}

void updateHighlighting()
{
m_layout = nullptr;
}

private:
void doLayout()
{
if (!m_layout) {
m_layout = std::make_unique<QTextLayout>();
m_layout->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
const auto& ansiFreeLine = Util::removeAnsi(m_text);
m_layout->setText(ansiFreeLine);
}

m_layout->setFormats(m_highlighter->format(m_text));

m_layout->beginLayout();

// there is at most one line, so we don't need to check this multiple times
QTextLine line = m_layout->createLine();
if (line.isValid()) {
line.setPosition(QPointF(0, 0));
}
m_layout->endLayout();
}

HighlightingImplementation* m_highlighter;
QString m_text;
std::unique_ptr<QTextLayout> m_layout;
};

HighlightedText::HighlightedText(KSyntaxHighlighting::Repository* repository, QObject* parent)
: QObject(parent)
#if KFSyntaxHighlighting_FOUND
, m_repository(repository)
#endif
, m_layout(std::make_unique<QTextLayout>())
{
m_layout->setCacheEnabled(true);
m_layout->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}

HighlightedText::~HighlightedText() = default;
Expand All @@ -212,69 +257,39 @@ void HighlightedText::setText(const QStringList& text)
} else {
m_highlighter = std::make_unique<HighlightingImplementation>(m_repository);
}
m_highlighter->themeChanged();
m_isUsingAnsi = usesAnsi;
emit usesAnsiChanged(usesAnsi);
}

m_highlighter->themeChanged();
m_highlighter->format(text);

m_lines.reserve(text.size());
m_cleanedLines.reserve(text.size());
m_highlightedLines.reserve(text.size());
std::transform(text.cbegin(), text.cend(), std::back_inserter(m_highlightedLines), [this](const QString& text) {
return HighlightedLine {m_highlighter.get(), text};
});

QString formattedText;
connect(this, &HighlightedText::definitionChanged, this, &HighlightedText::updateHighlighting);

for (const auto& line : text) {
const auto& lineWithNewline = QLatin1String("%1%2").arg(line, QChar::LineSeparator);
const auto& ansiFreeLine = Util::removeAnsi(lineWithNewline);
m_cleanedLines.push_back(ansiFreeLine);
m_lines.push_back(lineWithNewline);
formattedText += ansiFreeLine;
}

m_layout->setText(formattedText);

applyFormatting();
m_cleanedLines.reserve(text.size());
std::transform(text.cbegin(), text.cend(), std::back_inserter(m_cleanedLines), Util::removeAnsi);
}

void HighlightedText::setDefinition(const KSyntaxHighlighting::Definition& definition)
{
Q_ASSERT(m_highlighter);
m_highlighter->setHighlightingDefinition(definition);
emit definitionChanged(definition.name());
applyFormatting();
}

QString HighlightedText::textAt(int index) const
{
Q_ASSERT(m_highlighter);
Q_ASSERT(index < m_cleanedLines.size());
return m_cleanedLines.at(index);
}

QTextLine HighlightedText::lineAt(int index) const
{
Q_ASSERT(m_layout);
return m_layout->lineAt(index);
}

void HighlightedText::applyFormatting()
{
Q_ASSERT(m_highlighter);

m_layout->setFormats(m_highlighter->format(m_lines));

m_layout->clearLayout();
m_layout->beginLayout();

while (true) {
QTextLine line = m_layout->createLine();
if (!line.isValid())
break;

line.setPosition(QPointF(0, 0));
}
m_layout->endLayout();
auto& line = m_highlightedLines[index];
return line.layout()->lineAt(0);
}

QString HighlightedText::definition() const
Expand All @@ -284,7 +299,14 @@ QString HighlightedText::definition() const
return m_highlighter->definitionName();
}

QTextLayout* HighlightedText::layout() const
QTextLayout* HighlightedText::layoutForLine(int index)
{
return m_highlightedLines[index].layout();
}

void HighlightedText::updateHighlighting()
{
return m_layout.get();
m_highlighter->themeChanged();
std::for_each(m_highlightedLines.begin(), m_highlightedLines.end(),
[](HighlightedLine& line) { line.updateHighlighting(); });
}
11 changes: 5 additions & 6 deletions src/models/highlightedtext.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Repository;
}

class HighlightingImplementation;
class HighlightedLine;

class HighlightedText : public QObject
{
Expand All @@ -44,23 +45,21 @@ class HighlightedText : public QObject
}

// for testing
QTextLayout* layout() const;
QTextLayout* layoutForLine(int index);

signals:
void definitionChanged(const QString& definition);
void usesAnsiChanged(bool usesAnsi);

private slots:
void applyFormatting();

private:
public slots:
void updateHighlighting();

private:
#if KFSyntaxHighlighting_FOUND
KSyntaxHighlighting::Repository* m_repository;
#endif
std::unique_ptr<HighlightingImplementation> m_highlighter;
std::unique_ptr<QTextLayout> m_layout;
mutable std::vector<HighlightedLine> m_highlightedLines;
QStringList m_lines;
QStringList m_cleanedLines;
bool m_isUsingAnsi = false;
Expand Down
8 changes: 8 additions & 0 deletions src/resultsdisassemblypage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -614,4 +614,12 @@ void ResultsDisassemblyPage::setArch(const QString& arch)
m_arch = arch.trimmed().toLower();
}

void ResultsDisassemblyPage::changeEvent(QEvent* event)
{
if (event->type() == QEvent::PaletteChange) {
m_sourceCodeModel->highlightedText()->updateHighlighting();
m_disassemblyModel->highlightedText()->updateHighlighting();
}
}

#include "resultsdisassemblypage.moc"
3 changes: 3 additions & 0 deletions src/resultsdisassemblypage.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class ResultsDisassemblyPage : public QWidget
void navigateToCode(const QString& file, int lineNumber, int columnNumber);
void stackChanged();

protected:
void changeEvent(QEvent* event) override;

private:
void setupAsmViewModel();
void showDisassembly(const DisassemblyOutput& disassemblyOutput);
Expand Down
Loading

0 comments on commit 5445957

Please sign in to comment.