fix: rescan performance overhaul, background thread, WinDbg regions

Move rescan to background thread via ScanEngine::startRescan() to
prevent UI freeze. Fix populateTable bottleneck caused by
QHeaderView::ResizeToContents iterating all rows (6s -> 0ms for 512
results). Add chunked batch reads (256KB spans), enumerateRegions()
for WinDbg/ProcessMemory providers, cancel support, and diagnostic
logging throughout the scanner pipeline.
This commit is contained in:
IChooseYou
2026-02-28 12:53:25 -07:00
committed by IChooseYou
parent 41e2f9f662
commit 851d744263
14 changed files with 910 additions and 104 deletions

View File

@@ -440,6 +440,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setCentralWidget(m_mdiArea);
createWorkspaceDock();
createScannerDock();
createMenus();
createStatusBar();
@@ -611,6 +612,11 @@ void MainWindow::createMenus() {
view->addSeparator();
view->addAction(m_workspaceDock->toggleViewAction());
{
auto* scanAct = m_scannerDock->toggleViewAction();
scanAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S));
view->addAction(scanAct);
}
// Tools
auto* tools = m_titleBar->menuBar()->addMenu("&Tools");
@@ -1813,6 +1819,25 @@ void MainWindow::applyTheme(const Theme& theme) {
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Scanner dock
if (m_scannerPanel)
m_scannerPanel->applyTheme(theme);
if (m_scanDockTitle) {
QPalette lp = m_scanDockTitle->palette();
lp.setColor(QPalette::WindowText, theme.textDim);
m_scanDockTitle->setPalette(lp);
}
if (auto* titleBar = m_scannerDock ? m_scannerDock->titleBarWidget() : nullptr) {
QPalette tbPal = titleBar->palette();
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
titleBar->setPalette(tbPal);
}
if (m_scanDockCloseBtn)
m_scanDockCloseBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Rendered C/C++ views: update lexer colors, paper, margins
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
@@ -1933,6 +1958,11 @@ void MainWindow::setEditorFont(const QString& fontName) {
// Sync dock titlebar font
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(f);
// Sync scanner panel font
if (m_scannerPanel)
m_scannerPanel->setEditorFont(f);
if (m_scanDockTitle)
m_scanDockTitle->setFont(f);
}
RcxController* MainWindow::activeController() const {
@@ -2814,6 +2844,87 @@ void MainWindow::createWorkspaceDock() {
});
}
// ── Scanner Dock ──
void MainWindow::createScannerDock() {
m_scannerDock = new QDockWidget("Scanner", this);
m_scannerDock->setObjectName("ScannerDock");
m_scannerDock->setAllowedAreas(
Qt::BottomDockWidgetArea | Qt::RightDockWidgetArea | Qt::LeftDockWidgetArea);
m_scannerDock->setFeatures(
QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable |
QDockWidget::DockWidgetFloatable);
// Custom titlebar: label + close button (matches workspace dock)
{
const auto& t = ThemeManager::instance().current();
auto* titleBar = new QWidget(m_scannerDock);
titleBar->setFixedHeight(24);
titleBar->setAutoFillBackground(true);
{
QPalette tbPal = titleBar->palette();
tbPal.setColor(QPalette::Window, t.backgroundAlt);
titleBar->setPalette(tbPal);
}
auto* layout = new QHBoxLayout(titleBar);
layout->setContentsMargins(6, 2, 2, 2);
layout->setSpacing(0);
m_scanDockTitle = new QLabel("Scanner", titleBar);
{
QPalette lp = m_scanDockTitle->palette();
lp.setColor(QPalette::WindowText, t.textDim);
m_scanDockTitle->setPalette(lp);
}
layout->addWidget(m_scanDockTitle);
layout->addStretch();
m_scanDockCloseBtn = new QToolButton(titleBar);
m_scanDockCloseBtn->setText(QStringLiteral("\u2715"));
m_scanDockCloseBtn->setAutoRaise(true);
m_scanDockCloseBtn->setCursor(Qt::PointingHandCursor);
m_scanDockCloseBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
"QToolButton:hover { color: %2; }")
.arg(t.textDim.name(), t.indHoverSpan.name()));
connect(m_scanDockCloseBtn, &QToolButton::clicked, m_scannerDock, &QDockWidget::close);
layout->addWidget(m_scanDockCloseBtn);
m_scannerDock->setTitleBarWidget(titleBar);
}
m_scannerPanel = new ScannerPanel(m_scannerDock);
m_scannerPanel->applyTheme(ThemeManager::instance().current());
{
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
m_scannerPanel->setEditorFont(f);
m_scanDockTitle->setFont(f);
}
m_scannerDock->setWidget(m_scannerPanel);
addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock);
m_scannerDock->hide();
// Wire provider getter: lazily captures the active tab's provider at scan time
m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr<rcx::Provider> {
auto* ctrl = activeController();
return ctrl ? ctrl->document()->provider : nullptr;
});
// Wire "Go to Address" to rebase the active tab
connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) {
auto* ctrl = activeController();
if (!ctrl) return;
ctrl->document()->tree.baseAddress = addr;
ctrl->document()->tree.baseAddressFormula.clear();
ctrl->refresh();
});
}
void MainWindow::rebuildAllDocs() {
m_allDocs.clear();
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)

