Skip to main content

netspeed_cli/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4#[serde(rename_all = "snake_case")]
5pub enum PhaseState {
6    Completed,
7    Skipped,
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct PhaseResult {
12    pub state: PhaseState,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub reason: Option<String>,
15}
16
17impl PhaseResult {
18    #[must_use]
19    pub fn completed() -> Self {
20        Self {
21            state: PhaseState::Completed,
22            reason: None,
23        }
24    }
25
26    #[must_use]
27    pub fn skipped(reason: impl Into<String>) -> Self {
28        Self {
29            state: PhaseState::Skipped,
30            reason: Some(reason.into()),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct TestPhases {
37    pub ping: PhaseResult,
38    pub download: PhaseResult,
39    pub upload: PhaseResult,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct Server {
44    #[serde(rename = "@id")]
45    pub id: String,
46    #[serde(rename = "@url")]
47    pub url: String,
48    #[serde(rename = "@name")]
49    pub name: String,
50    #[serde(rename = "@sponsor")]
51    pub sponsor: String,
52    #[serde(rename = "@country")]
53    pub country: String,
54    #[serde(rename = "@lat")]
55    pub lat: f64,
56    #[serde(rename = "@lon")]
57    pub lon: f64,
58    #[serde(skip)]
59    pub distance: f64,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct TestResult {
64    pub status: String,
65    pub version: String, // CLI version for API compatibility
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub test_id: Option<String>, // Unique identifier for this test run
68    pub server: ServerInfo,
69    pub ping: Option<f64>,
70    pub jitter: Option<f64>,
71    pub packet_loss: Option<f64>,
72    pub download: Option<f64>,
73    pub download_peak: Option<f64>,
74    pub download_cv: Option<f64>, // coefficient of variation (0-1) for variance
75    pub upload: Option<f64>,
76    pub upload_peak: Option<f64>,
77    pub upload_cv: Option<f64>, // coefficient of variation (0-1) for variance
78    pub download_ci_95: Option<(f64, f64)>, // (lower, upper) 95% CI in Mbps
79    pub upload_ci_95: Option<(f64, f64)>, // (lower, upper) 95% CI in Mbps
80    pub latency_download: Option<f64>,
81    pub latency_upload: Option<f64>,
82    pub download_samples: Option<Vec<f64>>,
83    pub upload_samples: Option<Vec<f64>>,
84    pub ping_samples: Option<Vec<f64>>,
85    pub timestamp: String,
86    pub client_ip: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub client_location: Option<ClientLocation>,
89    // Computed grades for machine-readable output (screen readers, scripts)
90    pub overall_grade: Option<String>,
91    pub download_grade: Option<String>,
92    pub upload_grade: Option<String>,
93    pub connection_rating: Option<String>,
94    pub phases: TestPhases,
95}
96
97/// Builder for constructing `TestResult` — separates construction from data types (SRP).
98///
99/// Uses injected `StatsService` for statistical computations,
100/// enabling alternative algorithms without modifying `TestResult`.
101pub struct TestResultBuilder {
102    server: ServerInfo,
103    ping: Option<f64>,
104    jitter: Option<f64>,
105    packet_loss: Option<f64>,
106    ping_samples: Vec<f64>,
107    download_avg_bps: f64,
108    download_peak_bps: f64,
109    download_samples: Vec<f64>,
110    download_latency: Option<f64>,
111    upload_avg_bps: f64,
112    upload_peak_bps: f64,
113    upload_samples: Vec<f64>,
114    upload_latency: Option<f64>,
115    client_ip: Option<String>,
116    client_location: Option<ClientLocation>,
117}
118
119impl TestResultBuilder {
120    pub fn new(server: ServerInfo) -> Self {
121        Self {
122            server,
123            ping: None,
124            jitter: None,
125            packet_loss: None,
126            ping_samples: Vec::new(),
127            download_avg_bps: 0.0,
128            download_peak_bps: 0.0,
129            download_samples: Vec::new(),
130            download_latency: None,
131            upload_avg_bps: 0.0,
132            upload_peak_bps: 0.0,
133            upload_samples: Vec::new(),
134            upload_latency: None,
135            client_ip: None,
136            client_location: None,
137        }
138    }
139
140    pub fn ping(mut self, ping: f64) -> Self {
141        self.ping = Some(ping);
142        self
143    }
144
145    pub fn jitter(mut self, jitter: f64) -> Self {
146        self.jitter = Some(jitter);
147        self
148    }
149
150    pub fn packet_loss(mut self, loss: f64) -> Self {
151        self.packet_loss = Some(loss);
152        self
153    }
154
155    pub fn ping_samples(mut self, samples: &[f64]) -> Self {
156        self.ping_samples = samples.to_vec();
157        self
158    }
159
160    pub fn download_stats(
161        mut self,
162        avg_bps: f64,
163        peak_bps: f64,
164        samples: &[f64],
165        latency_under_load: Option<f64>,
166    ) -> Self {
167        self.download_avg_bps = avg_bps;
168        self.download_peak_bps = peak_bps;
169        self.download_samples = samples.to_vec();
170        self.download_latency = latency_under_load;
171        self
172    }
173
174    pub fn upload_stats(
175        mut self,
176        avg_bps: f64,
177        peak_bps: f64,
178        samples: &[f64],
179        latency_under_load: Option<f64>,
180    ) -> Self {
181        self.upload_avg_bps = avg_bps;
182        self.upload_peak_bps = peak_bps;
183        self.upload_samples = samples.to_vec();
184        self.upload_latency = latency_under_load;
185        self
186    }
187
188    pub fn client_ip(mut self, ip: impl Into<String>) -> Self {
189        self.client_ip = Some(ip.into());
190        self
191    }
192
193    pub fn client_location(mut self, location: ClientLocation) -> Self {
194        self.client_location = Some(location);
195        self
196    }
197
198    /// Build the `TestResult` using the given stats service for CV/CI computation.
199    pub fn build(self, stats: &dyn StatsService) -> TestResult {
200        fn opt_samples(v: &[f64]) -> Option<Vec<f64>> {
201            if v.is_empty() { None } else { Some(v.to_vec()) }
202        }
203        fn opt_positive(v: f64) -> Option<f64> {
204            if v > 0.0 { Some(v) } else { None }
205        }
206
207        TestResult {
208            status: "ok".to_string(),
209            version: env!("CARGO_PKG_VERSION").to_string(),
210            test_id: Some(uuid_v4()),
211            server: self.server,
212            ping: self.ping,
213            jitter: self.jitter,
214            packet_loss: self.packet_loss,
215            download: opt_positive(self.download_avg_bps),
216            download_peak: opt_positive(self.download_peak_bps),
217            download_cv: stats.coefficient_of_variation(&self.download_samples),
218            upload: opt_positive(self.upload_avg_bps),
219            upload_peak: opt_positive(self.upload_peak_bps),
220            upload_cv: stats.coefficient_of_variation(&self.upload_samples),
221            download_ci_95: stats.confidence_interval_95(&self.download_samples, 1_000_000.0),
222            upload_ci_95: stats.confidence_interval_95(&self.upload_samples, 1_000_000.0),
223            latency_download: self.download_latency,
224            latency_upload: self.upload_latency,
225            download_samples: opt_samples(&self.download_samples),
226            upload_samples: opt_samples(&self.upload_samples),
227            ping_samples: opt_samples(&self.ping_samples),
228            timestamp: chrono::Utc::now().to_rfc3339(),
229            client_ip: self.client_ip,
230            client_location: self.client_location,
231            overall_grade: None,
232            download_grade: None,
233            upload_grade: None,
234            connection_rating: None,
235            phases: TestPhases {
236                ping: PhaseResult::completed(),
237                download: PhaseResult::completed(),
238                upload: PhaseResult::completed(),
239            },
240        }
241    }
242}
243
244impl TestResult {
245    /// Build a `TestResult` from ping test output and download/upload test runs.
246    /// Delegates to `TestResultBuilder` for construction.
247    #[allow(clippy::too_many_arguments)]
248    #[must_use]
249    pub fn from_test_runs(
250        server: ServerInfo,
251        ping: Option<f64>,
252        jitter: Option<f64>,
253        packet_loss: Option<f64>,
254        ping_samples: &[f64],
255        dl: &crate::task_runner::TestRunResult,
256        ul: &crate::task_runner::TestRunResult,
257        client_ip: Option<String>,
258        client_location: Option<ClientLocation>,
259    ) -> Self {
260        let mut builder = TestResultBuilder::new(server)
261            .ping_samples(ping_samples)
262            .download_stats(
263                dl.avg_bps,
264                dl.peak_bps,
265                &dl.speed_samples,
266                dl.latency_under_load,
267            )
268            .upload_stats(
269                ul.avg_bps,
270                ul.peak_bps,
271                &ul.speed_samples,
272                ul.latency_under_load,
273            );
274
275        if let Some(p) = ping {
276            builder = builder.ping(p);
277        }
278        if let Some(j) = jitter {
279            builder = builder.jitter(j);
280        }
281        if let Some(pl) = packet_loss {
282            builder = builder.packet_loss(pl);
283        }
284        if let Some(ip) = client_ip {
285            builder = builder.client_ip(ip);
286        }
287        if let Some(loc) = client_location {
288            builder = builder.client_location(loc);
289        }
290
291        builder.build(&DefaultStats)
292    }
293}
294
295/// Compute coefficient of variation (CV) for a sample set.
296/// Returns None for empty or single-element sets, or when mean is zero.
297fn compute_cv(samples: &[f64]) -> Option<f64> {
298    if samples.len() < 2 {
299        return None;
300    }
301    // Safe: sample counts are small (≤1000), well under 2^53.
302    let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
303    if mean == 0.0 {
304        return None;
305    }
306    // Safe: sample counts are small (≤1000), well under 2^53.
307    let variance: f64 =
308        samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (samples.len() - 1) as f64;
309    let std_dev = variance.sqrt();
310    Some(std_dev / mean)
311}
312
313/// Compute 95% confidence interval for the mean bandwidth.
314/// Returns `(lower, upper)` in Mbps. Uses t-distribution approximation for small samples.
315fn compute_ci_95(samples: &[f64], scale: f64) -> Option<(f64, f64)> {
316    let n = samples.len();
317    if n < 2 {
318        return None;
319    }
320    // Safe: sample counts are small (≤1000), well under 2^53.
321    let mean: f64 = samples.iter().sum::<f64>() / n as f64;
322    if n < 30 {
323        // Small sample: use t ≈ 2.045 (df=29, 95% CI) as conservative estimate
324        // Safe: n is small (≤1000), well under 2^53.
325        let variance: f64 =
326            samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
327        let std_err = variance.sqrt() / (n as f64).sqrt();
328        let margin = 2.045 * std_err;
329        Some(((mean - margin) / scale, (mean + margin) / scale))
330    } else {
331        // Large sample: use z = 1.96
332        // Safe: n is small (≤1000), well under 2^53.
333        let variance: f64 =
334            samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
335        let std_err = variance.sqrt() / (n as f64).sqrt();
336        let margin = 1.96 * std_err;
337        Some(((mean - margin) / scale, (mean + margin) / scale))
338    }
339}
340
341/// Trait for statistical computations - enables alternative algorithms.
342pub trait StatsService: Send + Sync {
343    fn coefficient_of_variation(&self, samples: &[f64]) -> Option<f64>;
344    fn confidence_interval_95(&self, samples: &[f64], scale: f64) -> Option<(f64, f64)>;
345}
346
347/// Default statistics implementation using standard formulas.
348pub struct DefaultStats;
349
350impl DefaultStats {
351    pub fn new() -> Self {
352        Self
353    }
354}
355
356impl Default for DefaultStats {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362impl StatsService for DefaultStats {
363    fn coefficient_of_variation(&self, samples: &[f64]) -> Option<f64> {
364        compute_cv(samples)
365    }
366
367    fn confidence_interval_95(&self, samples: &[f64], scale: f64) -> Option<(f64, f64)> {
368        compute_ci_95(samples, scale)
369    }
370}
371
372#[derive(Debug, Clone, Serialize)]
373pub struct ServerInfo {
374    pub id: String,
375    pub name: String,
376    pub sponsor: String,
377    pub country: String,
378    pub distance: f64,
379}
380
381/// Client geographic location derived from speedtest.net config API.
382#[derive(Debug, Clone, Serialize, Default)]
383pub struct ClientLocation {
384    pub lat: f64,
385    pub lon: f64,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub city: Option<String>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub country: Option<String>,
390}
391
392#[derive(Debug, Clone, Serialize)]
393pub struct CsvOutput {
394    pub server_id: String,
395    pub sponsor: String,
396    pub server_name: String,
397    pub timestamp: String,
398    pub distance: f64,
399    pub ping: f64,
400    pub jitter: f64,
401    pub packet_loss: f64,
402    pub download: f64,
403    pub download_peak: f64,
404    pub upload: f64,
405    pub upload_peak: f64,
406    pub ip_address: String,
407}
408
409#[cfg(test)]
410#[allow(clippy::items_after_test_module)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_server_serialization() {
416        let server = Server {
417            id: "1234".to_string(),
418            url: "http://example.com".to_string(),
419            name: "Test Server".to_string(),
420            sponsor: "Test ISP".to_string(),
421            country: "US".to_string(),
422            lat: 40.7128,
423            lon: -74.0060,
424            distance: 100.5,
425        };
426
427        let json = serde_json::to_string(&server).unwrap();
428        // With @ prefix for XML attributes, serde serializes them as normal fields in JSON
429        assert!(json.contains("\"1234\""));
430        assert!(json.contains("\"Test Server\""));
431    }
432
433    #[test]
434    fn test_test_result_serialization() {
435        let result = TestResult {
436            status: "ok".to_string(),
437            version: env!("CARGO_PKG_VERSION").to_string(),
438            test_id: None,
439            server: ServerInfo {
440                id: "1234".to_string(),
441                name: "Test Server".to_string(),
442                sponsor: "Test ISP".to_string(),
443                country: "US".to_string(),
444                distance: 100.5,
445            },
446            ping: Some(15.234),
447            jitter: Some(1.2),
448            packet_loss: Some(0.0),
449            download: Some(150_000_000.0),
450            download_peak: Some(180_000_000.0),
451            upload: Some(50_000_000.0),
452            upload_peak: Some(60_000_000.0),
453            latency_download: Some(25.0),
454            latency_upload: Some(30.0),
455            download_samples: None,
456            upload_samples: None,
457            ping_samples: None,
458            timestamp: "2026-04-04T12:00:00Z".to_string(),
459            client_ip: Some("192.168.1.1".to_string()),
460            client_location: None,
461            download_cv: None,
462            upload_cv: None,
463            download_ci_95: None,
464            upload_ci_95: None,
465            overall_grade: None,
466            download_grade: None,
467            upload_grade: None,
468            connection_rating: None,
469            phases: TestPhases {
470                ping: PhaseResult::completed(),
471                download: PhaseResult::completed(),
472                upload: PhaseResult::completed(),
473            },
474        };
475
476        let json = serde_json::to_string(&result).unwrap();
477        assert!(json.contains("\"status\":\"ok\""));
478        assert!(json.contains("\"ping\":15.234"));
479        assert!(json.contains("\"jitter\":1.2"));
480        assert!(json.contains("\"download\":150000000.0"));
481        assert!(json.contains("\"phases\""));
482    }
483
484    #[test]
485    fn test_csv_output_serialization() {
486        let csv = CsvOutput {
487            server_id: "1234".to_string(),
488            sponsor: "Test ISP".to_string(),
489            server_name: "Test Server".to_string(),
490            timestamp: "2026-04-04T12:00:00Z".to_string(),
491            distance: 100.5,
492            ping: 15.234,
493            jitter: 1.2,
494            packet_loss: 0.0,
495            download: 150_000_000.0,
496            download_peak: 180_000_000.0,
497            upload: 50_000_000.0,
498            upload_peak: 60_000_000.0,
499            ip_address: "192.168.1.1".to_string(),
500        };
501
502        let json = serde_json::to_string(&csv).unwrap();
503        assert!(json.contains("\"server_id\":\"1234\""));
504        assert!(json.contains("\"ping\":15.234"));
505        assert!(json.contains("\"jitter\":1.2"));
506    }
507
508    #[test]
509    fn test_server_clone() {
510        let server = Server {
511            id: "1234".to_string(),
512            url: "http://example.com".to_string(),
513            name: "Test".to_string(),
514            sponsor: "ISP".to_string(),
515            country: "US".to_string(),
516            lat: 40.0,
517            lon: -74.0,
518            distance: 0.0,
519        };
520
521        let cloned = server.clone();
522        assert_eq!(cloned.id, server.id);
523        assert_eq!(cloned.name, server.name);
524    }
525
526    #[test]
527    fn test_phase_result_skipped_serialization() {
528        let phase = PhaseResult::skipped("disabled by user");
529        let json = serde_json::to_string(&phase).unwrap();
530        assert!(json.contains("\"state\":\"skipped\""));
531        assert!(json.contains("\"reason\":\"disabled by user\""));
532    }
533
534    #[test]
535    fn test_uuid_v4_format() {
536        let id = uuid_v4();
537        // UUID v4 format: 8-4-4-4-12 hex characters
538        assert_eq!(id.len(), 36);
539        assert!(id.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
540        assert_eq!(&id[14..15], "4"); // Version 4 marker
541    }
542
543    #[test]
544    fn test_uuid_v4_unique() {
545        let id1 = uuid_v4();
546        let id2 = uuid_v4();
547        assert_ne!(id1, id2);
548    }
549
550    #[test]
551    fn test_client_location_serialization() {
552        let loc = ClientLocation {
553            lat: 40.7128,
554            lon: -74.0060,
555            city: Some("New York".to_string()),
556            country: Some("US".to_string()),
557        };
558        let json = serde_json::to_string(&loc).unwrap();
559        assert!(json.contains("\"lat\":40.7128"));
560        assert!(json.contains("\"lon\":-74.006"));
561        assert!(json.contains("\"city\":\"New York\""));
562    }
563
564    #[test]
565    fn test_client_location_minimal() {
566        let loc = ClientLocation {
567            lat: 0.0,
568            lon: 0.0,
569            city: None,
570            country: None,
571        };
572        let json = serde_json::to_string(&loc).unwrap();
573        assert!(!json.contains("city"));
574        assert!(!json.contains("country"));
575    }
576
577    #[test]
578    fn test_result_builder_full() {
579        let server = ServerInfo {
580            id: "1".to_string(),
581            name: "Server".to_string(),
582            sponsor: "ISP".to_string(),
583            country: "US".to_string(),
584            distance: 50.0,
585        };
586        let result = TestResultBuilder::new(server)
587            .ping(10.0)
588            .jitter(2.0)
589            .packet_loss(0.5)
590            .ping_samples(&[10.0, 12.0])
591            .download_stats(
592                100_000_000.0,
593                120_000_000.0,
594                &[90_000_000.0, 110_000_000.0],
595                Some(25.0),
596            )
597            .upload_stats(
598                50_000_000.0,
599                60_000_000.0,
600                &[45_000_000.0, 55_000_000.0],
601                Some(30.0),
602            )
603            .client_ip("1.2.3.4")
604            .client_location(ClientLocation {
605                lat: 40.0,
606                lon: -74.0,
607                city: Some("NYC".to_string()),
608                country: Some("US".to_string()),
609            })
610            .build(&DefaultStats);
611
612        assert_eq!(result.status, "ok");
613        assert_eq!(result.ping, Some(10.0));
614        assert_eq!(result.jitter, Some(2.0));
615        assert_eq!(result.packet_loss, Some(0.5));
616        assert_eq!(result.download, Some(100_000_000.0));
617        assert_eq!(result.download_peak, Some(120_000_000.0));
618        assert_eq!(result.upload, Some(50_000_000.0));
619        assert_eq!(result.client_ip, Some("1.2.3.4".to_string()));
620        assert!(result.download_samples.is_some());
621        assert!(result.ping_samples.is_some());
622        assert!(result.download_cv.is_some());
623        assert!(result.download_ci_95.is_some());
624        assert!(result.test_id.is_some());
625    }
626
627    #[test]
628    fn test_result_builder_minimal() {
629        let server = ServerInfo {
630            id: "0".to_string(),
631            name: "".to_string(),
632            sponsor: "".to_string(),
633            country: "".to_string(),
634            distance: 0.0,
635        };
636        let result = TestResultBuilder::new(server).build(&DefaultStats);
637
638        assert_eq!(result.status, "ok");
639        assert!(result.ping.is_none());
640        assert!(result.jitter.is_none());
641        assert!(result.download.is_none());
642        assert!(result.upload.is_none());
643        assert!(result.client_ip.is_none());
644        assert!(result.download_samples.is_none());
645        assert!(result.ping_samples.is_none());
646        assert!(result.download_cv.is_none());
647        assert!(result.upload_cv.is_none());
648    }
649
650    #[test]
651    fn test_stats_service_cv_known_data() {
652        let stats = DefaultStats::new();
653        let cv = stats.coefficient_of_variation(&[10.0, 12.0]);
654        assert!(cv.is_some());
655        let val = cv.unwrap();
656        assert!(val > 0.0);
657    }
658
659    #[test]
660    fn test_stats_service_cv_empty() {
661        let stats = DefaultStats::new();
662        assert!(stats.coefficient_of_variation(&[]).is_none());
663    }
664
665    #[test]
666    fn test_stats_service_cv_single() {
667        let stats = DefaultStats::new();
668        assert!(stats.coefficient_of_variation(&[42.0]).is_none());
669    }
670
671    #[test]
672    fn test_stats_service_cv_zero_mean() {
673        let stats = DefaultStats::new();
674        assert!(stats.coefficient_of_variation(&[0.0, 0.0, 0.0]).is_none());
675    }
676
677    #[test]
678    fn test_stats_service_ci95_known_data() {
679        let stats = DefaultStats::new();
680        let ci = stats.confidence_interval_95(&[100.0, 102.0, 98.0, 101.0, 99.0], 1.0);
681        assert!(ci.is_some());
682        let (lo, hi) = ci.unwrap();
683        assert!(lo < hi);
684        let mid = (lo + hi) / 2.0;
685        assert!((mid - 100.0).abs() < 2.0);
686    }
687
688    #[test]
689    fn test_stats_service_ci95_too_few_samples() {
690        let stats = DefaultStats::new();
691        assert!(stats.confidence_interval_95(&[50.0], 1.0).is_none());
692    }
693}
694
695/// Generate a simple UUID v4-like identifier using timestamp and random bytes.
696/// This is not a standards-compliant UUID but provides uniqueness for test tracking.
697fn uuid_v4() -> String {
698    use std::time::{SystemTime, UNIX_EPOCH};
699    let timestamp = SystemTime::now()
700        .duration_since(UNIX_EPOCH)
701        .unwrap_or_default()
702        .as_nanos();
703    let random: u64 = rand_simple();
704    // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
705    format!(
706        "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
707        (timestamp as u64) & 0xFFFFFFFF,
708        ((random >> 48) & 0xFFFF) as u16,
709        ((random >> 32) & 0xFFF) as u16,
710        ((random >> 16) & 0xFFFF) as u16,
711        random & 0xFFFFFFFFFFFF
712    )
713}
714
715/// Simple pseudo-random number generator based on xorshift.
716/// Not cryptographically secure, but sufficient for test ID generation.
717fn rand_simple() -> u64 {
718    use std::sync::atomic::{AtomicU64, Ordering};
719    static STATE: AtomicU64 = AtomicU64::new(0x123456789ABCDEF0);
720    let mut state = STATE.load(Ordering::Relaxed);
721    if state == 0 {
722        state = 0x123456789ABCDEF0;
723    }
724    state ^= state << 13;
725    state ^= state >> 7;
726    state ^= state << 17;
727    STATE.store(state, Ordering::Relaxed);
728    state
729}