mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: workspace panel visual overhaul, perf optimizations, remove kernel base addresses
Workspace panel: - Custom WorkspaceDelegate: struct names bright, metadata dimmed, child types in teal - Search box: monospace font, search icon, bordered with focus highlight - Selection: accent bar, all fonts synced to 10pt monospace - Remove rebuildWorkspaceModel from visibilityChanged (fixes double-click refresh) - Incremental sync (syncProjectExplorer) preserves tree expansion state Performance: - childrenOf() O(1) via cached parent→children hash map - Debounced workspace rebuilds (50ms coalesce) - Pre-reserve node vector in NodeTree::fromJson - Benchmark suite (bench_project) Data: - Remove kernel baseAddress from Vergilius/WinSDK examples (default to 0x400000)
This commit is contained in:
282
tests/bench_project.cpp
Normal file
282
tests/bench_project.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* bench_project — benchmark project lifecycle operations:
|
||||
* - New class creation
|
||||
* - Loading large .rcx files (WinSDK, Vergilius)
|
||||
* - Workspace model building
|
||||
* - Workspace search filtering
|
||||
* - JSON parsing vs model building breakdown
|
||||
*/
|
||||
#include <QtTest/QtTest>
|
||||
#include <QElapsedTimer>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include "core.h"
|
||||
#include "controller.h"
|
||||
#include "workspace_model.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class BenchProject : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void benchNewClass();
|
||||
void benchLoadVergilius();
|
||||
void benchLoadWinSDK();
|
||||
void benchJsonParse();
|
||||
void benchNodeTreeFromJson();
|
||||
void benchBuildWorkspaceModel();
|
||||
void benchWorkspaceSearch();
|
||||
};
|
||||
|
||||
static QString findExample(const QString& name) {
|
||||
// Try relative to executable, then common build layout
|
||||
QStringList candidates = {
|
||||
QCoreApplication::applicationDirPath() + "/examples/" + name,
|
||||
QCoreApplication::applicationDirPath() + "/../src/examples/" + name,
|
||||
QStringLiteral("src/examples/") + name,
|
||||
QStringLiteral("../src/examples/") + name,
|
||||
};
|
||||
for (const auto& c : candidates)
|
||||
if (QFileInfo::exists(c)) return c;
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── New class (just the core operations, no UI) ──
|
||||
|
||||
void BenchProject::benchNewClass()
|
||||
{
|
||||
const int ITERS = 1000;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("NewClass");
|
||||
root.structTypeName = QStringLiteral("NewClass");
|
||||
root.classKeyword = QStringLiteral("class");
|
||||
tree.addNode(root);
|
||||
// Add 8 hex64 padding fields (what buildEmptyStruct does)
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Hex64;
|
||||
pad.name = QString();
|
||||
pad.parentId = rootId;
|
||||
pad.offset = j * 8;
|
||||
tree.addNode(pad);
|
||||
}
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== New Class (core tree build) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-new:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Load .rcx files ──
|
||||
|
||||
static bool loadRcx(const QString& path, NodeTree& tree) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) return false;
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
tree = NodeTree::fromJson(jdoc.object());
|
||||
return !tree.nodes.isEmpty();
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadVergilius()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) { QSKIP("Vergilius_25H2.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load Vergilius_25H2.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadWinSDK()
|
||||
{
|
||||
QString path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("WinSDK.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load WinSDK.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Breakdown: JSON parse vs NodeTree build ──
|
||||
|
||||
void BenchProject::benchJsonParse()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("No large .rcx found"); return; }
|
||||
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const int ITERS = 5;
|
||||
|
||||
// Phase 1: raw JSON parse
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QJsonDocument jdoc;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
jdoc = QJsonDocument::fromJson(data);
|
||||
qint64 jsonMs = timer.elapsed();
|
||||
|
||||
// Phase 2: NodeTree::fromJson
|
||||
QJsonObject root = jdoc.object();
|
||||
timer.start();
|
||||
NodeTree tree;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
tree = NodeTree::fromJson(root);
|
||||
qint64 treeMs = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== JSON Parse Breakdown ===" << QFileInfo(path).fileName();
|
||||
qDebug() << " File:" << data.size() / 1024 << "KB," << tree.nodes.size() << "nodes";
|
||||
qDebug() << " JSON parse:" << (double)jsonMs / ITERS << "ms/iter";
|
||||
qDebug() << " NodeTree build:" << (double)treeMs / ITERS << "ms/iter";
|
||||
qDebug() << " Total per-load:" << (double)(jsonMs + treeMs) / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchNodeTreeFromJson()
|
||||
{
|
||||
// Already covered by benchJsonParse breakdown
|
||||
QVERIFY(true);
|
||||
}
|
||||
|
||||
// ── Workspace model building ──
|
||||
|
||||
void BenchProject::benchBuildWorkspaceModel()
|
||||
{
|
||||
// Load both large examples if available
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
// Build TabInfo array
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
const int ITERS = 20;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
buildProjectExplorer(&model, tabs);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
// Count items
|
||||
int topLevel = model.rowCount();
|
||||
int totalChildren = 0;
|
||||
for (int i = 0; i < topLevel; ++i)
|
||||
totalChildren += model.item(i)->rowCount();
|
||||
|
||||
int totalNodes = 0;
|
||||
for (const auto& t : trees) totalNodes += t.nodes.size();
|
||||
fprintf(stderr, "\n=== Build Workspace Model ===\n");
|
||||
fprintf(stderr, " Trees: %d total nodes: %d\n", (int)trees.size(), totalNodes);
|
||||
fprintf(stderr, " Top-level items: %d child items: %d\n", topLevel, totalChildren);
|
||||
fprintf(stderr, " Iterations: %d\n", ITERS);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-build: %.1f ms\n", (double)elapsed / ITERS);
|
||||
}
|
||||
|
||||
// ── Workspace search filtering ──
|
||||
|
||||
void BenchProject::benchWorkspaceSearch()
|
||||
{
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QSortFilterProxyModel proxy;
|
||||
proxy.setSourceModel(&model);
|
||||
proxy.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy.setRecursiveFilteringEnabled(true);
|
||||
|
||||
const QStringList queries = {
|
||||
"EPROCESS", "KTHREAD", "LIST_ENTRY", "HAL", "DMA",
|
||||
"xyz_no_match", "a", "Dispatch"
|
||||
};
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
for (const auto& q : queries)
|
||||
proxy.setFilterFixedString(q);
|
||||
proxy.setFilterFixedString(QString()); // clear
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
int totalOps = ITERS * (queries.size() + 1);
|
||||
fprintf(stderr, "\n=== Workspace Search Filter ===\n");
|
||||
fprintf(stderr, " Model rows: %d queries: %d\n", model.rowCount(), (int)queries.size());
|
||||
fprintf(stderr, " Iterations: %d total filter ops: %d\n", ITERS, totalOps);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-filter: %.2f ms\n", (double)elapsed / totalOps);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchProject)
|
||||
#include "bench_project.moc"
|
||||
@@ -517,16 +517,11 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Single "Project" root
|
||||
// Flat model: Player at root (has 2 non-hex members → lazy placeholder)
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->text(), QString("Project"));
|
||||
|
||||
// 1 type directly under Project: Player (no member fields)
|
||||
QCOMPARE(project->rowCount(), 1);
|
||||
QVERIFY(project->child(0)->text().contains("Player"));
|
||||
QVERIFY(project->child(0)->text().contains("struct"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QVERIFY(model.item(0)->text().contains("Player"));
|
||||
QVERIFY(model.item(0)->text().contains("struct"));
|
||||
QVERIFY(model.item(0)->rowCount() > 0); // children populated directly
|
||||
}
|
||||
|
||||
void testWorkspace_twoRootTree() {
|
||||
@@ -535,15 +530,10 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
|
||||
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
||||
QCOMPARE(project->rowCount(), 2);
|
||||
QVERIFY(project->child(0)->text().contains("Alpha"));
|
||||
QVERIFY(project->child(1)->text().contains("Bravo"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
// Flat model: 2 types at root
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QVERIFY(model.item(0)->text().contains("Alpha"));
|
||||
QVERIFY(model.item(1)->text().contains("Bravo"));
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_rootCount() {
|
||||
@@ -552,25 +542,19 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||
QCOMPARE(model.rowCount(), 3); // Ball, Cat, Pet
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_sorted() {
|
||||
void testWorkspace_richTree_insertionOrder() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Sorted alphabetically: Ball, Cat, Pet
|
||||
QVERIFY(project->child(0)->text().contains("Ball"));
|
||||
QVERIFY(project->child(1)->text().contains("Cat"));
|
||||
QVERIFY(project->child(2)->text().contains("Pet"));
|
||||
// No member fields under type nodes
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
QCOMPARE(project->child(2)->rowCount(), 0);
|
||||
// Types at root in insertion order: Pet, Cat, Ball
|
||||
QVERIFY(model.item(0)->text().contains("Pet"));
|
||||
QVERIFY(model.item(1)->text().contains("Cat"));
|
||||
QVERIFY(model.item(2)->text().contains("Ball"));
|
||||
}
|
||||
|
||||
void testWorkspace_emptyTree() {
|
||||
@@ -579,10 +563,8 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Still has the "Project" root, just no children
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.item(0)->text(), QString("Project"));
|
||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
||||
// Flat model: no types means no rows
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_structIdRole() {
|
||||
@@ -591,15 +573,11 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Project root has kGroupSentinel
|
||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||
|
||||
// Player type item should have structId
|
||||
QStandardItem* player = project->child(0);
|
||||
// Flat model: first item is the Player type with its structId
|
||||
QVERIFY(model.rowCount() > 0);
|
||||
QStandardItem* player = model.item(0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user