|
| 1 | +use fast_uuid_v7::{gen_id_with_sub_ms_12, gen_id_with_sub_ms_4, gen_id_with_sub_ms_8}; |
| 2 | +use std::time::{SystemTime, UNIX_EPOCH}; |
| 3 | + |
| 4 | +const DEFAULT_SAMPLES: usize = 20_000; |
| 5 | + |
| 6 | +fn main() { |
| 7 | + let samples = std::env::args() |
| 8 | + .nth(1) |
| 9 | + .map(|arg| { |
| 10 | + arg.parse::<usize>() |
| 11 | + .expect("sample count must be a positive integer") |
| 12 | + }) |
| 13 | + .unwrap_or(DEFAULT_SAMPLES); |
| 14 | + |
| 15 | + println!("Sub-ms diagnostic using SystemTime before/after windows"); |
| 16 | + println!("Samples per variant: {samples}"); |
| 17 | + println!(); |
| 18 | + |
| 19 | + run_variant("gen_id_with_sub_ms_4", 4, gen_id_with_sub_ms_4, samples); |
| 20 | + run_variant("gen_id_with_sub_ms_8", 8, gen_id_with_sub_ms_8, samples); |
| 21 | + run_variant("gen_id_with_sub_ms_12", 12, gen_id_with_sub_ms_12, samples); |
| 22 | +} |
| 23 | + |
| 24 | +fn run_variant(name: &str, bits: u8, gen_id: fn() -> u128, samples: usize) { |
| 25 | + let mut interval_errors_ns = Vec::with_capacity(samples); |
| 26 | + let mut midpoint_errors_ns = Vec::with_capacity(samples); |
| 27 | + let mut signed_midpoint_errors_ns = Vec::with_capacity(samples); |
| 28 | + let mut call_windows_ns = Vec::with_capacity(samples); |
| 29 | + let mut overlap_count = 0usize; |
| 30 | + let mut lagging_count = 0usize; |
| 31 | + let mut leading_count = 0usize; |
| 32 | + |
| 33 | + for _ in 0..samples { |
| 34 | + let before = unix_time_ns(); |
| 35 | + let id = gen_id(); |
| 36 | + let after = unix_time_ns(); |
| 37 | + |
| 38 | + let (id_start_ns, id_end_ns) = decode_sub_ms_window_ns(id, bits); |
| 39 | + let interval_error_ns = interval_gap_ns(id_start_ns, id_end_ns, before, after); |
| 40 | + let observed_mid_ns = before + (after.saturating_sub(before) / 2); |
| 41 | + let id_mid_ns = id_start_ns + ((id_end_ns.saturating_sub(id_start_ns)) / 2); |
| 42 | + let midpoint_error_ns = id_mid_ns.abs_diff(observed_mid_ns); |
| 43 | + let signed_midpoint_error_ns = id_mid_ns as i128 - observed_mid_ns as i128; |
| 44 | + |
| 45 | + if interval_error_ns == 0 { |
| 46 | + overlap_count += 1; |
| 47 | + } |
| 48 | + if signed_midpoint_error_ns < 0 { |
| 49 | + lagging_count += 1; |
| 50 | + } else if signed_midpoint_error_ns > 0 { |
| 51 | + leading_count += 1; |
| 52 | + } |
| 53 | + |
| 54 | + interval_errors_ns.push(interval_error_ns); |
| 55 | + midpoint_errors_ns.push(midpoint_error_ns); |
| 56 | + signed_midpoint_errors_ns.push(signed_midpoint_error_ns); |
| 57 | + call_windows_ns.push(after.saturating_sub(before)); |
| 58 | + } |
| 59 | + |
| 60 | + interval_errors_ns.sort_unstable(); |
| 61 | + midpoint_errors_ns.sort_unstable(); |
| 62 | + signed_midpoint_errors_ns.sort_unstable(); |
| 63 | + call_windows_ns.sort_unstable(); |
| 64 | + |
| 65 | + println!("{name} ({bits} bits)"); |
| 66 | + print_stats("call window", &call_windows_ns); |
| 67 | + print_stats("interval gap", &interval_errors_ns); |
| 68 | + print_stats("midpoint error", &midpoint_errors_ns); |
| 69 | + print_signed_stats("signed drift", &signed_midpoint_errors_ns); |
| 70 | + println!( |
| 71 | + " overlap rate : {:>6.2}%\n lagging rate : {:>6.2}%\n leading rate : {:>6.2}%", |
| 72 | + percentage(overlap_count, samples), |
| 73 | + percentage(lagging_count, samples), |
| 74 | + percentage(leading_count, samples), |
| 75 | + ); |
| 76 | + println!( |
| 77 | + " <=100us gap : {:>6.2}%\n <=250us gap : {:>6.2}%\n <=500us gap : {:>6.2}%\n <=1ms gap : {:>6.2}%", |
| 78 | + percentage_within(&interval_errors_ns, 100_000), |
| 79 | + percentage_within(&interval_errors_ns, 250_000), |
| 80 | + percentage_within(&interval_errors_ns, 500_000), |
| 81 | + percentage_within(&interval_errors_ns, 1_000_000), |
| 82 | + ); |
| 83 | + println!(); |
| 84 | +} |
| 85 | + |
| 86 | +fn unix_time_ns() -> u128 { |
| 87 | + SystemTime::now() |
| 88 | + .duration_since(UNIX_EPOCH) |
| 89 | + .expect("system clock should be after UNIX_EPOCH") |
| 90 | + .as_nanos() |
| 91 | +} |
| 92 | + |
| 93 | +fn decode_sub_ms_window_ns(id: u128, bits: u8) -> (u128, u128) { |
| 94 | + let ms = id >> 80; |
| 95 | + let rand_a = (id >> 64) & 0x0FFF; |
| 96 | + let fraction = rand_a >> (12 - bits); |
| 97 | + let slots = 1u128 << bits; |
| 98 | + let base_ns = ms * 1_000_000; |
| 99 | + let start_ns = base_ns + (fraction * 1_000_000) / slots; |
| 100 | + let end_ns = base_ns + (((fraction + 1) * 1_000_000).saturating_sub(1)) / slots; |
| 101 | + (start_ns, end_ns) |
| 102 | +} |
| 103 | + |
| 104 | +fn interval_gap_ns(a_start: u128, a_end: u128, b_start: u128, b_end: u128) -> u128 { |
| 105 | + if a_end < b_start { |
| 106 | + b_start - a_end |
| 107 | + } else if b_end < a_start { |
| 108 | + a_start - b_end |
| 109 | + } else { |
| 110 | + 0 |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +fn print_stats(label: &str, values: &[u128]) { |
| 115 | + println!( |
| 116 | + " {label:<14} p50={:>8}us p90={:>8}us p99={:>8}us max={:>8}us", |
| 117 | + ns_to_us(percentile(values, 50)), |
| 118 | + ns_to_us(percentile(values, 90)), |
| 119 | + ns_to_us(percentile(values, 99)), |
| 120 | + ns_to_us(*values.last().unwrap_or(&0)), |
| 121 | + ); |
| 122 | +} |
| 123 | + |
| 124 | +fn print_signed_stats(label: &str, values: &[i128]) { |
| 125 | + println!( |
| 126 | + " {label:<14} p10={:>8}us p50={:>8}us p90={:>8}us", |
| 127 | + signed_ns_to_us(percentile_signed(values, 10)), |
| 128 | + signed_ns_to_us(percentile_signed(values, 50)), |
| 129 | + signed_ns_to_us(percentile_signed(values, 90)), |
| 130 | + ); |
| 131 | +} |
| 132 | + |
| 133 | +fn percentile(sorted: &[u128], percentile: usize) -> u128 { |
| 134 | + if sorted.is_empty() { |
| 135 | + return 0; |
| 136 | + } |
| 137 | + |
| 138 | + let rank = ((sorted.len() - 1) * percentile) / 100; |
| 139 | + sorted[rank] |
| 140 | +} |
| 141 | + |
| 142 | +fn ns_to_us(value: u128) -> u128 { |
| 143 | + value / 1_000 |
| 144 | +} |
| 145 | + |
| 146 | +fn signed_ns_to_us(value: i128) -> i128 { |
| 147 | + value / 1_000 |
| 148 | +} |
| 149 | + |
| 150 | +fn percentile_signed(sorted: &[i128], percentile: usize) -> i128 { |
| 151 | + if sorted.is_empty() { |
| 152 | + return 0; |
| 153 | + } |
| 154 | + |
| 155 | + let rank = ((sorted.len() - 1) * percentile) / 100; |
| 156 | + sorted[rank] |
| 157 | +} |
| 158 | + |
| 159 | +fn percentage_within(sorted: &[u128], limit_ns: u128) -> f64 { |
| 160 | + if sorted.is_empty() { |
| 161 | + return 0.0; |
| 162 | + } |
| 163 | + |
| 164 | + let count = sorted.partition_point(|value| *value <= limit_ns); |
| 165 | + percentage(count, sorted.len()) |
| 166 | +} |
| 167 | + |
| 168 | +fn percentage(count: usize, total: usize) -> f64 { |
| 169 | + if total == 0 { |
| 170 | + return 0.0; |
| 171 | + } |
| 172 | + |
| 173 | + (count as f64 * 100.0) / total as f64 |
| 174 | +} |
0 commit comments