Skip to main content

hpx_emulation/emulation/
rand.rs

1use strum::VariantArray;
2
3use super::{Emulation, EmulationOS, EmulationOption};
4
5// Market share weights per browser family.
6const CHROME_WEIGHT: f32 = 0.714;
7const SAFARI_WEIGHT: f32 = 0.150;
8const EDGE_WEIGHT: f32 = 0.050;
9const FIREFOX_WEIGHT: f32 = 0.030;
10const OPERA_WEIGHT: f32 = 0.020;
11const OKHTTP_WEIGHT: f32 = 0.010;
12
13// ponytail: flat per-variant distribution within each family.
14// Upgrade to per-variant weights when old-version usage data is available.
15macro_rules! weight_variants {
16    ($weight:expr, $($v:expr),+ $(,)?) => {{
17        const COUNT: usize = [$(stringify!($v)),+].len();
18        const W: f32 = $weight / COUNT as f32;
19        &[$(($v, W)),+] as &[(Emulation, f32)]
20    }};
21}
22
23const WEIGHTED: &[&[(Emulation, f32)]] = &[
24    weight_variants!(
25        CHROME_WEIGHT,
26        Emulation::Chrome100,
27        Emulation::Chrome101,
28        Emulation::Chrome104,
29        Emulation::Chrome105,
30        Emulation::Chrome106,
31        Emulation::Chrome107,
32        Emulation::Chrome108,
33        Emulation::Chrome109,
34        Emulation::Chrome110,
35        Emulation::Chrome114,
36        Emulation::Chrome116,
37        Emulation::Chrome117,
38        Emulation::Chrome118,
39        Emulation::Chrome119,
40        Emulation::Chrome120,
41        Emulation::Chrome123,
42        Emulation::Chrome124,
43        Emulation::Chrome126,
44        Emulation::Chrome127,
45        Emulation::Chrome128,
46        Emulation::Chrome129,
47        Emulation::Chrome130,
48        Emulation::Chrome131,
49        Emulation::Chrome132,
50        Emulation::Chrome133,
51        Emulation::Chrome134,
52        Emulation::Chrome135,
53        Emulation::Chrome136,
54        Emulation::Chrome137,
55        Emulation::Chrome138,
56        Emulation::Chrome139,
57        Emulation::Chrome140,
58        Emulation::Chrome141,
59        Emulation::Chrome142,
60        Emulation::Chrome143,
61        Emulation::Chrome144,
62        Emulation::Chrome145,
63        Emulation::Chrome146,
64        Emulation::Chrome147,
65        Emulation::Chrome148,
66        Emulation::Chrome149,
67    ),
68    weight_variants!(
69        SAFARI_WEIGHT,
70        Emulation::SafariIos17_2,
71        Emulation::SafariIos17_4_1,
72        Emulation::SafariIos16_5,
73        Emulation::Safari15_3,
74        Emulation::Safari15_5,
75        Emulation::Safari15_6_1,
76        Emulation::Safari16,
77        Emulation::Safari16_5,
78        Emulation::Safari17_0,
79        Emulation::Safari17_2_1,
80        Emulation::Safari17_4_1,
81        Emulation::Safari17_5,
82        Emulation::Safari17_6,
83        Emulation::Safari18,
84        Emulation::SafariIPad18,
85        Emulation::Safari18_2,
86        Emulation::SafariIos18_1_1,
87        Emulation::Safari18_3,
88        Emulation::Safari18_3_1,
89        Emulation::Safari18_5,
90        Emulation::Safari26,
91        Emulation::Safari26_1,
92        Emulation::Safari26_2,
93        Emulation::SafariIPad26,
94        Emulation::SafariIPad26_2,
95        Emulation::SafariIos26,
96        Emulation::SafariIos26_2,
97        Emulation::Safari19,
98        Emulation::SafariIos19,
99        Emulation::SafariIPad19,
100        Emulation::Safari20,
101        Emulation::SafariIos20,
102        Emulation::SafariIPad20,
103        Emulation::Safari21,
104        Emulation::SafariIos21,
105        Emulation::SafariIPad21,
106        Emulation::Safari22,
107        Emulation::SafariIos22,
108        Emulation::SafariIPad22,
109        Emulation::Safari23,
110        Emulation::SafariIos23,
111        Emulation::SafariIPad23,
112        Emulation::Safari24,
113        Emulation::SafariIos24,
114        Emulation::SafariIPad24,
115        Emulation::Safari25,
116        Emulation::SafariIos25,
117        Emulation::SafariIPad25,
118        Emulation::Safari26_3,
119        Emulation::SafariIos26_3,
120        Emulation::SafariIPad26_3,
121        Emulation::Safari26_4,
122        Emulation::SafariIos26_4,
123        Emulation::SafariIPad26_4,
124    ),
125    weight_variants!(
126        EDGE_WEIGHT,
127        Emulation::Edge101,
128        Emulation::Edge122,
129        Emulation::Edge127,
130        Emulation::Edge131,
131        Emulation::Edge134,
132        Emulation::Edge135,
133        Emulation::Edge136,
134        Emulation::Edge137,
135        Emulation::Edge138,
136        Emulation::Edge139,
137        Emulation::Edge140,
138        Emulation::Edge141,
139        Emulation::Edge142,
140        Emulation::Edge143,
141        Emulation::Edge144,
142        Emulation::Edge145,
143        Emulation::Edge146,
144        Emulation::Edge147,
145        Emulation::Edge148,
146    ),
147    weight_variants!(
148        FIREFOX_WEIGHT,
149        Emulation::Firefox109,
150        Emulation::Firefox117,
151        Emulation::Firefox128,
152        Emulation::Firefox133,
153        Emulation::Firefox135,
154        Emulation::FirefoxPrivate135,
155        Emulation::FirefoxAndroid135,
156        Emulation::Firefox136,
157        Emulation::FirefoxPrivate136,
158        Emulation::Firefox137,
159        Emulation::Firefox138,
160        Emulation::Firefox139,
161        Emulation::Firefox140,
162        Emulation::Firefox141,
163        Emulation::Firefox142,
164        Emulation::Firefox143,
165        Emulation::Firefox144,
166        Emulation::Firefox145,
167        Emulation::Firefox146,
168        Emulation::Firefox147,
169        Emulation::Firefox148,
170        Emulation::Firefox149,
171        Emulation::Firefox150,
172        Emulation::Firefox151,
173    ),
174    weight_variants!(
175        OPERA_WEIGHT,
176        Emulation::Opera116,
177        Emulation::Opera117,
178        Emulation::Opera118,
179        Emulation::Opera119,
180        Emulation::Opera120,
181        Emulation::Opera121,
182        Emulation::Opera122,
183        Emulation::Opera123,
184        Emulation::Opera124,
185        Emulation::Opera125,
186        Emulation::Opera126,
187        Emulation::Opera127,
188        Emulation::Opera128,
189        Emulation::Opera129,
190        Emulation::Opera130,
191        Emulation::Opera131,
192    ),
193    weight_variants!(
194        OKHTTP_WEIGHT,
195        Emulation::OkHttp3_9,
196        Emulation::OkHttp3_11,
197        Emulation::OkHttp3_13,
198        Emulation::OkHttp3_14,
199        Emulation::OkHttp4_9,
200        Emulation::OkHttp4_10,
201        Emulation::OkHttp4_12,
202        Emulation::OkHttp5,
203    ),
204];
205
206impl Emulation {
207    /// Returns a random variant of the `Emulation` enum.
208    ///
209    /// This method uses the `rand` crate to select a random variant
210    /// from the `Emulation::VARIANTS` array.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use hpx_emulation::Emulation;
216    ///
217    /// let random_emulation = Emulation::random();
218    /// println!("{:?}", random_emulation);
219    /// ```
220    ///
221    /// # Panics
222    ///
223    /// This method will panic if the `Emulation::VARIANTS` array is empty.
224    #[inline]
225    pub fn random() -> EmulationOption {
226        let emulation = Emulation::VARIANTS;
227        let emulation_os = EmulationOS::VARIANTS;
228        let rand = rand::random::<u64>() as usize;
229        EmulationOption::builder()
230            .emulation(emulation[rand % emulation.len()])
231            .emulation_os(emulation_os[rand % emulation_os.len()])
232            .build()
233    }
234
235    /// Returns a weighted-random `EmulationOption` biased by browser market share.
236    ///
237    /// Weights approximate global desktop+mobile browser share:
238    /// Chrome ~71.4%, Safari ~15%, Edge ~5%, Firefox ~3%, Opera ~2%, OkHttp ~1%.
239    ///
240    /// The OS is chosen uniformly from [`EmulationOS::VARIANTS`].
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use hpx_emulation::Emulation;
246    ///
247    /// let opt = Emulation::weighted_random();
248    /// println!("{:?}", opt);
249    /// ```
250    #[inline]
251    pub fn weighted_random() -> EmulationOption {
252        let r = rand::random::<f32>();
253        let mut cumulative = 0.0f32;
254        let emulation = 'outer: {
255            for variants in WEIGHTED {
256                for &(emu, w) in *variants {
257                    cumulative += w;
258                    if r < cumulative {
259                        break 'outer emu;
260                    }
261                }
262            }
263            // ponytail: float rounding tail — pick last variant.
264            Emulation::OkHttp5
265        };
266        let emulation_os = EmulationOS::VARIANTS;
267        let os_idx = rand::random::<u64>() as usize % emulation_os.len();
268        EmulationOption::builder()
269            .emulation(emulation)
270            .emulation_os(emulation_os[os_idx])
271            .build()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use std::{sync::Arc, thread};
278
279    use parking_lot::Mutex;
280
281    use super::*;
282
283    #[test]
284    fn test_concurrent_get_random_emulation() {
285        const THREAD_COUNT: usize = 10;
286        const ITERATIONS: usize = 100;
287
288        let results = Arc::new(Mutex::new(Vec::new()));
289
290        let mut handles = vec![];
291
292        for _ in 0..THREAD_COUNT {
293            let results = Arc::clone(&results);
294            let handle = thread::spawn(move || {
295                for _ in 0..ITERATIONS {
296                    let emulation = Emulation::random();
297                    let mut results = results.lock();
298                    results.push(emulation);
299                }
300            });
301            handles.push(handle);
302        }
303
304        for handle in handles {
305            handle.join().expect("worker thread panicked");
306        }
307
308        let results = results.lock();
309        println!("Total results: {}", results.len());
310    }
311
312    #[test]
313    fn test_weighted_random_chrome_frequency() {
314        const N: usize = 10_000;
315        let mut chrome_count: usize = 0;
316        for _ in 0..N {
317            let opt = Emulation::weighted_random();
318            if format!("{:?}", opt.emulation).starts_with("Chrome") {
319                chrome_count += 1;
320            }
321        }
322        let freq = chrome_count as f64 / N as f64;
323        assert!(
324            (0.66..=0.77).contains(&freq),
325            "Chrome frequency {freq:.3} outside [0.66, 0.77]"
326        );
327    }
328}