Skip to main content

statsig_rust/specs_response/
specs_hash_map.rs

1use std::{borrow::Cow, cell::Cell, sync::Arc};
2
3use ahash::{HashMap, HashMapExt};
4use rkyv::{primitive::ArchivedU64, vec::ArchivedVec};
5use serde::{
6    ser::{SerializeSeq, SerializeStruct},
7    Deserialize, Deserializer, Serialize, Serializer,
8};
9use serde_json::value::RawValue;
10
11use crate::{
12    evaluation::evaluation_data::SpecView,
13    interned_string::InternedString,
14    interned_values::{
15        mmap_data_v2::{
16            ArchivedMmapDynamicString, ArchivedMmapReturnable, ArchivedMmapRule, ArchivedMmapSpec,
17        },
18        InternedStore,
19    },
20    log_e,
21    specs_response::spec_types::Spec,
22};
23
24const TAG: &str = "SpecsHashMap";
25
26#[derive(PartialEq, Debug, Default)] /* DO_NOT_CLONE */
27pub struct SpecsHashMap(pub HashMap<InternedString, SpecPointer>);
28
29#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
30pub(crate) struct SpecDecodeStats {
31    pub(crate) total: usize,
32    pub(crate) mmap: usize,
33}
34
35thread_local! {
36    // Count representation while maps are already being materialized so telemetry does not
37    // require another O(n) walk over production-sized specs. Delta decoding seeds this count from
38    // the previous snapshot before applying its changes.
39    static ACTIVE_SPEC_DECODE_STATS: Cell<Option<SpecDecodeStats>> = const { Cell::new(None) };
40}
41
42struct SpecDecodeStatsGuard {
43    previous: Option<SpecDecodeStats>,
44}
45
46impl Drop for SpecDecodeStatsGuard {
47    fn drop(&mut self) {
48        ACTIVE_SPEC_DECODE_STATS.with(|stats| stats.set(self.previous));
49    }
50}
51
52pub(crate) fn track_spec_decodes<T>(callback: impl FnOnce() -> T) -> (T, SpecDecodeStats) {
53    let previous =
54        ACTIVE_SPEC_DECODE_STATS.with(|stats| stats.replace(Some(SpecDecodeStats::default())));
55    let guard = SpecDecodeStatsGuard { previous };
56    let result = callback();
57    let stats = ACTIVE_SPEC_DECODE_STATS.with(Cell::get).unwrap_or_default();
58    drop(guard);
59    (result, stats)
60}
61
62pub(crate) fn seed_spec_decode_stats(stats: SpecDecodeStats) {
63    ACTIVE_SPEC_DECODE_STATS.with(|active| {
64        if active.get().is_some() {
65            active.set(Some(stats));
66        }
67    });
68}
69
70fn record_specs_cleared(values: &HashMap<InternedString, SpecPointer>) {
71    ACTIVE_SPEC_DECODE_STATS.with(|active| {
72        let Some(mut stats) = active.get() else {
73            return;
74        };
75
76        stats.total -= values.len();
77        stats.mmap -= values.values().filter(|value| value.is_mmap()).count();
78        active.set(Some(stats));
79    });
80}
81
82fn record_spec_change(previous_is_mmap: Option<bool>, next_is_mmap: Option<bool>) {
83    ACTIVE_SPEC_DECODE_STATS.with(|active| {
84        let Some(mut stats) = active.get() else {
85            return;
86        };
87
88        if let Some(previous_is_mmap) = previous_is_mmap {
89            stats.total -= 1;
90            stats.mmap -= usize::from(previous_is_mmap);
91        }
92        if let Some(next_is_mmap) = next_is_mmap {
93            stats.total += 1;
94            stats.mmap += usize::from(next_is_mmap);
95        }
96        active.set(Some(stats));
97    });
98}
99
100#[cfg(test)]
101mod decode_stats_tests {
102    use super::{record_spec_change, seed_spec_decode_stats, track_spec_decodes, SpecDecodeStats};
103
104    #[test]
105    fn seeded_stats_track_replacements_and_deletions() {
106        let (_, stats) = track_spec_decodes(|| {
107            seed_spec_decode_stats(SpecDecodeStats { total: 3, mmap: 2 });
108            record_spec_change(Some(true), Some(false));
109            record_spec_change(Some(false), None);
110        });
111
112        assert_eq!(stats, SpecDecodeStats { total: 2, mmap: 1 });
113    }
114}
115
116impl<'de> Deserialize<'de> for SpecsHashMap {
117    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
118    where
119        D: Deserializer<'de>,
120    {
121        let raw_values: HashMap<InternedString, Box<RawValue>> =
122            Deserialize::deserialize(_deserializer)?;
123
124        let mut result = SpecsHashMap(HashMap::with_capacity(raw_values.len()));
125        for (key, raw_value) in raw_values.into_iter() {
126            let json_string = raw_value.get();
127
128            let mut preloaded = None;
129            if InternedStore::has_preloaded_mmap_v2() {
130                if let Ok(identity) = serde_json::from_str::<SpecIdentity<'_>>(json_string) {
131                    preloaded =
132                        InternedStore::try_get_preloaded_spec(&key, identity.entity.as_ref());
133                    if let Some(spec) = &preloaded {
134                        let existing_checksum = spec.view().checksum().map(|value| value.as_str());
135                        match (identity.checksum.as_deref(), existing_checksum) {
136                            (Some(checksum), Some(existing)) if existing == checksum => {
137                                result.insert(key, preloaded.expect("preloaded spec must exist"));
138                                continue;
139                            }
140                            (None, None) => {}
141                            _ => preloaded = None,
142                        }
143                    }
144                }
145            }
146
147            let spec: Spec = match serde_json::from_str(json_string) {
148                Ok(spec) => spec,
149                Err(e) => {
150                    log_e!(TAG, "Failed to deserialize spec: {}", e);
151                    continue;
152                }
153            };
154
155            if preloaded
156                .as_ref()
157                .is_some_and(|preloaded| preloaded.matches_owned_spec(&spec))
158            {
159                result.insert(key, preloaded.take().expect("preloaded spec must exist"));
160            } else {
161                result.insert(key, SpecPointer::Pointer(Arc::new(spec)));
162            }
163        }
164
165        Ok(result)
166    }
167}
168
169#[derive(Deserialize)]
170struct SpecIdentity<'a> {
171    #[serde(borrow)]
172    checksum: Option<Cow<'a, str>>,
173    #[serde(borrow)]
174    entity: Cow<'a, str>,
175}
176
177impl Serialize for SpecsHashMap {
178    fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
179    where
180        S: Serializer,
181    {
182        self.0.serialize(_serializer)
183    }
184}
185
186/// Feel free to add more HashMap pass-through methods here as needed.
187impl SpecsHashMap {
188    pub fn get(&self, key: &InternedString) -> Option<&SpecPointer> {
189        self.0.get(key)
190    }
191
192    pub fn keys(&self) -> impl Iterator<Item = &InternedString> {
193        self.0.keys()
194    }
195
196    pub fn iter(&self) -> impl Iterator<Item = (&InternedString, &SpecPointer)> {
197        self.0.iter()
198    }
199
200    pub fn insert(&mut self, key: InternedString, value: SpecPointer) {
201        let next_is_mmap = value.is_mmap();
202        let previous = self.0.insert(key, value);
203        record_spec_change(
204            previous.as_ref().map(SpecPointer::is_mmap),
205            Some(next_is_mmap),
206        );
207    }
208
209    pub fn len(&self) -> usize {
210        self.0.len()
211    }
212
213    pub fn is_empty(&self) -> bool {
214        self.0.is_empty()
215    }
216
217    pub fn clear(&mut self) {
218        record_specs_cleared(&self.0);
219        self.0.clear();
220    }
221
222    pub fn remove(&mut self, key: &InternedString) -> Option<SpecPointer> {
223        let previous = self.0.remove(key);
224        record_spec_change(previous.as_ref().map(SpecPointer::is_mmap), None);
225        previous
226    }
227}
228
229#[derive(Clone, Copy)]
230struct MmapSpecHandle {
231    spec: &'static ArchivedMmapSpec,
232}
233
234impl MmapSpecHandle {
235    pub(crate) fn new(spec: &'static ArchivedMmapSpec) -> Self {
236        Self { spec }
237    }
238
239    pub(crate) fn archived(self) -> &'static ArchivedMmapSpec {
240        self.spec
241    }
242}
243
244#[derive(Clone /* Clone Ok because Arc or process-lifetime mmap */)]
245pub struct SpecPointer {
246    inner: SpecPointerInner,
247}
248
249#[derive(Clone)]
250enum SpecPointerInner {
251    Pointer(Arc<Spec>),
252    Static(&'static Spec),
253    Mmap(MmapSpecHandle),
254}
255
256impl SpecPointer {
257    #[allow(non_snake_case)]
258    pub fn Pointer(spec: Arc<Spec>) -> Self {
259        Self {
260            inner: SpecPointerInner::Pointer(spec),
261        }
262    }
263
264    #[allow(non_snake_case)]
265    pub fn Static(spec: &'static Spec) -> Self {
266        Self {
267            inner: SpecPointerInner::Static(spec),
268        }
269    }
270
271    pub fn as_spec_ref(&self) -> &Spec {
272        match &self.inner {
273            SpecPointerInner::Pointer(spec) => spec,
274            SpecPointerInner::Static(spec) => spec,
275            SpecPointerInner::Mmap(handle) => {
276                InternedStore::materialize_mmap_spec(handle.archived())
277            }
278        }
279    }
280
281    pub(crate) fn view(&self) -> SpecView<'_> {
282        match &self.inner {
283            SpecPointerInner::Pointer(spec) => SpecView::Owned(spec),
284            SpecPointerInner::Static(spec) => SpecView::Owned(spec),
285            SpecPointerInner::Mmap(handle) => SpecView::Archived(handle.archived()),
286        }
287    }
288
289    pub(crate) fn from_mmap(spec: &'static ArchivedMmapSpec) -> Self {
290        Self {
291            inner: SpecPointerInner::Mmap(MmapSpecHandle::new(spec)),
292        }
293    }
294
295    pub(crate) fn matches_owned_spec(&self, spec: &Spec) -> bool {
296        match &self.inner {
297            SpecPointerInner::Mmap(handle) => {
298                handle.archived().content_hash.to_native()
299                    == crate::interned_values::mmap_data_v2::spec_content_hash(spec)
300            }
301            SpecPointerInner::Pointer(existing) => existing.as_ref() == spec,
302            SpecPointerInner::Static(existing) => *existing == spec,
303        }
304    }
305
306    pub(crate) fn into_pointer(self) -> Option<Arc<Spec>> {
307        match self.inner {
308            SpecPointerInner::Pointer(spec) => Some(spec),
309            SpecPointerInner::Static(_) | SpecPointerInner::Mmap(_) => None,
310        }
311    }
312
313    pub(crate) fn is_mmap(&self) -> bool {
314        matches!(self.inner, SpecPointerInner::Mmap(_))
315    }
316}
317
318impl Serialize for SpecPointer {
319    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320    where
321        S: Serializer,
322    {
323        match &self.inner {
324            SpecPointerInner::Pointer(spec) => spec.serialize(serializer),
325            SpecPointerInner::Static(spec) => spec.serialize(serializer),
326            SpecPointerInner::Mmap(handle) => handle.serialize(serializer),
327        }
328    }
329}
330
331impl Serialize for MmapSpecHandle {
332    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
333    where
334        S: Serializer,
335    {
336        let spec = self.spec;
337        let field_count = 7
338            + usize::from(spec.checksum.is_some())
339            + usize::from(spec.explicit_parameters.is_some())
340            + usize::from(spec.has_shared_params.is_some())
341            + usize::from(spec.is_active.is_some())
342            + usize::from(spec.version.is_some())
343            + usize::from(spec.target_app_ids.is_some())
344            + usize::from(spec.forward_all_exposures.is_some())
345            + usize::from(spec.fields_used.is_some())
346            + usize::from(spec.use_new_layer_eval.is_some());
347        let mut state = serializer.serialize_struct("Spec", field_count)?;
348        if let Some(checksum) = spec.checksum.as_ref() {
349            state.serialize_field("checksum", &MmapString(checksum))?;
350        }
351        state.serialize_field("type", &MmapString(&spec.spec_type))?;
352        state.serialize_field("salt", &MmapString(&spec.salt))?;
353        state.serialize_field("defaultValue", &MmapReturnable(&spec.default_value))?;
354        state.serialize_field("enabled", &spec.enabled)?;
355        state.serialize_field("rules", &MmapRules(&spec.rules))?;
356        state.serialize_field("idType", &MmapString(&spec.id_type))?;
357        if let Some(parameters) = spec.explicit_parameters.as_ref() {
358            state.serialize_field("explicitParameters", &MmapStrings(parameters))?;
359        }
360        state.serialize_field("entity", &MmapString(&spec.entity))?;
361        if let Some(value) = spec.has_shared_params.as_ref() {
362            state.serialize_field("hasSharedParams", value)?;
363        }
364        if let Some(value) = spec.is_active.as_ref() {
365            state.serialize_field("isActive", value)?;
366        }
367        if let Some(value) = spec.version.as_ref() {
368            state.serialize_field("version", &value.to_native())?;
369        }
370        if let Some(values) = spec.target_app_ids.as_ref() {
371            state.serialize_field("targetAppIDs", &MmapStrings(values))?;
372        }
373        if let Some(value) = spec.forward_all_exposures.as_ref() {
374            state.serialize_field("forwardAllExposures", value)?;
375        }
376        if let Some(values) = spec.fields_used.as_ref() {
377            state.serialize_field("fieldsUsed", &MmapStrings(values))?;
378        }
379        if let Some(value) = spec.use_new_layer_eval.as_ref() {
380            state.serialize_field("useNewLayerEval", value)?;
381        }
382        state.end()
383    }
384}
385
386struct MmapString<'a>(&'a ArchivedU64);
387
388impl Serialize for MmapString<'_> {
389    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
390    where
391        S: Serializer,
392    {
393        serializer.serialize_str(mmap_string(self.0))
394    }
395}
396
397struct MmapStrings<'a>(&'a ArchivedVec<ArchivedU64>);
398
399impl Serialize for MmapStrings<'_> {
400    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
401    where
402        S: Serializer,
403    {
404        let mut sequence = serializer.serialize_seq(Some(self.0.len()))?;
405        for value in self.0.iter() {
406            sequence.serialize_element(&MmapString(value))?;
407        }
408        sequence.end()
409    }
410}
411
412struct MmapRules<'a>(&'a ArchivedVec<ArchivedMmapRule>);
413
414impl Serialize for MmapRules<'_> {
415    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
416    where
417        S: Serializer,
418    {
419        let mut sequence = serializer.serialize_seq(Some(self.0.len()))?;
420        for rule in self.0.iter() {
421            sequence.serialize_element(&MmapRule(rule))?;
422        }
423        sequence.end()
424    }
425}
426
427struct MmapRule<'a>(&'a ArchivedMmapRule);
428
429impl Serialize for MmapRule<'_> {
430    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
431    where
432        S: Serializer,
433    {
434        let rule = self.0;
435        let field_count = 6
436            + usize::from(rule.salt.is_some())
437            + usize::from(rule.group_name.is_some())
438            + usize::from(rule.config_delegate.is_some())
439            + usize::from(rule.is_experiment_group.is_some())
440            + usize::from(rule.sampling_rate.is_some());
441        let mut state = serializer.serialize_struct("Rule", field_count)?;
442        state.serialize_field("name", &MmapString(&rule.name))?;
443        state.serialize_field("passPercentage", &rule.pass_percentage.to_native())?;
444        state.serialize_field("returnValue", &MmapReturnable(&rule.return_value))?;
445        state.serialize_field("id", &MmapString(&rule.id))?;
446        if let Some(value) = rule.salt.as_ref() {
447            state.serialize_field("salt", &MmapString(value))?;
448        }
449        state.serialize_field("conditions", &MmapStrings(&rule.conditions))?;
450        state.serialize_field("idType", &MmapDynamicString(&rule.id_type))?;
451        if let Some(value) = rule.group_name.as_ref() {
452            state.serialize_field("groupName", &MmapString(value))?;
453        }
454        if let Some(value) = rule.config_delegate.as_ref() {
455            state.serialize_field("configDelegate", &MmapString(value))?;
456        }
457        if let Some(value) = rule.is_experiment_group.as_ref() {
458            state.serialize_field("isExperimentGroup", value)?;
459        }
460        if let Some(value) = rule.sampling_rate.as_ref() {
461            state.serialize_field("samplingRate", &value.to_native())?;
462        }
463        state.end()
464    }
465}
466
467struct MmapDynamicString<'a>(&'a ArchivedMmapDynamicString);
468
469impl Serialize for MmapDynamicString<'_> {
470    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
471    where
472        S: Serializer,
473    {
474        let value = mmap_string(&self.0.value);
475        match value.parse::<bool>() {
476            Ok(value) => serializer.serialize_bool(value),
477            Err(_) => serializer.serialize_str(value),
478        }
479    }
480}
481
482struct MmapReturnable<'a>(&'a ArchivedMmapReturnable);
483
484impl Serialize for MmapReturnable<'_> {
485    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
486    where
487        S: Serializer,
488    {
489        InternedStore::get_mmap_returnable(self.0).serialize(serializer)
490    }
491}
492
493fn mmap_string(hash: &ArchivedU64) -> &'static str {
494    InternedStore::get_mmap_string(hash.to_native())
495        .expect("validated mmap string reference must exist")
496}
497
498impl SpecPointer {
499    pub fn from_spec(spec: Spec) -> Self {
500        Self::Pointer(Arc::new(spec))
501    }
502}
503
504impl PartialEq for SpecPointer {
505    fn eq(&self, other: &Self) -> bool {
506        match (&self.inner, &other.inner) {
507            (SpecPointerInner::Mmap(left), SpecPointerInner::Mmap(right))
508                if std::ptr::eq(left.archived(), right.archived()) =>
509            {
510                true
511            }
512            _ => self.as_spec_ref() == other.as_spec_ref(),
513        }
514    }
515}
516
517impl std::fmt::Debug for SpecPointer {
518    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        match &self.inner {
520            SpecPointerInner::Pointer(spec) => {
521                formatter.debug_tuple("Pointer").field(spec).finish()
522            }
523            SpecPointerInner::Static(spec) => formatter.debug_tuple("Static").field(spec).finish(),
524            SpecPointerInner::Mmap(_) => formatter.write_str("Mmap"),
525        }
526    }
527}