Skip to content

Commit 5a7a6a5

Browse files
feat(rust): per-layer byte stats in mlt-ffi
The parsers record each column's byte span, exposed as `Layer::bytes()`. mlt-ffi adds `MltTileStats`, and the Java wrapper adds `MltLayerStats.of` and `MltEncoder.lastLayerSizes()`, so Planetiler can drop `org.maplibre:mlt`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 06a3872 commit 5a7a6a5

32 files changed

Lines changed: 2302 additions & 24 deletions

‎rust/mlt-core/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ required-features = ["__private"]
106106
name = "geojson02"
107107
required-features = ["__private-v2"]
108108

109+
[[test]]
110+
name = "layer_bytes"
111+
required-features = ["unstable-v2"]
112+
109113
[[test]]
110114
name = "mvt_round_trip"
111115
required-features = ["__private"]

‎rust/mlt-core/src/decoder/layer.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::codecs::varint::parse_varint;
22
#[cfg(feature = "unstable-v2")]
33
use crate::decoder::root02::parse_layer02;
4-
use crate::decoder::{ColumnDecl, ColumnStorage, Layer01, ParsedLayer01, Unknown};
4+
use crate::decoder::{ColumnDecl, ColumnStorage, Layer01, LayerBytes, ParsedLayer01, Unknown};
55
#[cfg(feature = "unstable-v2")]
66
use crate::decoder::{Layer02, ParsedLayer02};
77
use crate::utils::{parse_u8, take};
@@ -22,6 +22,17 @@ impl<'a, S: DecodeState> Layer<'a, S> {
2222
Self::Unknown(_) => None,
2323
}
2424
}
25+
26+
/// The bytes the layer spent on the wire, or `None` for a tag this build does not know.
27+
#[must_use]
28+
pub fn bytes(&self) -> Option<LayerBytes> {
29+
match self {
30+
Self::Tag01(l) => Some(l.bytes()),
31+
#[cfg(feature = "unstable-v2")]
32+
Self::Tag02(l) => Some(l.layer().bytes()),
33+
Self::Unknown(_) => None,
34+
}
35+
}
2536
}
2637

2738
impl<'a> Layer<'a> {
@@ -107,6 +118,7 @@ impl<'a> Layer01<'a, Lazy> {
107118
.into_iter()
108119
.map(|p| p.into_parsed(dec))
109120
.collect::<MltResult<Vec<_>>>()?,
121+
bytes: self.bytes,
110122
#[cfg(fuzzing)]
111123
layer_order: self.layer_order,
112124
})

‎rust/mlt-core/src/decoder/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub use iterators::{
3939
PropValueRef,
4040
};
4141
pub use limits::{Decoder, Parser};
42-
pub use model::{Layer, ParsedLayer, Unknown};
42+
pub use model::{Layer, LayerBytes, ParsedLayer, Unknown};
4343
pub(crate) use model01::{BASE_TYPE_MASK, Column, OPTIONAL_FLAG};
4444
pub use model01::{ColumnType, Layer01, ParsedLayer01};
4545
#[cfg(feature = "unstable-v2")]

‎rust/mlt-core/src/decoder/model.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,58 @@ pub enum Layer<'a, S: DecodeState = Lazy> {
3030
}
3131
pub type ParsedLayer<'a> = Layer<'a, Parsed>;
3232

