• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 22718209454

05 Mar 2026 12:36PM UTC coverage: 61.96% (-0.02%) from 61.976%
22718209454

push

github

web-flow
feat: add new reorg metrics (#7697)

Description
---
add seperate metrics for blocks added and blocks removed during a reorg.
Grafana cannot display the values correctly if they are grouped like the
current metric does

0 of 20 new or added lines in 2 files covered. (0.0%)

15 existing lines in 2 files now uncovered.

71841 of 115948 relevant lines covered (61.96%)

224766.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.94
/base_layer/core/src/base_node/metrics.rs
1
//  Copyright 2022, The Tari Project
2
//
3
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
//  following conditions are met:
5
//
6
//  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
//  disclaimer.
8
//
9
//  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
//  following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
//  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
//  products derived from this software without specific prior written permission.
14
//
15
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
//  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
//  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
//  USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use once_cell::sync::Lazy;
24
use primitive_types::U512;
25
use tari_common_types::types::FixedHash;
26
use tari_metrics::{Gauge, IntCounter, IntCounterVec, IntGauge, IntGaugeVec};
27
use tari_utilities::hex::Hex;
28

29
pub fn tip_height() -> &'static IntGauge {
36✔
30
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
31
        tari_metrics::register_int_gauge("base_node::blockchain::tip_height", "The current tip height").unwrap()
1✔
32
    });
1✔
33

34
    &METER
36✔
35
}
36✔
36

37
pub fn target_difficulty_sha() -> &'static IntGauge {
28✔
38
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
39
        tari_metrics::register_int_gauge(
1✔
40
            "base_node::blockchain::target_difficulty_sha",
1✔
41
            "The current miner target difficulty for the sha3 PoW algo",
1✔
42
        )
43
        .unwrap()
1✔
44
    });
1✔
45

46
    &METER
28✔
47
}
28✔
48

49
pub fn target_difficulty_monero_randomx() -> &'static IntGauge {
×
50
    static METER: Lazy<IntGauge> = Lazy::new(|| {
×
51
        tari_metrics::register_int_gauge(
×
52
            "base_node::blockchain::target_difficulty_monero",
×
53
            "The current miner target difficulty for the monero PoW algo",
×
54
        )
55
        .unwrap()
×
56
    });
×
57

58
    &METER
×
59
}
×
60
pub fn target_difficulty_tari_randomx() -> &'static IntGauge {
×
61
    static METER: Lazy<IntGauge> = Lazy::new(|| {
×
62
        tari_metrics::register_int_gauge(
×
63
            "base_node::blockchain::target_difficulty_tari_rx",
×
64
            "The current miner target difficulty for the tari rx PoW algo",
×
65
        )
66
        .unwrap()
×
67
    });
×
68

69
    &METER
×
70
}
×
71

72
pub fn target_difficulty_cuckaroo() -> &'static IntGauge {
×
73
    static METER: Lazy<IntGauge> = Lazy::new(|| {
×
74
        tari_metrics::register_int_gauge(
×
75
            "base_node::blockchain::target_difficulty_cuckaroo",
×
76
            "The current miner target difficulty for the cuckaroo PoW algo",
×
77
        )
78
        .unwrap()
×
79
    });
×
80

81
    &METER
×
82
}
×
83

84
/// The target difficulty at the given height
85
pub fn target_difficulty() -> &'static IntGauge {
28✔
86
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
87
        tari_metrics::register_int_gauge("base_node::blockchain::target_diff", "target_difficulty at height").unwrap()
1✔
88
    });
1✔
89

90
    &METER
28✔
91
}
28✔
92

93
/// The accumulated difficulty indicator (log_2 scale) at the given height
94
pub fn accumulated_difficulty_indicator() -> &'static IntGauge {
28✔
95
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
96
        tari_metrics::register_int_gauge(
1✔
97
            "base_node::blockchain::acc_diff_indicator",
1✔
98
            "log2(accumulated_difficulty at height) * 1000",
1✔
99
        )
100
        .unwrap()
1✔
101
    });
1✔
102

103
    &METER
28✔
104
}
28✔
105

106
/// The target difficulty indicator (log_2 scale) at the given height
107
pub fn target_difficulty_indicator() -> &'static IntGauge {
28✔
108
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
109
        tari_metrics::register_int_gauge(
1✔
110
            "base_node::blockchain::target_diff_indicator",
1✔
111
            "log2(target_difficulty at height) * 1000",
1✔
112
        )
113
        .unwrap()
