Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ ninja -C $BUILD_DIR unit_test # Build unit tests
cd $BUILD_DIR && ctest -j "$(nproc)" -LE _tests
```

**Tip:** ctest runs take a long time. Always log output to a temp file so you can grep/tail without re-running:
```bash
cd $BUILD_DIR && ctest -j "$(nproc)" -LE "(nonparallelizable_tests|long_running_tests|wasm_spec_tests)" --output-on-failure --timeout 1000 2>&1 | tee /tmp/ctest-run.log
# Then analyze without re-running:
grep "Failed" /tmp/ctest-run.log
grep "% tests passed" /tmp/ctest-run.log
```

### Run Specific Test Suite
```bash
# Run a single Boost.Test suite
Expand Down Expand Up @@ -131,6 +139,10 @@ FC_REFLECT(my_namespace::my_type, (field1)(field2)(field3))
FC_REFLECT_ENUM(my_namespace::my_enum, (value1)(value2)(value3))
```

## Git Practices

**NEVER use `git add -A` or `git add .`** — these will stage build artifacts, core dumps, submodules, and other untracked files. Always stage specific files by name.

## Code Style

Uses `.clang-format` with LLVM base style and these key differences:
Expand Down Expand Up @@ -180,3 +192,108 @@ The `tests/` directory contains Python integration tests using TestHarness frame
cd $BUILD_DIR && python3 tests/<test_name>.py
```
Do NOT run from the source root — that would use stale or missing binaries.

## Smart Contract Compilation

Contracts are compiled with the Wire CDT (C/C++ Development Toolkit). The CDT repo is typically at `../wire-cdt-db-kv` (or `../wire-cdt`) with build dir `cmake-build-debug-vcpkg`.

```bash
CDT=<path-to-wire-cdt>/cmake-build-debug-vcpkg

# Production contracts
$CDT/bin/cdt-cpp -abigen -I contracts -o contracts/sysio.token/sysio.token.wasm contracts/sysio.token/sysio.token.cpp
$CDT/bin/cdt-cpp -abigen -I contracts/sysio.bios -I contracts -o contracts/sysio.bios/sysio.bios.wasm contracts/sysio.bios/sysio.bios.cpp
$CDT/bin/cdt-cpp -abigen -I contracts -o contracts/sysio.msig/sysio.msig.wasm contracts/sysio.msig/sysio.msig.cpp
$CDT/bin/cdt-cpp -abigen -I contracts -I contracts/sysio.system/include -o contracts/sysio.roa/sysio.roa.wasm contracts/sysio.roa/sysio.roa.cpp
$CDT/bin/cdt-cpp -abigen -I contracts/sysio.system/include -I contracts -o contracts/sysio.system/sysio.system.wasm contracts/sysio.system/src/*.cpp

# Test contracts (example)
$CDT/bin/cdt-cpp -abigen -o unittests/test-contracts/<name>/<name>.wasm unittests/test-contracts/<name>/<name>.cpp
```

### After Recompiling Contracts

Compiled WASMs must be copied to the build directory locations where tests load them from:

```bash
# Production contracts used by contract tests
cp contracts/sysio.token/sysio.token.{wasm,abi} $BUILD_DIR/contracts/sysio.token/
cp contracts/sysio.system/sysio.system.{wasm,abi} $BUILD_DIR/contracts/sysio.system/
cp contracts/sysio.msig/sysio.msig.{wasm,abi} $BUILD_DIR/contracts/sysio.msig/
cp contracts/sysio.roa/sysio.roa.{wasm,abi} $BUILD_DIR/contracts/sysio.roa/

# Embedded contracts (INCBIN in libtester) — requires .o deletion to force rebuild
cp contracts/sysio.bios/sysio.bios.{wasm,abi} $BUILD_DIR/libraries/testing/contracts/sysio.bios/
cp contracts/sysio.roa/sysio.roa.{wasm,abi} $BUILD_DIR/libraries/testing/contracts/sysio.roa/
rm -f $BUILD_DIR/libraries/testing/CMakeFiles/sysio_testing.dir/contracts.cpp.o

# Test contracts
cp unittests/test-contracts/<name>/<name>.wasm $BUILD_DIR/unittests/test-contracts/<name>/
```

