2026-10-17 04:16:51 +02:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-10-31 20:51:14 +01:00
// Crafter.DB against a cleaned-up copy of the legacy format it replaces
2026-10-17 04:16:51 +02:00
// (iostream, uint16-prefixed strings inline, linear scans) on the five
2026-10-31 20:51:14 +01:00
// operations that log served. Prints a table; never fails. Runs on 100k
2026-10-17 04:16:51 +02:00
// rows so a full suite stays fast; raise `rowCount` for a serious run.
import Crafter . DB ;
import std ;
using namespace Crafter ;
namespace fs = std : : filesystem ;
using TimePoint = std : : chrono : : system_clock : : time_point ;
using Clock = std : : chrono : : steady_clock ;
namespace {
enum class Currency : std : : uint16_t { Euro } ;
struct Transaction {
std : : string name ;
std : : string bic ;
std : : string iban ;
std : : string description ;
Currency currency ;
std : : int64_t amount ;
TimePoint timePoint ;
static constexpr std : : uint32_t version = 1 ;
static constexpr auto orderedBy = & Transaction : : timePoint ;
} ;
inline constexpr DB : : Column < & Transaction : : iban > iban { } ;
inline constexpr DB : : Column < & Transaction : : timePoint > timePoint { } ;
constexpr std : : uint64_t rowCount = 100'000 ;
std : : vector < Transaction > MakeRows ( ) {
std : : mt19937_64 rng ( 42 ) ;
const std : : array < std : : string_view , 6 > names = { " Albert Heijn " , " Jumbo Supermarkten " , " NS Reizigers " , " Vattenfall " , " Bol.com " , " Gemeente Utrecht " } ;
const std : : array < std : : string_view , 4 > bics = { " ABNANL2A " , " RABONL2U " , " INGBNL2A " , " TRIONL2U " } ;
std : : vector < std : : string > ibans ;
for ( std : : uint32_t i = 0 ; i < 1000 ; i + + ) ibans . push_back ( std : : format ( " NL{:02}ABNA{:010} " , i % 97 , 417164300u + i ) ) ;
std : : vector < Transaction > rows ;
rows . reserve ( rowCount ) ;
for ( std : : uint64_t i = 0 ; i < rowCount ; i + + ) {
rows . push_back ( { std : : string ( names [ rng ( ) % names . size ( ) ] ) , std : : string ( bics [ rng ( ) % bics . size ( ) ] ) , ibans [ rng ( ) % ibans . size ( ) ] , std : : format ( " payment {} for order {} " , i , rng ( ) % 100000 ) , Currency : : Euro , static_cast < std : : int64_t > ( rng ( ) % 200000 ) - 100000 , TimePoint ( std : : chrono : : microseconds ( 1'700'000'000'000'000 + i * 60'000'000 ) ) } ) ;
}
return rows ;
}
2026-10-31 20:51:14 +01:00
// --- the legacy format, cleaned up -----------------------------------------
namespace Legacy {
2026-10-17 04:16:51 +02:00
void WriteString ( std : : ofstream & file , std : : string_view str ) {
const std : : uint16_t size = static_cast < std : : uint16_t > ( str . size ( ) ) ;
file . write ( reinterpret_cast < const char * > ( & size ) , sizeof ( size ) ) ;
file . write ( str . data ( ) , size ) ;
}
void ReadString ( std : : ifstream & file , std : : string & str ) {
std : : uint16_t size ;
file . read ( reinterpret_cast < char * > ( & size ) , sizeof ( size ) ) ;
str . resize ( size ) ;
file . read ( str . data ( ) , size ) ;
}
void Save ( const fs : : path & path , std : : span < const Transaction > rows ) {
std : : ofstream file ( path , std : : ios : : binary | std : : ios : : app ) ;
for ( const Transaction & tr : rows ) {
WriteString ( file , tr . name ) ;
WriteString ( file , tr . bic ) ;
WriteString ( file , tr . iban ) ;
WriteString ( file , tr . description ) ;
file . write ( reinterpret_cast < const char * > ( & tr . amount ) , sizeof ( tr . amount ) ) ;
file . write ( reinterpret_cast < const char * > ( & tr . currency ) , sizeof ( Currency ) ) ;
const std : : int64_t time = std : : chrono : : duration_cast < std : : chrono : : microseconds > ( tr . timePoint . time_since_epoch ( ) ) . count ( ) ;
file . write ( reinterpret_cast < const char * > ( & time ) , sizeof ( time ) ) ;
}
}
bool Load ( std : : ifstream & file , Transaction & tr ) {
if ( file . peek ( ) = = std : : ifstream : : traits_type : : eof ( ) ) return false ;
ReadString ( file , tr . name ) ;
ReadString ( file , tr . bic ) ;
ReadString ( file , tr . iban ) ;
ReadString ( file , tr . description ) ;
file . read ( reinterpret_cast < char * > ( & tr . amount ) , sizeof ( tr . amount ) ) ;
file . read ( reinterpret_cast < char * > ( & tr . currency ) , sizeof ( tr . currency ) ) ;
std : : int64_t time ;
file . read ( reinterpret_cast < char * > ( & time ) , sizeof ( time ) ) ;
tr . timePoint = TimePoint ( std : : chrono : : microseconds ( time ) ) ;
return true ;
}
TimePoint LastTime ( const fs : : path & path ) {
std : : ifstream file ( path , std : : ios : : binary ) ;
file . seekg ( - static_cast < std : : streamoff > ( sizeof ( std : : int64_t ) ) , std : : ios : : end ) ;
std : : int64_t time ;
file . read ( reinterpret_cast < char * > ( & time ) , sizeof ( time ) ) ;
return TimePoint ( std : : chrono : : microseconds ( time ) ) ;
}
std : : vector < Transaction > LoadAll ( const fs : : path & path ) {
std : : ifstream file ( path , std : : ios : : binary ) ;
std : : vector < Transaction > out ;
Transaction tr ;
while ( Load ( file , tr ) ) out . push_back ( tr ) ;
return out ;
}
std : : vector < Transaction > LoadRange ( const fs : : path & path , TimePoint from , TimePoint to ) {
std : : ifstream file ( path , std : : ios : : binary ) ;
std : : vector < Transaction > out ;
Transaction tr ;
while ( Load ( file , tr ) ) {
if ( tr . timePoint > = from & & tr . timePoint < = to ) out . push_back ( tr ) ;
if ( tr . timePoint > to ) break ;
}
return out ;
}
std : : vector < Transaction > LoadByIban ( const fs : : path & path , std : : string_view wanted ) {
std : : ifstream file ( path , std : : ios : : binary ) ;
std : : vector < Transaction > out ;
Transaction tr ;
while ( Load ( file , tr ) ) {
if ( tr . iban = = wanted ) out . push_back ( tr ) ;
}
return out ;
}
}
template < class F >
double Millis ( F & & f ) {
const Clock : : time_point start = Clock : : now ( ) ;
f ( ) ;
return std : : chrono : : duration < double , std : : milli > ( Clock : : now ( ) - start ) . count ( ) ;
}
struct Line {
std : : string name ;
2026-10-31 20:51:14 +01:00
double legacy ;
2026-10-17 04:16:51 +02:00
double crafter ;
std : : string note ;
} ;
}
int main ( ) {
const fs : : path dir = fs : : temp_directory_path ( ) / " crafter-db-benchmark " ;
fs : : remove_all ( dir ) ;
fs : : create_directories ( dir ) ;
2026-10-31 20:51:14 +01:00
const fs : : path legacyPath = dir / " legacy.bin " ;
2026-10-17 04:16:51 +02:00
const fs : : path crafterPath = dir / " crafter.cdb " ;
const std : : vector < Transaction > rows = MakeRows ( ) ;
const std : : string_view wantedIban = rows [ rowCount / 2 ] . iban ;
const TimePoint from = rows [ rowCount * 45 / 100 ] . timePoint ;
const TimePoint to = rows [ rowCount * 55 / 100 ] . timePoint ;
std : : vector < Line > lines ;
std : : size_t sink = 0 ;
// append
{
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] { Legacy : : Save ( legacyPath , rows ) ; } ) ;
2026-10-17 04:16:51 +02:00
const double crafter = Millis ( [ & ] {
DB : : Table < Transaction > table ( crafterPath ) ;
table . Append ( rows ) ;
table . Flush ( ) ;
} ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { std : : format ( " append {} rows " , rowCount ) , legacy , crafter , " incl. open, Crafter.DB incl. fdatasync " } ) ;
2026-10-17 04:16:51 +02:00
}
// last timestamp (1000 opens each)
{
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] { for ( std : : int32_t i = 0 ; i < 1000 ; i + + ) sink + = Legacy : : LastTime ( legacyPath ) . time_since_epoch ( ) . count ( ) & 1 ; } ) ;
2026-10-17 04:16:51 +02:00
const double crafter = Millis ( [ & ] {
for ( std : : int32_t i = 0 ; i < 1000 ; i + + ) {
const DB : : Table < Transaction > table ( crafterPath , { . readOnly = true } ) ;
sink + = table . Back ( ) [ & Transaction : : timePoint ] . time_since_epoch ( ) . count ( ) & 1 ;
}
} ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { " last timestamp (1000x open+read) " , legacy , crafter , " open dominates both " } ) ;
2026-10-17 04:16:51 +02:00
}
DB : : Table < Transaction > table ( crafterPath , { . readOnly = true } ) ;
// load all
{
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] { sink + = Legacy : : LoadAll ( legacyPath ) . size ( ) ; } ) ;
2026-10-17 04:16:51 +02:00
const double crafter = Millis ( [ & ] { sink + = table . Materialize ( ) . size ( ) ; } ) ;
double zeroCopy = Millis ( [ & ] {
std : : int64_t total = 0 ;
for ( DB : : Row < Transaction > row : table ) total + = row [ & Transaction : : amount ] + static_cast < std : : int64_t > ( row [ & Transaction : : iban ] . Length ( ) ) ;
sink + = static_cast < std : : size_t > ( total & 1 ) ;
} ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { " load all (materialised) " , legacy , crafter , std : : format ( " zero-copy scan of two columns: {:.2f} ms " , zeroCopy ) } ) ;
2026-10-17 04:16:51 +02:00
}
// time range (10% of rows)
{
2026-10-31 20:51:14 +01:00
std : : size_t legacyCount = 0 ;
2026-10-17 04:16:51 +02:00
std : : size_t crafterCount = 0 ;
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] { legacyCount = Legacy : : LoadRange ( legacyPath , from , to ) . size ( ) ; } ) ;
2026-10-17 04:16:51 +02:00
const double crafter = Millis ( [ & ] { crafterCount = table . Range < & Transaction : : timePoint > ( from , to ) . Materialize ( ) . size ( ) ; } ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { " time range (10% of rows, materialised) " , legacy , crafter , std : : format ( " {} / {} rows " , legacyCount , crafterCount ) } ) ;
2026-10-17 04:16:51 +02:00
}
// iban ==
{
2026-10-31 20:51:14 +01:00
std : : size_t legacyCount = 0 ;
2026-10-17 04:16:51 +02:00
std : : size_t keyCount = 0 ;
std : : size_t exprCount = 0 ;
std : : size_t viewCount = 0 ;
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] { legacyCount = Legacy : : LoadByIban ( legacyPath , wantedIban ) . size ( ) ; } ) ;
2026-10-17 04:16:51 +02:00
const double withKey = Millis ( [ & ] {
const DB : : Key key ( wantedIban ) ;
keyCount = table . Where ( [ & ] ( DB : : Row < Transaction > r ) { return r [ & Transaction : : iban ] = = key ; } ) . Count ( ) ;
} ) ;
const double withExpression = Millis ( [ & ] { exprCount = table . Where ( iban = = wantedIban ) . Count ( ) ; } ) ;
const double withView = Millis ( [ & ] { viewCount = table . Where ( [ & ] ( DB : : Row < Transaction > r ) { return r [ & Transaction : : iban ] = = wantedIban ; } ) . Count ( ) ; } ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { " iban == x (lambda + Key) " , legacy , withKey , std : : format ( " {} / {} rows, {:.1f} ns/row " , legacyCount , keyCount , withKey * 1e6 / rowCount ) } ) ;
lines . push_back ( { " iban == x (expression) " , legacy , withExpression , std : : format ( " {} rows, {:.1f} ns/row " , exprCount , withExpression * 1e6 / rowCount ) } ) ;
lines . push_back ( { " iban == x (lambda + string_view) " , legacy , withView , std : : format ( " {} rows, {:.1f} ns/row " , viewCount , withView * 1e6 / rowCount ) } ) ;
2026-10-17 04:16:51 +02:00
}
// range + iban
{
2026-10-31 20:51:14 +01:00
std : : size_t legacyCount = 0 ;
2026-10-17 04:16:51 +02:00
std : : size_t crafterCount = 0 ;
2026-10-31 20:51:14 +01:00
const double legacy = Millis ( [ & ] {
for ( const Transaction & tr : Legacy : : LoadRange ( legacyPath , from , to ) ) legacyCount + = tr . iban = = wantedIban ? 1 : 0 ;
2026-10-17 04:16:51 +02:00
} ) ;
const double crafter = Millis ( [ & ] { crafterCount = table . Where ( timePoint > = from & & timePoint < = to & & iban = = wantedIban ) . Count ( ) ; } ) ;
2026-10-31 20:51:14 +01:00
lines . push_back ( { " time range + iban == x " , legacy , crafter , std : : format ( " {} / {} rows " , legacyCount , crafterCount ) } ) ;
2026-10-17 04:16:51 +02:00
}
2026-10-31 20:51:14 +01:00
std : : println ( " {:<42} {:>12} {:>12} {:>8} {} " , " operation " , " legacy ms " , " crafter ms " , " speedup " , " note " ) ;
2026-10-17 04:16:51 +02:00
for ( const Line & line : lines ) {
2026-10-31 20:51:14 +01:00
std : : println ( " {:<42} {:>12.3f} {:>12.3f} {:>7.1f}x {} " , line . name , line . legacy , line . crafter , line . legacy / std : : max ( line . crafter , 0.0001 ) , line . note ) ;
2026-10-17 04:16:51 +02:00
}
2026-10-31 20:51:14 +01:00
std : : println ( " files: legacy {} bytes, crafter {} bytes " , fs : : file_size ( legacyPath ) , fs : : file_size ( crafterPath ) ) ;
2026-10-17 04:16:51 +02:00
std : : println ( " (sink {}) " , sink & 1 ) ;
fs : : remove_all ( dir ) ;
return 0 ;
}