1✔
114
    });
1✔
115

116
    &METER
28✔
117
}
28✔
118

119
/// The block height associated with the current difficulty indicators
120
pub fn difficulty_indicator_height() -> &'static IntGauge {
28✔
121
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
122
        tari_metrics::register_int_gauge(
1✔
123
            "base_node::blockchain::diff_indicator_height",
1✔
124
            "block height associated with difficulty indicators",
1✔
125
        )
126
        .unwrap()
1✔
127
    });
1✔
128
    &METER
28✔
129
}
28✔
130

131
/// floor(log2(total_accumulated_difficulty)) at height [reconstruction: (acc_diff_sig53 / 2^52) * 2^acc_diff_exp2]
132
pub fn accumulated_difficulty_exp2() -> &'static IntGauge {
28✔
133
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
134
        tari_metrics::register_int_gauge(
1✔
135
            "base_node::blockchain::acc_diff_exp2",
1✔
136
            "floor(log2(total_accumulated_difficulty)) at height [reconstruction: (acc_diff_sig53 / 2^52) * \
1✔
137
             2^acc_diff_exp2]",
1✔
138
        )
139
        .unwrap()
1✔
140
    });
1✔
141
    &METER
28✔
142
}
28✔
143

144
/// Top 53 bits of total_accumulated_difficulty at height [reconstruction: (acc_diff_sig53 / 2^52) * 2^acc_diff_exp2]
145
pub fn accumulated_difficulty_sig53() -> &'static IntGauge {
28✔
146
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
147
        tari_metrics::register_int_gauge(
1✔
148
            "base_node::blockchain::acc_diff_sig53",
1✔
149
            "Top 53 bits of total_accumulated_difficulty at height [reconstruction: (acc_diff_sig53 / 2^52) * \
1✔
150
             2^acc_diff_exp2]",
1✔
151
        )
152
        .unwrap()
1✔
153
    });
1✔
154
    &METER
28✔
155
}
28✔
156

157
/// Approximate total_accumulated_difficulty at height as an f64 [reconstruction: (acc_diff_sig53 / 2^52) *
158
/// 2^acc_diff_exp2] Note: This is less accurate than doing the computation directly in the client, but is provided for
159
/// convenience.
160
pub fn accumulated_difficulty_as_f64() -> &'static Gauge {
28✔
161
    static METER: Lazy<Gauge> = Lazy::new(|| {
1✔
162
        tari_metrics::register_gauge(
1✔
163
            "base_node::blockchain::acc_diff_as_f64",
1✔
164
            "Approximate total_accumulated_difficulty at height as an f64 [approximation: (acc_diff_sig53 / 2^52) * \
1✔
165
             2^acc_diff_exp2]",
1✔
166
        )
167
        .unwrap()
1✔
168
    });
1✔
169
    &METER
28✔
170
}
28✔
171

172
pub fn reorg(fork_height: u64, num_added: usize, num_removed: usize) -> IntGauge {
1✔
173
    static METER: Lazy<IntGaugeVec> = Lazy::new(|| {
1✔
174
        tari_metrics::register_int_gauge_vec("base_node::blockchain::reorgs", "Reorg stats", &[
1✔
175
            "fork_height",
1✔
176
            "num_added",
1✔
177
            "num_removed",
1✔
178
        ])
1✔
179
        .unwrap()
1✔
180
    });
1✔
181

182
    METER.with_label_values(&[
1✔
183
        &fork_height.to_string(),
1✔
184
        &num_added.to_string(),
1✔
185
        &num_removed.to_string(),
1✔
186
    ])
1✔
187
}
1✔
188

NEW
189
pub fn reorg_blocks_added() -> &'static IntGauge {
×
NEW
190
    static METER: Lazy<IntGauge> = Lazy::new(|| {
×
NEW
191
        tari_metrics::register_int_gauge(
×
NEW
192
            "base_node::blockchain::reorg_blocks_added_total",
×
NEW
193
            "Total number of blocks added due to chain reorgs",
×
194
        )
NEW
195
        .unwrap()
×
NEW
196
    });
×
197

NEW
198
    &METER
×
NEW
199
}
×
200

NEW
201
pub fn reorg_blocks_removed() -> &'static IntGauge {
×
NEW
202
    static METER: Lazy<IntGauge> = Lazy::new(|| {
×
NEW
203
        tari_metrics::register_int_gauge(
×
NEW
204
            "base_node::blockchain::reorg_blocks_removed_total",
×
NEW
205
            "Total number of blocks removed due to chain reorgs",
×
206
        )
NEW
207
        .unwrap()
×
NEW
208
    });