33+
/// The bytes a layer spends on the wire, by what they hold.
34+
///
35+
/// Every byte the columns leave over is layer metadata: the tag, name, header and column schema.
36+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37+
pub struct LayerBytes {
38+
pub(crate) size: u32,
39+
pub(crate) geometry: u32,
40+
pub(crate) properties: u32,
41+
pub(crate) ids: u32,
42+
}
43+
44+
impl LayerBytes {
45+
/// Start counting a layer whose body is `body_len` bytes.
46+
pub(crate) fn of_body(body_len: usize) -> Self {
47+
Self {
48+
// The body never exceeds the u32 size varint that framed it.
49+
size: u32::try_from(body_len).map_or(u32::MAX, |len| len.saturating_add(1)),
50+
..Self::default()
51+
}
52+
}
53+
54+
/// The value of the layer's size varint: the tag and the body, without the varint itself.
55+
#[must_use]
56+
pub fn size(&self) -> u32 {
57+
self.size
58+
}
59+
60+
/// The geometry column, or the v2 geometry section.
61+
#[must_use]
62+
pub fn geometry(&self) -> u32 {
63+
self.geometry
64+
}
65+
66+
/// Every column that is neither geometry nor the id, nested and m-value columns included.
67+
#[must_use]
68+
pub fn properties(&self) -> u32 {
69+
self.properties
70+
}
71+
72+
/// The id column.
73+
#[must_use]
74+
pub fn ids(&self) -> u32 {
75+
self.ids
76+
}
77+
78+
/// What is left of [`Self::size`] once every column is taken out.
79+
#[must_use]
80+
pub fn metadata(&self) -> u32 {
81+
self.size - self.geometry - self.properties - self.ids
82+
}
83+
}
84+
3385
/// Unknown layer data, stored as encoded bytes.
3486
///
3587
/// Returned inside [`Layer::Unknown`] for any layer tag that is not recognized

‎rust/mlt-core/src/decoder/model01.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::io::Write;
99
use num_enum::TryFromPrimitive;
1010

1111
use crate::MltError::ParsingColumnType;
12-
use crate::decoder::{Geometry, GeometryValues, Id, ParsedProperty, Property};
12+
use crate::decoder::{Geometry, GeometryValues, Id, LayerBytes, ParsedProperty, Property};
1313
use crate::tile::Extent;
1414
use crate::utils::{BinarySerializer as _, parse_string, parse_u8};
1515
use crate::{DecodeState, Lazy, MltRefResult, Parsed, Parser};
@@ -41,6 +41,7 @@ pub struct Layer01<'a, S: DecodeState = Lazy> {
4141
pub(crate) id: Option<Id<'a, S>>,
4242
pub(crate) geometry: Geometry<'a, S>,
4343
pub(crate) properties: Vec<Property<'a, S>>,
44+
pub(crate) bytes: LayerBytes,
4445
#[cfg(fuzzing)]
4546
pub(crate) layer_order: Vec<crate::decoder::fuzzing::LayerOrdering>,
4647
}
@@ -57,6 +58,12 @@ impl<'a, S: DecodeState> Layer01<'a, S> {
5758
pub fn extent(&self) -> Extent {
5859
self.extent
5960
}
61+
62+
/// The bytes the layer spent on the wire, by what they hold.
63+
#[must_use]
64+
pub fn bytes(&self) -> LayerBytes {
65+
self.bytes
66+
}
6067
}
6168

6269
impl ParsedLayer01<'_> {
@@ -168,6 +175,15 @@ impl ColumnType {
168175
)
169176
}
170177

