Skip to main content

turbopack_trace_utils/
trace_writer.rs

1use std::{debug_assert, io::Write, sync::Arc, thread::JoinHandle, time::Duration};
2
3use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError, bounded, unbounded};
4use crossbeam_utils::CachePadded;
5use parking_lot::{Mutex, MutexGuard};
6use thread_local::ThreadLocal;
7
8use crate::tracing::{DeltaEncodedTimestamp, TRACE_HEADER, TimestampEncoder, TraceRow};
9
10type ThreadLocalState = CachePadded<Mutex<Option<TraceInfoBuffer>>>;
11
12/// The amount of data that is accumulated in the thread local buffer before it is sent to the
13/// writer. The buffer might grow if a single write is larger than this size.
14pub(crate) const THREAD_LOCAL_INITIAL_BUFFER_SIZE: usize = 1024 * 1024;
15/// Data buffered by the write thread before issuing a filesystem write
16const WRITE_BUFFER_SIZE: usize = 100 * 1024 * 1024;
17
18struct TraceInfoBuffer {
19    buffer: Vec<u8>,
20    /// The marker of the last marked row in the buffer, see [`WriteGuard::mark`].
21    last_row_marker: Option<u64>,
22    /// The offset where the last marked row starts in `buffer`.
23    last_row_start: usize,
24    /// The offset where the last marked row ends in `buffer`. It's only still the last row in
25    /// the buffer when this is the length of the buffer.
26    last_row_end: usize,
27    /// Encodes the timestamps of the rows in `buffer`. The buffer contents are self-contained:
28    /// the first timestamp is preceded by a [`TraceRow::TimestampBase`] row.
29    timestamps: TimestampEncoder,
30    /// The state of `timestamps` before the last marked row. Restored when the row is removed.
31    last_row_timestamps: TimestampEncoder,
32}
33
34impl TraceInfoBuffer {
35    fn new(capacity: usize) -> Self {
36        Self {
37            buffer: Vec::with_capacity(capacity),
38            last_row_marker: None,
39            last_row_start: 0,
40            last_row_end: 0,
41            timestamps: TimestampEncoder::default(),
42            last_row_timestamps: TimestampEncoder::default(),
43        }
44    }
45
46    fn push(&mut self, data: u8) {
47        self.buffer.push(data);
48    }
49
50    fn extend(&mut self, data: &[u8]) {
51        self.buffer.extend_from_slice(data);
52    }
53
54    fn clear(&mut self) {
55        self.buffer.clear();
56        self.last_row_marker = None;
57        self.last_row_start = 0;
58        self.last_row_end = 0;
59        self.timestamps = TimestampEncoder::default();
60        self.last_row_timestamps = TimestampEncoder::default();
61    }
62
63    fn write_timestamp_base(&mut self, ts: u64) {
64        let row: TraceRow<'_> = TraceRow::TimestampBase { ts };
65        self.buffer = postcard::to_extend(&row, std::mem::take(&mut self.buffer)).unwrap();
66    }
67}
68
69#[derive(Clone)]
70pub struct TraceWriter {
71    data_tx: Sender<Option<TraceInfoBuffer>>,
72    return_rx: Receiver<TraceInfoBuffer>,
73    thread_locals: Arc<ThreadLocal<ThreadLocalState>>,
74}
75
76impl TraceWriter {
77    /// This is a non-blocking writer that writes a file in a background thread.
78    /// This is inspired by tracing-appender non_blocking, but has some
79    /// differences:
80    /// * It allows writing an owned [`Vec<u8>`] instead of a reference, so avoiding additional
81    ///   allocation.
82    /// * It uses an unbounded channel to avoid slowing down the application at all (memory) cost.
83    /// * It issues less writes by buffering the data into chunks of `WRITE_BUFFER_SIZE`, when
84    ///   possible.
85    pub fn new<W: Write + Send + 'static>(mut writer: W) -> (Self, TraceWriterGuard) {
86        let (data_tx, data_rx) = unbounded::<Option<TraceInfoBuffer>>();
87        let (return_tx, return_rx) = bounded::<TraceInfoBuffer>(1024);
88        let thread_locals: Arc<ThreadLocal<ThreadLocalState>> = Default::default();
89
90        let trace_writer = Self {
91            data_tx: data_tx.clone(),
92            return_rx: return_rx.clone(),
93            thread_locals: thread_locals.clone(),
94        };
95
96        fn steal_from_thread_locals(
97            thread_locals: &Arc<ThreadLocal<ThreadLocalState>>,
98            stolen_buffers: &mut Vec<TraceInfoBuffer>,
99        ) {
100            for state in thread_locals.iter() {
101                let mut buffer = state.lock();
102                // Empty buffers (e.g. after the only row was removed again) are left in place.
103                // An empty buffer would be interpreted as exit signal.
104                if buffer
105                    .as_ref()
106                    .is_some_and(|buffer| !buffer.buffer.is_empty())
107                    && let Some(buffer) = buffer.take()
108                {
109                    stolen_buffers.push(buffer);
110                }
111            }
112        }
113
114        let handle: std::thread::JoinHandle<()> = std::thread::spawn(move || {
115            let _ = writer.write(TRACE_HEADER);
116            let mut buf = Vec::with_capacity(WRITE_BUFFER_SIZE);
117            let mut stolen_buffers = Vec::new();
118            let mut should_exit = false;
119            'outer: loop {
120                if !buf.is_empty() {
121                    let _ = writer.write_all(&buf);
122                    let _ = writer.flush();
123                    buf.clear();
124                }
125
126                let recv = if should_exit {
127                    Ok(None)
128                } else {
129                    data_rx.recv_timeout(Duration::from_secs(1))
130                };
131
132                let mut data = match recv {
133                    Ok(Some(data)) => data,
134                    result => {
135                        if result.is_ok() {
136                            // On exit signal
137                            should_exit = true;
138                        }
139                        // When we receive no data for a second or we want to exit we poll the
140                        // thread local buffers to steal some data. This
141                        // prevents unsend data if a thread is hanging or the
142                        // system just go into idle.
143                        steal_from_thread_locals(&thread_locals, &mut stolen_buffers);
144                        if let Some(data) = stolen_buffers.pop() {
145                            data
146                        } else {
147                            match result {
148                                Ok(Some(_)) => unreachable!(),
149                                Ok(None) | Err(RecvTimeoutError::Disconnected) => {
150                                    // We should exit.
151                                    break 'outer;
152                                }
153                                Err(RecvTimeoutError::Timeout) => {
154                                    // No data stolen, wait again
155                                    continue;
156                                }
157                            }
158                        }
159                    }
160                };
161                if data.buffer.len() > buf.capacity() {
162                    let _ = writer.write_all(&data.buffer);
163                } else {
164                    buf.extend_from_slice(&data.buffer);
165                }
166                data.clear();
167                let _ = return_tx.try_send(data);
168                loop {
169                    let recv = stolen_buffers.pop().map(Some).ok_or(()).or_else(|_| {
170                        if should_exit {
171                            Ok(None)
172                        } else {
173                            data_rx.try_recv()
174                        }
175                    });
176                    match recv {
177                        Ok(Some(mut data)) => {
178                            let data_buffer = &data.buffer;
179                            if data_buffer.is_empty() {
180                                break 'outer;
181                            }
182                            if buf.len() + data_buffer.len() > buf.capacity() {
183                                let _ = writer.write_all(&buf);
184                                buf.clear();
185                                if data_buffer.len() > buf.capacity() {
186                                    let _ = writer.write_all(data_buffer);
187                                } else {
188                                    buf.extend_from_slice(data_buffer);
189                                }
190                            } else {
191                                buf.extend_from_slice(data_buffer);
192                            }
193                            data.clear();
194                            let _ = return_tx.try_send(data);
195                        }
196                        Ok(None) | Err(TryRecvError::Disconnected) => {
197                            should_exit = true;
198                            break;
199                        }
200                        Err(TryRecvError::Empty) => {
201                            break;
202                        }
203                    }
204                }
205            }
206            drop(writer);
207        });
208
209        let guard = TraceWriterGuard {
210            data_tx: Some(data_tx),
211            return_rx: Some(return_rx),
212            handle: Some(handle),
213        };
214        (trace_writer, guard)
215    }
216
217    fn send(&self, data: TraceInfoBuffer) {
218        debug_assert!(!data.buffer.is_empty());
219        let _ = self.data_tx.send(Some(data));
220    }
221
222    fn get_empty_buffer(&self, capacity: usize) -> TraceInfoBuffer {
223        self.return_rx
224            .try_recv()
225            .ok()
226            .unwrap_or_else(|| TraceInfoBuffer::new(capacity))
227    }
228
229    pub fn start_write(&self) -> WriteGuard<'_> {
230        let thread_local_buffer = self.thread_locals.get_or_default();
231        let buffer = thread_local_buffer.lock();
232        WriteGuard::new(buffer, self)
233    }
234}
235
236pub struct TraceWriterGuard {
237    data_tx: Option<Sender<Option<TraceInfoBuffer>>>,
238    return_rx: Option<Receiver<TraceInfoBuffer>>,
239    handle: Option<JoinHandle<()>>,
240}
241
242impl Drop for TraceWriterGuard {
243    fn drop(&mut self) {
244        // Send exit signal, we can't use disconnect since there is another instance in TraceWriter
245        let _ = self.data_tx.take().unwrap().send(None);
246        // Receive all return buffers and drop them here. The thread is already busy writing.
247        let return_rx = self.return_rx.take().unwrap();
248        while return_rx.recv().is_ok() {}
249        // Wait for the thread to finish completely
250        let _ = self.handle.take().unwrap().join();
251    }
252}
253
254pub struct WriteGuard<'l> {
255    // Safety: The buffer must not be None
256    buffer: MutexGuard<'l, Option<TraceInfoBuffer>>,
257    trace_writer: &'l TraceWriter,
258}
259
260impl<'l> WriteGuard<'l> {
261    fn new(
262        mut buffer: MutexGuard<'l, Option<TraceInfoBuffer>>,
263        trace_writer: &'l TraceWriter,
264    ) -> Self {
265        // Safety: The buffer must not be None, so we initialize it here
266        buffer
267            .get_or_insert_with(|| trace_writer.get_empty_buffer(THREAD_LOCAL_INITIAL_BUFFER_SIZE));
268        Self {
269            buffer,
270            trace_writer,
271        }
272    }
273
274    fn buffer(&mut self) -> &mut TraceInfoBuffer {
275        // Safety: The struct invariant ensures that the buffer is not None
276        unsafe { self.buffer.as_mut().unwrap_unchecked() }
277    }
278
279    pub fn push(&mut self, data: u8) {
280        self.buffer().push(data);
281    }
282
283    pub fn extend(&mut self, data: &[u8]) {
284        self.buffer().extend(data);
285    }
286
287    /// Converts the absolute timestamp `ts` of the row written with this guard into the value to
288    /// serialize (see "Timestamps" in the docs of [`TraceRow`]). Writes a
289    /// [`TraceRow::TimestampBase`] row first, when one is needed. Must be called before the row
290    /// itself is written (inside the callback of [`WriteGuard::mark`] for a marked row), and only
291    /// once per row.
292    pub fn encode_timestamp(&mut self, ts: u64) -> DeltaEncodedTimestamp {
293        let buffer = self.buffer();
294        let (base, encoded) = buffer.timestamps.encode(ts);
295        if let Some(ts) = base {
296            buffer.write_timestamp_base(ts);
297        }
298        encoded
299    }
300
301    /// Marks exactly what `write` writes with this guard as a row with `marker`. As long as
302    /// nothing else is written on this thread and the row wasn't sent to the writer thread in
303    /// between, it can be removed again with [`WriteGuard::remove_last_row`].
304    pub fn mark(&mut self, marker: u64, write: impl FnOnce(&mut Self)) {
305        let buffer = self.buffer();
306        let start = buffer.buffer.len();
307        let timestamps = buffer.timestamps;
308        write(self);
309        let buffer = self.buffer();
310        buffer.last_row_marker = Some(marker);
311        buffer.last_row_start = start;
312        buffer.last_row_end = buffer.buffer.len();
313        buffer.last_row_timestamps = timestamps;
314    }
315
316    /// Removes the last row written on this thread, if it was marked with `marker` by
317    /// [`WriteGuard::mark`] and is still at the end of the thread local buffer. Returns whether
318    /// it was removed. The timestamp encoder is restored to its state before the removed row,
319    /// and a [`TraceRow::TimestampBase`] row written for it is removed as well.
320    pub fn remove_last_row(&mut self, marker: u64) -> bool {
321        let buffer = self.buffer();
322        if buffer.last_row_marker != Some(marker) || buffer.last_row_end != buffer.buffer.len() {
323            return false;
324        }
325        buffer.last_row_marker = None;
326        buffer.buffer.truncate(buffer.last_row_start);
327        buffer.timestamps = buffer.last_row_timestamps;
328        true
329    }
330}
331
332impl Drop for WriteGuard<'_> {
333    fn drop(&mut self) {
334        if self.buffer().buffer.capacity() * 2 < self.buffer().buffer.len() * 3 {
335            let capacity = self.buffer().buffer.capacity();
336            let new_buffer = self.trace_writer.get_empty_buffer(capacity);
337            let buffer = std::mem::replace(self.buffer(), new_buffer);
338            self.trace_writer.send(buffer);
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use std::{
346        io::{self, Write},
347        sync::{Arc, Mutex},
348    };
349
350    use crate::{
351        trace_writer::{THREAD_LOCAL_INITIAL_BUFFER_SIZE, TraceWriter, WriteGuard},
352        tracing::{TRACE_HEADER, TimestampDecoder, TraceRow},
353    };
354
355    #[derive(Clone, Default)]
356    struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
357
358    impl Write for SharedBuffer {
359        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
360            self.0.lock().unwrap().extend_from_slice(buf);
361            Ok(buf.len())
362        }
363
364        fn flush(&mut self) -> io::Result<()> {
365            Ok(())
366        }
367    }
368
369    fn with_writer(f: impl FnOnce(&TraceWriter)) -> Vec<u8> {
370        let buffer = SharedBuffer::default();
371        let (writer, guard) = TraceWriter::new(buffer.clone());
372        f(&writer);
373        drop(writer);
374        drop(guard);
375        let data = Arc::try_unwrap(buffer.0).unwrap().into_inner().unwrap();
376        data.strip_prefix(TRACE_HEADER).unwrap().to_vec()
377    }
378
379    #[test]
380    fn removes_marked_last_row() {
381        let data = with_writer(|writer| {
382            writer.start_write().extend(b"aa");
383            writer.start_write().mark(1, |guard| guard.extend(b"bbb"));
384            let mut guard = writer.start_write();
385            assert!(guard.remove_last_row(1));
386            guard.extend(b"c");
387        });
388        assert_eq!(data, b"aac");
389    }
390
391    /// The mark covers exactly what is written in the callback, not what this guard wrote
392    /// before or after it.
393    #[test]
394    fn marks_only_the_callback() {
395        let data = with_writer(|writer| {
396            let mut guard = writer.start_write();
397            guard.extend(b"aa");
398            guard.mark(1, |guard| guard.extend(b"bbb"));
399            drop(guard);
400            let mut guard = writer.start_write();
401            assert!(guard.remove_last_row(1));
402            guard.extend(b"c");
403        });
404        assert_eq!(data, b"aac");
405
406        let data = with_writer(|writer| {
407            let mut guard = writer.start_write();
408            guard.mark(1, |guard| guard.extend(b"a"));
409            guard.extend(b"b");
410            drop(guard);
411            assert!(!writer.start_write().remove_last_row(1));
412        });
413        assert_eq!(data, b"ab");
414    }
415
416    #[test]
417    fn requires_a_matching_marker() {
418        let data = with_writer(|writer| {
419            writer.start_write().mark(1, |guard| guard.extend(b"a"));
420            assert!(!writer.start_write().remove_last_row(2));
421            // The row is still marked
422            assert!(writer.start_write().remove_last_row(1));
423            // Already removed
424            assert!(!writer.start_write().remove_last_row(1));
425        });
426        assert_eq!(data, b"");
427    }
428
429    #[test]
430    fn marker_is_invalidated_by_the_next_write() {
431        let data = with_writer(|writer| {
432            writer.start_write().mark(1, |guard| guard.extend(b"a"));
433            writer.start_write().extend(b"b");
434            assert!(!writer.start_write().remove_last_row(1));
435        });
436        assert_eq!(data, b"ab");
437    }
438
439    #[test]
440    fn marker_is_dropped_when_buffer_is_sent() {
441        // Just below the threshold where the buffer is sent to the writer thread
442        let fill = THREAD_LOCAL_INITIAL_BUFFER_SIZE * 2 / 3 - 10;
443        let data = with_writer(|writer| {
444            writer.start_write().extend(&vec![0; fill]);
445            // This exceeds the threshold, so the buffer is sent when the guard is dropped
446            writer.start_write().mark(1, |guard| guard.extend(&[1; 20]));
447            assert!(!writer.start_write().remove_last_row(1));
448        });
449        assert_eq!(data.len(), fill + 20);
450    }
451
452    #[test]
453    fn removing_the_only_row_leaves_an_empty_buffer() {
454        let data = with_writer(|writer| {
455            writer.start_write().mark(1, |guard| guard.extend(b"a"));
456            assert!(writer.start_write().remove_last_row(1));
457        });
458        assert_eq!(data, b"");
459    }
460
461    /// Decodes rows written as `TraceRow`s and resolves their timestamps.
462    fn decode_rows(mut data: &[u8]) -> Vec<TraceRow<'_, u64>> {
463        let mut decoder = TimestampDecoder::default();
464        let mut rows = Vec::new();
465        while !data.is_empty() {
466            let (row, rest): (TraceRow<'_>, _) = postcard::take_from_bytes(data).unwrap();
467            rows.push(decoder.decode(row).unwrap());
468            data = rest;
469        }
470        rows
471    }
472
473    /// Writes an `End` row like the raw trace layer does.
474    fn write_end(guard: &mut WriteGuard<'_>, ts: u64) {
475        let ts = guard.encode_timestamp(ts);
476        let row = TraceRow::End { ts, id: 1 };
477        guard.extend(&postcard::to_stdvec(&row).unwrap());
478    }
479
480    fn timeline(rows: &[TraceRow<'_, u64>]) -> Vec<(&'static str, u64)> {
481        rows.iter()
482            .map(|row| match row {
483                TraceRow::TimestampBase { ts } => ("base", *ts),
484                TraceRow::End { ts, .. } => ("end", *ts),
485                _ => unreachable!(),
486            })
487            .collect()
488    }
489
490    #[test]
491    fn timestamps_are_deltas_to_a_base_per_buffer() {
492        let data = with_writer(|writer| {
493            write_end(&mut writer.start_write(), 100);
494            write_end(&mut writer.start_write(), 105);
495            write_end(&mut writer.start_write(), 103);
496        });
497        let rows = decode_rows(&data);
498        assert_eq!(
499            timeline(&rows),
500            [("base", 100), ("end", 100), ("end", 105), ("end", 103)]
501        );
502        // The signed deltas +5 and -2 on the wire
503        let (_, rest): (TraceRow<'_>, _) = postcard::take_from_bytes(&data).unwrap();
504        let wire: Vec<i64> = {
505            let mut rest = rest;
506            let mut wire = Vec::new();
507            while !rest.is_empty() {
508                let (row, r): (TraceRow<'_>, _) = postcard::take_from_bytes(rest).unwrap();
509                if let TraceRow::End { ts, .. } = row {
510                    wire.push(ts.0);
511                }
512                rest = r;
513            }
514            wire
515        };
516        assert_eq!(wire, [0, 5, -2]);
517    }
518
519    #[test]
520    fn removing_a_row_restores_the_previous_timestamp() {
521        let data = with_writer(|writer| {
522            write_end(&mut writer.start_write(), 100);
523            writer.start_write().mark(1, |guard| write_end(guard, 150));
524            assert!(writer.start_write().remove_last_row(1));
525            write_end(&mut writer.start_write(), 120);
526        });
527        assert_eq!(
528            timeline(&decode_rows(&data)),
529            [("base", 100), ("end", 100), ("end", 120)]
530        );
531    }
532
533    #[test]
534    fn removing_the_first_row_after_the_base() {
535        let data = with_writer(|writer| {
536            // The base is written by the same write as the removed row.
537            writer.start_write().mark(1, |guard| write_end(guard, 100));
538            assert!(writer.start_write().remove_last_row(1));
539            write_end(&mut writer.start_write(), 120);
540            write_end(&mut writer.start_write(), 125);
541        });
542        assert_eq!(
543            timeline(&decode_rows(&data)),
544            [("base", 120), ("end", 120), ("end", 125)]
545        );
546    }
547
548    #[test]
549    fn stolen_buffer_starts_a_new_base() {
550        let data = with_writer(|writer| {
551            write_end(&mut writer.start_write(), 100);
552            writer.start_write().mark(1, |guard| write_end(guard, 110));
553            // The writer thread steals the thread local buffer when it doesn't receive any data
554            // for a second
555            std::thread::sleep(std::time::Duration::from_millis(2500));
556            let mut guard = writer.start_write();
557            assert!(!guard.remove_last_row(1));
558            write_end(&mut guard, 105);
559        });
560        assert_eq!(
561            timeline(&decode_rows(&data)),
562            [
563                ("base", 100),
564                ("end", 100),
565                ("end", 110),
566                ("base", 105),
567                ("end", 105)
568            ]
569        );
570    }
571
572    #[test]
573    fn out_of_range_delta_writes_a_new_base() {
574        let data = with_writer(|writer| {
575            write_end(&mut writer.start_write(), 0);
576            write_end(&mut writer.start_write(), u64::MAX);
577            write_end(&mut writer.start_write(), u64::MAX - 1);
578        });
579        let mut rest = data.as_slice();
580        let mut wire = Vec::new();
581        while !rest.is_empty() {
582            let (row, r): (TraceRow<'_>, _) = postcard::take_from_bytes(rest).unwrap();
583            wire.push(match row {
584                TraceRow::TimestampBase { ts } => ("base", i128::from(ts)),
585                TraceRow::End { ts, .. } => ("end", i128::from(ts.0)),
586                _ => unreachable!(),
587            });
588            rest = r;
589        }
590        assert_eq!(
591            wire,
592            [
593                ("base", 0),
594                ("end", 0),
595                ("base", i128::from(u64::MAX)),
596                ("end", 0),
597                ("end", -1)
598            ]
599        );
600        assert_eq!(
601            timeline(&decode_rows(&data)),
602            [
603                ("base", 0),
604                ("end", 0),
605                ("base", u64::MAX),
606                ("end", u64::MAX),
607                ("end", u64::MAX - 1)
608            ]
609        );
610    }
611
612    #[test]
613    fn rows_without_timestamp_keep_the_previous_timestamp() {
614        let data = with_writer(|writer| {
615            write_end(&mut writer.start_write(), 100);
616            let mut guard = writer.start_write();
617            let record: TraceRow<'_> = TraceRow::Record {
618                id: 1,
619                values: Vec::new(),
620            };
621            guard.extend(&postcard::to_stdvec(&record).unwrap());
622            drop(guard);
623            write_end(&mut writer.start_write(), 90);
624        });
625        let rows = decode_rows(&data);
626        let timeline: Vec<_> = rows
627            .iter()
628            .map(|row| match row {
629                TraceRow::TimestampBase { ts } => ("base", *ts),
630                TraceRow::End { ts, .. } => ("end", *ts),
631                TraceRow::Record { .. } => ("record", 0),
632                _ => unreachable!(),
633            })
634            .collect();
635        // No base was written for the record, and the delta of the last row refers to 100
636        assert_eq!(
637            timeline,
638            [("base", 100), ("end", 100), ("record", 0), ("end", 90)]
639        );
640    }
641
642    #[test]
643    fn buffer_starting_with_a_row_without_timestamp_has_no_base_before_it() {
644        let data = with_writer(|writer| {
645            let mut guard = writer.start_write();
646            let record: TraceRow<'_> = TraceRow::Record {
647                id: 1,
648                values: Vec::new(),
649            };
650            guard.extend(&postcard::to_stdvec(&record).unwrap());
651            drop(guard);
652            write_end(&mut writer.start_write(), 60);
653        });
654        let rows = decode_rows(&data);
655        // The base is written before the first row with a timestamp
656        assert!(matches!(rows[0], TraceRow::Record { .. }));
657        assert!(matches!(rows[1], TraceRow::TimestampBase { ts: 60 }));
658        assert!(matches!(rows[2], TraceRow::End { ts: 60, .. }));
659        assert_eq!(rows.len(), 3);
660    }
661
662    #[test]
663    fn sent_buffer_starts_a_new_base() {
664        // Just below the threshold where the buffer is sent to the writer thread
665        let fill = THREAD_LOCAL_INITIAL_BUFFER_SIZE * 2 / 3 - 10;
666        let data = with_writer(|writer| {
667            write_end(&mut writer.start_write(), 100);
668            let mut guard = writer.start_write();
669            guard.mark(1, |guard| write_end(guard, 110));
670            // A row that exceeds the threshold, so the buffer is sent when the guard is dropped
671            let filler: TraceRow<'_> = TraceRow::Record {
672                id: 1,
673                values: vec![(
674                    "filler".into(),
675                    crate::tracing::TraceValue::String("x".repeat(fill).into()),
676                )],
677            };
678            guard.extend(&postcard::to_stdvec(&filler).unwrap());
679            drop(guard);
680            // The marked row was sent and can't be removed anymore
681            assert!(!writer.start_write().remove_last_row(1));
682            write_end(&mut writer.start_write(), 105);
683        });
684        let rows = decode_rows(&data);
685        let timeline: Vec<_> = rows
686            .iter()
687            .filter_map(|row| match row {
688                TraceRow::TimestampBase { ts } => Some(("base", *ts)),
689                TraceRow::End { ts, .. } => Some(("end", *ts)),
690                _ => None,
691            })
692            .collect();
693        assert_eq!(
694            timeline,
695            [
696                ("base", 100),
697                ("end", 100),
698                ("end", 110),
699                ("base", 105),
700                ("end", 105)
701            ]
702        );
703    }
704}