×
209

NEW
210
    &METER
×
NEW
211
}
×
212

213
pub fn compact_block_tx_misses(height: u64) -> IntGauge {
6✔
214
    static METER: Lazy<IntGaugeVec> = Lazy::new(|| {
1✔
215
        tari_metrics::register_int_gauge_vec(
1✔
216
            "base_node::blockchain::compact_block_unknown_transactions",
1✔
217
            "Number of unknown transactions from the incoming compact block",
1✔
218
            &["height"],
1✔
219
        )
220
        .unwrap()
1✔
221
    });
1✔
222

223
    METER.with_label_values(&[&height.to_string()])
6✔
224
}
6✔
225

226
pub fn compact_block_full_misses(height: u64) -> IntCounter {
4✔
227
    static METER: Lazy<IntCounterVec> = Lazy::new(|| {
1✔
228
        tari_metrics::register_int_counter_vec(
1✔
229
            "base_node::blockchain::compact_block_miss",
1✔
230
            "Number of full blocks that had to be requested",
1✔
231
            &["height"],
1✔
232
        )
233
        .unwrap()
1✔
234
    });
1✔
235

236
    METER.with_label_values(&[&height.to_string()])
4✔
237
}
4✔
238

239
pub fn compact_block_mmr_mismatch(height: u64) -> IntCounter {
×
240
    static METER: Lazy<IntCounterVec> = Lazy::new(|| {
×
241
        tari_metrics::register_int_counter_vec(
×
242
            "base_node::blockchain::compact_block_mmr_mismatch",
×
243
            "Number of full blocks that had to be requested because of MMR mismatch",
×
244
            &["height"],
×
245
        )
246
        .unwrap()
×
247
    });
×
248

249
    METER.with_label_values(&[&height.to_string()])
×
250
}
×
251

252
pub fn orphaned_blocks() -> IntCounter {
×
253
    static METER: Lazy<IntCounter> = Lazy::new(|| {
×
254
        tari_metrics::register_int_counter(
×
255
            "base_node::blockchain::orphaned_blocks",
×
256
            "Number of valid orphan blocks accepted by the base node",
×
257
        )
258
        .unwrap()
×
259
    });
×
260

261
    METER.clone()
×
262
}
×
263

264
pub fn rejected_blocks(height: u64, hash: &FixedHash) -> IntCounter {
2✔
265
    static METER: Lazy<IntCounterVec> = Lazy::new(|| {
1✔
266
        tari_metrics::register_int_counter_vec(
1✔
267
            "base_node::blockchain::rejected_blocks",
1✔
268
            "Number of block rejected by the base node",
1✔
269
            &["height", "block_hash"],
1✔
270
        )
271
        .unwrap()
1✔
272
    });
1✔
273

274
    METER.with_label_values(&[&height.to_string(), &hash.to_hex()])
2✔
275
}
2✔
276

277
pub fn active_sync_peers() -> &'static IntGauge {
12✔
278
    static METER: Lazy<IntGauge> = Lazy::new(|| {
2✔
279
        tari_metrics::register_int_gauge(
2✔
280
            "base_node::sync::active_peers",
2✔
281
            "Number of active peers syncing from this node",
2✔
282
        )
283
        .unwrap()
2✔
284
    });
2✔
285

286
    &METER
12✔
287
}
12✔
288

289
pub fn utxo_set_size() -> &'static IntGauge {
28✔
290
    static METER: Lazy<IntGauge> = Lazy::new(|| {
1✔
291
        tari_metrics::register_int_gauge(
1✔
292
            "base_node::blockchain::utxo_set_size",
1✔
293
            "The number of UTXOs in the current UTXO set",
1✔
294
        )
295
        .unwrap()
1✔
296
    });
1✔
297

298
    &METER
28✔
299
}
28✔
300

301
// Ref: IEEE 754-2008, binary64 format.
302
// f64 has a 53-bit significand (52 explicit + 1 implicit bit).
303
// Scaling by 1/2^52 ensures we map the 53-bit integer into [1, 2),
304
// which is exactly representable in f64 without rounding.
305
const INV_2P52: f64 = 1.0 / ((1u64 << 52) as f64);
306

