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:
@@ -500,6 +500,14 @@ if(BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||||
|
|
||||||
|
add_executable(bench_project tests/bench_project.cpp)
|
||||||
|
target_include_directories(bench_project PRIVATE src)
|
||||||
|
target_link_libraries(bench_project PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(bench_project PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||||
|
endif()
|
||||||
|
add_test(NAME bench_project COMMAND bench_project)
|
||||||
|
|
||||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||||
if(TARGET ${QT}::windeployqt)
|
if(TARGET ${QT}::windeployqt)
|
||||||
|
|||||||
14
src/core.h
14
src/core.h
@@ -333,6 +333,7 @@ struct NodeTree {
|
|||||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||||
uint64_t m_nextId = 1;
|
uint64_t m_nextId = 1;
|
||||||
mutable QHash<uint64_t, int> m_idCache;
|
mutable QHash<uint64_t, int> m_idCache;
|
||||||
|
mutable QHash<uint64_t, QVector<int>> m_childCache;
|
||||||
|
|
||||||
int addNode(const Node& n) {
|
int addNode(const Node& n) {
|
||||||
Node copy = n;
|
Node copy = n;
|
||||||
@@ -342,13 +343,15 @@ struct NodeTree {
|
|||||||
nodes.append(copy);
|
nodes.append(copy);
|
||||||
if (!m_idCache.isEmpty())
|
if (!m_idCache.isEmpty())
|
||||||
m_idCache[copy.id] = idx;
|
m_idCache[copy.id] = idx;
|
||||||
|
if (!m_childCache.isEmpty())
|
||||||
|
m_childCache[copy.parentId].append(idx);
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve a unique ID atomically (for use before pushing undo commands)
|
// Reserve a unique ID atomically (for use before pushing undo commands)
|
||||||
uint64_t reserveId() { return m_nextId++; }
|
uint64_t reserveId() { return m_nextId++; }
|
||||||
|
|
||||||
void invalidateIdCache() const { m_idCache.clear(); }
|
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
|
||||||
|
|
||||||
int indexOfId(uint64_t id) const {
|
int indexOfId(uint64_t id) const {
|
||||||
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
|
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
|
||||||
@@ -359,11 +362,11 @@ struct NodeTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QVector<int> childrenOf(uint64_t parentId) const {
|
QVector<int> childrenOf(uint64_t parentId) const {
|
||||||
QVector<int> result;
|
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
|
||||||
for (int i = 0; i < nodes.size(); i++) {
|
for (int i = 0; i < nodes.size(); i++)
|
||||||
if (nodes[i].parentId == parentId) result.append(i);
|
m_childCache[nodes[i].parentId].append(i);
|
||||||
}
|
}
|
||||||
return result;
|
return m_childCache.value(parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect node + all descendants (iterative, cycle-safe)
|
// Collect node + all descendants (iterative, cycle-safe)
|
||||||
@@ -483,6 +486,7 @@ struct NodeTree {
|
|||||||
t.pointerSize = o["pointerSize"].toInt(8);
|
t.pointerSize = o["pointerSize"].toInt(8);
|
||||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||||
QJsonArray arr = o["nodes"].toArray();
|
QJsonArray arr = o["nodes"].toArray();
|
||||||
|
t.nodes.reserve(arr.size());
|
||||||
for (const auto& v : arr) {
|
for (const auto& v : arr) {
|
||||||
Node n = Node::fromJson(v.toObject());
|
Node n = Node::fromJson(v.toObject());
|
||||||
t.nodes.append(n);
|
t.nodes.append(n);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"baseAddress": "FFFFF80000000000",
|
|
||||||
"nextId": "20010",
|
"nextId": "20010",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"baseAddress": "fffff80000000000",
|
|
||||||
"nextId": "18212",
|
"nextId": "18212",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|||||||
97
src/main.cpp
97
src/main.cpp
@@ -1582,7 +1582,6 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
m_activeDocDock = dock;
|
m_activeDocDock = dock;
|
||||||
updateWindowTitle();
|
updateWindowTitle();
|
||||||
rebuildWorkspaceModel();
|
|
||||||
// Sync view toggle buttons to this tab's active pane
|
// Sync view toggle buttons to this tab's active pane
|
||||||
auto it = m_tabs.find(dock);
|
auto it = m_tabs.find(dock);
|
||||||
if (it != m_tabs.end()) {
|
if (it != m_tabs.end()) {
|
||||||
@@ -2388,19 +2387,30 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
||||||
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
||||||
|
|
||||||
// Workspace tree: colors from theme (selection + text)
|
// Workspace tree: delegate colors, palette, stylesheet
|
||||||
|
if (m_workspaceDelegate)
|
||||||
|
m_workspaceDelegate->setThemeColors(theme);
|
||||||
if (m_workspaceTree) {
|
if (m_workspaceTree) {
|
||||||
QPalette tp = m_workspaceTree->palette();
|
QPalette tp = m_workspaceTree->palette();
|
||||||
tp.setColor(QPalette::Text, theme.textDim);
|
tp.setColor(QPalette::Text, theme.textDim);
|
||||||
tp.setColor(QPalette::Highlight, theme.hover);
|
tp.setColor(QPalette::Highlight, theme.selected);
|
||||||
tp.setColor(QPalette::HighlightedText, theme.text);
|
tp.setColor(QPalette::HighlightedText, theme.text);
|
||||||
m_workspaceTree->setPalette(tp);
|
m_workspaceTree->setPalette(tp);
|
||||||
|
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||||
|
"QTreeView { background: %1; border: none; }")
|
||||||
|
.arg(theme.background.name()));
|
||||||
|
m_workspaceTree->viewport()->update();
|
||||||
}
|
}
|
||||||
if (m_workspaceSearch) {
|
if (m_workspaceSearch) {
|
||||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||||
"QLineEdit { background: %1; color: %2; border: none;"
|
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
" padding: 4px 8px; }"
|
||||||
.arg(theme.background.name(), theme.textDim.name(), theme.border.name()));
|
"QLineEdit:focus { border-color: %4; }"
|
||||||
|
"QLineEdit QToolButton { padding: 0px 4px; }"
|
||||||
|
"QLineEdit QToolButton:hover { background: %5; }")
|
||||||
|
.arg(theme.background.name(), theme.textDim.name(),
|
||||||
|
theme.border.name(), theme.borderFocused.name(),
|
||||||
|
theme.hover.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dock titlebar: restyle via palette + close button
|
// Dock titlebar: restyle via palette + close button
|
||||||
@@ -2590,15 +2600,17 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sync workspace tree font (match tab bar size)
|
// Sync workspace tree, title, and search font (10pt monospace)
|
||||||
if (m_workspaceTree) {
|
{
|
||||||
QFont wf(fontName, 10);
|
QFont wf(fontName, 10);
|
||||||
wf.setFixedPitch(true);
|
wf.setFixedPitch(true);
|
||||||
m_workspaceTree->setFont(wf);
|
if (m_workspaceTree)
|
||||||
|
m_workspaceTree->setFont(wf);
|
||||||
|
if (m_dockTitleLabel)
|
||||||
|
m_dockTitleLabel->setFont(wf);
|
||||||
|
if (m_workspaceSearch)
|
||||||
|
m_workspaceSearch->setFont(wf);
|
||||||
}
|
}
|
||||||
// Sync dock titlebar font
|
|
||||||
if (m_dockTitleLabel)
|
|
||||||
m_dockTitleLabel->setFont(f);
|
|
||||||
// Sync scanner panel font
|
// Sync scanner panel font
|
||||||
if (m_scannerPanel)
|
if (m_scannerPanel)
|
||||||
m_scannerPanel->setEditorFont(f);
|
m_scannerPanel->setEditorFont(f);
|
||||||
@@ -3314,6 +3326,10 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
QPalette lp = m_dockTitleLabel->palette();
|
QPalette lp = m_dockTitleLabel->palette();
|
||||||
lp.setColor(QPalette::WindowText, t.textDim);
|
lp.setColor(QPalette::WindowText, t.textDim);
|
||||||
m_dockTitleLabel->setPalette(lp);
|
m_dockTitleLabel->setPalette(lp);
|
||||||
|
QSettings s("Reclass", "Reclass");
|
||||||
|
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||||
|
f.setFixedPitch(true);
|
||||||
|
m_dockTitleLabel->setFont(f);
|
||||||
}
|
}
|
||||||
layout->addWidget(m_dockTitleLabel);
|
layout->addWidget(m_dockTitleLabel);
|
||||||
|
|
||||||
@@ -3342,12 +3358,35 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
m_workspaceSearch = new QLineEdit(dockContainer);
|
m_workspaceSearch = new QLineEdit(dockContainer);
|
||||||
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search..."));
|
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search..."));
|
||||||
m_workspaceSearch->setClearButtonEnabled(true);
|
m_workspaceSearch->setClearButtonEnabled(true);
|
||||||
|
{
|
||||||
|
QSettings s("Reclass", "Reclass");
|
||||||
|
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||||
|
f.setFixedPitch(true);
|
||||||
|
m_workspaceSearch->setFont(f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto* searchAction = m_workspaceSearch->addAction(
|
||||||
|
QIcon(QStringLiteral(":/vsicons/search.svg")),
|
||||||
|
QLineEdit::LeadingPosition);
|
||||||
|
// Find the QToolButton created for the action and shrink its icon
|
||||||
|
for (auto* btn : m_workspaceSearch->findChildren<QToolButton*>()) {
|
||||||
|
if (btn->defaultAction() == searchAction) {
|
||||||
|
btn->setIconSize(QSize(14, 14));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||||
"QLineEdit { background: %1; color: %2; border: none;"
|
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
" padding: 4px 8px; }"
|
||||||
.arg(t.background.name(), t.textDim.name(), t.border.name()));
|
"QLineEdit:focus { border-color: %4; }"
|
||||||
|
"QLineEdit QToolButton { padding: 0px 4px; }"
|
||||||
|
"QLineEdit QToolButton:hover { background: %5; }")
|
||||||
|
.arg(t.background.name(), t.textDim.name(),
|
||||||
|
t.border.name(), t.borderFocused.name(),
|
||||||
|
t.hover.name()));
|
||||||
}
|
}
|
||||||
dockLayout->addWidget(m_workspaceSearch);
|
dockLayout->addWidget(m_workspaceSearch);
|
||||||
|
|
||||||
@@ -3380,14 +3419,22 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
m_workspaceTree->collapseAll();
|
m_workspaceTree->collapseAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Override palette: selection + hover use theme colors (not default blue)
|
// Custom delegate for rich text rendering (name bright, metadata dim)
|
||||||
{
|
{
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
|
m_workspaceDelegate = new rcx::WorkspaceDelegate(m_workspaceTree);
|
||||||
|
m_workspaceDelegate->setThemeColors(t);
|
||||||
|
m_workspaceTree->setItemDelegate(m_workspaceDelegate);
|
||||||
|
|
||||||
QPalette tp = m_workspaceTree->palette();
|
QPalette tp = m_workspaceTree->palette();
|
||||||
tp.setColor(QPalette::Text, t.textDim);
|
tp.setColor(QPalette::Text, t.textDim);
|
||||||
tp.setColor(QPalette::Highlight, t.hover);
|
tp.setColor(QPalette::Highlight, t.selected);
|
||||||
tp.setColor(QPalette::HighlightedText, t.text);
|
tp.setColor(QPalette::HighlightedText, t.text);
|
||||||
m_workspaceTree->setPalette(tp);
|
m_workspaceTree->setPalette(tp);
|
||||||
|
|
||||||
|
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||||
|
"QTreeView { background: %1; border: none; }")
|
||||||
|
.arg(t.background.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
dockLayout->addWidget(m_workspaceTree);
|
dockLayout->addWidget(m_workspaceTree);
|
||||||
@@ -3714,16 +3761,28 @@ void MainWindow::rebuildAllDocs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::rebuildWorkspaceModel() {
|
void MainWindow::rebuildWorkspaceModel() {
|
||||||
|
// Debounce: coalesce rapid calls into a single rebuild
|
||||||
|
if (!m_workspaceRebuildTimer) {
|
||||||
|
m_workspaceRebuildTimer = new QTimer(this);
|
||||||
|
m_workspaceRebuildTimer->setSingleShot(true);
|
||||||
|
m_workspaceRebuildTimer->setInterval(50);
|
||||||
|
connect(m_workspaceRebuildTimer, &QTimer::timeout,
|
||||||
|
this, &MainWindow::rebuildWorkspaceModelNow);
|
||||||
|
}
|
||||||
|
m_workspaceRebuildTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::rebuildWorkspaceModelNow() {
|
||||||
QVector<rcx::TabInfo> tabs;
|
QVector<rcx::TabInfo> tabs;
|
||||||
QSet<RcxDocument*> seenDocs;
|
QSet<RcxDocument*> seenDocs;
|
||||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||||
TabState& tab = it.value();
|
TabState& tab = it.value();
|
||||||
if (seenDocs.contains(tab.doc)) continue; // skip duplicate doc views
|
if (seenDocs.contains(tab.doc)) continue;
|
||||||
seenDocs.insert(tab.doc);
|
seenDocs.insert(tab.doc);
|
||||||
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
||||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||||
}
|
}
|
||||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
rcx::syncProjectExplorer(m_workspaceModel, tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::addRecentFile(const QString& path) {
|
void MainWindow::addRecentFile(const QString& path) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace rcx {
|
|||||||
class McpBridge;
|
class McpBridge;
|
||||||
class ShimmerLabel;
|
class ShimmerLabel;
|
||||||
class DockGripWidget;
|
class DockGripWidget;
|
||||||
|
class WorkspaceDelegate;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -155,11 +156,14 @@ private:
|
|||||||
QStandardItemModel* m_workspaceModel = nullptr;
|
QStandardItemModel* m_workspaceModel = nullptr;
|
||||||
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||||
QLineEdit* m_workspaceSearch = nullptr;
|
QLineEdit* m_workspaceSearch = nullptr;
|
||||||
|
WorkspaceDelegate* m_workspaceDelegate = nullptr;
|
||||||
QLabel* m_dockTitleLabel = nullptr;
|
QLabel* m_dockTitleLabel = nullptr;
|
||||||
QToolButton* m_dockCloseBtn = nullptr;
|
QToolButton* m_dockCloseBtn = nullptr;
|
||||||
DockGripWidget* m_dockGrip = nullptr;
|
DockGripWidget* m_dockGrip = nullptr;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel();
|
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
||||||
|
void rebuildWorkspaceModelNow(); // immediate rebuild
|
||||||
|
QTimer* m_workspaceRebuildTimer = nullptr;
|
||||||
void updateBorderColor(const QColor& color);
|
void updateBorderColor(const QColor& color);
|
||||||
|
|
||||||
// Scanner dock
|
// Scanner dock
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include "themes/theme.h"
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QApplication>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -13,15 +17,85 @@ struct TabInfo {
|
|||||||
void* subPtr; // QDockWidget* as void*
|
void* subPtr; // QDockWidget* as void*
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
// Helper: is a Hex padding node
|
||||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
inline bool isHexPad(NodeKind k) {
|
||||||
|
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||||
|
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build child rows for a struct item.
|
||||||
|
inline void buildStructChildren(QStandardItem* item,
|
||||||
|
const NodeTree* tree, uint64_t structId,
|
||||||
|
void* subPtr) {
|
||||||
|
item->removeRows(0, item->rowCount());
|
||||||
|
|
||||||
|
QVector<int> members = tree->childrenOf(structId);
|
||||||
|
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||||
|
return tree->nodes[a].offset < tree->nodes[b].offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
auto memberTypeName = [](const Node& m) -> QString {
|
||||||
|
if (m.kind == NodeKind::Struct) {
|
||||||
|
return m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||||
|
: m.structTypeName;
|
||||||
|
}
|
||||||
|
return QString::fromLatin1(kindToString(m.kind));
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int mi : members) {
|
||||||
|
const Node& m = tree->nodes[mi];
|
||||||
|
if (isHexPad(m.kind)) continue;
|
||||||
|
QString childDisplay = QStringLiteral("%1 %2")
|
||||||
|
.arg(memberTypeName(m), m.name);
|
||||||
|
auto* childItem = new QStandardItem(childDisplay);
|
||||||
|
childItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||||
|
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||||
|
item->appendRow(childItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build display string for a type entry.
|
||||||
|
inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
|
||||||
|
auto nameOf = [](const Node* n) {
|
||||||
|
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||||
|
};
|
||||||
|
if (node->resolvedClassKeyword() == QStringLiteral("enum")) {
|
||||||
|
return QStringLiteral("%1 (%2) \u2014 %3")
|
||||||
|
.arg(nameOf(node), node->resolvedClassKeyword(),
|
||||||
|
QString::number(node->enumMembers.size()));
|
||||||
|
}
|
||||||
|
QVector<int> members = tree->childrenOf(node->id);
|
||||||
|
int vc = 0;
|
||||||
|
for (int mi : members)
|
||||||
|
if (!isHexPad(tree->nodes[mi].kind)) ++vc;
|
||||||
|
return QStringLiteral("%1 (%2) \u2014 %3")
|
||||||
|
.arg(nameOf(node), node->resolvedClassKeyword(),
|
||||||
|
QString::number(vc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a new item for a type entry.
|
||||||
|
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
|
||||||
|
void* subPtr) {
|
||||||
|
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
|
||||||
|
auto* item = new QStandardItem(
|
||||||
|
QIcon(isEnum ? ":/vsicons/symbol-enum.svg"
|
||||||
|
: ":/vsicons/symbol-structure.svg"),
|
||||||
|
typeDisplayString(node, tree));
|
||||||
|
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||||
|
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
|
||||||
|
|
||||||
|
if (!isEnum)
|
||||||
|
buildStructChildren(item, tree, node->id, subPtr);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full rebuild — used by benchmarks and first build.
|
||||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||||
const QVector<TabInfo>& tabs) {
|
const QVector<TabInfo>& tabs) {
|
||||||
model->clear();
|
model->clear();
|
||||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||||
|
|
||||||
// Collect all top-level structs/enums across all tabs
|
|
||||||
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
|
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
|
||||||
QVector<Entry> types, enums;
|
QVector<Entry> types, enums;
|
||||||
for (const auto& tab : tabs) {
|
for (const auto& tab : tabs) {
|
||||||
@@ -36,76 +110,185 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto nameOf = [](const Node* n) {
|
for (const auto& e : types)
|
||||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
};
|
for (const auto& e : enums)
|
||||||
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
// Helper: is a Hex padding node
|
|
||||||
auto isHexPad = [](NodeKind k) {
|
|
||||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
|
||||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper: type display string for a member node
|
|
||||||
auto memberTypeName = [](const Node& m) -> QString {
|
|
||||||
if (m.kind == NodeKind::Struct) {
|
|
||||||
QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
|
||||||
: m.structTypeName;
|
|
||||||
return stn;
|
|
||||||
}
|
|
||||||
return QString::fromLatin1(kindToString(m.kind));
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: re-enable sorting once startup perf is acceptable
|
|
||||||
// auto countVisible = [&](const Entry& e) { ... };
|
|
||||||
// std::sort(types.begin(), types.end(), cmpChildren);
|
|
||||||
// std::sort(enums.begin(), enums.end(), cmpName);
|
|
||||||
|
|
||||||
for (const auto& e : types) {
|
|
||||||
QVector<int> members = e.tree->childrenOf(e.node->id);
|
|
||||||
|
|
||||||
// Count non-hex members for display
|
|
||||||
int visibleCount = 0;
|
|
||||||
for (int mi : members)
|
|
||||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount;
|
|
||||||
|
|
||||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
|
||||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
|
||||||
QString::number(visibleCount));
|
|
||||||
auto* item = new QStandardItem(
|
|
||||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
|
||||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
|
||||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
|
||||||
|
|
||||||
// Add child rows sorted by offset (skip Hex padding)
|
|
||||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
|
||||||
return e.tree->nodes[a].offset < e.tree->nodes[b].offset;
|
|
||||||
});
|
|
||||||
for (int mi : members) {
|
|
||||||
const Node& m = e.tree->nodes[mi];
|
|
||||||
if (isHexPad(m.kind)) continue;
|
|
||||||
QString childDisplay = QStringLiteral("%1 %2")
|
|
||||||
.arg(memberTypeName(m), m.name);
|
|
||||||
auto* childItem = new QStandardItem(childDisplay);
|
|
||||||
childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
|
||||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
|
||||||
item->appendRow(childItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
model->appendRow(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& e : enums) {
|
|
||||||
int count = e.node->enumMembers.size();
|
|
||||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
|
||||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
|
||||||
QString::number(count));
|
|
||||||
auto* item = new QStandardItem(
|
|
||||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
|
||||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
|
||||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
|
||||||
model->appendRow(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incremental sync — preserves tree expansion/scroll state.
|
||||||
|
inline void syncProjectExplorer(QStandardItemModel* model,
|
||||||
|
const QVector<TabInfo>& tabs) {
|
||||||
|
// First call — full build
|
||||||
|
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
||||||
|
buildProjectExplorer(model, tabs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect desired entries
|
||||||
|
struct Entry { uint64_t id; const Node* node; void* subPtr; const NodeTree* tree; bool isEnum; };
|
||||||
|
QVector<Entry> desired;
|
||||||
|
for (const auto& tab : tabs) {
|
||||||
|
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||||
|
for (int idx : topLevel) {
|
||||||
|
const Node& n = tab.tree->nodes[idx];
|
||||||
|
if (n.kind != NodeKind::Struct) continue;
|
||||||
|
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
|
||||||
|
desired.append({n.id, &n, tab.subPtr, tab.tree, ie});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<uint64_t, int> desiredMap;
|
||||||
|
desiredMap.reserve(desired.size());
|
||||||
|
for (int i = 0; i < desired.size(); ++i)
|
||||||
|
desiredMap[desired[i].id] = i;
|
||||||
|
|
||||||
|
// Remove stale items (backwards)
|
||||||
|
for (int i = model->rowCount() - 1; i >= 0; --i) {
|
||||||
|
uint64_t id = model->item(i)->data(Qt::UserRole + 1).toULongLong();
|
||||||
|
if (!desiredMap.contains(id))
|
||||||
|
model->removeRow(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing items
|
||||||
|
QSet<uint64_t> existing;
|
||||||
|
for (int i = 0; i < model->rowCount(); ++i) {
|
||||||
|
auto* item = model->item(i);
|
||||||
|
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||||
|
existing.insert(id);
|
||||||
|
auto dit = desiredMap.find(id);
|
||||||
|
if (dit == desiredMap.end()) continue;
|
||||||
|
const Entry& e = desired[*dit];
|
||||||
|
|
||||||
|
QString display = typeDisplayString(e.node, e.tree);
|
||||||
|
if (item->text() != display)
|
||||||
|
item->setText(display);
|
||||||
|
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||||
|
|
||||||
|
// Refresh children for structs
|
||||||
|
if (!e.isEnum)
|
||||||
|
buildStructChildren(item, e.tree, id, e.subPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new items
|
||||||
|
for (const auto& e : desired) {
|
||||||
|
if (existing.contains(e.id)) continue;
|
||||||
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model->horizontalHeaderItem(0) == nullptr)
|
||||||
|
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom delegate for rich workspace tree rendering ──
|
||||||
|
|
||||||
|
class WorkspaceDelegate : public QStyledItemDelegate {
|
||||||
|
public:
|
||||||
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
|
|
||||||
|
void setThemeColors(const Theme& t) {
|
||||||
|
m_text = t.text;
|
||||||
|
m_textDim = t.textDim;
|
||||||
|
m_textMuted = t.textMuted;
|
||||||
|
m_syntaxType = t.syntaxType;
|
||||||
|
m_hover = t.hover;
|
||||||
|
m_selected = t.selected;
|
||||||
|
m_accent = t.borderFocused; // left accent bar
|
||||||
|
m_bg = t.background;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem& option,
|
||||||
|
const QModelIndex& index) const override {
|
||||||
|
QSize s = QStyledItemDelegate::sizeHint(option, index);
|
||||||
|
int pad = index.parent().isValid() ? 6 : 10;
|
||||||
|
s.setHeight(option.fontMetrics.height() + pad);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
|
const QModelIndex& index) const override {
|
||||||
|
painter->save();
|
||||||
|
|
||||||
|
QStyleOptionViewItem opt = option;
|
||||||
|
initStyleOption(&opt, index);
|
||||||
|
opt.text.clear();
|
||||||
|
opt.icon = QIcon(); // we draw icon manually
|
||||||
|
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
|
||||||
|
|
||||||
|
// Custom background for selection/hover
|
||||||
|
if (opt.state & QStyle::State_Selected) {
|
||||||
|
painter->fillRect(opt.rect, m_selected);
|
||||||
|
// Left accent bar
|
||||||
|
painter->fillRect(QRect(opt.rect.x(), opt.rect.y(), 2, opt.rect.height()), m_accent);
|
||||||
|
} else if (opt.state & QStyle::State_MouseOver) {
|
||||||
|
painter->fillRect(opt.rect, m_hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isChild = index.parent().isValid();
|
||||||
|
QString fullText = index.data(Qt::DisplayRole).toString();
|
||||||
|
QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
|
||||||
|
|
||||||
|
// Draw icon for top-level items
|
||||||
|
if (!isChild) {
|
||||||
|
QVariant iconVar = index.data(Qt::DecorationRole);
|
||||||
|
if (iconVar.isValid()) {
|
||||||
|
QIcon icon = iconVar.value<QIcon>();
|
||||||
|
int iconSz = opt.fontMetrics.height();
|
||||||
|
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
|
||||||
|
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
|
||||||
|
textRect.setLeft(textRect.left() + iconSz + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->setFont(opt.font);
|
||||||
|
|
||||||
|
if (!isChild) {
|
||||||
|
// Top-level: "StructName (class) — 3"
|
||||||
|
int dashPos = fullText.indexOf(QChar(0x2014));
|
||||||
|
int parenPos = dashPos > 0 ? fullText.lastIndexOf(QStringLiteral(" ("), dashPos) : -1;
|
||||||
|
if (parenPos > 0) {
|
||||||
|
QString name = fullText.left(parenPos);
|
||||||
|
QString meta = fullText.mid(parenPos);
|
||||||
|
|
||||||
|
painter->setPen(m_text);
|
||||||
|
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
||||||
|
int nameW = opt.fontMetrics.horizontalAdvance(name);
|
||||||
|
|
||||||
|
QRect metaRect = textRect;
|
||||||
|
metaRect.setLeft(textRect.left() + nameW);
|
||||||
|
painter->setPen(m_textMuted);
|
||||||
|
painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta);
|
||||||
|
} else {
|
||||||
|
painter->setPen(m_text);
|
||||||
|
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Child: "TypeName fieldName"
|
||||||
|
int spacePos = fullText.indexOf(' ');
|
||||||
|
if (spacePos > 0) {
|
||||||
|
QString typeName = fullText.left(spacePos);
|
||||||
|
QString fieldName = fullText.mid(spacePos);
|
||||||
|
|
||||||
|
painter->setPen(m_syntaxType);
|
||||||
|
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, typeName);
|
||||||
|
int typeW = opt.fontMetrics.horizontalAdvance(typeName);
|
||||||
|
|
||||||
|
QRect fieldRect = textRect;
|
||||||
|
fieldRect.setLeft(textRect.left() + typeW);
|
||||||
|
painter->setPen(m_textDim);
|
||||||
|
painter->drawText(fieldRect, Qt::AlignLeft | Qt::AlignVCenter, fieldName);
|
||||||
|
} else {
|
||||||
|
painter->setPen(m_textDim);
|
||||||
|
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QColor m_text, m_textDim, m_textMuted, m_syntaxType;
|
||||||
|
QColor m_hover, m_selected, m_accent, m_bg;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
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 }};
|
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
// Single "Project" root
|
// Flat model: Player at root (has 2 non-hex members → lazy placeholder)
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 1);
|
||||||
QStandardItem* project = model.item(0);
|
QVERIFY(model.item(0)->text().contains("Player"));
|
||||||
QCOMPARE(project->text(), QString("Project"));
|
QVERIFY(model.item(0)->text().contains("struct"));
|
||||||
|
QVERIFY(model.item(0)->rowCount() > 0); // children populated directly
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_twoRootTree() {
|
void testWorkspace_twoRootTree() {
|
||||||
@@ -535,15 +530,10 @@ private slots:
|
|||||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QCOMPARE(model.rowCount(), 1);
|
// Flat model: 2 types at root
|
||||||
QStandardItem* project = model.item(0);
|
QCOMPARE(model.rowCount(), 2);
|
||||||
|
QVERIFY(model.item(0)->text().contains("Alpha"));
|
||||||
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
QVERIFY(model.item(1)->text().contains("Bravo"));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_richTree_rootCount() {
|
void testWorkspace_richTree_rootCount() {
|
||||||
@@ -552,25 +542,19 @@ private slots:
|
|||||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* project = model.item(0);
|
QCOMPARE(model.rowCount(), 3); // Ball, Cat, Pet
|
||||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_richTree_sorted() {
|
void testWorkspace_richTree_insertionOrder() {
|
||||||
auto tree = makeRichTree();
|
auto tree = makeRichTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* project = model.item(0);
|
// Types at root in insertion order: Pet, Cat, Ball
|
||||||
// Sorted alphabetically: Ball, Cat, Pet
|
QVERIFY(model.item(0)->text().contains("Pet"));
|
||||||
QVERIFY(project->child(0)->text().contains("Ball"));
|
QVERIFY(model.item(1)->text().contains("Cat"));
|
||||||
QVERIFY(project->child(1)->text().contains("Cat"));
|
QVERIFY(model.item(2)->text().contains("Ball"));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_emptyTree() {
|
void testWorkspace_emptyTree() {
|
||||||
@@ -579,10 +563,8 @@ private slots:
|
|||||||
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
// Still has the "Project" root, just no children
|
// Flat model: no types means no rows
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 0);
|
||||||
QCOMPARE(model.item(0)->text(), QString("Project"));
|
|
||||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_structIdRole() {
|
void testWorkspace_structIdRole() {
|
||||||
@@ -591,15 +573,11 @@ private slots:
|
|||||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||||
buildProjectExplorer(&model, tabs);
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* project = model.item(0);
|
// Flat model: first item is the Player type with its structId
|
||||||
// Project root has kGroupSentinel
|
QVERIFY(model.rowCount() > 0);
|
||||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
QStandardItem* player = model.item(0);
|
||||||
|
|
||||||
// Player type item should have structId
|
|
||||||
QStandardItem* player = project->child(0);
|
|
||||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user