mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
perf: TypeSelector — zero-alloc fuzzy scorer, warm popup 75% faster
Stack arrays + pre-lowered QChars in fuzzyScore eliminate all heap allocations in the hot path. applyFilter uses indices instead of deep-copying TypeEntry. popup() width estimated from cached max name length. QListView: uniform sizes, batched layout, cached sizeHint. Benchmark (5000 structs): warm popup 27ms→7ms, filter 5ms→1.7ms.
This commit is contained in:
@@ -57,51 +57,73 @@ TypeSpec parseTypeSpec(const QString& text) {
|
||||
}
|
||||
|
||||
// ── Fuzzy scorer: subsequence match with word-boundary bonuses ──
|
||||
// Hot path — uses stack arrays and pre-lowered QChars to avoid heap allocs.
|
||||
|
||||
static constexpr int kMaxFuzzyLen = 64;
|
||||
|
||||
static int fuzzyScore(const QString& pattern, const QString& text,
|
||||
QVector<int>* outPositions = nullptr) {
|
||||
int pLen = pattern.size(), tLen = text.size();
|
||||
if (pLen == 0) return 1;
|
||||
if (pLen > tLen) return 0;
|
||||
if (pLen > kMaxFuzzyLen || tLen > 256) {
|
||||
// Fallback: prefix match only for very long names
|
||||
if (text.startsWith(pattern, Qt::CaseInsensitive)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Quick subsequence reject
|
||||
// Pre-compute lowercase chars on the stack
|
||||
QChar pLow[kMaxFuzzyLen];
|
||||
for (int i = 0; i < pLen; i++) pLow[i] = pattern[i].toLower();
|
||||
QChar tLow[256];
|
||||
for (int i = 0; i < tLen; i++) tLow[i] = text[i].toLower();
|
||||
|
||||
// Quick subsequence reject using pre-lowered arrays
|
||||
{ int pi = 0;
|
||||
for (int ti = 0; ti < tLen && pi < pLen; ti++)
|
||||
if (pattern[pi].toLower() == text[ti].toLower()) pi++;
|
||||
if (pLow[pi] == tLow[ti]) pi++;
|
||||
if (pi < pLen) return 0;
|
||||
}
|
||||
|
||||
// Recursive best-match (bounded: max 4 branches per pattern char)
|
||||
QVector<int> bestPos;
|
||||
// Stack arrays instead of QVector to avoid heap allocation
|
||||
int bestPos[kMaxFuzzyLen];
|
||||
int curPos[kMaxFuzzyLen];
|
||||
int best = 0;
|
||||
int bestLen = 0;
|
||||
|
||||
auto solve = [&](auto& self, int pi, int ti, QVector<int>& cur, int score) -> void {
|
||||
auto solve = [&](auto& self, int pi, int ti, int curLen, int score) -> void {
|
||||
if (pi == pLen) {
|
||||
if (score > best) { best = score; bestPos = cur; }
|
||||
if (score > best) {
|
||||
best = score;
|
||||
bestLen = curLen;
|
||||
memcpy(bestPos, curPos, curLen * sizeof(int));
|
||||
}
|
||||
return;
|
||||
}
|
||||
int maxTi = tLen - (pLen - pi);
|
||||
int branches = 0;
|
||||
for (int i = ti; i <= maxTi && branches < 4; i++) {
|
||||
if (pattern[pi].toLower() != text[i].toLower()) continue;
|
||||
if (pLow[pi] != tLow[i]) continue;
|
||||
int bonus = 1;
|
||||
if (i == 0) bonus = 10;
|
||||
else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8;
|
||||
else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8;
|
||||
if (!cur.isEmpty() && i == cur.last() + 1) bonus += 5;
|
||||
cur.append(i);
|
||||
self(self, pi + 1, i + 1, cur, score + bonus);
|
||||
cur.removeLast();
|
||||
if (curLen > 0 && i == curPos[curLen - 1] + 1) bonus += 5;
|
||||
curPos[curLen] = i;
|
||||
self(self, pi + 1, i + 1, curLen + 1, score + bonus);
|
||||
branches++;
|
||||
}
|
||||
};
|
||||
|
||||
QVector<int> cur;
|
||||
solve(solve, 0, 0, cur, 0);
|
||||
solve(solve, 0, 0, 0, 0);
|
||||
if (best > 0) {
|
||||
best += qMax(0, 20 - (tLen - pLen)); // tightness bonus
|
||||
if (pLen == tLen) best += 20; // exact match bonus
|
||||
if (outPositions) *outPositions = bestPos;
|
||||
if (outPositions) {
|
||||
outPositions->resize(bestLen);
|
||||
memcpy(outPositions->data(), bestPos, bestLen * sizeof(int));
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -113,7 +135,7 @@ public:
|
||||
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
|
||||
: QStyledItemDelegate(parent), m_popup(popup) {}
|
||||
|
||||
void setFont(const QFont& f) { m_font = f; }
|
||||
void setFont(const QFont& f) { m_font = f; updateCachedSizeHint(); }
|
||||
void setLoading(bool v) { m_isLoading = v; }
|
||||
void setFilteredTypes(const QVector<TypeEntry>* filtered) {
|
||||
m_filtered = filtered;
|
||||
@@ -287,13 +309,13 @@ public:
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
||||
const QModelIndex& index) const override {
|
||||
const QModelIndex& /*index*/) const override {
|
||||
return m_cachedSizeHint;
|
||||
}
|
||||
|
||||
void updateCachedSizeHint() {
|
||||
QFontMetrics fm(m_font);
|
||||
int row = index.row();
|
||||
bool isSection = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].entryKind == TypeEntry::Section);
|
||||
int h = isSection ? fm.height() + 2 : fm.height() + 8;
|
||||
return QSize(200, h);
|
||||
m_cachedSizeHint = QSize(200, fm.height() + 8);
|
||||
}
|
||||
|
||||
bool helpEvent(QHelpEvent* event, QAbstractItemView* view,
|
||||
@@ -322,6 +344,7 @@ public:
|
||||
private:
|
||||
TypeSelectorPopup* m_popup = nullptr;
|
||||
QFont m_font;
|
||||
QSize m_cachedSizeHint{200, 20};
|
||||
bool m_isLoading = false;
|
||||
const QVector<TypeEntry>* m_filtered = nullptr;
|
||||
const QVector<QVector<int>>* m_matchPositions = nullptr;
|
||||
@@ -448,6 +471,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->setAccessibleName(QStringLiteral("Type list"));
|
||||
m_listView->setUniformItemSizes(true);
|
||||
m_listView->setLayoutMode(QListView::Batched);
|
||||
m_listView->setBatchSize(50);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
auto* delegate = new TypeSelectorDelegate(this, m_listView);
|
||||
@@ -826,6 +852,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
if (delegate) delegate->setLoading(false);
|
||||
|
||||
m_allTypes = types;
|
||||
// Cache max display name length for popup width calculation
|
||||
m_cachedMaxNameLen = 0;
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (t.entryKind != TypeEntry::Section)
|
||||
m_cachedMaxNameLen = qMax(m_cachedMaxNameLen, (int)t.displayName.size());
|
||||
}
|
||||
if (current) {
|
||||
m_currentEntry = *current;
|
||||
m_hasCurrent = true;
|
||||
@@ -858,13 +890,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
|
||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QFontMetrics fm(m_font);
|
||||
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type "));
|
||||
for (const auto& t : m_allTypes) {
|
||||
int iconColW = fm.height() + 4;
|
||||
int w = iconColW + fm.horizontalAdvance(t.displayName) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(480, maxTextW + 24, 560);
|
||||
constexpr int kMaxPopupW = 560;
|
||||
// Estimate max width from cached max name length (avoids iterating all types)
|
||||
int iconColW = fm.height() + 4;
|
||||
int estMaxW = iconColW + fm.horizontalAdvance(QChar('W')) * m_cachedMaxNameLen + 16;
|
||||
int maxTextW = qMax(fm.horizontalAdvance(QStringLiteral("Choose element type ")), estMaxW);
|
||||
int popupW = qBound(480, maxTextW + 24, kMaxPopupW);
|
||||
int rowH = fm.height() + 8;
|
||||
int headerH = rowH * 2 + 10; // filter + chips + separator
|
||||
int footerH = rowH + 6; // separator + action row
|
||||
@@ -973,13 +1004,22 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
};
|
||||
|
||||
int primCount = 0, typeCount = 0, enumCount = 0;
|
||||
const int totalTypes = m_allTypes.size();
|
||||
|
||||
// Pre-reserve to avoid realloc churn
|
||||
m_filteredTypes.reserve(totalTypes);
|
||||
m_matchPositions.reserve(totalTypes);
|
||||
displayStrings.reserve(totalTypes);
|
||||
|
||||
if (!filterBase.isEmpty()) {
|
||||
// ── Fuzzy search: flat ranked list, no section headers ──
|
||||
struct Scored { TypeEntry entry; int score; QVector<int> pos; };
|
||||
// Use index + score to avoid deep-copying TypeEntry structs
|
||||
struct Scored { int idx; int score; QVector<int> pos; };
|
||||
QVector<Scored> scored;
|
||||
scored.reserve(totalTypes);
|
||||
|
||||
for (const auto& t : m_allTypes) {
|
||||
for (int i = 0; i < totalTypes; i++) {
|
||||
const auto& t = m_allTypes[i];
|
||||
if (t.entryKind == TypeEntry::Section) continue;
|
||||
QVector<int> pos;
|
||||
int sc = fuzzyScore(filterBase, t.displayName, &pos);
|
||||
@@ -988,15 +1028,15 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
else if (t.category == TypeEntry::CatEnum) enumCount++;
|
||||
else typeCount++;
|
||||
if (catAllowed(t))
|
||||
scored.append({t, sc, pos});
|
||||
scored.append({i, sc, std::move(pos)});
|
||||
}
|
||||
std::sort(scored.begin(), scored.end(),
|
||||
[](const Scored& a, const Scored& b) { return a.score > b.score; });
|
||||
|
||||
for (const auto& s : scored) {
|
||||
m_filteredTypes.append(s.entry);
|
||||
m_filteredTypes.append(m_allTypes[s.idx]);
|
||||
m_matchPositions.append(s.pos);
|
||||
displayStrings << makeLabel(s.entry);
|
||||
displayStrings << makeLabel(m_allTypes[s.idx]);
|
||||
}
|
||||
} else {
|
||||
// ── No filter: grouped sections, alphabetical ──
|
||||
|
||||
@@ -120,6 +120,7 @@ private:
|
||||
int m_pointerSize = 8;
|
||||
bool m_loading = false;
|
||||
QFont m_font;
|
||||
int m_cachedMaxNameLen = 0; // longest displayName length (chars)
|
||||
|
||||
void applyFilter(const QString& text);
|
||||
void updateModifierPreview();
|
||||
|
||||
@@ -322,6 +322,104 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Benchmark: large SDK (5000 structs) ──
|
||||
|
||||
void benchmarkLargeSDK() {
|
||||
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
|
||||
|
||||
// Build 5000 composite types with field summaries (simulates WinSDK)
|
||||
QVector<TypeEntry> types;
|
||||
types.reserve(5000);
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
TypeEntry e;
|
||||
e.entryKind = TypeEntry::Composite;
|
||||
e.structId = (uint64_t)(i + 1);
|
||||
e.displayName = QStringLiteral("_STRUCT_%1").arg(i, 4, 10, QChar('0'));
|
||||
e.classKeyword = QStringLiteral("struct");
|
||||
e.sizeBytes = 64 + (i % 256) * 8;
|
||||
e.alignment = 8;
|
||||
e.fieldCount = 5 + (i % 20);
|
||||
for (int f = 0; f < qMin(6, e.fieldCount); f++)
|
||||
e.fieldSummary << QStringLiteral("0x%1: int32_t field_%2")
|
||||
.arg(f * 4, 2, 16, QChar('0')).arg(f);
|
||||
types.append(e);
|
||||
}
|
||||
|
||||
QFont font("Consolas", 12);
|
||||
font.setFixedPitch(true);
|
||||
auto* popup = new TypeSelectorPopup();
|
||||
popup->warmUp();
|
||||
popup->setFont(font);
|
||||
|
||||
// Measure setTypes (data loading)
|
||||
QElapsedTimer t;
|
||||
t.start();
|
||||
popup->setTypes(types, nullptr);
|
||||
qint64 tSetTypes = t.nsecsElapsed();
|
||||
|
||||
// Measure popup show (broken down)
|
||||
t.restart();
|
||||
popup->popup(QPoint(100, 100));
|
||||
qint64 tPopupCall = t.nsecsElapsed();
|
||||
t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 tProcessEvents = t.nsecsElapsed();
|
||||
qint64 tShow = tPopupCall + tProcessEvents;
|
||||
|
||||
// Second popup show (warm)
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
t.restart();
|
||||
popup->popup(QPoint(100, 100));
|
||||
qint64 tPopup2 = t.nsecsElapsed();
|
||||
t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 tProcess2 = t.nsecsElapsed();
|
||||
|
||||
// Measure filter with 1-char (worst case: most matches)
|
||||
t.restart();
|
||||
auto* filterEdit = popup->findChild<QLineEdit*>();
|
||||
QVERIFY(filterEdit);
|
||||
|
||||
filterEdit->setText(QStringLiteral("S"));
|
||||
qint64 tFilter1 = t.nsecsElapsed();
|
||||
|
||||
// Measure filter with 3-char (moderate filtering)
|
||||
t.restart();
|
||||
filterEdit->setText(QStringLiteral("STR"));
|
||||
qint64 tFilter3 = t.nsecsElapsed();
|
||||
|
||||
// Measure filter with 6-char (narrow results)
|
||||
t.restart();
|
||||
filterEdit->setText(QStringLiteral("STRUCT"));
|
||||
qint64 tFilter6 = t.nsecsElapsed();
|
||||
|
||||
// Measure clear filter (back to grouped view)
|
||||
t.restart();
|
||||
filterEdit->setText(QString());
|
||||
qint64 tClear = t.nsecsElapsed();
|
||||
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug().noquote() << "=== Large SDK Benchmark (5000 structs) ===";
|
||||
qDebug().noquote() << QString(" setTypes: %1 ms").arg(ms(tSetTypes));
|
||||
qDebug().noquote() << QString(" popup() call: %1 ms").arg(ms(tPopupCall));
|
||||
qDebug().noquote() << QString(" processEvents: %1 ms").arg(ms(tProcessEvents));
|
||||
qDebug().noquote() << QString(" popup total: %1 ms").arg(ms(tShow));
|
||||
qDebug().noquote() << QString(" popup2() call: %1 ms (warm)").arg(ms(tPopup2));
|
||||
qDebug().noquote() << QString(" processEvents2: %1 ms (warm)").arg(ms(tProcess2));
|
||||
qDebug().noquote() << QString(" popup2 total: %1 ms (warm)").arg(ms(tPopup2 + tProcess2));
|
||||
qDebug().noquote() << QString(" filter 'S': %1 ms").arg(ms(tFilter1));
|
||||
qDebug().noquote() << QString(" filter 'STR': %1 ms").arg(ms(tFilter3));
|
||||
qDebug().noquote() << QString(" filter 'STRUCT': %1 ms").arg(ms(tFilter6));
|
||||
qDebug().noquote() << QString(" clear filter: %1 ms").arg(ms(tClear));
|
||||
QVERIFY(tSetTypes > 0);
|
||||
|
||||
delete popup;
|
||||
}
|
||||
|
||||
// ── Popup data model ──
|
||||
|
||||
void testPopupListsRootStructs() {
|
||||
|
||||
Reference in New Issue
Block a user