307
/// Computes log₂(value) as f64 for a U512 using a 53-bit normalized significand.
308
/// Uses IEEE 754 binary64 rules to avoid precision loss:
309
/// - Exact powers of two return an integer result.
310
/// - Non-powers-of-two are guaranteed to be strictly less than the next integer, preventing rounding-up artifacts.
311
#[allow(clippy::cast_possible_truncation)]
312
pub fn log2_u512(value_u512: &U512) -> Option<f64> {
14✔
313
    if value_u512.is_zero() {
14✔
314
        return None;
1✔
315
    }
13✔
316

317
    let (total_bits, sig53) = u512_into_parts(value_u512);
13✔
318

319
    // Converts the 53-bit integer significand into a floating-point number in the range [1, 2)
320
    let x = (sig53 as f64) * INV_2P52;
13✔
321

322
    let frac = x.log2();
13✔
323
    let mut res = (f64::from(total_bits) - 1.0) + frac;
13✔
324

325
    // If not an exact power of two (sig53 > 2^52), ensure we never round up to the next integer.
326
    if sig53 > (1u64 << 52) {
13✔
327
        res = next_down(res);
8✔
328
    }
8✔
329
    Some(res)
13✔
330
}
14✔
331

332
// Returns (exp2, sig53) where:
333
//   - exp2 = floor(log2(value)) (u32)
334
//   - sig53 = top 53 bits (u64)
335
#[allow(clippy::cast_possible_truncation)]
336
fn u512_into_parts(value_u512: &U512) -> (u32, u64) {
17✔
337
    let total_bits: u32 = value_u512.bits() as u32; // total bits
17✔
338

339
    // Build exact 53-bit significand with MSB at bit 52 into u64
340
    let sig53: u64 = if total_bits > 53 {
17✔
341
        // Keep only the top 53 bits
342
        (value_u512 >> (total_bits - 53)).as_u64()
13✔
343
    } else {
344
        // Move the most significant bit to position 52
345
        (value_u512 << (53 - total_bits)).as_u64()
4✔
346
    };
347
    debug_assert!(((1u64 << 52)..(1u64 << 53)).contains(&sig53));
17✔
348
    (total_bits, sig53)
17✔
349
}
17✔
350

351
/// Returns (exp2, sig53) where:
352
///   - exp2 = floor(log2(value)) (i64)
353
///   - sig53 = top 53 bits (i64, always positive, safe up to 2^53-1)
354
///   - Returns None if value == 0.
355
#[allow(clippy::cast_possible_truncation)]
356
pub fn u512_exp2_sig53(value: &U512) -> Option<(i64, i64)> {
4✔
357
    if value.is_zero() {
4✔
358
        return None;
×
359
    }
4✔
360
    let (total_bits, sig53) = u512_into_parts(value);
4✔
361
    Some((i64::from(total_bits) - 1, i64::try_from(sig53).unwrap_or(i64::MAX)))
4✔
362
}
4✔
363

364
/// Approximate a U512 as f64 using a 53-bit significand and exponent as `(sig53 / 2^52) * 2^exp2`.
365
///   - exp2 = floor(log2(value)) (i64)
366
///   - sig53 = top 53 bits (i64, always positive, safe up to 2^53-1)
367
///   - Returns None if value == 0.
368
#[allow(clippy::cast_possible_truncation)]
369
pub fn approximate_u512_with_f64(value: &U512) -> Option<f64> {
1✔
370
    if value.is_zero() {
1✔
371
        return None;
×
372
    }
1✔
373
    let (exp2, sig53) = u512_exp2_sig53(value).unwrap();
1✔
374

375
    // Grafana-style reconstruction: (sig53 / 2^52) * 2^exp2
376
    // This is not exact (limited by f64), but should be within a tiny relative error.
377
    const TWO_P52: f64 = 4503599627370496.0; // 2^52
378
                                             // Build 2^exp2 by setting the exponent (bias 1023), mantissa 0
379
    let two_pow_exp2 = f64::from_bits(((exp2 + 1023) as u64) << 52);
1✔
380
    Some((sig53 as f64 / TWO_P52) * two_pow_exp2)
1✔
381
}
1✔
382

383
// Nudge one floating point unit (ULP) down to counter rounding-up at integer boundaries.
384
#[inline]
385
fn next_down(x: f64) -> f64 {
12✔
386
    // Move to the next representable float toward -∞
387
    f64::from_bits(x.to_bits().saturating_sub(1))
12✔
388
}
12✔
389