Then rebuild: `ninja -C $BUILD_DIR -j6 unit_test contracts_unit_test`

### CDT-Generated Artifacts

CDT generates `.actions.cpp`, `.dispatch.cpp`, and `.desc` files alongside compiled contracts. These are **not committed** — `.gitignore` files in `contracts/` and `unittests/test-contracts/` exclude them. If they appear as untracked, delete them:
```bash
find contracts/ unittests/test-contracts/ -name "*.actions.cpp" -o -name "*.dispatch.cpp" -o -name "*.desc" | xargs rm -f
```

### Action Name Constraints

SYSIO action names must be valid SYSIO names: max 13 characters, only `a-z`, `1-5`, `.`. CDT will error with "not a valid sysio name" if violated.

## Regenerating Test Reference Data

Some tests compare against pre-generated reference data. When contracts are recompiled (different WASM = different action merkle roots), this data must be regenerated.

### Deep Mind Log

The `deep_mind_tests` compare against `unittests/deep-mind/deep-mind.log`. To regenerate:
```bash
$BUILD_DIR/unittests/unit_test --run_test=deep_mind_tests -- --sys-vm --save-dmlog
```

### Snapshot Compatibility Data

The `snapshot_part2_tests/test_compatible_versions` test uses reference blockchain and snapshot files in `unittests/snapshots/`. To regenerate:

**Step 1:** Delete stale files from BOTH source and build directories. The `--save-snapshot` run replays blocks.log if it exists — if it contains WASMs with old host function signatures, replay fails with `wasm_serialization_error: wrong type for imported function`. You must delete first:
```bash
rm -f unittests/snapshots/blocks.* unittests/snapshots/snap_v1.*
rm -f $BUILD_DIR/unittests/snapshots/blocks.* $BUILD_DIR/unittests/snapshots/snap_v1.*
```

**Step 2:** Regenerate. This creates a fresh blockchain, deploys the current embedded contracts, and writes new reference files:
```bash
$BUILD_DIR/unittests/unit_test --run_test="snapshot_part2_tests/*" -- --sys-vm --save-snapshot --generate-snapshot-log
```
The test writes to `$BUILD_DIR/unittests/snapshots/` (NOT the source tree, despite the flag description).

**Step 3:** Copy from build dir to source tree (for git), and ensure the build dir has them for subsequent test runs:
```bash
cp $BUILD_DIR/unittests/snapshots/blocks.* $BUILD_DIR/unittests/snapshots/snap_v1.* unittests/snapshots/
```

**Step 4:** Re-run CMake or ninja so `configure_file` picks up the new source-tree files:
```bash
ninja -C $BUILD_DIR -j6 unit_test
```
If CMake fails because snapshot files are missing from the source tree, run step 3 first.

**Common pitfall:** If you only delete source-tree files but not build-dir files, the test replays the stale build-dir blocks.log and fails. Always delete from both locations.

### Consensus Blockchain Data

The `savanna_misc_tests/verify_block_compatibitity` test uses `unittests/test-data/consensus_blockchain/`. To regenerate:
```bash
$BUILD_DIR/unittests/unit_test -t "savanna_misc_tests/verify_block_compatibitity" -- --sys-vm --save-blockchain
```

### When to Regenerate

Regenerate all reference data whenever:
- Any production contract is recompiled (changes action merkle roots)
- Chain-level serialization changes (block format, snapshot format)
- Genesis intrinsics change (different genesis state)
262 changes: 262 additions & 0 deletions benchmark/kv_benchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
#include <boost/test/unit_test.hpp>

#include <chainbase/chainbase.hpp>
#include <sysio/chain/kv_table_objects.hpp>

#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <random>
#include <sstream>
#include <vector>

using namespace sysio::chain;
namespace fs = std::filesystem;

// Helper: RAII temp dir
struct temp_db_dir {
fs::path path;
temp_db_dir() : path(fs::temp_directory_path() / ("kv_bench_" + std::to_string(getpid()))) {
fs::create_directories(path);
}
~temp_db_dir() { fs::remove_all(path); }
};

