From 87d0cf7deba0803ce7e07445f63f18a88fca8482 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 29 May 2025 22:20:58 +0300 Subject: [PATCH] db: make statistics optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think most Bolt users never care about this data, so we're just wasting time for nothing. This is also one of the exclusive locks that we have on the View() path. While this patch doesn't change much on its own, because the other lock is still here (subject to a different patch), once that lock is removed the difference in concurrent View() test is pretty clear. With NoStatistics=false: workers samples min avg 50% 80% 90% max 1 10 123.905µs 969.042µs 1.062529ms 1.065585ms 1.071537ms 1.071537ms 10 100 34.636µs 178.176µs 89.7µs 110.439µs 943.753µs 1.055165ms 100 1000 31.79µs 280.166µs 51.358µs 526.992µs 1.034306ms 2.47819ms 1000 10000 30.608µs 818.098µs 86.464µs 935.799µs 2.681115ms 10.595186ms 10000 100000 30.569µs 3.060826ms 64.132µs 6.56151ms 11.199984ms 64.855384ms NoStatistics=true: workers samples min avg 50% 80% 90% max 1 10 68.049µs 962.039µs 1.060335ms 1.064633ms 1.066087ms 1.066087ms 10 100 34.846µs 315.346µs 90.943µs 862.499µs 1.00516ms 1.08366ms 100 1000 31.45µs 225.53µs 36.88µs 236.63µs 939.115µs 1.466286ms 1000 10000 30.539µs 207.383µs 43.643µs 110.841µs 408.146µs 5.689001ms 10000 100000 30.488µs 152.603µs 39.636µs 90.622µs 145.266µs 9.28235ms The default behavior is kept for compatibility. In future the option can be extended to avoid collecting transaction statistics as well. Now that stats is a pointer we can also revert a part of 26f89a595140f163a4e8a7c86b689990f6335788 and make the structure cleaner. Signed-off-by: Roman Khimov --- db.go | 54 ++++++++++++++++++++++++++++++++++-------------------- tx.go | 16 +++++++++------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/db.go b/db.go index 280ddc273..5d3e26496 100644 --- a/db.go +++ b/db.go @@ -36,12 +36,6 @@ const ( // All data access is performed through transactions which can be obtained through the DB. // All the functions on DB will return a ErrDatabaseNotOpen if accessed before Open() is called. type DB struct { - // Put `stats` at the first field to ensure it's 64-bit aligned. Note that - // the first word in an allocated struct can be relied upon to be 64-bit - // aligned. Refer to https://pkg.go.dev/sync/atomic#pkg-note-BUG. Also - // refer to discussion in https://github.com/etcd-io/bbolt/issues/577. - stats Stats - // When enabled, the database will perform a Check() after every commit. // A panic is issued if the database is in an inconsistent state. This // flag has a large performance impact so it should only be used for @@ -138,6 +132,7 @@ type DB struct { pageSize int opened bool rwtx *Tx + stats *Stats freelist fl.Interface freelistLoad sync.Once @@ -203,6 +198,10 @@ func Open(path string, mode os.FileMode, options *Options) (db *DB, err error) { db.MaxBatchDelay = common.DefaultMaxBatchDelay db.AllocSize = common.DefaultAllocSize + if !options.NoStatistics { + db.stats = new(Stats) + } + if options.Logger == nil { db.logger = getDiscardLogger() } else { @@ -430,7 +429,9 @@ func (db *DB) loadFreelist() { // Read free list from freelist page. db.freelist.Read(db.page(db.meta().Freelist())) } - db.stats.FreePageN = db.freelist.FreeCount() + if db.stats != nil { + db.stats.FreePageN = db.freelist.FreeCount() + } }) } @@ -808,10 +809,12 @@ func (db *DB) beginTx() (*Tx, error) { db.metalock.Unlock() // Update the transaction stats. - db.statlock.Lock() - db.stats.TxN++ - db.stats.OpenTxN++ - db.statlock.Unlock() + if db.stats != nil { + db.statlock.Lock() + db.stats.TxN++ + db.stats.OpenTxN++ + db.statlock.Unlock() + } return t, nil } @@ -867,10 +870,12 @@ func (db *DB) removeTx(tx *Tx) { db.metalock.Unlock() // Merge statistics. - db.statlock.Lock() - db.stats.OpenTxN-- - db.stats.TxStats.add(&tx.stats) - db.statlock.Unlock() + if db.stats != nil { + db.statlock.Lock() + db.stats.OpenTxN-- + db.stats.TxStats.add(&tx.stats) + db.statlock.Unlock() + } } // Update executes a function within the context of a read-write managed transaction. @@ -1088,9 +1093,13 @@ func (db *DB) Sync() (err error) { // Stats retrieves ongoing performance stats for the database. // This is only updated when a transaction closes. func (db *DB) Stats() Stats { - db.statlock.RLock() - defer db.statlock.RUnlock() - return db.stats + var s Stats + if db.stats != nil { + db.statlock.RLock() + s = *db.stats + db.statlock.RUnlock() + } + return s } // This is for internal access to the raw data bytes from the C cursor, use @@ -1340,6 +1349,11 @@ type Options struct { // Logger is the logger used for bbolt. Logger Logger + + // NoStatistics turns off statistics collection, Stats method will + // return empty structure in this case. This can be beneficial for + // performance under high-concurrency read-only transactions. + NoStatistics bool } func (o *Options) String() string { @@ -1347,8 +1361,8 @@ func (o *Options) String() string { return "{}" } - return fmt.Sprintf("{Timeout: %s, NoGrowSync: %t, NoFreelistSync: %t, PreLoadFreelist: %t, FreelistType: %s, ReadOnly: %t, MmapFlags: %x, InitialMmapSize: %d, PageSize: %d, MaxSize: %d, NoSync: %t, OpenFile: %p, Mlock: %t, Logger: %p}", - o.Timeout, o.NoGrowSync, o.NoFreelistSync, o.PreLoadFreelist, o.FreelistType, o.ReadOnly, o.MmapFlags, o.InitialMmapSize, o.PageSize, o.MaxSize, o.NoSync, o.OpenFile, o.Mlock, o.Logger) + return fmt.Sprintf("{Timeout: %s, NoGrowSync: %t, NoFreelistSync: %t, PreLoadFreelist: %t, FreelistType: %s, ReadOnly: %t, MmapFlags: %x, InitialMmapSize: %d, PageSize: %d, MaxSize: %d, NoSync: %t, OpenFile: %p, Mlock: %t, Logger: %p, NoStatistics: %t}", + o.Timeout, o.NoGrowSync, o.NoFreelistSync, o.PreLoadFreelist, o.FreelistType, o.ReadOnly, o.MmapFlags, o.InitialMmapSize, o.PageSize, o.MaxSize, o.NoSync, o.OpenFile, o.Mlock, o.Logger, o.NoStatistics) } diff --git a/tx.go b/tx.go index 5eb383c4b..38d34f8af 100644 --- a/tx.go +++ b/tx.go @@ -357,13 +357,15 @@ func (tx *Tx) close() { tx.db.rwlock.Unlock() // Merge statistics. - tx.db.statlock.Lock() - tx.db.stats.FreePageN = freelistFreeN - tx.db.stats.PendingPageN = freelistPendingN - tx.db.stats.FreeAlloc = (freelistFreeN + freelistPendingN) * tx.db.pageSize - tx.db.stats.FreelistInuse = freelistAlloc - tx.db.stats.TxStats.add(&tx.stats) - tx.db.statlock.Unlock() + if tx.db.stats != nil { + tx.db.statlock.Lock() + tx.db.stats.FreePageN = freelistFreeN + tx.db.stats.PendingPageN = freelistPendingN + tx.db.stats.FreeAlloc = (freelistFreeN + freelistPendingN) * tx.db.pageSize + tx.db.stats.FreelistInuse = freelistAlloc + tx.db.stats.TxStats.add(&tx.stats) + tx.db.statlock.Unlock() + } } else { tx.db.removeTx(tx) }