390
/// Computes log₂(value) as f64 for a u128 using a 53-bit normalized significand.
391
/// Uses IEEE 754 binary64 rules to avoid precision loss:
392
/// - Exact powers of two return an integer result.
393
/// - Non-powers-of-two are guaranteed to be strictly less than the next integer, preventing rounding-up artifacts.
394
#[inline]
395
#[allow(clippy::cast_possible_truncation)]
396
pub fn log2_u128(value_u128: u128) -> Option<f64> {
10✔
397
    if value_u128 == 0 {
10✔
398
        return None;
1✔
399
    }
9✔
400
    let total_bits: u32 = 128 - value_u128.leading_zeros(); // In the range 1..=128
9✔
401

402
    // Build exact 53-bit significand with MSB at bit 52 into u64
403
    let sig53: u64 = if total_bits > 53 {
9✔
404
        // Keep only the top 53 bits
405
        (value_u128 >> (total_bits - 53)) as u64
5✔
406
    } else {
407
        // Move the most significant bit to position 52
408
        (value_u128 << (53 - total_bits)) as u64
4✔
409
    };
410
    debug_assert!(((1u64 << 52)..(1u64 << 53)).contains(&sig53));
9✔
411

412
    // Converts the 53-bit integer significand into a floating-point number in the range [1, 2)
413
    let x = (sig53 as f64) * INV_2P52;
9✔
414

415
    let frac = x.log2();
9✔
416
    let mut res = (f64::from(total_bits) - 1.0) + frac;
9✔
417

418
    // If not an exact power of two (sig53 > 2^52), ensure we never round up to the next integer.
419
    if sig53 > (1u64 << 52) {
9✔
420
        res = next_down(res);
4✔
421
    }
5✔
422
    Some(res)
9✔
423
}
10✔
424

425
/// Converts a floating point number of bits into an integer number of milli-bits (1/1000 bits).
426
#[inline]
427
#[allow(clippy::cast_possible_truncation)]
428
pub fn milli_bits(x: f64) -> i64 {
4✔
429
    (x * 1000.0).round() as i64
4✔
430
}
4✔
431