// Helper: measure execution time
template<typename Func>
double measure_ns(Func&& f, int iterations) {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
f(i);
}
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() / double(iterations);
}

// Helper: generate random bytes
static std::vector<char> random_bytes(std::mt19937& rng, size_t len) {
std::vector<char> v(len);
for (auto& c : v) c = static_cast<char>(rng() & 0xFF);
return v;
}

// Helper: format ns/op
static std::string fmt_ns(double ns) {
std::ostringstream ss;
if (ns >= 1e6) ss << std::fixed << std::setprecision(2) << ns / 1e6 << " ms";
else if (ns >= 1e3) ss << std::fixed << std::setprecision(1) << ns / 1e3 << " us";
else ss << std::fixed << std::setprecision(0) << ns << " ns";
return ss.str();
}

BOOST_AUTO_TEST_SUITE(kv_benchmark)

BOOST_AUTO_TEST_CASE(chainbase_micro_benchmark) {
temp_db_dir dir;
chainbase::database db(dir.path, chainbase::database::read_write, 1024 * 1024 * 256); // 256 MB

// Register KV indices
db.add_index<kv_index>();
db.add_index<kv_index_index>();

std::mt19937 rng(42); // deterministic seed

const std::vector<int> row_counts = {100, 1000, 10000};

std::cout << "\n========== KV Database Micro-Benchmark ==========\n";
std::cout << std::left << std::setw(25) << "Operation"
<< std::setw(10) << "Rows"
<< std::setw(15) << "ns/op" << "\n";
std::cout << std::string(50, '-') << "\n";

for (int N : row_counts) {
auto session = db.start_undo_session(true);

// --- INSERT benchmark ---
std::vector<std::vector<char>> kv_keys(N);
auto value = random_bytes(rng, 128);

double kv_insert = measure_ns([&](int i) {
// 8-byte big-endian key
uint64_t k = static_cast<uint64_t>(i);
char key_buf[8];
for (int j = 7; j >= 0; --j) { key_buf[j] = static_cast<char>(k & 0xFF); k >>= 8; }
kv_keys[i].assign(key_buf, key_buf + 8);

db.create<kv_object>([&](auto& o) {
o.code = "benchmark"_n;
o.key_assign(kv_keys[i].data(), kv_keys[i].size());
o.value.assign(value.data(), value.size());
});
}, N);

std::cout << std::setw(25) << "Insert"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_insert) << "\n";

// --- POINT LOOKUP benchmark ---
auto& kv_idx = db.get_index<kv_index, by_code_key>();

double kv_find = measure_ns([&](int i) {
int idx = i % N;
auto sv = std::string_view(kv_keys[idx].data(), kv_keys[idx].size());
auto itr = kv_idx.find(boost::make_tuple(name("benchmark"), config::kv_format_standard, sv));
BOOST_REQUIRE(itr != kv_idx.end());
}, N);

std::cout << std::setw(25) << "Point Lookup"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_find) << "\n";

// --- SEQUENTIAL ITERATION benchmark ---
double kv_iter = measure_ns([&](int) {
auto itr = kv_idx.lower_bound(boost::make_tuple(name("benchmark"), config::kv_format_standard));
int count = 0;
while (itr != kv_idx.end() && itr->code == name("benchmark")) {
++count;
++itr;
}
BOOST_REQUIRE_EQUAL(count, N);
}, 1);

std::cout << std::setw(25) << "Full Iteration"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_iter / N) << "\n";

// --- UPDATE benchmark ---
auto new_value = random_bytes(rng, 128);

double kv_update = measure_ns([&](int i) {
int idx = i % N;
auto sv = std::string_view(kv_keys[idx].data(), kv_keys[idx].size());
auto itr = kv_idx.find(boost::make_tuple(name("benchmark"), config::kv_format_standard, sv));
db.modify(*itr, [&](auto& o) {
o.value.assign(new_value.data(), new_value.size());
});
}, N);

std::cout << std::setw(25) << "Update"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_update) << "\n";

