Perf: debounce palette filter + default to optimized build

Two fixes for the Alt+G typing lag:

1. Default CMAKE_BUILD_TYPE to RelWithDebInfo when unspecified — the
   shipped plugin was -O0. On mobydick (17k symbols) the worst
   type-forward keystroke drops ~340ms -> ~29ms (~12x).

2. Debounce the PaletteWidget filter with an 80ms single-shot timer so a
   fast typist's burst triggers one re-rank, not one per character.
   applyQueryNow() flushes the pending query on Enter so activation uses
   the latest text; activate() resets the debounce state.

The broad early-query keystrokes (1-2 chars matching most of N) are the
only ones still ~O(N); debounce ensures they are paid at most once per
pause. docs/PERF.md updated with measured numbers.
This commit is contained in:
Levi Neely 2026-10-08 11:13:37 +02:00
parent 2bf8d4a3c2
commit a5b8d628b3
4 changed files with 60 additions and 9 deletions

View File

@ -9,6 +9,14 @@ set(CMAKE_AUTOMOC ON)
set(QT_MIN_VERSION 6.5.0)
set(KF_MIN_VERSION 6.0.0)
# Default to an optimized build when the user did not pick one. The palette
# re-rank and indexing are measurably ~5-8x slower under -O0 (see docs/PERF.md),
# so an unqualified `cmake -B build` should not ship a Debug plugin.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
"Build type (Debug, Release, RelWithDebInfo, MinSizeRel)" FORCE)
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})

View File

@ -57,11 +57,24 @@ Effect once the set narrows (Release, type-forward):
Early keystrokes (`s`, `se`) remain full-N because almost everything matches —
correct and unavoidable without a prefix index.
## Fix 2 — build the installed plugin optimized (recommended)
## Fix 2 — build the installed plugin optimized (done)
The shipped `olliepalette.so` is a **Debug** build (`-g`, no `-O`). Release is
~5-8× faster on every hot path above. Install a `Release` or `RelWithDebInfo`
build.
The root `CMakeLists.txt` now defaults `CMAKE_BUILD_TYPE` to `RelWithDebInfo`
when the user does not specify one, so an unqualified `cmake -B build` no longer
ships a `-O0` plugin. Measured effect on the symbol switcher (mobydick,
type-forward "serv", 17k symbols): **~340 ms → ~29 ms per keystroke** (~12×).
## Fix 3 — debounce the filter (done)
`PaletteWidget` coalesces rapid keystrokes with an 80 ms single-shot timer
before re-ranking (`applyQueryNow` flushes on Enter so activation always uses
the latest text). A fast typist's burst (`s`,`se`,`ser`…) triggers one re-rank
instead of three, so the broad-query early keystrokes — the only ones that still
scan most of N — are paid at most once per pause, not per character.
Net result after fixes 1-3: the worst single symbol keystroke on a 17k-symbol
project is ~29 ms (RelWithDebInfo), and bursts are coalesced; the Alt+G panel
is responsive.
## Follow-up — move indexing off the UI thread (done)

View File

@ -11,6 +11,7 @@
#include <QStyledItemDelegate>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTimer>
#include <QVBoxLayout>
#include <QPainter>
#include <QPoint>
@ -118,12 +119,19 @@ PaletteWidget::PaletteWidget(QWidget *parent)
layout->addWidget(m_filter);
layout->addWidget(m_list);
// Live, company-style updates: re-rank and keep the top row selected.
// Live updates, but debounced: a fast typist generates several keystrokes
// in a few ms, and on a large project each re-rank scores thousands of
// items. Coalesce bursts into a single re-rank so early (broad) queries do
// not stall the UI. 80 ms is below the threshold of feeling laggy while
// still absorbing a typing burst.
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(80);
connect(m_filterDebounce, &QTimer::timeout, this, [this]() { applyQueryNow(); });
connect(m_filter, &QLineEdit::textChanged, this, [this](const QString &text) {
m_model->setQuery(text);
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
}
m_pendingQuery = text;
m_filterDebounce->start(); // (re)arm; applied on timeout
});
// Mouse: a click activates immediately.
@ -143,9 +151,20 @@ void PaletteWidget::setItems(const QList<PaletteItem> &items)
}
}
void PaletteWidget::applyQueryNow()
{
m_filterDebounce->stop();
m_model->setQuery(m_pendingQuery);
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
}
}
void PaletteWidget::activate()
{
m_filterDebounce->stop();
m_filter->clear();
m_pendingQuery.clear();
m_model->setQuery(QString());
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
@ -170,6 +189,11 @@ void PaletteWidget::moveSelection(int delta)
void PaletteWidget::acceptCurrent()
{
// Flush any pending debounced query so Enter acts on the latest text even
// if the user typed and immediately hit Enter within the debounce window.
if (m_filterDebounce->isActive()) {
applyQueryNow();
}
const int row = m_list->currentIndex().row();
const PaletteItem *item = m_model->itemAt(row);
if (!item) {

View File

@ -19,9 +19,11 @@
#include <QFrame>
#include <QList>
#include <QString>
class QLineEdit;
class QListView;
class QTimer;
namespace katecustom
{
@ -52,10 +54,14 @@ private:
void moveSelection(int delta);
void acceptCurrent();
void dismiss();
// Apply the pending filter text to the model now (flushes the debounce).
void applyQueryNow();
PaletteModel *m_model;
QLineEdit *m_filter;
QListView *m_list;
QTimer *m_filterDebounce; // coalesces rapid keystrokes before re-ranking
QString m_pendingQuery;
};
} // namespace katecustom