Skip to main content

stt_optimize/
packed.rs

1//! Packed-tileset reading for post-build analysis (`stt-optimize inspect`/`diff`).
2//!
3//! Thin wrapper over [`stt_core::pack::PackedReader`] that also keeps the
4//! parsed [`Manifest`] around, so the profiler passes can see container facts
5//! the reader alone doesn't expose (pack table, directory layout). Both
6//! directory container shapes — single whole-load and paged — decode through
7//! the same stt-core path ([`PackedReader::open`] branches on
8//! `directory.layout`), so callers here never need to care which one a
9//! dataset ships.
10
11use anyhow::{Context, Result};
12use std::fs;
13use std::path::{Path, PathBuf};
14use stt_core::TileEntry;
15use stt_core::arrow_tile::DecodedLayer;
16use stt_core::metadata::Metadata;
17use stt_core::pack::{Manifest, PackedReader};
18use stt_core::types::TimeRange;
19
20/// A local packed STT dataset (`manifest.json` + `index/*.sttd` + `packs/*.sttp`)
21/// opened for analysis.
22///
23/// Directory entries are cheap (decoded once at open); payload reads go
24/// through the pack mmaps on demand, so directory-only passes never touch
25/// tile bytes.
26pub struct PackedTileset {
27    reader: PackedReader,
28    manifest: Manifest,
29    manifest_path: PathBuf,
30}
31
32impl PackedTileset {
33    /// Open a packed dataset. `path` may be either the dataset directory
34    /// (containing `manifest.json`) or the `manifest.json` file itself.
35    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
36        let path = path.as_ref();
37        let manifest_path = if path.is_dir() {
38            path.join("manifest.json")
39        } else {
40            path.to_path_buf()
41        };
42        let manifest = Manifest::from_json_bytes(
43            &fs::read(&manifest_path)
44                .with_context(|| format!("reading {}", manifest_path.display()))?,
45        )?;
46        let reader = PackedReader::open(&manifest_path)
47            .with_context(|| format!("opening packed dataset {}", manifest_path.display()))?;
48        Ok(Self {
49            reader,
50            manifest,
51            manifest_path,
52        })
53    }
54
55    /// Path of the `manifest.json` this tileset was opened from.
56    pub fn manifest_path(&self) -> &Path {
57        &self.manifest_path
58    }
59
60    /// The parsed packed manifest (pack table, directory pointer, metadata).
61    pub fn manifest(&self) -> &Manifest {
62        &self.manifest
63    }
64
65    /// Dataset metadata (the manifest's folded `Metadata`).
66    pub fn metadata(&self) -> &Metadata {
67        self.reader.metadata()
68    }
69
70    /// Dataset name from the metadata.
71    pub fn name(&self) -> &str {
72        &self.reader.metadata().name
73    }
74
75    /// `(min_zoom, max_zoom)` from the metadata.
76    pub fn zoom_range(&self) -> (u8, u8) {
77        let m = self.reader.metadata();
78        (m.min_zoom, m.max_zoom)
79    }
80
81    /// Time range (Unix ms) from the metadata.
82    pub fn time_range(&self) -> TimeRange {
83        self.reader.metadata().time_range
84    }
85
86    /// Total directory entry count (derived from the directory at write time).
87    pub fn tile_count(&self) -> u64 {
88        self.reader.metadata().tile_count
89    }
90
91    /// Total feature records summed across tiles (a feature landing in N
92    /// tiles counts N times — the index-weighted total).
93    pub fn feature_count(&self) -> u64 {
94        self.reader.metadata().feature_count
95    }
96
97    /// Base temporal bucket size in milliseconds.
98    pub fn temporal_bucket_ms(&self) -> u64 {
99        self.reader.metadata().temporal_bucket_ms
100    }
101
102    /// Number of pack objects in the manifest's pack table.
103    pub fn pack_count(&self) -> usize {
104        self.manifest.packs.len()
105    }
106
107    /// Whether the directory ships in the paged container (root + leaf pages)
108    /// rather than the single whole-load object. Reading is identical either
109    /// way; this only matters for reporting.
110    pub fn is_paged(&self) -> bool {
111        self.manifest.directory.is_paged()
112    }
113
114    /// All directory entries, sorted by (zoom, hilbert, time_start).
115    ///
116    /// Each [`TileEntry`] carries the tile address (`zoom`, `x`, `y`,
117    /// `time_start`/`time_end`, `temporal_bucket_ms`) and the blob location
118    /// (`pack_id`, `offset`, `length`, `crc32c`). Byte-identical tiles were
119    /// deduped at write time, so `(pack_id, offset)` identifies the physical
120    /// blob: entries sharing that pair share one blob.
121    pub fn entries(&self) -> &[TileEntry] {
122        self.reader.entries()
123    }
124
125    /// Read and decompress one tile's raw payload bytes (CRC-verified).
126    pub fn read_payload(&self, entry: &TileEntry) -> Result<Vec<u8>> {
127        Ok(self.reader.read_payload(entry)?)
128    }
129
130    /// Read one tile and decode it into its Arrow layers (each a named
131    /// [`RecordBatch`](arrow::array::RecordBatch)).
132    pub fn read_layers(&self, entry: &TileEntry) -> Result<Vec<DecodedLayer>> {
133        Ok(self.reader.read_layers(entry)?)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use stt_core::arrow_tile::{encode_tile, ColumnarLayer, GeometryColumn};
141    use stt_core::curve::BlobOrdering;
142    use stt_core::pack::PackWriter;
143    use stt_core::tile::TileId;
144
145    fn tiny_tile(seed: u64) -> Vec<u8> {
146        let n = 4usize;
147        encode_tile(&[ColumnarLayer {
148            name: "default".to_string(),
149            feature_ids: (0..n as u64).map(|i| seed * 100 + i).collect(),
150            start_times: vec![0; n],
151            end_times: vec![100; n],
152            geometry: GeometryColumn::Point(vec![[-73.6, 45.5]; n]),
153            vertex_times: None,
154            vertex_values: None,
155            triangles: None,
156            vertex_value_matrix: None,
157            properties: vec![],
158        }])
159        .unwrap()
160    }
161
162    /// `open` accepts both the dataset dir and the manifest path, and the
163    /// wrapper's accessors agree with the written dataset.
164    #[test]
165    fn open_by_dir_or_manifest_and_read_back() {
166        let dir = tempfile::tempdir().unwrap();
167        let out = dir.path().join("dataset");
168        let mut w = PackWriter::create(&out, BlobOrdering::Auto, 64 * 1024).unwrap();
169        let payloads: Vec<Vec<u8>> = (0..3).map(tiny_tile).collect();
170        for (k, p) in payloads.iter().enumerate() {
171            let id = TileId::new(7, k as u32, 0, 0);
172            w.add_tile_full(&id, 0, 3_599_999, Some(0), 4, Some(3_600_000), p)
173                .unwrap();
174        }
175        let meta =
176            stt_core::metadata::Metadata::new("packed-open").with_temporal_bucket_ms(3_600_000);
177        w.finalize(&meta).unwrap();
178
179        let by_dir = PackedTileset::open(&out).unwrap();
180        let by_manifest = PackedTileset::open(out.join("manifest.json")).unwrap();
181        for ts in [&by_dir, &by_manifest] {
182            assert_eq!(ts.name(), "packed-open");
183            assert_eq!(ts.tile_count(), 3);
184            assert_eq!(ts.feature_count(), 12);
185            assert_eq!(ts.temporal_bucket_ms(), 3_600_000);
186            assert_eq!(ts.entries().len(), 3);
187            assert!(!ts.is_paged());
188            assert!(ts.pack_count() >= 1);
189        }
190
191        // Payload + Arrow decode round-trip through the wrapper.
192        let e = &by_dir.entries()[0];
193        let payload = by_dir.read_payload(e).unwrap();
194        assert!(payloads.contains(&payload));
195        let layers = by_dir.read_layers(e).unwrap();
196        assert_eq!(layers.len(), 1);
197        assert_eq!(layers[0].name, "default");
198        assert_eq!(layers[0].batch.num_rows(), 4);
199    }
200
201    /// A paged-directory dataset opens and reads identically (the stt-core
202    /// reader subsumes both container layouts).
203    #[test]
204    fn paged_layout_reads_like_single() {
205        let dir = tempfile::tempdir().unwrap();
206        let out = dir.path().join("paged");
207        let mut w = PackWriter::create(&out, BlobOrdering::Auto, 64 * 1024)
208            .unwrap()
209            .with_paging(Some(2));
210        for k in 0..6u64 {
211            let p = tiny_tile(k);
212            let id = TileId::new(9, k as u32, 0, 0);
213            w.add_tile_full(&id, 0, 100, None, 4, None, &p).unwrap();
214        }
215        w.finalize(&stt_core::metadata::Metadata::new("paged-open"))
216            .unwrap();
217
218        let ts = PackedTileset::open(&out).unwrap();
219        assert!(ts.is_paged());
220        assert_eq!(ts.entries().len(), 6);
221        for e in ts.entries() {
222            assert_eq!(ts.read_layers(e).unwrap()[0].batch.num_rows(), 4);
223        }
224    }
225}