View File

@@ -2,6 +2,7 @@
#include "controller.h"
#include "titlebar.h"
#include "pluginmanager.h"
#include "scannerpanel.h"
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
@@ -152,6 +153,13 @@ private:
void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color);
// Scanner dock
QDockWidget* m_scannerDock = nullptr;
ScannerPanel* m_scannerPanel = nullptr;
QLabel* m_scanDockTitle = nullptr;
QToolButton* m_scanDockCloseBtn = nullptr;
void createScannerDock();
protected:
void changeEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;

View File

@@ -1,11 +1,21 @@
#pragma once
#include <QByteArray>
#include <QString>
#include <QVector>
#include <cstdint>
#include <cstring>
namespace rcx {
struct MemoryRegion {
uint64_t base = 0;
uint64_t size = 0;
bool readable = true;
bool writable = false;
bool executable = false;
QString moduleName;
};
class Provider {
public:
virtual ~Provider() = default;
@@ -54,6 +64,11 @@ public:
return 0;
}
// Enumerate committed/readable memory regions.
// Used by the scan engine to know what address ranges to scan.
// Default: returns empty (scan engine falls back to [0, size())).
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }

View File

@@ -1,8 +1,11 @@
#include "scanner.h"
#include <QtConcurrent>
#include <QMetaObject>
#include <QElapsedTimer>
#include <QDebug>
#include <cstring>
#include <cmath>
#include <algorithm>
namespace rcx {
@@ -393,12 +396,20 @@ void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& re
QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
const ScanRequest& req)
{
QElapsedTimer timer;
timer.start();
QVector<ScanResult> results;
if (!prov || req.pattern.isEmpty())
return results;
auto regions = prov->enumerateRegions();
qDebug() << "[scan] regions:" << regions.size()
<< " pattern:" << req.pattern.size() << "bytes"
<< " align:" << req.alignment
<< " filterExec:" << req.filterExecutable
<< " filterWrite:" << req.filterWritable;
// Fallback for providers that don't enumerate regions (file/buffer)
if (regions.isEmpty()) {
@@ -424,6 +435,8 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
totalBytes += r.size;
}
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
if (totalBytes == 0) return results;
uint64_t scannedBytes = 0;
@@ -473,6 +486,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
ScanResult r;
r.address = region.base + off + (uint64_t)i;
r.regionModule = region.moduleName;
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
results.append(r);
if (results.size() >= req.maxResults)
@@ -501,6 +515,100 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
}
done:
qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
<< " scanned:" << (scannedBytes / 1024) << "KB";
return results;
}
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize) {
if (isRunning()) return;
m_abort.store(false);
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
m_watcher = watcher;
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
auto results = watcher->result();
watcher->deleteLater();
if (m_watcher == watcher)
m_watcher = nullptr;
emit rescanFinished(results);
});
watcher->setFuture(QtConcurrent::run(
[this, provider, results = std::move(results), readSize]() mutable {
return runRescan(provider, std::move(results), readSize);
}));
}
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize) {
QElapsedTimer timer;
timer.start();
int total = results.size();
if (total == 0 || !prov) return results;
qDebug() << "[rescan] start: " << total << "results, readSize:" << readSize;
// Save previous values
for (auto& r : results)
r.previousValue = r.scanValue;
// Sort indices by address for sequential chunked reads
QVector<int> order(total);
for (int i = 0; i < total; i++) order[i] = i;
std::sort(order.begin(), order.end(), [&results](int a, int b) {
return results[a].address < results[b].address;
});
constexpr int kChunk = 256 * 1024;
int updated = 0;
int lastPct = -1;
int chunks = 0;
uint64_t totalBytesRead = 0;
int i = 0;
while (i < total && !m_abort.load()) {
uint64_t spanBase = results[order[i]].address;
int spanEnd = i;
// Extend span while next result fits in the same chunk
while (spanEnd + 1 < total) {
uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
if (endAddr - spanBase > (uint64_t)kChunk) break;
spanEnd++;
}
uint64_t spanLast = results[order[spanEnd]].address;
int chunkLen = (int)(spanLast + readSize - spanBase);
QByteArray chunk(chunkLen, '\0');
prov->read(spanBase, chunk.data(), chunkLen);
for (int j = i; j <= spanEnd; j++) {
auto& r = results[order[j]];
int off = (int)(r.address - spanBase);
r.scanValue = chunk.mid(off, readSize);
}
chunks++;
totalBytesRead += chunkLen;
updated += (spanEnd - i + 1);
i = spanEnd + 1;
int pct = updated * 100 / total;
if (pct != lastPct) {
lastPct = pct;
QMetaObject::invokeMethod(this, "progress",
Qt::QueuedConnection, Q_ARG(int, pct));
}
}
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
<< timer.elapsed() << "ms |" << chunks << "chunks,"
<< (totalBytesRead / 1024) << "KB read";
return results;
}

