datadog_agent_metrics_v3/
writer.rs

1//! V3 columnar metrics writer.
2//!
3//! The [`V3Writer`] accumulates metrics in columnar format with dictionary deduplication,
4//! then produces [`V3EncodedData`] ready for protobuf serialization.
5
6use std::fmt;
7
8use protobuf::CodedOutputStream;
9
10use crate::{
11    constants::{
12        DICT_NAME_STR_FIELD_NUMBER, DICT_ORIGIN_INFO_FIELD_NUMBER, DICT_RESOURCE_LEN_FIELD_NUMBER,
13        DICT_RESOURCE_NAME_FIELD_NUMBER, DICT_RESOURCE_STR_FIELD_NUMBER, DICT_RESOURCE_TYPE_FIELD_NUMBER,
14        DICT_SOURCE_TYPE_NAME_FIELD_NUMBER, DICT_TAGSETS_FIELD_NUMBER, DICT_TAGS_STR_FIELD_NUMBER,
15        DICT_UNIT_STR_FIELD_NUMBER, INTERVALS_FIELD_NUMBER, NAMES_FIELD_NUMBER, NUM_POINTS_FIELD_NUMBER,
16        ORIGIN_INFO_FIELD_NUMBER, RESOURCES_FIELD_NUMBER, SKETCH_BIN_CNTS_FIELD_NUMBER, SKETCH_BIN_KEYS_FIELD_NUMBER,
17        SKETCH_NUM_BINS_FIELD_NUMBER, SOURCE_TYPE_NAME_FIELD_NUMBER, TAGS_FIELD_NUMBER, TIMESTAMPS_FIELD_NUMBER,
18        TYPES_FIELD_NUMBER, UNIT_REFS_FIELD_NUMBER, VALS_FLOAT32_FIELD_NUMBER, VALS_FLOAT64_FIELD_NUMBER,
19        VALS_SINT64_FIELD_NUMBER,
20    },
21    interner::Interner,
22    types::{value_type_for_values, V3MetricType, V3ValueType},
23};
24
25/// Error encountered while encoding a V3 payload.
26#[derive(Debug)]
27pub struct V3EncodeError(protobuf::Error);
28
29impl fmt::Display for V3EncodeError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "failed to encode V3 payload: {}", self.0)
32    }
33}
34
35impl std::error::Error for V3EncodeError {
36    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
37        Some(&self.0)
38    }
39}
40
41impl From<protobuf::Error> for V3EncodeError {
42    fn from(e: protobuf::Error) -> Self {
43        Self(e)
44    }
45}
46
47impl V3EncodeError {
48    /// Returns the underlying `protobuf` error.
49    pub fn into_inner(self) -> protobuf::Error {
50        self.0
51    }
52}
53
54pub(crate) const FLAG_NO_INDEX: u64 = 0x100;
55pub(crate) const FLAG_HAS_UNIT: u64 = 0x200;
56
57/// Encoded V3 payload data ready for protobuf serialization.
58///
59/// Used primarily as a helper for testing.
60#[derive(Debug, Default)]
61pub(crate) struct V3EncodedData {
62    // Dictionary encoded bytes (varint-length-prefixed strings)
63    dict_name_bytes: Vec<u8>,
64    dict_tags_bytes: Vec<u8>,
65    dict_tagsets: Vec<i64>,
66    dict_resource_str_bytes: Vec<u8>,
67    dict_resource_len: Vec<i64>,
68    dict_resource_type: Vec<i64>,
69    dict_resource_name: Vec<i64>,
70    dict_source_type_bytes: Vec<u8>,
71    dict_origin_info: Vec<i32>,
72    dict_unit_bytes: Vec<u8>,
73
74    // Per-metric columns (one entry per metric, except conditional columns)
75    types: Vec<u64>,
76    names: Vec<i64>,
77    tags: Vec<i64>,
78    resources: Vec<i64>,
79    intervals: Vec<u64>,
80    num_points: Vec<u64>,
81    source_type_names: Vec<i64>,
82    origin_infos: Vec<i64>,
83    unit_refs: Vec<i64>, // Present only for metrics with FLAG_HAS_UNIT set.
84
85    // Point data (varies per metric based on num_points)
86    timestamps: Vec<i64>,
87    vals_sint64: Vec<i64>,
88    vals_float32: Vec<f32>,
89    vals_float64: Vec<f64>,
90
91    // Sketch data
92    sketch_num_bins: Vec<u64>,
93    sketch_bin_keys: Vec<i32>,
94    sketch_bin_cnts: Vec<u32>,
95
96    value_encoding_stats: V3ValueEncodingStats,
97}
98
99/// Encoded V3 metrics payload with telemetry data.
100pub struct V3EncodedMetrics {
101    /// Serialized `MetricData` protobuf payload.
102    pub payload: Vec<u8>,
103    /// Telemetry produced while encoding the payload.
104    pub stats: V3EncoderStats,
105}
106
107/// Telemetry data produced while encoding a V3 metrics payload.
108pub struct V3EncoderStats {
109    /// Counts of how many point values were compacted into each value column.
110    pub value_encoding_stats: V3ValueEncodingStats,
111    /// Raw bytes written for each present column, keyed by field number.
112    pub columns: Vec<V3ColumnBytes>,
113}
114
115/// Counts of how many point values were encoded into each value column.
116#[derive(Clone, Copy, Debug, Default)]
117pub struct V3ValueEncodingStats {
118    /// Number of point values that were zero and required no explicit storage.
119    pub zero: u64,
120    /// Number of point values stored as signed integers.
121    pub sint64: u64,
122    /// Number of point values stored as 32-bit floats.
123    pub float32: u64,
124    /// Number of point values stored as 64-bit floats.
125    pub float64: u64,
126}
127
128/// Raw stream bytes for a single V3 column before protobuf field framing.
129pub struct V3ColumnBytes {
130    /// Protocol Buffers field number this column corresponds to.
131    pub field_number: u32,
132    /// Column contents, framed as an unwrapped (no field tag) protobuf value.
133    pub bytes: Vec<u8>,
134    /// Reserved for the compressed length of `bytes`; currently always `0`.
135    pub compressed_len: usize,
136}
137
138/// V3 columnar metrics writer.
139///
140/// Accumulates metrics in columnar format with dictionary deduplication.
141/// Call [`V3Writer::write`] for each metric, then [`V3Writer::finalize`] to finalize
142/// and get the encoded data.
143#[derive(Debug, Default)]
144pub struct V3Writer {
145    // Interners for dictionary deduplication
146    name_interner: Interner<String>,
147    tag_interner: Interner<String>,
148    tagset_interner: Interner<Vec<i64>>,
149    resource_str_interner: Interner<String>,
150    resource_interner: Interner<Vec<(i64, i64)>>,
151    source_type_interner: Interner<String>,
152    origin_interner: Interner<(i32, i32, i32)>,
153    unit_interner: Interner<String>,
154
155    // Dictionary encoded bytes
156    dict_name_bytes: Vec<u8>,
157    dict_tags_bytes: Vec<u8>,
158    dict_tagsets: Vec<i64>,
159    dict_resource_str_bytes: Vec<u8>,
160    dict_resource_len: Vec<i64>,
161    dict_resource_type: Vec<i64>,
162    dict_resource_name: Vec<i64>,
163    dict_source_type_bytes: Vec<u8>,
164    dict_origin_info: Vec<i32>,
165    dict_unit_bytes: Vec<u8>,
166
167    // Per-metric columns (one entry per metric, except conditional columns)
168    types: Vec<u64>,
169    names: Vec<i64>,
170    tags: Vec<i64>,
171    resources: Vec<i64>,
172    intervals: Vec<u64>,
173    num_points: Vec<u64>,
174    source_type_names: Vec<i64>,
175    origin_infos: Vec<i64>,
176    unit_refs: Vec<i64>, // Present only for metrics with FLAG_HAS_UNIT set.
177
178    // Point data
179    timestamps: Vec<i64>,
180    vals_sint64: Vec<i64>,
181    vals_float32: Vec<f32>,
182    vals_float64: Vec<f64>,
183
184    // Sketch data
185    sketch_num_bins: Vec<u64>,
186    sketch_bin_keys: Vec<i32>,
187    sketch_bin_cnts: Vec<u32>,
188
189    // Scratch data
190    tag_ids: Vec<i64>,
191    resource_ids: Vec<(i64, i64)>,
192    value_encoding_stats: V3ValueEncodingStats,
193}
194
195impl V3Writer {
196    /// Creates a new V3 writer.
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Returns the number of metrics written so far.
202    pub fn metric_count(&self) -> usize {
203        self.types.len()
204    }
205
206    /// Returns an estimate, in bytes, of the uncompressed size of the payload this writer would currently finalize to.
207    ///
208    /// This exists so callers can decide when to stop adding metrics to a batch, rather than encoding an oversized
209    /// batch and discovering afterwards that it has to be split and re-encoded. It is deliberately cheap: it inspects
210    /// only the lengths of the accumulated columns, so it is O(1) and safe to call after every metric.
211    ///
212    /// The figure is an estimate, not the exact finalized size. Dictionary string buffers are counted exactly, since
213    /// they dominate for the high-cardinality workloads this guards against, while packed integer columns are counted
214    /// at a fixed per-entry allowance because their true varint widths are only known once delta encoding runs during
215    /// [`V3Writer::finalize`]. The allowance errs on the high side, so batches are cut slightly early rather than
216    /// slightly late; callers **MUST** still check the finalized payload against their real size limits.
217    pub fn estimated_uncompressed_len(&self) -> usize {
218        // Upper-bound allowance for a delta-encoded varint entry. Delta-encoded values are small in practice (a
219        // varint byte or two), so this leaves headroom without being as pessimistic as the 10-byte worst case.
220        const VARINT_LEN_ALLOWANCE: usize = 4;
221
222        // Dictionary string buffers, counted exactly.
223        let dict_bytes = self.dict_name_bytes.len()
224            + self.dict_tags_bytes.len()
225            + self.dict_resource_str_bytes.len()
226            + self.dict_source_type_bytes.len()
227            + self.dict_unit_bytes.len();
228
229        // Packed integer columns, counted at the per-entry allowance.
230        let varint_entries = self.dict_tagsets.len()
231            + self.dict_resource_len.len()
232            + self.dict_resource_type.len()
233            + self.dict_resource_name.len()
234            + self.dict_origin_info.len()
235            + self.types.len()
236            + self.names.len()
237            + self.tags.len()
238            + self.resources.len()
239            + self.intervals.len()
240            + self.num_points.len()
241            + self.source_type_names.len()
242            + self.origin_infos.len()
243            + self.unit_refs.len()
244            + self.timestamps.len()
245            + self.vals_sint64.len()
246            + self.sketch_num_bins.len()
247            + self.sketch_bin_keys.len()
248            + self.sketch_bin_cnts.len();
249
250        // Fixed-width float columns, counted exactly.
251        let float_bytes = (self.vals_float32.len() * size_of::<f32>()) + (self.vals_float64.len() * size_of::<f64>());
252
253        dict_bytes + (varint_entries * VARINT_LEN_ALLOWANCE) + float_bytes
254    }
255
256    /// Begins writing a new metric.
257    ///
258    /// Returns a [`V3MetricBuilder`] that must be used to set the metric's
259    /// properties and add points, then closed with [`V3MetricBuilder::close`].
260    pub fn write(&mut self, metric_type: V3MetricType, name: &str) -> V3MetricBuilder<'_> {
261        let name_id = self.intern_name(name);
262        let metric_idx = self.types.len();
263        let point_start_idx = self.vals_float64.len();
264        let sint64_start_idx = self.vals_sint64.len();
265
266        // Initialize the per-metric columns with default values
267        self.types.push(metric_type.as_u64());
268        self.names.push(name_id);
269        self.tags.push(0);
270        self.resources.push(0);
271        self.intervals.push(0);
272        self.num_points.push(0);
273        self.source_type_names.push(0);
274        self.origin_infos.push(0);
275
276        V3MetricBuilder {
277            writer: self,
278            point_start_idx,
279            sint64_start_idx,
280            metric_idx,
281            unit_ref_idx: None,
282        }
283    }
284
285    fn finalize_inner(mut self) -> V3EncodedData {
286        // Delta encode all of the index arrays first.
287        delta_encode(&mut self.names);
288        delta_encode(&mut self.tags);
289        delta_encode(&mut self.resources);
290        delta_encode(&mut self.source_type_names);
291        delta_encode(&mut self.origin_infos);
292        delta_encode(&mut self.unit_refs);
293        delta_encode(&mut self.timestamps);
294
295        V3EncodedData {
296            dict_name_bytes: self.dict_name_bytes,
297            dict_tags_bytes: self.dict_tags_bytes,
298            dict_tagsets: self.dict_tagsets,
299            dict_resource_str_bytes: self.dict_resource_str_bytes,
300            dict_resource_len: self.dict_resource_len,
301            dict_resource_type: self.dict_resource_type,
302            dict_resource_name: self.dict_resource_name,
303            dict_source_type_bytes: self.dict_source_type_bytes,
304            dict_origin_info: self.dict_origin_info,
305            dict_unit_bytes: self.dict_unit_bytes,
306            types: self.types,
307            names: self.names,
308            tags: self.tags,
309            resources: self.resources,
310            intervals: self.intervals,
311            num_points: self.num_points,
312            source_type_names: self.source_type_names,
313            origin_infos: self.origin_infos,
314            unit_refs: self.unit_refs,
315            timestamps: self.timestamps,
316            vals_sint64: self.vals_sint64,
317            vals_float32: self.vals_float32,
318            vals_float64: self.vals_float64,
319            sketch_num_bins: self.sketch_num_bins,
320            sketch_bin_keys: self.sketch_bin_keys,
321            sketch_bin_cnts: self.sketch_bin_cnts,
322            value_encoding_stats: self.value_encoding_stats,
323        }
324    }
325
326    /// Finalizes the writer and serializes the data to the given output buffer.
327    ///
328    /// This performs delta encoding on all index arrays.
329    pub fn finalize(self) -> Result<V3EncodedMetrics, V3EncodeError> {
330        let data = self.finalize_inner();
331        let mut output = Vec::new();
332        let mut columns = Vec::new();
333
334        // Dictionary fields (bytes - varint-length-prefixed strings concatenated)
335        if !data.dict_name_bytes.is_empty() {
336            write_bytes_column(
337                &mut output,
338                &mut columns,
339                DICT_NAME_STR_FIELD_NUMBER,
340                &data.dict_name_bytes,
341            )?;
342        }
343        if !data.dict_tags_bytes.is_empty() {
344            write_bytes_column(
345                &mut output,
346                &mut columns,
347                DICT_TAGS_STR_FIELD_NUMBER,
348                &data.dict_tags_bytes,
349            )?;
350        }
351
352        // Packed repeated fields for dictionaries
353        write_packed_column(
354            &mut output,
355            &mut columns,
356            DICT_TAGSETS_FIELD_NUMBER,
357            |os| os.write_repeated_packed_sint64(DICT_TAGSETS_FIELD_NUMBER, &data.dict_tagsets),
358            |os| os.write_repeated_packed_sint64_no_tag(&data.dict_tagsets),
359        )?;
360
361        if !data.dict_resource_str_bytes.is_empty() {
362            write_bytes_column(
363                &mut output,
364                &mut columns,
365                DICT_RESOURCE_STR_FIELD_NUMBER,
366                &data.dict_resource_str_bytes,
367            )?;
368        }
369
370        write_packed_column(
371            &mut output,
372            &mut columns,
373            DICT_RESOURCE_LEN_FIELD_NUMBER,
374            |os| os.write_repeated_packed_int64(DICT_RESOURCE_LEN_FIELD_NUMBER, &data.dict_resource_len),
375            |os| os.write_repeated_packed_int64_no_tag(&data.dict_resource_len),
376        )?;
377        write_packed_column(
378            &mut output,
379            &mut columns,
380            DICT_RESOURCE_TYPE_FIELD_NUMBER,
381            |os| os.write_repeated_packed_sint64(DICT_RESOURCE_TYPE_FIELD_NUMBER, &data.dict_resource_type),
382            |os| os.write_repeated_packed_sint64_no_tag(&data.dict_resource_type),
383        )?;
384        write_packed_column(
385            &mut output,
386            &mut columns,
387            DICT_RESOURCE_NAME_FIELD_NUMBER,
388            |os| os.write_repeated_packed_sint64(DICT_RESOURCE_NAME_FIELD_NUMBER, &data.dict_resource_name),
389            |os| os.write_repeated_packed_sint64_no_tag(&data.dict_resource_name),
390        )?;
391
392        if !data.dict_source_type_bytes.is_empty() {
393            write_bytes_column(
394                &mut output,
395                &mut columns,
396                DICT_SOURCE_TYPE_NAME_FIELD_NUMBER,
397                &data.dict_source_type_bytes,
398            )?;
399        }
400
401        write_packed_column(
402            &mut output,
403            &mut columns,
404            DICT_ORIGIN_INFO_FIELD_NUMBER,
405            |os| os.write_repeated_packed_int32(DICT_ORIGIN_INFO_FIELD_NUMBER, &data.dict_origin_info),
406            |os| os.write_repeated_packed_int32_no_tag(&data.dict_origin_info),
407        )?;
408        if !data.dict_unit_bytes.is_empty() {
409            write_bytes_column(
410                &mut output,
411                &mut columns,
412                DICT_UNIT_STR_FIELD_NUMBER,
413                &data.dict_unit_bytes,
414            )?;
415        }
416
417        // Per-metric columns
418        write_packed_column(
419            &mut output,
420            &mut columns,
421            TYPES_FIELD_NUMBER,
422            |os| os.write_repeated_packed_uint64(TYPES_FIELD_NUMBER, &data.types),
423            |os| os.write_repeated_packed_uint64_no_tag(&data.types),
424        )?;
425        write_packed_column(
426            &mut output,
427            &mut columns,
428            NAMES_FIELD_NUMBER,
429            |os| os.write_repeated_packed_sint64(NAMES_FIELD_NUMBER, &data.names),
430            |os| os.write_repeated_packed_sint64_no_tag(&data.names),
431        )?;
432        write_packed_column(
433            &mut output,
434            &mut columns,
435            TAGS_FIELD_NUMBER,
436            |os| os.write_repeated_packed_sint64(TAGS_FIELD_NUMBER, &data.tags),
437            |os| os.write_repeated_packed_sint64_no_tag(&data.tags),
438        )?;
439        write_packed_column(
440            &mut output,
441            &mut columns,
442            RESOURCES_FIELD_NUMBER,
443            |os| os.write_repeated_packed_sint64(RESOURCES_FIELD_NUMBER, &data.resources),
444            |os| os.write_repeated_packed_sint64_no_tag(&data.resources),
445        )?;
446        write_packed_column(
447            &mut output,
448            &mut columns,
449            INTERVALS_FIELD_NUMBER,
450            |os| os.write_repeated_packed_uint64(INTERVALS_FIELD_NUMBER, &data.intervals),
451            |os| os.write_repeated_packed_uint64_no_tag(&data.intervals),
452        )?;
453        write_packed_column(
454            &mut output,
455            &mut columns,
456            NUM_POINTS_FIELD_NUMBER,
457            |os| os.write_repeated_packed_uint64(NUM_POINTS_FIELD_NUMBER, &data.num_points),
458            |os| os.write_repeated_packed_uint64_no_tag(&data.num_points),
459        )?;
460        write_packed_column(
461            &mut output,
462            &mut columns,
463            SOURCE_TYPE_NAME_FIELD_NUMBER,
464            |os| os.write_repeated_packed_sint64(SOURCE_TYPE_NAME_FIELD_NUMBER, &data.source_type_names),
465            |os| os.write_repeated_packed_sint64_no_tag(&data.source_type_names),
466        )?;
467        write_packed_column(
468            &mut output,
469            &mut columns,
470            ORIGIN_INFO_FIELD_NUMBER,
471            |os| os.write_repeated_packed_sint64(ORIGIN_INFO_FIELD_NUMBER, &data.origin_infos),
472            |os| os.write_repeated_packed_sint64_no_tag(&data.origin_infos),
473        )?;
474        write_packed_column(
475            &mut output,
476            &mut columns,
477            UNIT_REFS_FIELD_NUMBER,
478            |os| os.write_repeated_packed_sint64(UNIT_REFS_FIELD_NUMBER, &data.unit_refs),
479            |os| os.write_repeated_packed_sint64_no_tag(&data.unit_refs),
480        )?;
481
482        // Point data
483        write_packed_column(
484            &mut output,
485            &mut columns,
486            TIMESTAMPS_FIELD_NUMBER,
487            |os| os.write_repeated_packed_sint64(TIMESTAMPS_FIELD_NUMBER, &data.timestamps),
488            |os| os.write_repeated_packed_sint64_no_tag(&data.timestamps),
489        )?;
490        write_packed_column(
491            &mut output,
492            &mut columns,
493            VALS_SINT64_FIELD_NUMBER,
494            |os| os.write_repeated_packed_sint64(VALS_SINT64_FIELD_NUMBER, &data.vals_sint64),
495            |os| os.write_repeated_packed_sint64_no_tag(&data.vals_sint64),
496        )?;
497        write_packed_column(
498            &mut output,
499            &mut columns,
500            VALS_FLOAT32_FIELD_NUMBER,
501            |os| os.write_repeated_packed_float(VALS_FLOAT32_FIELD_NUMBER, &data.vals_float32),
502            |os| os.write_repeated_packed_float_no_tag(&data.vals_float32),
503        )?;
504        write_packed_column(
505            &mut output,
506            &mut columns,
507            VALS_FLOAT64_FIELD_NUMBER,
508            |os| os.write_repeated_packed_double(VALS_FLOAT64_FIELD_NUMBER, &data.vals_float64),
509            |os| os.write_repeated_packed_double_no_tag(&data.vals_float64),
510        )?;
511
512        // Sketch data
513        write_packed_column(
514            &mut output,
515            &mut columns,
516            SKETCH_NUM_BINS_FIELD_NUMBER,
517            |os| os.write_repeated_packed_uint64(SKETCH_NUM_BINS_FIELD_NUMBER, &data.sketch_num_bins),
518            |os| os.write_repeated_packed_uint64_no_tag(&data.sketch_num_bins),
519        )?;
520        write_packed_column(
521            &mut output,
522            &mut columns,
523            SKETCH_BIN_KEYS_FIELD_NUMBER,
524            |os| os.write_repeated_packed_sint32(SKETCH_BIN_KEYS_FIELD_NUMBER, &data.sketch_bin_keys),
525            |os| os.write_repeated_packed_sint32_no_tag(&data.sketch_bin_keys),
526        )?;
527        write_packed_column(
528            &mut output,
529            &mut columns,
530            SKETCH_BIN_CNTS_FIELD_NUMBER,
531            |os| os.write_repeated_packed_uint32(SKETCH_BIN_CNTS_FIELD_NUMBER, &data.sketch_bin_cnts),
532            |os| os.write_repeated_packed_uint32_no_tag(&data.sketch_bin_cnts),
533        )?;
534
535        Ok(V3EncodedMetrics {
536            payload: output,
537            stats: V3EncoderStats {
538                value_encoding_stats: data.value_encoding_stats,
539                columns,
540            },
541        })
542    }
543
544    // Internal helper methods
545
546    fn intern_name(&mut self, name: &str) -> i64 {
547        if name.is_empty() {
548            return 0;
549        }
550        let (id, is_new) = self.name_interner.get_or_insert(name);
551        if is_new {
552            append_len_str(&mut self.dict_name_bytes, name);
553        }
554        id
555    }
556
557    fn intern_tag(&mut self, tag: &str) {
558        if tag.is_empty() {
559            self.tag_ids.push(0);
560            return;
561        }
562
563        let (id, is_new) = self.tag_interner.get_or_insert(tag);
564        if is_new {
565            append_len_str(&mut self.dict_tags_bytes, tag);
566        }
567        self.tag_ids.push(id);
568    }
569
570    fn intern_tagset<I, S>(&mut self, tags: I) -> i64
571    where
572        I: Iterator<Item = S>,
573        S: AsRef<str>,
574    {
575        self.tag_ids.clear();
576        for tag in tags {
577            self.intern_tag(tag.as_ref());
578        }
579
580        if self.tag_ids.is_empty() {
581            return 0;
582        }
583
584        let (id, is_new) = self.tagset_interner.get_or_insert(&self.tag_ids);
585        if is_new {
586            self.encode_tagset();
587        }
588        id
589    }
590
591    fn encode_tagset(&mut self) {
592        // Push the length
593        self.dict_tagsets.push(self.tag_ids.len() as i64);
594
595        let start = self.dict_tagsets.len();
596
597        // Add all tag IDs
598        self.dict_tagsets.extend_from_slice(&self.tag_ids);
599
600        // Sort and delta-encode the tagset portion
601        self.dict_tagsets[start..].sort_unstable();
602        delta_encode(&mut self.dict_tagsets[start..]);
603    }
604
605    fn intern_resource_str(&mut self, s: &str) -> i64 {
606        if s.is_empty() {
607            return 0;
608        }
609        let (id, is_new) = self.resource_str_interner.get_or_insert(s);
610        if is_new {
611            append_len_str(&mut self.dict_resource_str_bytes, s);
612        }
613        id
614    }
615
616    fn intern_resources(&mut self, resources: &[(&str, &str)]) -> i64 {
617        self.resource_ids.clear();
618        for (resource_type, resource_name) in resources {
619            let type_id = self.intern_resource_str(resource_type);
620            let name_id = self.intern_resource_str(resource_name);
621            self.resource_ids.push((type_id, name_id));
622        }
623
624        if self.resource_ids.is_empty() {
625            return 0;
626        }
627
628        let (id, is_new) = self.resource_interner.get_or_insert(&self.resource_ids);
629        if is_new {
630            self.encode_resources();
631        }
632        id
633    }
634
635    fn encode_resources(&mut self) {
636        self.dict_resource_len.push(self.resource_ids.len() as i64);
637
638        let type_start = self.dict_resource_type.len();
639        let name_start = self.dict_resource_name.len();
640
641        for (type_id, name_id) in &self.resource_ids {
642            self.dict_resource_type.push(*type_id);
643            self.dict_resource_name.push(*name_id);
644        }
645
646        delta_encode(&mut self.dict_resource_type[type_start..]);
647        delta_encode(&mut self.dict_resource_name[name_start..]);
648    }
649
650    fn intern_source_type(&mut self, s: &str) -> i64 {
651        if s.is_empty() {
652            return 0;
653        }
654        let (id, is_new) = self.source_type_interner.get_or_insert(s);
655        if is_new {
656            append_len_str(&mut self.dict_source_type_bytes, s);
657        }
658        id
659    }
660
661    fn intern_origin(&mut self, product: i32, category: i32, service: i32) -> i64 {
662        if product == 0 && category == 0 && service == 0 {
663            return 0;
664        }
665
666        let (id, is_new) = self.origin_interner.get_or_insert(&(product, category, service));
667        if is_new {
668            self.dict_origin_info.push(product);
669            self.dict_origin_info.push(category);
670            self.dict_origin_info.push(service);
671        }
672        id
673    }
674
675    fn intern_unit(&mut self, unit: &str) -> i64 {
676        if unit.is_empty() {
677            return 0;
678        }
679        let (id, is_new) = self.unit_interner.get_or_insert(unit);
680        if is_new {
681            append_len_str(&mut self.dict_unit_bytes, unit);
682        }
683        id
684    }
685}
686
687/// Builder for a single metric within a V3 payload.
688///
689/// Use the setter methods to configure the metric, add points with [`add_point`](Self::add_point),
690/// then call [`close`](Self::close) to finalize.
691pub struct V3MetricBuilder<'a> {
692    writer: &'a mut V3Writer,
693    point_start_idx: usize,
694    sint64_start_idx: usize,
695    metric_idx: usize,
696    unit_ref_idx: Option<usize>,
697}
698
699impl<'a> V3MetricBuilder<'a> {
700    /// Sets the tags for this metric.
701    ///
702    /// Tags should be in "key:value" format.
703    pub fn set_tags<I, S>(&mut self, tags: I)
704    where
705        I: Iterator<Item = S>,
706        S: AsRef<str>,
707    {
708        let tagset_id = self.writer.intern_tagset(tags);
709        self.writer.tags[self.metric_idx] = tagset_id;
710    }
711
712    /// Sets the resources for this metric.
713    ///
714    /// Resources are (type, name) pairs, for example, (`host`, `server1`).
715    pub fn set_resources(&mut self, resources: &[(&str, &str)]) {
716        let res_id = self.writer.intern_resources(resources);
717        self.writer.resources[self.metric_idx] = res_id;
718    }
719
720    /// Sets the interval for this metric (used for rate metrics).
721    pub fn set_interval(&mut self, interval: u64) {
722        self.writer.intervals[self.metric_idx] = interval;
723    }
724
725    /// Sets the source type name for this metric.
726    pub fn set_source_type(&mut self, source_type: &str) {
727        if source_type.is_empty() {
728            self.writer.source_type_names[self.metric_idx] = 0;
729            return;
730        }
731        let id = self.writer.intern_source_type(source_type);
732        self.writer.source_type_names[self.metric_idx] = id;
733    }
734
735    /// Sets the origin metadata for this metric.
736    pub fn set_origin(&mut self, product: u32, category: u32, service: u32, no_index: bool) {
737        let id = self
738            .writer
739            .intern_origin(product as i32, category as i32, service as i32);
740        self.writer.origin_infos[self.metric_idx] = id;
741        if no_index {
742            self.writer.types[self.metric_idx] |= FLAG_NO_INDEX;
743        }
744    }
745
746    /// Sets the unit for this metric.
747    pub fn set_unit(&mut self, unit: &str) {
748        if unit.is_empty() {
749            self.writer.types[self.metric_idx] &= !FLAG_HAS_UNIT;
750            if let Some(unit_ref_idx) = self.unit_ref_idx.take() {
751                self.writer.unit_refs.remove(unit_ref_idx);
752            }
753            return;
754        }
755
756        let id = self.writer.intern_unit(unit);
757        if let Some(unit_ref_idx) = self.unit_ref_idx {
758            self.writer.unit_refs[unit_ref_idx] = id;
759        } else {
760            self.unit_ref_idx = Some(self.writer.unit_refs.len());
761            self.writer.unit_refs.push(id);
762        }
763        self.writer.types[self.metric_idx] |= FLAG_HAS_UNIT;
764    }
765
766    /// Adds a data point to this metric.
767    pub fn add_point(&mut self, timestamp: i64, value: f64) {
768        self.writer.timestamps.push(timestamp);
769        self.writer.vals_float64.push(value);
770        self.writer.num_points[self.metric_idx] += 1;
771    }
772
773    /// Adds sketch data for a distribution metric.
774    ///
775    /// For sketches, the summary values (count, sum, min, max) are stored as points,
776    /// and the bin keys/counts are stored separately.
777    pub fn add_sketch(
778        &mut self, timestamp: i64, count: i64, sum: f64, min: f64, max: f64, bin_keys: &[i32], bin_counts: &[u32],
779    ) {
780        self.writer.timestamps.push(timestamp);
781
782        // Count goes in sint64, sum/min/max go in float64
783        self.writer.vals_sint64.push(count);
784        self.writer.vals_float64.push(sum);
785        self.writer.vals_float64.push(min);
786        self.writer.vals_float64.push(max);
787
788        // Store bin data
789        self.writer.sketch_num_bins.push(bin_keys.len() as u64);
790
791        let key_start = self.writer.sketch_bin_keys.len();
792        self.writer.sketch_bin_keys.extend_from_slice(bin_keys);
793        self.writer.sketch_bin_cnts.extend_from_slice(bin_counts);
794
795        // Delta-encode this sketch's bin keys
796        delta_encode_i32(&mut self.writer.sketch_bin_keys[key_start..]);
797
798        self.writer.num_points[self.metric_idx] += 1;
799    }
800
801    /// Finalizes this metric.
802    ///
803    /// This compacts the point values to use the smallest representation
804    /// that can hold all values without loss.
805    pub fn close(mut self) {
806        self.compact_values();
807    }
808
809    fn compact_values(&mut self) {
810        let count = self.writer.num_points[self.metric_idx] as usize;
811        if count == 0 {
812            return;
813        }
814
815        let start = self.point_start_idx;
816        let end = self.writer.vals_float64.len();
817
818        // Determine the best value type for all points in this metric.
819        let val_ty = value_type_for_values(self.writer.vals_float64[start..end].iter().copied());
820        let is_sketch = (self.writer.types[self.metric_idx] & 0x0F) == V3MetricType::Sketch as u64;
821        let float_values_len = end - start;
822        if is_sketch {
823            // Sketches always carry one integer count per point in addition to sum/min/max values.
824            self.writer.value_encoding_stats.sint64 += count as u64;
825        }
826
827        // Update the type field
828        self.writer.types[self.metric_idx] |= val_ty.as_u64();
829
830        // Convert values to the appropriate storage
831        match val_ty {
832            V3ValueType::Zero => {
833                self.writer.value_encoding_stats.zero += float_values_len as u64;
834                // Values are all zero, don't store anything
835                self.writer.vals_float64.truncate(start);
836            }
837            V3ValueType::Sint64 => {
838                self.writer.value_encoding_stats.sint64 += float_values_len as u64;
839                if is_sketch {
840                    // For sketches, vals_sint64 already has one count per point (pushed by add_sketch),
841                    // and vals_float64 has 3 values per point (sum, min, max). When compacting to Sint64,
842                    // we need to interleave them as: sum, min, max, cnt per point.
843                    let counts: Vec<i64> = self.writer.vals_sint64[self.sint64_start_idx..].to_vec();
844                    self.writer.vals_sint64.truncate(self.sint64_start_idx);
845                    for (i, cnt) in counts.into_iter().enumerate() {
846                        let f_off = start + i * 3;
847                        self.writer.vals_sint64.push(self.writer.vals_float64[f_off] as i64);
848                        self.writer.vals_sint64.push(self.writer.vals_float64[f_off + 1] as i64);
849                        self.writer.vals_sint64.push(self.writer.vals_float64[f_off + 2] as i64);
850                        self.writer.vals_sint64.push(cnt);
851                    }
852                } else {
853                    for i in start..end {
854                        self.writer.vals_sint64.push(self.writer.vals_float64[i] as i64);
855                    }
856                }
857                self.writer.vals_float64.truncate(start);
858            }
859            V3ValueType::Float32 => {
860                self.writer.value_encoding_stats.float32 += float_values_len as u64;
861                for i in start..end {
862                    self.writer.vals_float32.push(self.writer.vals_float64[i] as f32);
863                }
864                self.writer.vals_float64.truncate(start);
865            }
866            V3ValueType::Float64 => {
867                self.writer.value_encoding_stats.float64 += float_values_len as u64;
868                // Already stored in vals_float64, keep them
869            }
870        }
871    }
872}
873
874fn write_bytes_column(
875    output: &mut Vec<u8>, columns: &mut Vec<V3ColumnBytes>, field_number: u32, bytes: &[u8],
876) -> Result<(), V3EncodeError> {
877    let start = output.len();
878    {
879        let mut os = CodedOutputStream::vec(output);
880        os.write_bytes(field_number, bytes)?;
881        os.flush()?;
882    }
883
884    if output.len() != start {
885        columns.push(V3ColumnBytes {
886            field_number,
887            bytes: bytes.to_vec(),
888            compressed_len: 0,
889        });
890    }
891
892    Ok(())
893}
894
895fn write_packed_column<F, R>(
896    output: &mut Vec<u8>, columns: &mut Vec<V3ColumnBytes>, field_number: u32, write_framed: F, write_raw: R,
897) -> Result<(), V3EncodeError>
898where
899    F: FnOnce(&mut CodedOutputStream<'_>) -> protobuf::Result<()>,
900    R: FnOnce(&mut CodedOutputStream<'_>) -> protobuf::Result<()>,
901{
902    let start = output.len();
903    {
904        let mut os = CodedOutputStream::vec(output);
905        write_framed(&mut os)?;
906        os.flush()?;
907    }
908
909    if output.len() != start {
910        let mut bytes = Vec::new();
911        {
912            let mut os = CodedOutputStream::vec(&mut bytes);
913            write_raw(&mut os)?;
914            os.flush()?;
915        }
916        columns.push(V3ColumnBytes {
917            field_number,
918            bytes,
919            compressed_len: 0,
920        });
921    }
922
923    Ok(())
924}
925
926fn append_len_str(dst: &mut Vec<u8>, s: &str) {
927    let mut len = s.len() as u64;
928    loop {
929        let mut byte = (len & 0x7F) as u8;
930        len >>= 7;
931        if len != 0 {
932            byte |= 0x80;
933        }
934        dst.push(byte);
935        if len == 0 {
936            break;
937        }
938    }
939    dst.extend_from_slice(s.as_bytes());
940}
941
942fn delta_encode(s: &mut [i64]) {
943    if s.len() < 2 {
944        return;
945    }
946    for i in (1..s.len()).rev() {
947        s[i] -= s[i - 1];
948    }
949}
950
951fn delta_encode_i32(s: &mut [i32]) {
952    if s.len() < 2 {
953        return;
954    }
955    for i in (1..s.len()).rev() {
956        s[i] -= s[i - 1];
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    #[test]
965    fn test_delta_encode() {
966        let mut data = vec![100, 110, 130, 145];
967        delta_encode(&mut data);
968        assert_eq!(data, vec![100, 10, 20, 15]);
969    }
970
971    #[test]
972    fn estimated_uncompressed_len_grows_and_brackets_finalized_size() {
973        let mut writer = V3Writer::new();
974        assert_eq!(0, writer.metric_count());
975        let empty_estimate = writer.estimated_uncompressed_len();
976
977        for idx in 0..16 {
978            let mut builder = writer.write(V3MetricType::Count, &format!("estimate.metric.{idx}"));
979            builder.set_tags([format!("tag_key_{idx}:tag_value_{idx}")].into_iter());
980            builder.add_point(1_700_000_000 + idx as i64, idx as f64);
981            builder.close();
982        }
983
984        assert_eq!(16, writer.metric_count());
985        let estimate = writer.estimated_uncompressed_len();
986        assert!(
987            estimate > empty_estimate,
988            "estimate should grow as metrics are written: {estimate} vs {empty_estimate}"
989        );
990
991        // The estimate exists to bound batch sizes, so it must not undershoot the real payload: cutting a batch on an
992        // undershooting estimate is what forces the expensive re-encode path.
993        let encoded = writer.finalize().expect("metrics should encode");
994        assert!(
995            estimate >= encoded.payload.len(),
996            "estimate {estimate} should not undershoot finalized payload {}",
997            encoded.payload.len()
998        );
999    }
1000
1001    #[test]
1002    fn estimated_uncompressed_len_tracks_dictionary_growth() {
1003        // A metric whose tags are already interned should add far less to the estimate than one introducing new tag
1004        // strings, since dictionary bytes are counted exactly.
1005        let mut writer = V3Writer::new();
1006        let mut builder = writer.write(V3MetricType::Count, "dict.growth");
1007        builder.set_tags(["shared_key:shared_value".to_string()].into_iter());
1008        builder.add_point(1_700_000_000, 1.0);
1009        builder.close();
1010        let after_first = writer.estimated_uncompressed_len();
1011
1012        let mut builder = writer.write(V3MetricType::Count, "dict.growth");
1013        builder.set_tags(["shared_key:shared_value".to_string()].into_iter());
1014        builder.add_point(1_700_000_001, 2.0);
1015        builder.close();
1016        let after_deduped = writer.estimated_uncompressed_len();
1017
1018        let mut builder = writer.write(V3MetricType::Count, "dict.growth.novel");
1019        builder.set_tags(["a_completely_different_key:a_completely_different_value".to_string()].into_iter());
1020        builder.add_point(1_700_000_002, 3.0);
1021        builder.close();
1022        let after_novel = writer.estimated_uncompressed_len();
1023
1024        let deduped_growth = after_deduped - after_first;
1025        let novel_growth = after_novel - after_deduped;
1026        assert!(
1027            novel_growth > deduped_growth,
1028            "new dictionary entries should grow the estimate more than deduplicated ones: {novel_growth} vs {deduped_growth}"
1029        );
1030    }
1031
1032    #[test]
1033    fn test_delta_encode_empty() {
1034        let mut data: Vec<i64> = vec![];
1035        delta_encode(&mut data);
1036        assert!(data.is_empty());
1037    }
1038
1039    #[test]
1040    fn test_delta_encode_single() {
1041        let mut data = vec![42];
1042        delta_encode(&mut data);
1043        assert_eq!(data, vec![42]);
1044    }
1045
1046    #[test]
1047    fn test_append_len_str() {
1048        let mut buf = Vec::new();
1049        append_len_str(&mut buf, "hello");
1050        // Length 5 = 0x05, then "hello"
1051        assert_eq!(buf, vec![5, b'h', b'e', b'l', b'l', b'o']);
1052    }
1053
1054    #[test]
1055    fn test_writer_basic() {
1056        let mut writer = V3Writer::new();
1057
1058        {
1059            let mut metric = writer.write(V3MetricType::Gauge, "test.metric");
1060            metric.set_tags(["env:prod", "service:web"].iter().copied());
1061            metric.add_point(1000, 42.0);
1062            metric.add_point(1010, 43.5);
1063            metric.close();
1064        }
1065
1066        let data = writer.finalize_inner();
1067
1068        assert_eq!(data.types.len(), 1);
1069        assert_eq!(data.names.len(), 1);
1070        assert_eq!(data.timestamps.len(), 2);
1071    }
1072
1073    #[test]
1074    fn test_writer_unit() {
1075        let mut writer = V3Writer::new();
1076
1077        {
1078            let mut metric = writer.write(V3MetricType::Gauge, "has.unit");
1079            metric.set_unit("millisecond");
1080            metric.add_point(1000, 42.0);
1081            metric.close();
1082        }
1083        {
1084            let mut metric = writer.write(V3MetricType::Gauge, "no.unit");
1085            metric.add_point(1000, 43.0);
1086            metric.close();
1087        }
1088        {
1089            let mut metric = writer.write(V3MetricType::Gauge, "same.unit");
1090            metric.set_unit("millisecond");
1091            metric.add_point(1000, 44.0);
1092            metric.close();
1093        }
1094
1095        let data = writer.finalize_inner();
1096
1097        assert_eq!(data.unit_refs, vec![1, 0]);
1098        assert_eq!(data.dict_unit_bytes, b"\x0bmillisecond");
1099        assert_eq!(data.types[0] & FLAG_HAS_UNIT, FLAG_HAS_UNIT);
1100        assert_eq!(data.types[1] & FLAG_HAS_UNIT, 0);
1101        assert_eq!(data.types[2] & FLAG_HAS_UNIT, FLAG_HAS_UNIT);
1102    }
1103
1104    #[test]
1105    fn test_writer_multiple_metrics() {
1106        let mut writer = V3Writer::new();
1107
1108        {
1109            let mut m1 = writer.write(V3MetricType::Count, "metric1");
1110            m1.add_point(1000, 10.0);
1111            m1.close();
1112        }
1113
1114        {
1115            let mut m2 = writer.write(V3MetricType::Rate, "metric2");
1116            m2.set_interval(60);
1117            m2.add_point(2000, 20.0);
1118            m2.close();
1119        }
1120
1121        let data = writer.finalize_inner();
1122
1123        assert_eq!(data.types.len(), 2);
1124        assert_eq!(data.names.len(), 2);
1125        assert_eq!(data.intervals[0], 0);
1126        // Second metric's interval won't be 60 directly since names is delta-encoded,
1127        // but we can verify the structure is correct
1128    }
1129
1130    #[test]
1131    fn test_value_compaction_zero() {
1132        let mut writer = V3Writer::new();
1133
1134        {
1135            let mut metric = writer.write(V3MetricType::Gauge, "zero.metric");
1136            metric.add_point(1000, 0.0);
1137            metric.add_point(2000, 0.0);
1138            metric.close();
1139        }
1140
1141        let data = writer.finalize_inner();
1142
1143        // Values should be compacted - zero values don't need storage
1144        assert!(data.vals_float64.is_empty());
1145        assert!(data.vals_sint64.is_empty());
1146        assert!(data.vals_float32.is_empty());
1147    }
1148
1149    #[test]
1150    fn test_value_compaction_int() {
1151        let mut writer = V3Writer::new();
1152
1153        {
1154            let mut metric = writer.write(V3MetricType::Count, "int.metric");
1155            metric.add_point(1000, 100.0);
1156            metric.add_point(2000, 200.0);
1157            metric.close();
1158        }
1159
1160        let data = writer.finalize_inner();
1161
1162        // Integer values should be stored in sint64
1163        assert!(data.vals_float64.is_empty());
1164        assert_eq!(data.vals_sint64, vec![100, 200]);
1165        assert!(data.vals_float32.is_empty());
1166    }
1167
1168    #[test]
1169    fn test_serialize_empty() {
1170        let writer = V3Writer::new();
1171        let encoded = writer.finalize().unwrap();
1172        assert!(encoded.payload.is_empty());
1173    }
1174
1175    #[test]
1176    fn test_value_compaction_large_int_plus_float32() {
1177        // Regression test: a large integer (> 2^24) mixed with a fractional
1178        // float32 value must use Float64, not Float32, to avoid precision loss.
1179        let mut writer = V3Writer::new();
1180
1181        {
1182            let mut metric = writer.write(V3MetricType::Gauge, "mixed.metric");
1183            metric.add_point(1000, (1i64 << 30) as f64); // large int, doesn't fit in f32
1184            metric.add_point(2000, 1.5); // fractional, fits in f32
1185            metric.close();
1186        }
1187
1188        let data = writer.finalize_inner();
1189
1190        // Must be stored in float64, not float32
1191        assert!(
1192            data.vals_float32.is_empty(),
1193            "large int should not be stored as float32"
1194        );
1195        assert_eq!(data.vals_float64, vec![(1i64 << 30) as f64, 1.5]);
1196        assert!(data.vals_sint64.is_empty());
1197    }
1198
1199    #[test]
1200    fn test_value_compaction_small_int_plus_float32() {
1201        // Small integers (|v| <= 2^24) mixed with float32 values should
1202        // compact to Float32, since small ints fit losslessly in f32.
1203        let mut writer = V3Writer::new();
1204
1205        {
1206            let mut metric = writer.write(V3MetricType::Gauge, "small.mixed");
1207            metric.add_point(1000, 100.0);
1208            metric.add_point(2000, 1.5);
1209            metric.close();
1210        }
1211
1212        let data = writer.finalize_inner();
1213
1214        assert!(data.vals_float64.is_empty());
1215        assert_eq!(data.vals_float32, vec![100.0, 1.5]);
1216        assert!(data.vals_sint64.is_empty());
1217    }
1218
1219    #[test]
1220    fn test_serialize_basic_metric() {
1221        let mut writer = V3Writer::new();
1222
1223        {
1224            let mut metric = writer.write(V3MetricType::Gauge, "test.metric");
1225            metric.add_point(1000, 42.0);
1226            metric.close();
1227        }
1228
1229        let encoded = writer.finalize().unwrap();
1230
1231        // Should produce non-empty output
1232        assert!(!encoded.payload.is_empty());
1233        assert_eq!(encoded.stats.value_encoding_stats.sint64, 1);
1234    }
1235
1236    #[test]
1237    fn test_column_stats_for_bytes_column_use_raw_column_stream() {
1238        let mut writer = V3Writer::new();
1239
1240        {
1241            let mut metric = writer.write(V3MetricType::Gauge, "test.metric");
1242            metric.add_point(1000, 42.0);
1243            metric.close();
1244        }
1245
1246        let encoded = writer.finalize().unwrap();
1247        let name_column = encoded
1248            .stats
1249            .columns
1250            .iter()
1251            .find(|column| column.field_number == DICT_NAME_STR_FIELD_NUMBER)
1252            .expect("name dictionary column should be present");
1253
1254        let mut expected = Vec::new();
1255        append_len_str(&mut expected, "test.metric");
1256        assert_eq!(name_column.bytes, expected);
1257    }
1258
1259    #[test]
1260    fn test_column_stats_for_packed_column_use_raw_column_stream() {
1261        let mut writer = V3Writer::new();
1262
1263        {
1264            let mut metric = writer.write(V3MetricType::Gauge, "test.metric");
1265            metric.add_point(1000, 42.0);
1266            metric.close();
1267        }
1268
1269        let encoded = writer.finalize().unwrap();
1270        let timestamps_column = encoded
1271            .stats
1272            .columns
1273            .iter()
1274            .find(|column| column.field_number == TIMESTAMPS_FIELD_NUMBER)
1275            .expect("timestamp column should be present");
1276
1277        let mut expected = Vec::new();
1278        {
1279            let mut os = CodedOutputStream::vec(&mut expected);
1280            os.write_sint64_no_tag(1000).unwrap();
1281            os.flush().unwrap();
1282        }
1283        assert_eq!(timestamps_column.bytes, expected);
1284    }
1285
1286    #[test]
1287    fn test_column_stats_do_not_include_absent_columns() {
1288        let mut writer = V3Writer::new();
1289
1290        {
1291            let mut metric = writer.write(V3MetricType::Gauge, "test.metric");
1292            metric.add_point(1000, 42.0);
1293            metric.close();
1294        }
1295
1296        let encoded = writer.finalize().unwrap();
1297        assert!(!encoded
1298            .stats
1299            .columns
1300            .iter()
1301            .any(|column| column.field_number == UNIT_REFS_FIELD_NUMBER));
1302    }
1303}