// --- ERASE benchmark ---
double kv_erase = measure_ns([&](int i) {
auto sv = std::string_view(kv_keys[i].data(), kv_keys[i].size());
auto itr = kv_idx.find(boost::make_tuple(name("benchmark"), config::kv_format_standard, sv));
BOOST_REQUIRE(itr != kv_idx.end());
db.remove(*itr);
}, N);

std::cout << std::setw(25) << "Erase"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_erase) << "\n";

std::cout << std::string(50, '-') << "\n";

session.undo();
}

std::cout << "=================================================\n\n";
}

BOOST_AUTO_TEST_CASE(full_intrinsic_path_benchmark) {
temp_db_dir dir;
chainbase::database db(dir.path, chainbase::database::read_write, 1024 * 1024 * 256);

db.add_index<kv_index>();

std::mt19937 rng(42);

const std::vector<int> row_counts = {100, 1000, 10000};

std::cout << "\n===== KV Full Intrinsic Path Benchmark =====\n";
std::cout << std::left << std::setw(25) << "Operation"
<< std::setw(10) << "Rows"
<< std::setw(15) << "ns/op" << "\n";
std::cout << std::string(50, '-') << "\n";

for (int N : row_counts) {
auto session = db.start_undo_session(true);

auto value = random_bytes(rng, 128);
auto new_value = random_bytes(rng, 128);

// --- KV STORE: direct composite key lookup + create ---
auto& kv_idx = db.get_index<kv_index, by_code_key>();
std::vector<std::vector<char>> kv_keys(N);

double kv_store = measure_ns([&](int i) {
// KV: single composite key check + create
char key_buf[8];
uint64_t k = static_cast<uint64_t>(i);
for (int j = 7; j >= 0; --j) { key_buf[j] = static_cast<char>(k & 0xFF); k >>= 8; }
kv_keys[i].assign(key_buf, key_buf + 8);
auto sv = std::string_view(key_buf, 8);

// Check if exists (what kv_set does)
(void)kv_idx.find(boost::make_tuple(name("kvbench"), config::kv_format_standard, sv));
// Create new
db.create<kv_object>([&](auto& o) {
o.code = "kvbench"_n;
o.key_assign(key_buf, 8);
o.value.assign(value.data(), value.size());
});
}, N);

std::cout << std::setw(25) << "Store (full path)"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_store) << "\n";

// --- FIND benchmark ---
double kv_find = measure_ns([&](int i) {
int idx = i % N;
auto sv = std::string_view(kv_keys[idx].data(), kv_keys[idx].size());
auto itr = kv_idx.find(boost::make_tuple(name("kvbench"), config::kv_format_standard, sv));
BOOST_REQUIRE(itr != kv_idx.end());
}, N);

std::cout << std::setw(25) << "Find (full path)"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_find) << "\n";

// --- UPDATE benchmark ---
double kv_update = measure_ns([&](int i) {
int idx = i % N;
auto sv = std::string_view(kv_keys[idx].data(), kv_keys[idx].size());
auto itr = kv_idx.find(boost::make_tuple(name("kvbench"), config::kv_format_standard, sv));
// Compute delta on value size (simulating intrinsic path)
int64_t old_size = static_cast<int64_t>(itr->value.size());
int64_t new_size = static_cast<int64_t>(new_value.size());
volatile int64_t delta = new_size - old_size;
(void)delta;
db.modify(*itr, [&](auto& o) {
o.value.assign(new_value.data(), new_value.size());
});
}, N);

std::cout << std::setw(25) << "Update (full path)"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_update) << "\n";

// --- ERASE benchmark ---
double kv_erase = measure_ns([&](int i) {
auto sv = std::string_view(kv_keys[i].data(), kv_keys[i].size());
auto itr = kv_idx.find(boost::make_tuple(name("kvbench"), config::kv_format_standard, sv));
BOOST_REQUIRE(itr != kv_idx.end());
db.remove(*itr);
}, N);

std::cout << std::setw(25) << "Erase (full path)"
<< std::setw(10) << N
<< std::setw(15) << fmt_ns(kv_erase) << "\n";

std::cout << std::string(50, '-') << "\n";

session.undo();
}

std::cout << "=================================================\n\n";
}

BOOST_AUTO_TEST_SUITE_END()
Loading
Loading