178+
/// Check if the column holds the feature ids.
179+
#[must_use]
180+
pub(crate) fn is_id(self) -> bool {
181+
matches!(
182+
self,
183+
Self::Id | Self::OptId | Self::LongId | Self::OptLongId
184+
)
185+
}
186+
171187
/// Check if the column type has a presence stream
172188
#[must_use]
173189
pub(crate) fn is_optional(self) -> bool {

‎rust/mlt-core/src/decoder/root01.rs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::codecs::varint::parse_varint;
1212
use crate::decoder::stream::header01;
1313
use crate::decoder::{
1414
Column, ColumnType, DictLayout, DictionaryType, GeoTypes, Geometry, Id, IndexBase, Layer01,
15-
RawFloats, RawFsstData, RawGeometry, RawId, RawIdValue, RawPlainData, RawPresence, RawProperty,
16-
RawScalar, RawSharedDict, RawSharedDictEncoding, RawSharedDictItem, RawStrings,
15+
LayerBytes, RawFloats, RawFsstData, RawGeometry, RawId, RawIdValue, RawPlainData, RawPresence,
16+
RawProperty, RawScalar, RawSharedDict, RawSharedDictEncoding, RawSharedDictItem, RawStrings,
1717
RawStringsEncoding, StreamType, ValueKind,
1818
};
1919
use crate::errors::AsMltError as _;
@@ -24,6 +24,7 @@ use crate::{Lazy, MltRefResult, MltResult, Parser};
2424
impl<'a> Layer01<'a, Lazy> {
2525
/// Parse `v01::Layer` metadata, reserving decoded memory against the parser's budget.
2626
pub(crate) fn from_bytes(input: &'a [u8], parser: &mut Parser) -> MltResult<Self> {
27+
let mut bytes = LayerBytes::of_body(input.len());
2728
let (input, layer_name) = parse_string(input)?;
2829
if layer_name.is_empty() {
2930
return Err(MissingLayerName);
@@ -57,6 +58,7 @@ impl<'a> Layer01<'a, Lazy> {
5758
let presence;
5859
let value;
5960
let name = column.name.unwrap_or("");
61+
let before = input.len();
6062

6163
match column.typ {
6264
ColumnType::Id | ColumnType::OptId => {
@@ -124,6 +126,14 @@ impl<'a> Layer01<'a, Lazy> {
124126
properties.push(Raw(prop));
125127
}
126128
}
129+
let spent = u32::try_from(before - input.len())?;
130+
if column.typ == ColumnType::Geometry {
131+
bytes.geometry += spent;
132+
} else if column.typ.is_id() {
133+
bytes.ids += spent;
134+
} else {
135+
bytes.properties += spent;
136+
}
127137
}
128138
if input.is_empty() {
129139
Ok(Layer01 {
@@ -132,6 +142,7 @@ impl<'a> Layer01<'a, Lazy> {
132142
id: id_column,
133143
geometry: geometry.ok_or(MissingGeometry)?,
134144
properties,
145+
bytes,
135146
#[cfg(fuzzing)]
136147
layer_order,
137148
})

‎rust/mlt-core/src/decoder/root02.rs‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ use crate::decoder::stream::header02::{
5959
use crate::decoder::{
6060
Column02, ColumnCounts, ColumnKind02, ColumnType02, DataType02, Decoder, DictLayout,
6161
DictionaryType, FloatLogical, GeoLayout, GeoTypes, Id, IdWidth02, IndexBase, Layer01, Layer02,
62-
LayerHeader02, LayerLayout, LengthType, LogicalEncoding, MValues, Nested, Presence02,
63-
RawFloats, RawFloatsEncoding, RawFsstData, RawGeometry, RawId, RawIdValue, RawMValue,
64-
RawPlainData, RawPresence, RawProperty, RawScalar, RawSharedDict, RawSharedDictEncoding,
65-
RawSharedDictItem, RawStream, RawStrings, RawStringsEncoding, SharedDictKind, ValueType02,
66-
ValuesColumn02,
62+
LayerBytes, LayerHeader02, LayerLayout, LengthType, LogicalEncoding, MValues, Nested,
63+
Presence02, RawFloats, RawFloatsEncoding, RawFsstData, RawGeometry, RawId, RawIdValue,
64+
RawMValue, RawPlainData, RawPresence, RawProperty, RawScalar, RawSharedDict,
65+
RawSharedDictEncoding, RawSharedDictItem, RawStream, RawStrings, RawStringsEncoding,
66+
SharedDictKind, ValueType02, ValuesColumn02,
6767
};
6868
use crate::tile::{ColumnRole, Extent, reject_taken_name};
6969
use crate::utils::{SetOptionOnce as _, parse_string, parse_u8};
@@ -74,6 +74,7 @@ pub(crate) fn parse_layer02<'a>(
7474
input: &'a [u8],
7575
parser: &mut Parser,
7676
) -> MltResult<Layer02<'a, Lazy>> {
77+
let mut bytes = LayerBytes::of_body(input.len());
7778
let (input, layer_name) = parse_string(input)?;
7879
if layer_name.is_empty() {
7980
return Err(MissingLayerName);
@@ -93,7 +94,9 @@ pub(crate) fn parse_layer02<'a>(
9394
let (input, cols) = parse_shared_presence(input, layout, feature_count, parser)?;
9495

9596
// ── Geometry section ──────────────────────────────────────────────────
96-
let (input, geometry) = parse_geometry(input, header, layout.geometry, feature_count, parser)?;
97+
let (rest, geometry) = parse_geometry(input, header, layout.geometry, feature_count, parser)?;
98+
bytes.geometry = u32::try_from(input.len() - rest.len())?;
99+
let input = rest;
97100

98101
// ── Counted columns ───────────────────────────────────────────────────
99102
let (mut input, counts) = ColumnCounts::parse(input, header.m_values)?;
@@ -112,6 +115,7 @@ pub(crate) fn parse_layer02<'a>(
112115
let mut layer_order = vec![crate::decoder::fuzzing::LayerOrdering::Geometry];
113116

114117
for _ in 0..column_count {
118+
let before = input.len();
115119
let typ_byte;
116120
(input, typ_byte) = parse_u8(input)?;
117121
let typ = match cols.column(typ_byte)? {
@@ -124,6 +128,7 @@ pub(crate) fn parse_layer02<'a>(
124128
column_names.push((Cow::Owned(name), ColumnRole::Property));
125129
}
126130
properties.push(Raw(RawProperty::SharedDict(shared_dict)));
131+
bytes.properties += u32::try_from(before - input.len())?;
127132
#[cfg(fuzzing)]
128133
layer_order.push(crate::decoder::fuzzing::LayerOrdering::Property);
129134
continue;
@@ -148,6 +153,7 @@ pub(crate) fn parse_layer02<'a>(
148153
IdWidth02::Id64 => RawIdValue::Id64(data),
149154
};
150155
id_column.set_once(Raw(RawId { presence, value }))?;
156+
bytes.ids += u32::try_from(before - input.len())?;
151157
}
152158
ColumnKind02::Values(column) => {
153159
#[cfg(fuzzing)]
@@ -164,6 +170,7 @@ pub(crate) fn parse_layer02<'a>(
164170
parser,
165171
)?;
166172
properties.push(Raw(values.into()));
173+
bytes.properties += u32::try_from(before - input.len())?;
167174
}
168175
ColumnKind02::Nested(column) => {
169176
#[cfg(fuzzing)]
@@ -174,17 +181,20 @@ pub(crate) fn parse_layer02<'a>(
174181
(input, tree) =
175182
parse_nested(input, name, presence, column.root, data_count, parser)?;
176183
nested.push(Raw(tree));
184+
bytes.properties += u32::try_from(before - input.len())?;
177185
}
178186
}
179187
}
180188

181189
// ── M-value section ───────────────────────────────────────────────────
190+
let before = input.len();
182191
let m_values;
183192
(input, m_values) = if header.m_values {
184193
parse_m_values(input, counts.m_values, &cols, &mut column_names, parser)?
185194
} else {
186195
(input, Vec::new())
187196
};
197+
bytes.properties += u32::try_from(before - input.len())?;
188198

189199
if !input.is_empty() {
190200
return Err(TrailingLayerData(input.len()));
@@ -196,6 +206,7 @@ pub(crate) fn parse_layer02<'a>(
196206
id: id_column,
197207
geometry: Raw(geometry),
198208
properties,
209+
bytes,
199210
#[cfg(fuzzing)]
200211
layer_order,
201212
},

‎rust/mlt-core/src/lib.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ pub mod wire {
8484
LengthType, LogicalEncoding, LogicalTechnique, Morton, OffsetType, PhysicalEncoding,
8585
RleLayout, RleMeta, StreamMeta, StreamType, ValueKind, VertexLogical,
8686
};
87-
pub use crate::decoder::{ColumnDecl, ColumnStorage, ColumnType, DictLayout, StringLayout};
87+
pub use crate::decoder::{
88+
ColumnDecl, ColumnStorage, ColumnType, DictLayout, LayerBytes, StringLayout,
89+
};
8890
#[cfg(feature = "unstable-v2")]
8991
pub use crate::decoder::{GeoLayout, LayerLayout};
9092
pub use crate::utils::analyze::{Analyze, StatType};

‎rust/mlt-core/tests/layer_bytes.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use std::fs;
2+
use std::path::Path;
3+
4+
use mlt_core::Parser;
5+
use mlt_core::dump::{Region, annotate_tile};
6+
use test_each_file::test_each_path;
7+
8+
test_each_path! { for ["mlt"] in "../test/synthetic/0x01" as bytes_0x01 => matches_the_inspector }
9+
test_each_path! { for ["mlt"] in "../test/synthetic/0x01-rust" as bytes_0x01_rust => matches_the_inspector }
10+
test_each_path! { for ["mlt"] in "../test/synthetic/0x02" as bytes_0x02 => matches_the_inspector }
11+
12+
fn matches_the_inspector([path]: [&Path; 1]) {
13+
let buffer = fs::read(path).unwrap();
14+
let Ok(layers) = Parser::default().parse_layers(&buffer) else {
15+
return;
16+
};
17+
let parsed: Vec<_> = layers
18+
.iter()
19+
.filter_map(mlt_core::Layer::bytes)
20+
.map(|b| [b.size(), b.geometry(), b.properties(), b.ids()].map(|n| n as usize))
21+
.collect();
22+
let (tree, err) = annotate_tile(&buffer);
23+
assert!(err.is_none(), "{}: {err:?}", path.display());
24+
25+
assert_eq!(parsed, inspector_bytes(&tree.regions), "{}", path.display());
26+
}
27+
28+
/// The classification `inspector/src/tileStats.ts` applies to the annotated regions.
29+
fn inspector_bytes(regions: &[Region]) -> Vec<[usize; 4]> {
30+
let mut layers = Vec::new();
31+
for (at, layer) in regions.iter().enumerate() {
32+
if layer.depth != 0 || !layer.label.starts_with("layer[") {
33+
continue;
34+
}
35+
let kids = regions[at + 1..]
36+
.iter()
37+
.take_while(|r| r.depth > 0)
38+
.filter(|r| r.depth == 1);
39+
let mut size = layer.len;
40+
let (mut geometry, mut properties, mut ids) = (0, 0, 0);
41+
for kid in kids {
42+
if kid.label == "size" {
43+
size -= kid.len;
44+
}
45+
if !kid.container {
46+
continue;
47+
}
48+
let typ = kid
49+
.label
50+
.strip_prefix("column[")
51+
.or_else(|| kid.label.strip_prefix("m_value["))
52+
.and_then(|rest| rest.split(' ').nth(1));
53+
match (kid.label.as_str(), typ) {
54+
("geometry", _) | (_, Some("Geometry")) => geometry += kid.len,
55+
(_, Some("Id" | "OptId" | "LongId" | "OptLongId")) => ids += kid.len,
56+
(_, Some(_)) => properties += kid.len,
57+
_ => {}
58+
}
59+
}
60+
layers.push([size, geometry, properties, ids]);
61+
}
62+
layers
63+
}

‎rust/mlt-ffi/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
| `MltLayerBuilder` | Writes a layer one feature at a time from MVT geometry commands and encodes it into an `MltBuffer` |
1111
| `MltEncoderOptions` | Builder wrapping `EncoderConfig` - construct with `new()`, toggle flags with setters |
1212
| `MltBuffer` | Owned byte buffer with `.bytes` / `.len` accessors, `clear()`, and appending encodes |
13+
| `MltTileStats` | `from_bytes(bytes)` parses a tile without decoding it, then reports each layer's name and bytes |
14+
| `MltLayerBytes` | One layer's size, split into geometry, property, id and metadata bytes |
1315
| `ConvertError` | Opaque error with `.kind` (`ConvertErrorKind`) and `.message` accessors |
1416
| `MltWireVersion` | `V01` (default) or the experimental `V02` |
1517
| `MltMvtGeometryType` | The MVT geometry type of one `MltLayerBuilder` feature |

0 commit comments

Comments
 (0)