432
#[cfg(test)]
433
mod tests {
434
    use primitive_types::U512;
435

436
    use super::*;
437

438
    #[test]
439
    fn test_log2_u512() {
1✔
440
        // log2(1) == 0
441
        assert_eq!(log2_u512(&U512::from(1u64)), Some(0.0));
1✔
442

443
        // Exact powers of two
444
        assert_eq!(log2_u512(&(U512::from(2u64))), Some(1.0));
1✔
445
        assert_eq!(log2_u512(&(U512::from(8u64))), Some(3.0));
1✔
446
        assert_eq!(log2_u512(&(U512::from(1024u64))), Some(10.0));
1✔
447

448
        // 2^200 exactly
449
        let value = U512::from(1u64) << 200;
1✔
450
        assert_eq!(log2_u512(&value), Some(200.0));
1✔
451

452
        // 2^200 - 1  => strictly less than 200
453
        let value = (U512::from(1u64) << 200) - U512::from(1u64);
1✔
454
        assert!(log2_u512(&value).unwrap() < 200.0);
1✔
455

456
        // u128::MAX = 2^128 - 1  => strictly less than 128
457
        assert!(log2_u512(&U512::from(u128::MAX)).unwrap() < 128.0);
1✔
458

459
        // Test U512 max value = 2^512 - 1  => strictly less than 512
460
        let u512_max = U512::MAX;
1✔
461
        let log2_u512_max = log2_u512(&u512_max).unwrap();
1✔
462
        assert!(log2_u512_max < 512.0);
1✔
463
        assert!(log2_u512_max > 511.0);
1✔
464

465
        // log2(0) == None
466
        assert!(log2_u512(&U512::from(0u64)).is_none());
1✔
467
    }
1✔
468

469
    #[test]
470
    fn test_log2_u128() {
1✔
471
        // log2(1) == 0
472
        assert_eq!(log2_u128(1), Some(0.0));
1✔
473

474
        // exact powers of two
475
        assert_eq!(log2_u128(2), Some(1.0));
1✔
476
        assert_eq!(log2_u128(8), Some(3.0));
1✔
477
        assert_eq!(log2_u128(1024), Some(10.0));
1✔
478

479
        // 2^100 exactly
480
        let value = 1u128 << 100;
1✔
481
        assert_eq!(log2_u128(value), Some(100.0));
1✔
482

483
        // 2^100 - 1  => strictly less than 100
484
        let value = (1u128 << 100) - 1;
1✔
485
        assert!(log2_u128(value).unwrap() < 100.0);
1✔
486

487
        // u128::MAX = 2^128 - 1  => strictly less than 128
488
        assert!(log2_u128(u128::MAX).unwrap() < 128.0);
1✔
489

490
        // log2(0) == None
491
        assert!(log2_u128(0).is_none());
1✔
492
    }
1✔
493

494
    #[test]
495
    fn millibit_correctly_handles_small_difficulty_growth() {
1✔
496
        // Accumulated difficulty (U512)
497
        let acc_diff_v1 = U512::from_dec_str("3872628503165662556508806093911347954645375156922").unwrap();
1✔
498
        // Target difficulty (fits in u128)
499
        let target_diff_v1: u128 = 33_208_643_413_617_919;
1✔
500

501
        // --- Baseline milli-bits ---
502
        let acc_diff_v1_mbits = milli_bits(log2_u512(&acc_diff_v1).unwrap());
1✔
503
        let target_diff_v1_mbits = milli_bits(log2_u128(target_diff_v1).unwrap());
1✔
504

505
        // --- Apply +0.15% change (δ = 0.0015) --- (1/0.0015 = 666.666666667 ~= 667)
506
        let acc_diff_v2 = acc_diff_v1 + acc_diff_v1 / U512::from(667u64);
1✔
507
        let target_diff_v2 = target_diff_v1 + (target_diff_v1 / 667);
1✔
508

509
        let acc_diff_v2_mbits = milli_bits(log2_u512(&acc_diff_v2).unwrap());
1✔
510
        let target_diff_v2_mbits = milli_bits(log2_u128(target_diff_v2).unwrap());
1✔
511

512
        assert!(acc_diff_v2_mbits > acc_diff_v1_mbits);
1✔
513
        assert!(target_diff_v2_mbits > target_diff_v1_mbits);
1✔
514
    }
1✔
515

516
    #[test]
517
    fn exp2_sig53_power_of_two_and_neighbors() {
1✔
518
        // 2^200
519
        let v = U512::from(1u64) << 200;
1✔
520
        let (e, s) = u512_exp2_sig53(&v).unwrap();
1✔
521
        assert_eq!(e, 200);
1✔
522
        assert_eq!(s, 1i64 << 52, "exact power of two => normalized sig53 == 2^52");
1✔
523

524
        // 2^200 - 1  (just below)
525
        let v = (U512::from(1u64) << 200) - U512::from(1u64);
1✔
526
        let (e, s) = u512_exp2_sig53(&v).unwrap();
1✔
527
        assert_eq!(e, 199, "floor(log2(2^200-1)) = 199");
1✔
528
        assert!(((1i64 << 52)..(1i64 << 53)).contains(&s));
1✔
529

530
        // 2^200 + 1  (just above)
531
        let v = (U512::from(1u64) << 200) + U512::from(1u64);
1✔
532
        let (e, s) = u512_exp2_sig53(&v).unwrap();
1✔
533
        assert_eq!(e, 200);
1✔
534
        assert!(((1i64 << 52)..(1i64 << 53)).contains(&s));
1✔
535
    }
1✔
536

537
    #[test]
538
    fn it_can_reconstruct_u512_from_exp2_sig53_parts_as_f64() {
1✔
539
        use primitive_types::U512;
540

541
        let original_u512 = U512::from_dec_str("3872628503165662556508806093911347954645375156922").unwrap();
1✔
542
        let approximate_u512 = approximate_u512_with_f64(&original_u512).unwrap();
1✔
543

544
        // Expected bits using our robust log2 (with next_down ULP guard)
545
        let expected_bits = log2_u512(&original_u512).unwrap();
1✔
546

547
        // The two log2 values should match within a tiny epsilon (a few ULPs)
548
        let approx_bits = approximate_u512.log2();
1✔
549
        assert!(
1✔
550
            (approx_bits - expected_bits).abs() < 1e-9,
1✔
551
            "reconstructed log2 mismatch: approx={}, expected={}",
552
            approx_bits,
553
            expected_bits
554
        );
555

556
        // Relative error via logs (safer for huge values):
557
        let relative_err = (approximate_u512.log2() - log2_u512(&original_u512).unwrap()).abs() /
1✔
558
            log2_u512(&original_u512).unwrap().abs();
1✔
559
        assert!(relative_err < 1e-12);
1✔
560

561
        // println!("approx f64 = {}\noriginal   = {}", reconstructed_u512, original_u512);
562
    }
1✔
563
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc