Add per-table byte size metrics (lean_table_bytes)#244
Add per-table byte size metrics (lean_table_bytes)#244pablodeymo wants to merge 4 commits intomainfrom
Conversation
Track the byte size (key + value) of each storage table as a live
Prometheus gauge. This provides visibility into storage growth and
is a prerequisite for improving pruning decisions.
The approach uses one AtomicU64 per table in the Store struct:
- On startup, a full scan seeds the counters from existing data.
- On insert, the SSZ-encoded key+value size is added.
- On prune/delete, the size of removed entries is subtracted.
Exposed as lean_table_bytes{table="states"}, lean_table_bytes{table="block_headers"}, etc.
🤖 Codex Code Review
No consensus-layer correctness, SSZ, or signature-validation regressions stood out here; the PR is operational/metrics-focused. Main gap is correctness of the new accounting itself, and I would want tests covering overwrite/idempotent writes, metadata updates, and post-prune metric refresh before merging. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Kimi Code ReviewThe PR adds per-table storage byte metrics to track database growth. Overall the approach is sound, but there are several correctness and performance issues to address. Critical Issues1. Incorrect byte accounting on gossip signature upserts ( Fix by calculating the delta: if let Some(old_val) = old_value_len {
let old_bytes = (encoded_key.len() + old_val.len()) as u64;
self.sub_table_bytes(Table::GossipSignatures, old_bytes);
self.add_table_bytes(Table::GossipSignatures, new_bytes);
} else {
self.gossip_signatures_count.fetch_add(1, Ordering::Relaxed);
self.add_table_bytes(Table::GossipSignatures, new_bytes);
}2. Unhandled overwrites in other insert methods (
Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewNow I have everything I need. Here's my review: Review: Add per-table byte size metrics (
|
Greptile SummaryThis PR adds per-table Prometheus gauges (
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/metrics.rs | Adds update_table_bytes function using a LazyLock<IntGaugeVec> pattern consistent with the rest of the file. Minor: bytes as i64 cast should use try_into().unwrap_or(i64::MAX) to match the saturating-conversion pattern used by other metrics in the same file. |
| crates/storage/src/api/tables.rs | Adds Table::name() returning human-readable metric label strings. Exhaustive match ensures the compiler will catch any missing variant. Strings are identical to cf_name() in rocksdb.rs but serve a distinct purpose (metric labels vs stable CF names), so the duplication is intentional. |
| crates/storage/src/api/traits.rs | Adds estimate_table_bytes to StorageBackend with a sensible default of 0, keeping in-memory backends compatible without changes. Clean, minimal, non-breaking addition. |
| crates/storage/src/backend/rocksdb.rs | Implements estimate_table_bytes via property_int_value_cf("rocksdb.estimate-live-data-size") — an O(1) metadata-only lookup. Gracefully falls back to 0 if the CF handle is not found. Correct and idiomatic. |
| crates/storage/src/store.rs | Minimal pass-through estimate_table_bytes delegating directly to the backend. No state management needed since the approach relies on RocksDB's native property rather than manual counters. |
| crates/blockchain/src/lib.rs | Iterates over all 8 tables and emits lean_table_bytes after each block is processed. The 8 O(1) RocksDB property reads per block add negligible overhead. Straightforward integration. |
| crates/storage/src/lib.rs | Adds ALL_TABLES to the crate's public API surface so the blockchain crate can iterate over all tables. One-line change, correct. |
Sequence Diagram
sequenceDiagram
participant BC as BlockChainServer
participant Store as Store
participant Backend as RocksDBBackend
participant RDB as RocksDB
BC->>BC: process_block(signed_block)
loop for each table in ALL_TABLES
BC->>Store: estimate_table_bytes(table)
Store->>Backend: estimate_table_bytes(table)
Backend->>RDB: cf_handle(cf_name(table))
RDB-->>Backend: ColumnFamily
Backend->>RDB: property_int_value_cf(cf, "rocksdb.estimate-live-data-size")
RDB-->>Backend: u64 bytes
Backend-->>Store: u64 bytes
Store-->>BC: u64 bytes
BC->>BC: metrics::update_table_bytes(table.name(), bytes)
end
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/blockchain/src/metrics.rs
Line: 323
Comment:
**Silent negative metric for very large table sizes**
`bytes as i64` is a bitwise cast — for values above `i64::MAX` (~9.2 EB) it silently produces a **negative** Prometheus gauge value rather than panicking or clamping. While astronomically unlikely today, it is inconsistent with the pattern used elsewhere in this file (e.g. `update_validators_count` uses `.try_into().unwrap()`, `set_attestation_committee_count` uses `.try_into().unwrap_or_default()`). A negative byte-size metric would be confusing and hard to detect.
Prefer a saturating conversion to be consistent and safe:
```suggestion
.set(bytes.try_into().unwrap_or(i64::MAX));
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "Use RocksDB estimate..."
- Extract sum_entry_bytes() helper to deduplicate byte-summing in prune_old_states, prune_old_blocks, and delete_gossip_signatures - Merge count_gossip_signatures into scan_tables to avoid scanning the GossipSignatures table twice at startup - Remove dead (usize, u64) return from prune_by_slot — callers never used the bytes value since sub_table_bytes is called inside - Fix double-counting bug: insert_pending_block no longer tracks bytes since the same keys are overwritten by insert_signed_block - Revert unnecessary old_value_len rename in insert_gossip_signature
Replace the AtomicU64 bookkeeping approach with a direct query to RocksDB's per-column-family property. This eliminates: - Startup table scan (scan_tables) - Byte tracking on every insert and prune - BlockWriteBytes struct, sum_entry_bytes helper, add/sub_table_bytes The StorageBackend trait gains estimate_table_bytes(table) with a default returning 0. RocksDB implements it via property_int_value_cf on the "rocksdb.estimate-live-data-size" property — an O(1) metadata lookup, no scanning required.
rocksdb.estimate-live-data-size only counts flushed SST files, so the metric stays at 0 until RocksDB triggers a compaction. Add rocksdb.cur-size-all-mem-tables to capture in-memory data too.
Summary
lean_table_bytes{table="..."}).estimate-live-data-sizeproperty — an O(1) metadata lookup per column family, no scanning.How it works
StorageBackendtrait gainsestimate_table_bytes(table)with a default returning 0.RocksDBBackendimplements it viaproperty_int_value_cf("rocksdb.estimate-live-data-size").Store::estimate_table_bytes()passes through to the backend.lean_table_bytesIntGaugeVecwith atablelabel, updated after each block is processed.No startup scans, no atomic counters, no insert/prune instrumentation needed.
Tables tracked
statesblock_headersblock_bodiesblock_signaturesgossip_signaturesattestation_data_by_rootlive_chainmetadataFiles changed
crates/storage/src/api/traits.rs—estimate_table_bytesonStorageBackendtraitcrates/storage/src/backend/rocksdb.rs— RocksDB implementation viaproperty_int_value_cfcrates/storage/src/api/tables.rs—Table::name()for metric labelscrates/storage/src/store.rs—estimate_table_bytes()pass-throughcrates/blockchain/src/metrics.rs—lean_table_bytesIntGaugeVeccrates/blockchain/src/lib.rs— emit table bytes after each block processedHow to Test
GET :5054/metricslean_table_bytes{table="states"},lean_table_bytes{table="block_headers"}, etc. are present