haskoin-store 1.15.0 → 1.15.1
raw patch · 6 files changed
+190/−5 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Haskoin.Store.Runtime: runtimeMetrics :: Logger -> Stats -> IO ()
Files
- CHANGELOG.md +19/−0
- README.md +40/−1
- app/Main.hs +4/−1
- haskoin-store.cabal +3/−2
- src/Haskoin/Store/Runtime.hs +123/−0
- swagger.yaml +1/−1
CHANGELOG.md view
@@ -4,6 +4,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [1.15.1] - 2026-11-14++### Added+- **Metrics of the garbage collector.** With `--statsd`, the store sends+ GHC's statistics under `rts.*` once a second: collections, the time that+ every thread was stopped for them, the CPU that they used, the work of+ the non-moving collector, allocation, and the size of the live data and+ of the heap. Every collection stops every thread, so they say how much+ of a slow answer is the collector's. The executable now runs with+ `+RTS -T`, which GHC needs to keep them.++### Changed+- **The old generation is collected while the store runs.** The executable+ uses GHC's non-moving collector (`--nonmoving-gc`). The copying collector+ stopped every thread while it copied all live data, gigabytes for a store+ of mainnet, and under load every request waited for it, `/health`+ included. The heap can be larger, as old objects are not moved together.+ `GHCRTS=--copying-gc` goes back to the copying collector.+ ## [1.15.0] - 2026-11-14 Answers decode less of what they read, and write hex faster.
README.md view
@@ -739,6 +739,7 @@ | `Haskoin.Store.Fleet.Member`, `Haskoin.Store.Fleet.XPub` | A fleet, and its cache | | `Haskoin.Store.Web`, `Haskoin.Store.Web.Events` | The web server, and `/events` | | `Haskoin.Store.Web.Pieces` | Answers that are sent while they are read |+| `Haskoin.Store.Runtime` | GHC's garbage collector, as StatsD metrics | | `Haskoin.Store.Web.Publish` | Checking a transaction and handing it to the peers, or to a full node over RPC | The rules and queries of version 20 (`Haskoin.Store.Logic`,@@ -793,6 +794,7 @@ | `cache.*` | Cache hits, misses, reloads and full reloads | | `store.*` | Block and header heights, mempool size and other gauges | | `fleet.cache.*` | What a server of a fleet does with the fleet's cache of xpubs |+| `rts.*` | GHC's garbage collector | The metrics of the fleet's cache: @@ -816,6 +818,33 @@ - With `--debug` the log also has a line for each entry built and each block that the cache was brought to. +The metrics of the garbage collector, sent once a second:++| Metric | Kind | Meaning |+| --- | --- | --- |+| `rts.gcs` | counter | Collections |+| `rts.major_gcs` | counter | Collections of the whole heap |+| `rts.gc_pause_us` | counter | Microseconds that every thread was stopped for the collector |+| `rts.gc_pause_mean_us` | gauge | Mean pause of a collection in the last second |+| `rts.gc_pause_percent` | gauge | Share of the last second that every thread was stopped |+| `rts.gc_cpu_us` | counter | Microseconds of CPU that the collector used, on all cores |+| `rts.nonmoving_sync_us` | counter | Microseconds stopped to end a mark of the non-moving collector |+| `rts.nonmoving_mark_us` | counter | Microseconds that the non-moving collector marked, while the store ran |+| `rts.allocated_bytes` | counter | Bytes allocated |+| `rts.live_bytes` | gauge | Bytes live after the last collection |+| `rts.heap_bytes` | gauge | Bytes that the heap holds from the system |++- **Every collection stops every thread**, the small ones too. While it is+ stopped the store answers nothing, not even `/health`, so a high+ `rts.gc_pause_percent` shows up as slow answers everywhere.+- **The executable runs with `+RTS -T`**, which GHC needs to keep these+ statistics. Without it, as in a program that uses the library, the log+ says so and there are no `rts.*` metrics.+- **The `rts.nonmoving_*` metrics** are the work of the non-moving+ collector, which the executable uses for the old generation (see+ [Memory](#memory)). They stay at zero if the store runs with the copying+ collector instead.+ ### Database statistics `/dbstats` returns the memory that RocksDB uses for each column family, and@@ -832,8 +861,18 @@ `--disable-delayed-os-memory-return` for that. Without the flag, Linux keeps counting returned memory as resident until it needs it, and the heap looks as if it never shrinks after a peak.+- **The old generation is collected while the store runs**, with GHC's+ non-moving collector (`--nonmoving-gc`). The default copying collector+ stops every thread while it copies all live data, which takes longer as+ the heap grows. A store of mainnet holds gigabytes, and under load every+ request waited for it, `/health` included. Small collections of new+ objects still stop every thread, but they are short.+- **The price is memory.** Old objects are not moved together, so the heap+ can hold more than the copying collector would for the same live data.+ To go back to the copying collector, run the store with+ `GHCRTS=--copying-gc` in its environment, or `+RTS --copying-gc -RTS`. - If you build the store some other way, pass- `+RTS --disable-delayed-os-memory-return -RTS` yourself.+ `+RTS --nonmoving-gc --disable-delayed-os-memory-return -RTS` yourself. - **To tell such memory from a leak**, see `stress/results/2026-11-09-memory/README.md`: how to read `smaps`, and a heap profile of a busy store, with live data flat at about 10 MiB.
app/Main.hs view
@@ -33,6 +33,7 @@ withStore, ) import Haskoin.Store.Common (loopForever)+import Haskoin.Store.Runtime (runtimeMetrics) import NQE (receive, withSubscription) import Options.Applicative ( Mod,@@ -75,6 +76,7 @@ ( createDirectoryIfMissing, getAppUserDataDirectory, )+import UnliftIO.Async (withAsync) import UnliftIO.Environment (lookupEnv) -- | Version of the package, set at build time.@@ -802,7 +804,8 @@ reportSamples = not cfg.statsdNoEvents, reportStats = cfg.statsdAggregates }- (go . Just)+ -- the metrics of the garbage collector, beside those of the store+ (\stats -> withAsync (runtimeMetrics (withSource "Runtime" logger) stats) (\_ -> go (Just stats))) | otherwise = go Nothing level | cfg.debug = LevelDebug
haskoin-store.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: haskoin-store-version: 1.15.0+version: 1.15.1 synopsis: Storage and index for Bitcoin and Bitcoin Cash description: An indexer for Bitcoin and Bitcoin Cash. It downloads blocks and mempool transactions from the peer-to-peer network, indexes them in RocksDB, and@@ -76,6 +76,7 @@ Haskoin.Store.Logic.Layered Haskoin.Store.Manager Haskoin.Store.Outage+ Haskoin.Store.Runtime Haskoin.Store.Web Haskoin.Store.Web.Docs Haskoin.Store.Web.Events@@ -218,7 +219,7 @@ NoFieldSelectors OverloadedRecordDot OverloadedStrings- ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts "-with-rtsopts=-N --disable-delayed-os-memory-return"+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts "-with-rtsopts=-N -T --nonmoving-gc --disable-delayed-os-memory-return" build-depends: aeson >=2.2.5.1 && <2.3 , base >=4.21.2.0 && <4.22
+ src/Haskoin/Store/Runtime.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Haskoin.Store.Runtime+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- The garbage collector of GHC, as StatsD metrics.+--+-- Every collection of GHC stops every core, the small ones included, and a+-- busy store collects hundreds of times a second. While it is stopped, no+-- request moves: not even @\/health@, which only reads memory. So how long+-- the store spends stopped, and how the heap grows, say as much about how+-- it answers as the timings of its requests do. The statistics that GHC+-- keeps are read once a second and sent as metrics, under @rts.*@.+--+-- GHC keeps them only when the program runs with @+RTS -T@, which the+-- executable of the store turns on by default: it costs a few counters at+-- each collection. A program that uses the library without it gets a line+-- in the log that says so, and no metrics.+--+-- The statistics come as totals since the start, and are sent as what+-- changed in the second: counters, which StatsD sums and turns into rates.+-- The last collection's details would have given a pause time for each+-- collection; read once a second, they would have shown one collection in+-- hundreds, and not the longest. The mean pause of the second is sent+-- instead.+module Haskoin.Store.Runtime+ ( runtimeMetrics,+ )+where++import Control.Monad (forever, unless)+import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)+import Haskoin.Node (Logger, logInfo)+import System.Metrics.StatsD+ ( Stats,+ StatCounter,+ StatGauge,+ incrementCounter,+ newStatCounter,+ newStatGauge,+ setGauge,+ )+import UnliftIO.Concurrent (threadDelay)++-- | The metrics of the collector.+data Metrics = Metrics+ { gcs :: !StatCounter,+ majorGcs :: !StatCounter,+ paused :: !StatCounter,+ pauseMean :: !StatGauge,+ pausePercent :: !StatGauge,+ gcCpu :: !StatCounter,+ syncs :: !StatCounter,+ marking :: !StatCounter,+ allocated :: !StatCounter,+ live :: !StatGauge,+ heap :: !StatGauge+ }++-- | Send the statistics of the collector to StatsD once a second, forever:+-- run it in a thread of its own. If the program does not keep them, it+-- says so in the log and returns.+runtimeMetrics :: Logger -> Stats -> IO ()+runtimeMetrics logger stats = do+ enabled <- getRTSStatsEnabled+ if not enabled+ then logInfo logger "No garbage collector metrics: run with +RTS -T for them"+ else do+ m <- metrics+ start <- getRTSStats+ let loop before = do+ threadDelay 1000000+ now <- getRTSStats+ report m before now+ loop now+ forever (loop start)+ where+ -- one sample a second each: sent whole, unless only aggregates are wanted+ counter k = newStatCounter stats ("rts." <> k) 1+ gauge k = newStatGauge stats ("rts." <> k) 0+ metrics =+ Metrics+ <$> counter "gcs"+ <*> counter "major_gcs"+ <*> counter "gc_pause_us"+ <*> gauge "gc_pause_mean_us"+ <*> gauge "gc_pause_percent"+ <*> counter "gc_cpu_us"+ <*> counter "nonmoving_sync_us"+ <*> counter "nonmoving_mark_us"+ <*> counter "allocated_bytes"+ <*> gauge "live_bytes"+ <*> gauge "heap_bytes"++-- | Send what changed between two readings of the statistics.+report :: Metrics -> RTSStats -> RTSStats -> IO ()+report m before now = do+ add m.gcs collections+ add m.majorGcs (delta (.major_gcs))+ add m.paused (micros (.gc_elapsed_ns))+ setGauge m.pauseMean (if collections > 0 then micros (.gc_elapsed_ns) `div` collections else 0)+ setGauge m.pausePercent (if wall > 0 then 100 * delta (.gc_elapsed_ns) `div` wall else 0)+ add m.gcCpu (micros (.gc_cpu_ns))+ add m.syncs (micros (.nonmoving_gc_sync_elapsed_ns))+ add m.marking (micros (.nonmoving_gc_elapsed_ns))+ add m.allocated (delta (.allocated_bytes))+ setGauge m.live (fromIntegral now.gc.gcdetails_live_bytes)+ setGauge m.heap (fromIntegral now.gc.gcdetails_mem_in_use_bytes)+ where+ delta :: (Integral a) => (RTSStats -> a) -> Int+ delta f = fromIntegral (f now) - fromIntegral (f before)+ micros :: (Integral a) => (RTSStats -> a) -> Int+ micros f = delta f `div` 1000+ collections = delta (.gcs)+ wall = delta (.elapsed_ns)+ -- a counter that did not move is not sent+ add c n = unless (n <= 0) (incrementCounter c n)
swagger.yaml view
@@ -11,7 +11,7 @@ - url: './' info:- version: &version 1.15.0+ version: &version 1.15.1 title: Haskoin contact: name: JP Rupp