Skip to main content

flydra2/
write_data.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use libflate::{finish::AutoFinishUnchecked, gzip::Encoder};
5use std::{io::Write, sync::Arc};
6use tracing::info;
7
8use braid_types::{
9    BRAID_SCHEMA, CAM_SETTINGS_DIRNAME, CamInfoRow, FEATURE_DETECT_SETTINGS_DIRNAME,
10    IMAGES_DIRNAME, MyFloat, RECONSTRUCT_LATENCY_HLOG_FNAME, REPROJECTION_DIST_HLOG_FNAME,
11    TextlogRow, TrackingParams,
12};
13
14use braidz_types::BraidMetadata;
15
16use crate::{
17    ConnectedCamerasManager, ExperimentInfoRow, FrameDataAndPoints, HistogramWritingState,
18    KalmanEstimateRecord, OrderingWriter, Result, SaveToDiskMsg, StartSavingCsvConfig,
19    TrackingParamsSaver, finish_histogram, histogram_record, save_hlog,
20};
21
22struct WritingState {
23    output_dirname: std::path::PathBuf,
24    /// The readme file in the output directory.
25    ///
26    /// We keep this file open to establish locking on the open directory.
27    ///
28    /// In theory, we might prefer an open reference to the directory itself,
29    /// but this does not seem possible. So we have a potential slight race
30    /// condition when we have our directory but not yet the file handle on
31    /// readme.
32    readme_fd: Option<std::fs::File>,
33    save_empty_data2d: bool,
34    // kalman_estimates_wtr: Option<csv::Writer<Box<dyn std::io::Write>>>,
35    kalman_estimates_wtr: Option<OrderingWriter>,
36    data_assoc_wtr: Option<csv::Writer<Box<dyn std::io::Write + Send>>>,
37    data_2d_wtr: csv::Writer<Box<dyn std::io::Write + Send>>,
38    textlog_wtr: csv::Writer<Box<dyn std::io::Write + Send>>,
39    trigger_clock_info_wtr: csv::Writer<Box<dyn std::io::Write + Send>>,
40    experiment_info_wtr: csv::Writer<Box<dyn std::io::Write + Send>>,
41    writer_stats: Option<(usize, usize)>,
42    file_start_time: std::time::SystemTime,
43
44    reconstruction_latency_usec: Option<HistogramWritingState>,
45    reproj_dist_pixels: Option<HistogramWritingState>,
46    last_flush: std::time::Instant,
47}
48
49fn _test_writing_state_is_send() {
50    // Compile-time test to ensure WritingState implements Send trait.
51    fn implements<T: Send>() {}
52    implements::<WritingState>();
53}
54
55#[derive(Clone, Debug)]
56pub enum BraidMetadataBuilder {
57    GenerateNew(MetadataParts),
58    Existing(BraidMetadata),
59}
60
61impl BraidMetadataBuilder {
62    /// Constructor to help with backwards compatibility
63    pub fn saving_program_name<S: Into<String>>(saving_program_name: S) -> BraidMetadataBuilder {
64        BraidMetadataBuilder::GenerateNew(MetadataParts {
65            saving_program_name: saving_program_name.into(),
66        })
67    }
68}
69
70#[derive(Clone, Debug)]
71pub struct MetadataParts {
72    saving_program_name: String,
73}
74
75impl WritingState {
76    fn new(
77        cfg: StartSavingCsvConfig,
78        cam_info_rows: Vec<CamInfoRow>,
79        recon: &Option<flydra_mvg::FlydraMultiCameraSystem<MyFloat>>,
80        tracking_params: Arc<TrackingParams>,
81        save_empty_data2d: bool,
82        metadata_builder: BraidMetadataBuilder,
83    ) -> Result<Self> {
84        let output_dirname = cfg.out_dir;
85        let local = cfg.local;
86        let git_revision = cfg.git_rev;
87        let fps = cfg.fps;
88        let per_cam_data = cfg.per_cam_data;
89
90        // Backward-incompatible changes to what is saved require a BRAID_SCHEMA bump (see its definition).
91
92        // create output dir
93        std::fs::create_dir_all(&output_dirname)?;
94
95        // Until we obtain the readme file handle, we have a small race
96        // condition where another process could also open this directory.
97
98        let readme_fd = {
99            let readme_path = output_dirname.join(braid_types::README_MD_FNAME);
100
101            let mut fd = std::fs::File::create(readme_path)?;
102
103            // Start and end it with some newlines so the text is more
104            // readable.
105            fd.write_all(
106                "\n\nThis is data saved by the braid program. \
107                See https://strawlab.org/braid for more information.\n\n"
108                    .as_bytes(),
109            )
110            .unwrap();
111            Some(fd)
112        };
113
114        {
115            let braid_metadata_path = output_dirname.join(braid_types::BRAID_METADATA_YML_FNAME);
116
117            let metadata = match metadata_builder {
118                BraidMetadataBuilder::GenerateNew(parts) => {
119                    BraidMetadata {
120                        schema: BRAID_SCHEMA, // BraidMetadataSchemaTag
121                        git_revision: git_revision.clone(),
122                        original_recording_time: local,
123                        save_empty_data2d,
124                        saving_program_name: parts.saving_program_name,
125                    }
126                }
127                BraidMetadataBuilder::Existing(metadata) => metadata,
128            };
129            let metadata_buf = serde_yaml::to_string(&metadata).unwrap();
130
131            let mut fd = std::fs::File::create(braid_metadata_path)?;
132            fd.write_all(metadata_buf.as_bytes()).unwrap();
133        }
134
135        // write images
136        {
137            let mut image_path = output_dirname.clone();
138            image_path.push(IMAGES_DIRNAME);
139            std::fs::create_dir_all(&image_path)?;
140
141            for (raw_cam_name, data) in per_cam_data.iter() {
142                let buf = data.current_image_png.as_slice();
143                let fname = format!("{}.png", raw_cam_name.as_str());
144                let fullpath = image_path.clone().join(fname);
145                let mut fd = std::fs::File::create(&fullpath)?;
146                fd.write_all(buf)?;
147            }
148        }
149
150        // write camera settings
151        {
152            let mut cam_settings_path = output_dirname.clone();
153            cam_settings_path.push(CAM_SETTINGS_DIRNAME);
154            if per_cam_data
155                .iter()
156                .any(|(_, x)| x.cam_settings_data.is_some())
157            {
158                std::fs::create_dir_all(&cam_settings_path)?;
159            }
160
161            for (raw_cam_name, cam) in per_cam_data.iter() {
162                if let Some(data) = &cam.cam_settings_data {
163                    let fname = format!(
164                        "{}.{}",
165                        raw_cam_name.as_str(),
166                        data.current_cam_settings_extension
167                    );
168                    let fullpath = cam_settings_path.clone().join(fname);
169                    let mut fd = std::fs::File::create(&fullpath)?;
170                    fd.write_all(data.current_cam_settings_buf.as_bytes())?;
171                }
172            }
173        }
174
175        // write feature detection settings
176        {
177            let mut feature_detect_settings_path = output_dirname.clone();
178            feature_detect_settings_path.push(FEATURE_DETECT_SETTINGS_DIRNAME);
179            if per_cam_data
180                .iter()
181                .any(|(_, x)| x.feature_detect_settings.is_some())
182            {
183                std::fs::create_dir_all(&feature_detect_settings_path)?;
184            }
185
186            for (raw_cam_name, cam) in per_cam_data.iter() {
187                if let Some(data) = &cam.feature_detect_settings {
188                    let buf = toml::to_vec(&data.current_feature_detect_settings)?;
189                    let fname = format!("{}.toml", raw_cam_name.as_str());
190                    let fullpath = feature_detect_settings_path.join(fname);
191                    let mut fd = std::fs::File::create(&fullpath)?;
192                    fd.write_all(&buf)?;
193                }
194            }
195        }
196
197        // write cam info (pairs of CamNum and cam name)
198        {
199            let mut csv_path = output_dirname.clone();
200            csv_path.push(format!("{}.gz", braid_types::CAM_INFO_CSV_FNAME));
201            let fd = std::fs::File::create(&csv_path)?;
202            let fd: Box<dyn std::io::Write + Send> =
203                Box::new(AutoFinishUnchecked::new(Encoder::new(fd)?));
204            let mut cam_info_wtr = csv::Writer::from_writer(fd);
205            for row in cam_info_rows.iter() {
206                cam_info_wtr.serialize(row)?;
207            }
208        }
209
210        // write calibration
211        if let Some(recon) = recon {
212            let mut cal_path = output_dirname.clone();
213            cal_path.push(braid_types::CALIBRATION_XML_FNAME);
214            let fd = std::fs::File::create(&cal_path)?;
215            recon.to_flydra_xml(fd)?;
216        }
217
218        // open textlog and write initial message
219        let textlog_wtr = {
220            let local_datetime = chrono::Local::now();
221            let mainbrain_timestamp = strand_datetime_conversion::datetime_to_f64(&local_datetime);
222            let (tzname_str, tzname) = match iana_time_zone::get_timezone() {
223                Ok(tzname) => ("time_tzname0", tzname),
224                Err(_err) => {
225                    tracing::debug!("Could not get timezone, using UTC offset instead.");
226                    let offset = local_datetime.offset();
227                    use chrono::offset::Offset;
228                    let offset_secs = offset.fix().local_minus_utc();
229                    ("UTC_offset_secs", format!("{offset_secs}"))
230                }
231            };
232
233            let fps = match fps {
234                Some(fps) => format!("{fps}"),
235                None => "unknown".to_string(),
236            };
237            let version = "2.0.0";
238            let message = format!(
239                "MainBrain running at {fps} fps, (\
240                flydra_version {version}, git_revision {git_revision}, {tzname_str} {tzname})",
241            );
242
243            let tps = TrackingParamsSaver {
244                tracking_params: (*tracking_params).clone(),
245                git_revision,
246            };
247            let message2 = serde_json::to_string(&tps)?;
248
249            let textlog: Vec<TextlogRow> = vec![
250                TextlogRow {
251                    mainbrain_timestamp,
252                    cam_id: "mainbrain".to_string(),
253                    host_timestamp: mainbrain_timestamp,
254                    message,
255                },
256                TextlogRow {
257                    mainbrain_timestamp,
258                    cam_id: "mainbrain".to_string(),
259                    host_timestamp: mainbrain_timestamp,
260                    message: message2,
261                },
262            ];
263
264            // We do not stream this to .gz because we want to maximize chances
265            // that it is completely flushed to disk even in event of a panic.
266            let mut csv_path = output_dirname.clone();
267            csv_path.push(braid_types::TEXTLOG_CSV_FNAME);
268            let fd = std::fs::File::create(&csv_path)?;
269            let mut textlog_wtr =
270                csv::Writer::from_writer(Box::new(fd) as Box<dyn std::io::Write + Send>);
271            for row in textlog.iter() {
272                textlog_wtr.serialize(row)?;
273            }
274            // Flush to disk. In case braid crashes, at least we want to recover this info.
275            textlog_wtr.flush()?;
276            textlog_wtr
277        };
278
279        // kalman estimates
280        let kalman_estimates_wtr = if let Some(_recon) = recon {
281            let mut csv_path = output_dirname.clone();
282            csv_path.push(format!("{}.gz", braid_types::KALMAN_ESTIMATES_CSV_FNAME));
283            let fd = std::fs::File::create(&csv_path)?;
284            let fd: Box<dyn std::io::Write + Send> =
285                Box::new(AutoFinishUnchecked::new(Encoder::new(fd)?));
286            Some(OrderingWriter::new(csv::Writer::from_writer(fd)))
287        } else {
288            None
289        };
290
291        let trigger_clock_info_wtr = {
292            let mut csv_path = output_dirname.clone();
293            csv_path.push(format!("{}.gz", braid_types::TRIGGER_CLOCK_INFO_CSV_FNAME));
294            let fd = std::fs::File::create(&csv_path)?;
295            let fd: Box<dyn std::io::Write + Send> =
296                Box::new(AutoFinishUnchecked::new(Encoder::new(fd)?));
297            csv::Writer::from_writer(fd)
298        };
299
300        let experiment_info_wtr = {
301            // We do not stream this to .gz because we want to maximize chances
302            // that it is completely flushed to disk even in event of a panic.
303            let mut csv_path = output_dirname.clone();
304            csv_path.push(braid_types::EXPERIMENT_INFO_CSV_FNAME);
305            let fd = std::fs::File::create(&csv_path)?;
306            csv::Writer::from_writer(Box::new(fd) as Box<dyn std::io::Write + Send>)
307        };
308
309        let data_assoc_wtr = if let Some(_recon) = recon {
310            let mut csv_path = output_dirname.clone();
311            csv_path.push(format!("{}.gz", braid_types::DATA_ASSOCIATE_CSV_FNAME));
312            let fd = std::fs::File::create(&csv_path)?;
313            let fd: Box<dyn std::io::Write + Send> =
314                Box::new(AutoFinishUnchecked::new(Encoder::new(fd)?));
315            Some(csv::Writer::from_writer(fd))
316        } else {
317            None
318        };
319
320        let data_2d_wtr = {
321            let mut csv_path = output_dirname.clone();
322            csv_path.push(format!("{}.gz", braid_types::DATA2D_DISTORTED_CSV_FNAME));
323            let fd = std::fs::File::create(&csv_path)?;
324            let fd: Box<dyn std::io::Write + Send> =
325                Box::new(AutoFinishUnchecked::new(Encoder::new(fd)?));
326            csv::Writer::from_writer(fd)
327        };
328
329        let writer_stats = if cfg.print_stats { Some((0, 0)) } else { None };
330
331        let file_start_time = if let Some(local) = local {
332            local.into()
333        } else {
334            std::time::SystemTime::now()
335        };
336
337        let (reconstruction_latency_usec, reproj_dist_pixels) = if cfg.save_performance_histograms {
338            (
339                Some(HistogramWritingState::default()),
340                Some(HistogramWritingState::default()),
341            )
342        } else {
343            (None, None)
344        };
345
346        Ok(Self {
347            output_dirname,
348            readme_fd,
349            save_empty_data2d,
350            kalman_estimates_wtr,
351            data_assoc_wtr,
352            data_2d_wtr,
353            textlog_wtr,
354            trigger_clock_info_wtr,
355            experiment_info_wtr,
356            writer_stats,
357            file_start_time,
358            reconstruction_latency_usec,
359            reproj_dist_pixels,
360            last_flush: std::time::Instant::now(),
361        })
362    }
363
364    fn save_data_2d_distorted(&mut self, fdp: FrameDataAndPoints) -> Result<usize> {
365        let data2d_distorted = fdp.into_save(self.save_empty_data2d);
366        for row in data2d_distorted.iter() {
367            self.data_2d_wtr.serialize(row)?;
368        }
369        Ok(data2d_distorted.len())
370    }
371
372    fn flush_all(&mut self) -> Result<()> {
373        if let Some(ref mut kew) = self.kalman_estimates_wtr {
374            kew.flush()?;
375        }
376        if let Some(ref mut daw) = self.data_assoc_wtr {
377            daw.flush()?;
378        }
379        self.data_2d_wtr.flush()?;
380        self.textlog_wtr.flush()?;
381        self.trigger_clock_info_wtr.flush()?;
382        self.experiment_info_wtr.flush()?;
383        self.last_flush = std::time::Instant::now();
384        Ok(())
385    }
386}
387
388impl Drop for WritingState {
389    fn drop(&mut self) {
390        tracing::debug!("WritingState is being dropped, flushing all data to disk.");
391        fn dummy_csv() -> csv::Writer<Box<dyn std::io::Write + Send>> {
392            let fd = Box::new(Vec::with_capacity(0));
393            csv::Writer::from_writer(fd)
394        }
395
396        if let Some(count) = self.writer_stats {
397            info!(
398                "    {} rows of 2d detections, {} rows of kalman estimates",
399                count.0, count.1
400            );
401        }
402
403        // Drop all CSV files, which closes them.
404        {
405            self.kalman_estimates_wtr.take();
406            self.data_assoc_wtr.take();
407            // Could equivalently call `.flush()` on the writers?
408            self.data_2d_wtr = dummy_csv();
409            self.textlog_wtr = dummy_csv();
410            self.trigger_clock_info_wtr = dummy_csv();
411            self.experiment_info_wtr = dummy_csv();
412        }
413
414        // Move out original output name so that a subsequent call to `drop()`
415        // doesn't accidentally overwrite our real data.
416        let output_dirname = std::mem::take(&mut self.output_dirname);
417
418        let now_system = std::time::SystemTime::now();
419        {
420            if let Some(reconstruction_latency_usec) = &mut self.reconstruction_latency_usec {
421                finish_histogram(
422                    &mut reconstruction_latency_usec.current_store,
423                    self.file_start_time,
424                    &mut reconstruction_latency_usec.histograms,
425                    now_system,
426                )
427                .unwrap();
428
429                save_hlog(
430                    &output_dirname,
431                    RECONSTRUCT_LATENCY_HLOG_FNAME,
432                    &reconstruction_latency_usec.histograms,
433                    self.file_start_time,
434                );
435            }
436
437            if let Some(reproj_dist_pixels) = &mut self.reproj_dist_pixels {
438                finish_histogram(
439                    &mut reproj_dist_pixels.current_store,
440                    self.file_start_time,
441                    &mut reproj_dist_pixels.histograms,
442                    now_system,
443                )
444                .unwrap();
445
446                save_hlog(
447                    &output_dirname,
448                    REPROJECTION_DIST_HLOG_FNAME,
449                    &reproj_dist_pixels.histograms,
450                    self.file_start_time,
451                );
452            }
453        }
454
455        // Compress the saved directory into a .braidz file.
456        {
457            // TODO: read all the (forward) kalman estimates and smooth them to
458            // an additional file. If we do it here, it is done after the
459            // realtime tracking and thus does not interfere with recording
460            // data. On the other hand, if we smooth at the end of each
461            // trajectory, those smoothing costs are amortized throughout the
462            // experiment.
463
464            let replace_extension = match output_dirname.extension() {
465                Some(ext) => ext == "braid",
466                None => false,
467            };
468
469            // compute the name of the zip file.
470            let output_zipfile: std::path::PathBuf = if replace_extension {
471                output_dirname.with_extension("braidz")
472            } else {
473                let mut tmp = output_dirname.clone().into_os_string();
474                tmp.push(".braidz");
475                tmp.into()
476            };
477
478            info!("creating zip file {}", output_zipfile.display());
479            braidz_writer::dir_to_braidz(&output_dirname, output_zipfile).unwrap();
480
481            // Release the file so we no longer have exclusive access to the
482            // directory. (Until we remove the directory, we have a small race
483            // condition where another process could open the directory without
484            // obtaining the readme file handle.)
485            self.readme_fd = None;
486
487            // Once the original directory is written successfully to a zip
488            // file, we remove it.
489            info!(
490                "done creating zip file, removing {}",
491                output_dirname.display()
492            );
493            match std::fs::remove_dir_all(&output_dirname) {
494                Ok(()) => {}
495                Err(err) => {
496                    panic!(
497                        "Error removing original directory {}: {}",
498                        output_dirname.display(),
499                        err
500                    );
501                }
502            }
503        }
504        tracing::debug!("Done writing braidz data to disk.");
505    }
506}
507
508/// Listen to a Receiver for messages and save the data to disk.
509///
510/// This function only exits upon error or when the Sender counterpart to the
511/// Receiver has closed. It blocks and does not use an async context and thus
512/// should be spawned with `tokio::task::spawn_blocking`.
513#[tracing::instrument(level = "debug", skip_all)]
514pub(crate) fn writer_task_main(
515    mut braidz_write_rx: tokio::sync::mpsc::Receiver<SaveToDiskMsg>,
516    cam_manager: ConnectedCamerasManager,
517    recon: Option<flydra_mvg::FlydraMultiCameraSystem<MyFloat>>,
518    tracking_params: Arc<TrackingParams>,
519    save_empty_data2d: bool,
520    metadata_builder: BraidMetadataBuilder,
521    ignore_latency: bool,
522) -> Result<()> {
523    use crate::SaveToDiskMsg::*;
524    use std::time::Duration;
525
526    let mut writing_state: Option<WritingState> = None;
527
528    const FLUSH_INTERVAL: u64 = 1;
529    let flush_interval = Duration::from_secs(FLUSH_INTERVAL);
530
531    tracing::debug!("Starting braidz writer task.");
532
533    while let Some(msg) = braidz_write_rx.blocking_recv() {
534        // TODO: improve flushing. Specifically, if we block for a long time
535        // without receiving a message here, we will not flush to disk. To do
536        // that, though, we would have a timeout on `blocking_recv`, which
537        // doesn't seem possible.
538        match msg {
539            KalmanEstimate(ke) => {
540                let KalmanEstimateRecord {
541                    record,
542                    data_assoc_rows,
543                    mean_reproj_dist_100x,
544                    production_timestamp,
545                } = ke;
546                let trigger_timestamp = record.timestamp.clone();
547
548                // Now actually send the data to the writers.
549                if let Some(ref mut ws) = writing_state {
550                    if let Some(ref mut kew) = ws.kalman_estimates_wtr {
551                        kew.serialize(record)?;
552                        if let Some(count) = ws.writer_stats.as_mut() {
553                            count.1 += 1
554                        }
555                    }
556                    if let Some(ref mut daw) = ws.data_assoc_wtr {
557                        for row in data_assoc_rows.iter() {
558                            daw.serialize(row)?;
559                        }
560                    }
561
562                    if !ignore_latency {
563                        // Log reconstruction latency to histogram. The latency
564                        // is measured from frame acquisition to when the
565                        // tracker produced the estimate (NOT to now: this
566                        // writer dequeues rows behind a channel and a periodic
567                        // flush, and that delay is not tracking latency).
568                        if let (Some(trigger_timestamp), Some(produced_at)) =
569                            (trigger_timestamp, production_timestamp)
570                        {
571                            // `trigger_timestamp` is when this frame was acquired.
572                            // It may be None if it cannot be inferred while the
573                            // triggerbox clock model is first initializing.
574                            use chrono::{DateTime, Utc};
575                            let then: DateTime<Utc> = trigger_timestamp.into();
576                            let elapsed = produced_at.signed_duration_since(then);
577                            let now_system: std::time::SystemTime = produced_at.into();
578
579                            if let Some(latency_usec) = elapsed.num_microseconds()
580                                && latency_usec >= 0
581                                && let Some(reconstruction_latency_usec) =
582                                    &mut ws.reconstruction_latency_usec
583                            {
584                                // The latency should always be positive, but num_microseconds()
585                                // can return negative and we don't want to panic if time goes
586                                // backwards for some reason.
587                                match histogram_record(
588                                    latency_usec as u64,
589                                    &mut reconstruction_latency_usec.current_store,
590                                    1000 * 1000 * 60,
591                                    2,
592                                    ws.file_start_time,
593                                    &mut reconstruction_latency_usec.histograms,
594                                    now_system,
595                                ) {
596                                    Ok(()) => {}
597                                    Err(_) => tracing::error!(
598                                        "latency value {} out of expected range",
599                                        latency_usec
600                                    ),
601                                }
602                            }
603                        }
604                    }
605
606                    {
607                        if let Some(mean_reproj_dist_100x) = mean_reproj_dist_100x {
608                            let now_system = std::time::SystemTime::now();
609
610                            if let Some(reproj_dist_pixels) = &mut ws.reproj_dist_pixels {
611                                match histogram_record(
612                                    mean_reproj_dist_100x,
613                                    &mut reproj_dist_pixels.current_store,
614                                    1000000,
615                                    2,
616                                    ws.file_start_time,
617                                    &mut reproj_dist_pixels.histograms,
618                                    now_system,
619                                ) {
620                                    Ok(()) => {}
621                                    Err(_) => tracing::error!(
622                                        "mean reprojection 100x distance value {} out of expected range",
623                                        mean_reproj_dist_100x
624                                    ),
625                                }
626                            }
627                        }
628                    }
629                }
630
631                // simply drop data if no file opened
632            }
633            Data2dDistorted(fdp) => {
634                if let Some(ref mut ws) = writing_state {
635                    let rows = ws.save_data_2d_distorted(fdp)?;
636                    if let Some(count) = ws.writer_stats.as_mut() {
637                        count.0 += rows;
638                    }
639                }
640                // simply drop data if no file opened
641            }
642            StartSavingCsv(cfg) => {
643                writing_state = Some(WritingState::new(
644                    cfg,
645                    cam_manager.sample(),
646                    &recon,
647                    tracking_params.clone(),
648                    save_empty_data2d,
649                    metadata_builder.clone(),
650                )?);
651            }
652            StopSavingCsv => {
653                // This will drop `writing_state`, and thus the writers, and
654                // thus close them.
655                writing_state = None;
656            }
657            SetExperimentUuid(uuid) => {
658                let entry = ExperimentInfoRow { uuid };
659                if let Some(ref mut ws) = writing_state {
660                    ws.experiment_info_wtr.serialize(&entry)?;
661                }
662            }
663            Textlog(entry) => {
664                if let Some(ref mut ws) = writing_state {
665                    ws.textlog_wtr.serialize(&entry)?;
666                }
667                // simply drop data if no file opened
668            }
669            TriggerClockInfo(entry) => {
670                if let Some(ref mut ws) = writing_state {
671                    ws.trigger_clock_info_wtr.serialize(&entry)?;
672                }
673                // simply drop data if no file opened
674            }
675        }
676
677        if let Some(ref mut ws) = writing_state
678            && ws.last_flush.elapsed() > flush_interval
679        {
680            ws.flush_all()?;
681        }
682    }
683    tracing::info!("Done with braidz writer task.");
684    Ok(())
685}
686
687#[cfg(test)]
688mod test {
689    use super::*;
690    use std::sync::atomic::AtomicBool;
691
692    #[test]
693    fn test_save_braidz_on_drop() {
694        // create temporary dir to hold everything here.
695        let root = tempfile::tempdir().unwrap().keep(); // must manually cleanup
696
697        let braid_root = root.join("test.braid");
698        let braidz_name = root.join("test.braidz");
699
700        {
701            let cfg = StartSavingCsvConfig {
702                out_dir: braid_root.clone(),
703                local: None,
704                git_rev: "<impossible git rev>".into(),
705                fps: None,
706                per_cam_data: Default::default(),
707                print_stats: false,
708                save_performance_histograms: false,
709            };
710
711            let cam_manager = ConnectedCamerasManager::new(
712                &None,
713                std::collections::BTreeSet::new(),
714                Arc::new(AtomicBool::new(true)),
715                Arc::new(AtomicBool::new(true)),
716                None,
717                None,
718            );
719            let tracking_params = Arc::new(braid_types::default_tracking_params_full_3d());
720            let save_empty_data2d = false;
721
722            let ws = WritingState::new(
723                cfg,
724                cam_manager.sample(),
725                &None,
726                tracking_params,
727                save_empty_data2d,
728                BraidMetadataBuilder::saving_program_name(format!("{}:{}", file!(), line!())),
729            )
730            .unwrap();
731
732            // Check that original directory exists.
733            assert!(braid_root.exists());
734            // Ensure .braidz not present.
735            assert!(!braidz_name.exists());
736
737            std::mem::drop(ws);
738        }
739
740        // Check that original directory is gone.
741        assert!(!braid_root.exists());
742
743        // Check that .braidz is present.
744        assert!(braidz_name.exists());
745
746        std::fs::remove_dir_all(root).unwrap();
747    }
748
749    /// Ensure that .braidz files can exceed 4GB.
750    #[ignore]
751    #[test]
752    fn test_giant_braidz_for_zip64_support() -> Result<()> {
753        let root = tempfile::tempdir()?;
754
755        println!("saving giant files in temp dir {}", root.path().display());
756
757        let braid_root = root.path().join("test.braid");
758        let braidz_name = root.path().join("test.braidz");
759
760        fn make_frame_data(i: u64) -> FrameDataAndPoints {
761            let synced_frame = braid_types::SyncFno(i);
762            FrameDataAndPoints {
763                frame_data: crate::FrameData {
764                    block_id: None,
765                    cam_name: braid_types::RawCamName::new("cam".to_string()),
766                    cam_num: braid_types::CamNum(0),
767                    cam_received_timestamp: braid_types::FlydraFloatTimestampLocal::from_f64(
768                        i as f64 + 0.123,
769                    ),
770                    device_timestamp: None,
771                    synced_frame,
772                    tdpt: crate::TimeDataPassthrough {
773                        frame: synced_frame,
774                        timestamp: None,
775                    },
776                    time_delta: crate::SyncedFrameCount {
777                        frame: synced_frame,
778                    },
779                    trigger_timestamp: None,
780                },
781                points: vec![],
782            }
783        }
784
785        // At 4.5 bytes per row, this gets us above 5_000_000_000 bytes.
786        let num_rows = 1_200_000_000;
787
788        let save_empty_data2d = true;
789        {
790            let cfg = StartSavingCsvConfig {
791                out_dir: braid_root.clone(),
792                local: None,
793                git_rev: "<impossible git rev>".into(),
794                fps: None,
795                per_cam_data: Default::default(),
796                print_stats: false,
797                save_performance_histograms: false,
798            };
799
800            let cam_manager = ConnectedCamerasManager::new(
801                &None,
802                std::collections::BTreeSet::new(),
803                Arc::new(AtomicBool::new(true)),
804                Arc::new(AtomicBool::new(true)),
805                None,
806                None,
807            );
808            let tracking_params = Arc::new(braid_types::default_tracking_params_full_3d());
809
810            let mut ws = WritingState::new(
811                cfg,
812                cam_manager.sample(),
813                &None,
814                tracking_params,
815                save_empty_data2d,
816                BraidMetadataBuilder::saving_program_name(format!("{}:{}", file!(), line!())),
817            )?;
818
819            // Check that original directory exists.
820            assert!(braid_root.exists());
821            // Ensure .braidz not present.
822            assert!(!braidz_name.exists());
823
824            // Save a lot of data
825            for i in 0..num_rows {
826                if i % 10_000_000 == 0 {
827                    println!(
828                        "writing {}/{}: {}%",
829                        i,
830                        num_rows,
831                        i as f64 / num_rows as f64 * 100.0
832                    );
833                }
834                ws.save_data_2d_distorted(make_frame_data(i))?;
835            }
836
837            std::mem::drop(ws);
838        }
839
840        // Check that original directory is gone.
841        assert!(!braid_root.exists());
842
843        // Check that .braidz is present.
844        assert!(braidz_name.exists());
845
846        let metadata = std::fs::metadata(&braidz_name)?;
847        println!("metadata.len() {}", metadata.len());
848        assert!(metadata.len() > 5_000_000_000);
849
850        let zip_reader = std::fs::File::open(braidz_name)?;
851        let mut zip_archive = zip::ZipArchive::new(zip_reader).unwrap();
852
853        let data2d_fname = format!("{}.gz", braid_types::DATA2D_DISTORTED_CSV_FNAME);
854
855        let gz_rdr = zip_archive.by_name(&data2d_fname).unwrap();
856
857        let raw_csv_rdr = libflate::gzip::Decoder::new(gz_rdr)?;
858        let csv_rdr = csv::Reader::from_reader(raw_csv_rdr);
859        let csv_rdr2 = csv_rdr.into_deserialize();
860
861        let mut count = 0;
862        for (i, row) in csv_rdr2.into_iter().enumerate() {
863            if i % 10_000_000 == 0 {
864                println!(
865                    "reading {}/{}: {}%",
866                    i,
867                    num_rows,
868                    i as f64 / num_rows as f64 * 100.0
869                );
870            }
871            let actual: braid_types::Data2dDistortedRow = row?;
872            let mut expected_rows = make_frame_data(i as u64).into_save(save_empty_data2d);
873            assert_eq!(expected_rows.len(), 1);
874            let expected = expected_rows.pop().unwrap();
875            let actual: braid_types::Data2dDistortedRowF32 = actual.into();
876            assert_eq!(actual.frame, expected.frame);
877            count += 1;
878        }
879
880        assert_eq!(count, num_rows);
881
882        Ok(())
883    }
884}