Files
archived-Reclass/tests/bench_large_class.cpp
IChooseYou 596f410b96 perf: compose 30% faster — move semantics, BFS offsets, zero-alloc hex formatting
- compose.cpp: emitLine takes LineMeta&& (move, not copy) at all 22 call sites
- compose.cpp: reserve meta/text buffers, BFS offset computation O(N) vs O(N*D)
- compose.cpp: pre-compute typeNameLens[], merge global width loops
- format.cpp: bytesToHex uses stack buffer + lookup table (zero heap allocs)
- format.cpp: hexVal single QString::asprintf instead of 2-string concat
- editor.cpp: guard hover updates during applyDocument (stale index safety)
- core.h: assertion on makeArrayElemSelId negative index
- format.cpp: assertion on extractBits overflow
- main.cpp: tree lines enabled by default
- bench_large_class: add 2000-field benchComposeLarge test

Benchmark: 500 fields 0.70→0.51ms (27%), 2000 fields 2.28→1.57ms (31%)
2026-03-08 07:28:26 -06:00

256 lines
7.5 KiB
C++

/*
* bench_large_class — benchmark compose, applyDocument, hover highlight,
* and selection overlay on a large struct (500+ fields).
*
* Simulates EPROCESS-class structures to measure editor performance.
*/
#include <QtTest/QtTest>
#include <QElapsedTimer>
#include "core.h"
#include "editor.h"
#include "providers/buffer_provider.h"
using namespace rcx;
/* ── Build a large struct tree with N fields of mixed types ──────── */
static NodeTree buildLargeTree(int fieldCount)
{
NodeTree tree;
tree.baseAddress = 0x7FF600000000ULL;
// Root struct
Node root;
root.id = 1;
root.kind = NodeKind::Struct;
root.name = QStringLiteral("EPROCESS");
root.structTypeName = QStringLiteral("_EPROCESS");
root.parentId = 0;
root.offset = 0;
tree.addNode(root);
// Cycle through common field types
const NodeKind kinds[] = {
NodeKind::Int32, NodeKind::UInt64, NodeKind::Float,
NodeKind::Pointer64, NodeKind::Int16, NodeKind::UInt32,
NodeKind::Double, NodeKind::Bool, NodeKind::Hex8
};
const int kindCount = sizeof(kinds) / sizeof(kinds[0]);
int offset = 0;
for (int i = 0; i < fieldCount; ++i) {
Node n;
n.id = (uint64_t)(i + 2);
n.kind = kinds[i % kindCount];
n.name = QStringLiteral("field_%1").arg(i, 4, 10, QChar('0'));
n.parentId = 1;
n.offset = offset;
tree.addNode(n);
offset += sizeForKind(n.kind);
}
tree.m_nextId = (uint64_t)(fieldCount + 2);
return tree;
}
/* ══════════════════════════════════════════════════════════════════ */
class BenchLargeClass : public QObject {
Q_OBJECT
private:
NodeTree m_tree;
BufferProvider m_prov;
ComposeResult m_result;
private slots:
void initTestCase();
void benchCompose();
void benchComposeLarge();
void benchApplyDocument();
void benchHoverHighlight();
void benchSelectionOverlay();
void benchHoverHighlightRepeated();
public:
BenchLargeClass() : m_prov(QByteArray()) {}
};
void BenchLargeClass::initTestCase()
{
m_tree = buildLargeTree(500);
// Create buffer large enough for all fields
QByteArray buf(0x10000, '\0');
// Fill with pattern so values are non-zero
for (int i = 0; i < buf.size(); ++i)
buf[i] = (char)(i & 0xFF);
m_prov = BufferProvider(buf, QStringLiteral("bench_data"));
// Pre-compose for tests that need the result
m_result = rcx::compose(m_tree, m_prov);
qDebug() << "Tree:" << m_tree.nodes.size() << "nodes,"
<< m_result.meta.size() << "display lines,"
<< m_result.text.size() << "chars";
}
void BenchLargeClass::benchCompose()
{
const int ITERS = 100;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
ComposeResult r = rcx::compose(m_tree, m_prov);
Q_UNUSED(r);
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Compose Benchmark (500 fields) ===";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchComposeLarge()
{
// Build a 2000-field tree to stress-test compose at scale
NodeTree bigTree = buildLargeTree(2000);
QByteArray buf(0x40000, '\0');
for (int i = 0; i < buf.size(); ++i) buf[i] = (char)(i & 0xFF);
BufferProvider bigProv(buf, QStringLiteral("bench_large"));
// Warmup
{ ComposeResult w = rcx::compose(bigTree, bigProv); Q_UNUSED(w); }
const int ITERS = 50;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
ComposeResult r = rcx::compose(bigTree, bigProv);
Q_UNUSED(r);
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Compose Benchmark (2000 fields) ===";
qDebug() << " Tree:" << bigTree.nodes.size() << "nodes";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchApplyDocument()
{
RcxEditor editor;
editor.resize(800, 600);
const int ITERS = 50;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i)
editor.applyDocument(m_result);
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== ApplyDocument Benchmark (500 fields) ===";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-apply:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchHoverHighlight()
{
RcxEditor editor;
editor.resize(800, 600);
editor.applyDocument(m_result);
// Simulate hovering over the first field
// We need access to internals, so we measure via public methods
// by toggling selection which triggers applyHoverHighlight internally
QSet<uint64_t> sel;
sel.insert(2); // first field node id
const int ITERS = 200;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
editor.applySelectionOverlay(i % 2 == 0 ? sel : QSet<uint64_t>{});
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Hover/Selection Overlay Benchmark (500 fields) ===";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-cycle:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchSelectionOverlay()
{
RcxEditor editor;
editor.resize(800, 600);
editor.applyDocument(m_result);
// Select many nodes (simulate multi-select of 50 fields)
QSet<uint64_t> bigSel;
for (int i = 0; i < 50; ++i)
bigSel.insert((uint64_t)(i + 2));
const int ITERS = 100;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
editor.applySelectionOverlay(bigSel);
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Multi-Selection Overlay Benchmark (50 selected, 500 fields) ===";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-overlay:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchHoverHighlightRepeated()
{
RcxEditor editor;
editor.resize(800, 600);
editor.applyDocument(m_result);
// Simulate rapid hover changes: alternate between two different nodes
// This is the worst case - every call does a full marker clear + rescan
QSet<uint64_t> empty;
QSet<uint64_t> sel1; sel1.insert(10);
QSet<uint64_t> sel2; sel2.insert(100);
const int ITERS = 500;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
editor.applySelectionOverlay(i % 3 == 0 ? sel1 : (i % 3 == 1 ? sel2 : empty));
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Rapid Hover Change Benchmark (500 fields, alternating nodes) ===";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-change:" << (double)elapsed / ITERS << "ms";
qDebug() << " Simulated events/sec:" << (ITERS * 1000.0 / elapsed);
QVERIFY(elapsed > 0);
}
QTEST_MAIN(BenchLargeClass)
#include "bench_large_class.moc"