View File

@@ -65,16 +65,21 @@ public:
explicit ScanEngine(QObject* parent = nullptr);
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
void startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize);
void abort();
bool isRunning() const;
signals:
void progress(int percent);
void finished(QVector<ScanResult> results);
void rescanFinished(QVector<ScanResult> results);
void error(QString message);
private:
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize);
std::atomic<bool> m_abort{false};
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;

View File

@@ -1,6 +1,8 @@
#include "scannerpanel.h"
#include "addressparser.h"
#include <cstring>
#include <QElapsedTimer>
#include <QDebug>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
@@ -135,8 +137,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
m_resultTable->setColumnCount(2);
m_resultTable->horizontalHeader()->hide();
m_resultTable->verticalHeader()->hide();
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_resultTable->horizontalHeader()->setStretchLastSection(false);
m_resultTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
m_resultTable->horizontalHeader()->setStretchLastSection(true);
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection);
m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked);
@@ -169,11 +171,12 @@ ScannerPanel::ScannerPanel(QWidget* parent)
mainLayout->addLayout(actionRow);
// ── Initial visibility: signature mode ──
// ── Initial state: signature mode ──
m_typeLabel->hide();
m_typeCombo->hide();
m_valueLabel->hide();
m_valueEdit->hide();
m_execCheck->setChecked(true);
// ── Connections ──
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
@@ -225,6 +228,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
});
connect(m_engine, &ScanEngine::finished,
this, &ScannerPanel::onScanFinished);
connect(m_engine, &ScanEngine::rescanFinished,
this, &ScannerPanel::onRescanFinished);
connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) {
m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg));
m_scanBtn->setText(QStringLiteral("Scan"));
@@ -240,6 +245,8 @@ void ScannerPanel::setEditorFont(const QFont& font) {
m_resultTable->setFont(font);
QFontMetrics fm(font);
m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6);
// Address column width: "00000000`00000000" + padding
m_resultTable->setColumnWidth(0, fm.horizontalAdvance(QStringLiteral("00000000`00000000")) + 20);
m_patternEdit->setFont(font);
m_valueEdit->setFont(font);
m_modeCombo->setFont(font);
@@ -275,15 +282,16 @@ void ScannerPanel::onModeChanged(int index) {
m_typeCombo->setVisible(!isSig);
m_valueLabel->setVisible(!isSig);
m_valueEdit->setVisible(!isSig);
// Auto-toggle filters: signatures → executable code, values → writable data
m_execCheck->setChecked(isSig);
m_writeCheck->setChecked(!isSig);
}
void ScannerPanel::onScanClicked() {
if (m_engine->isRunning()) {
m_engine->abort();
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
m_statusLabel->setText(QStringLiteral("Scan cancelled"));
return;
return; // finished/rescanFinished handler resets UI
}
// Get provider
@@ -344,41 +352,40 @@ ScanRequest ScannerPanel::buildRequest() {
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
m_results = results;
m_results = std::move(results);
// Cache scan-time bytes
if (m_lastScanMode == 1) {
// Value mode — every result matched the same value, no re-read needed
for (auto& r : m_results) {
r.previousValue.clear();
// Bytes are cached by the engine during scan.
// Value mode: override with exact search pattern (engine caches raw chunk bytes).
for (auto& r : m_results) {
r.previousValue.clear();
if (m_lastScanMode == 1)
r.scanValue = m_lastPattern;
}
} else {
// Signature mode — wildcards mean each match may differ, read actual bytes
std::shared_ptr<Provider> prov;
if (m_providerGetter)
prov = m_providerGetter();
for (auto& r : m_results) {
r.previousValue.clear();
r.scanValue = prov ? prov->readBytes(r.address, 16) : QByteArray();
}
}
m_updateBtn->setEnabled(!m_results.isEmpty());
populateTable(false);
{
QElapsedTimer pt;
pt.start();
populateTable(false);
qDebug() << "[panel] populateTable(initial):" << m_results.size()
<< "results," << pt.elapsed() << "ms";
}
int n = m_results.size();
m_statusLabel->setText(QStringLiteral("%1 result%2")
.arg(m_results.size())
.arg(m_results.size() == 1 ? "" : "s"));
.arg(n).arg(n == 1 ? "" : "s"));
}
void ScannerPanel::populateTable(bool showPrevious) {
constexpr int kMaxRows = 10000;
m_resultTable->blockSignals(true);
int cols = showPrevious ? 3 : 2;
m_resultTable->setColumnCount(cols);
m_resultTable->setRowCount(m_results.size());
int displayCount = qMin(m_results.size(), kMaxRows);
m_resultTable->setRowCount(displayCount);
for (int i = 0; i < m_results.size(); i++) {
for (int i = 0; i < displayCount; i++) {
const auto& r = m_results[i];
// Address column — WinDbg backtick format: 00000000`00000000
@@ -402,13 +409,11 @@ void ScannerPanel::populateTable(bool showPrevious) {
}
}
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_resultTable->horizontalHeader()->setStretchLastSection(false);
m_resultTable->blockSignals(false);
}
void ScannerPanel::onUpdateClicked() {
if (m_results.isEmpty()) return;
if (m_results.isEmpty() || m_engine->isRunning()) return;
std::shared_ptr<Provider> prov;
if (m_providerGetter)
@@ -418,55 +423,34 @@ void ScannerPanel::onUpdateClicked() {
return;
}
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
m_updateBtn->setEnabled(false);
m_scanBtn->setEnabled(false);
m_scanBtn->setText(QStringLiteral("Cancel"));
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
m_progressBar->setValue(0);
m_progressBar->show();
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
int total = m_results.size();
m_engine->startRescan(prov, m_results, readSize);
}
// Single pass: read new values + build table rows
m_resultTable->blockSignals(true);
m_resultTable->setColumnCount(3);
m_resultTable->setRowCount(total);
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
m_results = std::move(results);
m_updateBtn->setEnabled(!m_results.isEmpty());
for (int i = 0; i < total; i++) {
auto& r = m_results[i];
r.previousValue = r.scanValue;
r.scanValue = prov->readBytes(r.address, readSize);
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
hexPart.insert(8, '`');
auto* addrItem = new QTableWidgetItem(hexPart);
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
m_resultTable->setItem(i, 0, addrItem);
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
m_resultTable->setItem(i, 1, valItem);
auto* prevItem = new QTableWidgetItem(formatValue(r.previousValue));
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
m_resultTable->setItem(i, 2, prevItem);
if ((i & 0xFF) == 0) {
m_progressBar->setValue(i * 100 / total);
QApplication::processEvents();
}
{
QElapsedTimer pt;
pt.start();
populateTable(true);
qDebug() << "[panel] populateTable(rescan):" << m_results.size()
<< "results," << pt.elapsed() << "ms";
}
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_resultTable->horizontalHeader()->setStretchLastSection(false);
m_resultTable->blockSignals(false);
m_progressBar->setValue(100);
m_progressBar->hide();
m_scanBtn->setEnabled(true);
m_updateBtn->setEnabled(true);
int n = m_results.size();
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
.arg(total).arg(total == 1 ? "" : "s"));
.arg(n).arg(n == 1 ? "" : "s"));
}
void ScannerPanel::onGoToAddress() {

View File

@@ -65,6 +65,7 @@ private slots:
void onResultDoubleClicked(int row, int col);
void onCellEdited(int row, int col);
void onUpdateClicked();
void onRescanFinished(QVector<ScanResult> results);
private:
ScanRequest buildRequest();