From 02b41ddc33edd0f90a63bdc6928f8e0877b93f84 Mon Sep 17 00:00:00 2001 From: Wolren Date: Mon, 26 Oct 2026 13:27:56 +0100 Subject: [PATCH 1/2] perf(data): read WKB coordinate runs in bulk, size the encoder buffer The coordinate loop checked the buffer bound and advanced the cursor once per value. Reader::coords now bounds-checks the whole run once and reads fixed offsets inside it, so a 2D coordinate costs two loads instead of a cursor move per field. Z and M values are still consumed through the stride; only X and Y are stored. A multi-geometry or collection with a single member is returned directly instead of being regrouped into a fresh geometry. A one-point multipoint still collapses to a point, as before. encode now sizes its buffer with a private wkb_size: 16 bytes per coordinate, 4 per count and 5 per geometry head. The round-trip test covers a two-part line, a polygon with a hole and a two-member multipolygon. --- crates/data/src/wkb.rs | 50 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/crates/data/src/wkb.rs b/crates/data/src/wkb.rs index 7a581a829..d2d007074 100644 --- a/crates/data/src/wkb.rs +++ b/crates/data/src/wkb.rs @@ -18,7 +18,7 @@ pub fn decode(bytes: &[u8]) -> Option { /// Encodes a geometry as ISO WKB (2D, little-endian). pub fn encode(g: &Geometry) -> Vec { - let mut out = Vec::new(); + let mut out = Vec::with_capacity(wkb_size(g)); let head = |out: &mut Vec, ty: u32| { out.push(1); out.extend_from_slice(&ty.to_le_bytes()); @@ -66,6 +66,24 @@ pub fn encode(g: &Geometry) -> Vec { out } +/// Encoded width: 16 bytes per coordinate, 4 per count and 5 per geometry head. +fn wkb_size(g: &Geometry) -> usize { + let ring = |r: &[Coord]| 4 + 16 * r.len(); + match g { + Geometry::Point(_) => 5 + 16, + Geometry::MultiPoint(v) => 5 + 4 + v.len() * (5 + 16), + Geometry::Line(parts) => 5 + 4 + parts.iter().map(|p| 5 + ring(p)).sum::(), + Geometry::Polygon(polys) => 5 + 4 + polys.iter().map(|p| 5 + 4 + p.iter().map(|r| ring(r)).sum::()).sum::(), + } +} + +/// One field of a coordinate chunk; `None` when the chunk is too short. +#[inline] +fn f64_at(ch: &[u8], off: usize, little: bool) -> Option { + let a: [u8; 8] = ch.get(off..off + 8)?.try_into().ok()?; + Some(if little { f64::from_le_bytes(a) } else { f64::from_be_bytes(a) }) +} + struct Reader<'a> { b: &'a [u8], pos: usize, @@ -141,11 +159,16 @@ impl Reader<'_> { /// A coordinate sequence, and whether every coordinate is finite. `None` when malformed. fn coords(&mut self, h: Head) -> Option<(Vec, bool)> { - let n = self.count(h, h.dims * 8)?; + let stride = h.dims * 8; + let n = self.count(h, stride)?; + let end = self.pos + n * stride; + let run = self.b.get(self.pos..end)?; + self.pos = end; let mut v = Vec::with_capacity(n); let mut finite = true; - for _ in 0..n { - let c = self.coord(h)?; + // The run is bounds-checked once; each chunk is exactly one coordinate. + for ch in run.chunks_exact(stride) { + let c = Coord::new(f64_at(ch, 0, h.little)?, f64_at(ch, 8, h.little)?); finite &= c.is_finite(); v.push(c); } @@ -209,7 +232,14 @@ impl Reader<'_> { /// Merges the members of a multi-geometry or collection into one geometry of the first member's /// kind; members of other kinds are dropped. -fn merge(parts: Vec) -> Option { +fn merge(mut parts: Vec) -> Option { + if parts.len() == 1 { + // A lone member needs no regrouping; a one-point multipoint still collapses. + return match parts.pop()? { + Geometry::MultiPoint(v) if v.len() == 1 => v.first().copied().map(Geometry::Point), + g => Some(g), + }; + } let first = parts.first()?.kind(); let mut points = Vec::new(); let mut lines = Vec::new(); @@ -246,8 +276,18 @@ mod tests { Geometry::Point(Coord::new(1.5, -2.0)), Geometry::MultiPoint(vec![Coord::new(1.0, 2.0), Coord::new(3.0, 4.0)]), Geometry::Line(vec![vec![Coord::new(0.0, 0.0), Coord::new(1.0, 1.0)]]), + Geometry::Line(vec![vec![Coord::new(0.0, 0.0), Coord::new(1.0, 1.0)], vec![Coord::new(2.0, 2.0), Coord::new(3.0, 3.0)]]), square(), + Geometry::Polygon(vec![vec![ + vec![Coord::new(0.0, 0.0), Coord::new(9.0, 0.0), Coord::new(9.0, 9.0), Coord::new(0.0, 0.0)], + vec![Coord::new(1.0, 1.0), Coord::new(2.0, 1.0), Coord::new(2.0, 2.0), Coord::new(1.0, 1.0)], + ]]), + Geometry::Polygon(vec![ + vec![vec![Coord::new(0.0, 0.0), Coord::new(1.0, 0.0), Coord::new(1.0, 1.0), Coord::new(0.0, 0.0)]], + vec![vec![Coord::new(5.0, 5.0), Coord::new(6.0, 5.0), Coord::new(6.0, 6.0), Coord::new(5.0, 5.0)]], + ]), ] { + assert_eq!(encode(&g).len(), wkb_size(&g)); assert_eq!(decode(&encode(&g)), Some(g)); } } From 9b8ee7d9b6dd3f550f84bf275c7142b6febc3012 Mon Sep 17 00:00:00 2001 From: Wolren Date: Mon, 26 Oct 2026 13:27:57 +0100 Subject: [PATCH 2/2] perf(data): write WKT into one buffer, drop the per-coordinate Vec The writer built a String per coordinate and per ring, then joined them with collect. It appends into a single buffer with fmt::Write instead; the output is byte-identical on a 1.58M-polygon corpus. WktParser::coord kept each coordinate's numbers in a Vec; it now uses a fixed 4-element array, and the geometry type word is no longer copied into a second String. Accepted inputs and error messages are unchanged. --- crates/data/src/interop.rs | 104 ++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 19 deletions(-) diff --git a/crates/data/src/interop.rs b/crates/data/src/interop.rs index 1599f7ae1..8ffbd29ce 100644 --- a/crates/data/src/interop.rs +++ b/crates/data/src/interop.rs @@ -1,5 +1,7 @@ //! Writers for exchange formats: KML, GPX and CSV with WKT geometry. +use std::fmt::Write as _; + use rustgis_geom::Coord; use crate::{Dataset, Geometry}; @@ -116,25 +118,87 @@ pub fn to_gpx(ds: &Dataset, name_field: Option) -> String { /// Well-known text for a geometry. pub fn wkt(g: &Geometry) -> String { - let c = |p: &Coord| format!("{} {}", p.x, p.y); - let list = |v: &[Coord]| v.iter().map(c).collect::>().join(", "); + fn coord(out: &mut String, p: &Coord) { + let _ = write!(out, "{} {}", p.x, p.y); + } + fn list(out: &mut String, v: &[Coord]) { + for (i, p) in v.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + coord(out, p); + } + } + fn rings(out: &mut String, rs: &[Vec]) { + for (i, r) in rs.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push('('); + list(out, r); + out.push(')'); + } + } + let mut out = String::new(); match g { - Geometry::Point(p) => format!("POINT ({})", c(p)), - Geometry::MultiPoint(v) => format!("MULTIPOINT ({})", v.iter().map(|p| format!("({})", c(p))).collect::>().join(", ")), - Geometry::Line(parts) if parts.len() == 1 => format!("LINESTRING ({})", parts.first().map(|p| list(p)).unwrap_or_default()), + Geometry::Point(p) => { + out.push_str("POINT ("); + coord(&mut out, p); + out.push(')'); + } + Geometry::MultiPoint(v) => { + out.push_str("MULTIPOINT ("); + for (i, p) in v.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push('('); + coord(&mut out, p); + out.push(')'); + } + out.push(')'); + } + Geometry::Line(parts) if parts.len() == 1 => { + out.push_str("LINESTRING ("); + if let Some(p) = parts.first() { + list(&mut out, p); + } + out.push(')'); + } Geometry::Line(parts) => { - format!("MULTILINESTRING ({})", parts.iter().map(|p| format!("({})", list(p))).collect::>().join(", ")) + out.push_str("MULTILINESTRING ("); + for (i, p) in parts.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push('('); + list(&mut out, p); + out.push(')'); + } + out.push(')'); } Geometry::Polygon(polys) => { - let poly = - |rings: &Vec>| format!("({})", rings.iter().map(|r| format!("({})", list(r))).collect::>().join(", ")); if polys.len() == 1 { - format!("POLYGON {}", polys.first().map(poly).unwrap_or_default()) + out.push_str("POLYGON ("); + if let Some(rs) = polys.first() { + rings(&mut out, rs); + } + out.push(')'); } else { - format!("MULTIPOLYGON ({})", polys.iter().map(poly).collect::>().join(", ")) + out.push_str("MULTIPOLYGON ("); + for (i, rs) in polys.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push('('); + rings(&mut out, rs); + out.push(')'); + } + out.push(')'); } } } + out } /// Decodes an encoded polyline (Google's algorithm; `precision` 5, or 6 as OSRM and Valhalla @@ -291,23 +355,26 @@ impl WktParser { /// `x y [z [m]]`. fn coord(&mut self) -> Result { - let mut v = Vec::with_capacity(4); + let mut nums = [0.0f64; 4]; + let mut len = 0usize; while let Some(WktTok::Num(n)) = self.peek() { // Two to four numbers: a fifth is already wrong, so stop there. - if v.len() == 4 { + if len == nums.len() { return Err("a coordinate needs two to four numbers".into()); } - v.push(*n); + nums[len] = *n; + len += 1; self.at += 1; } self.coords += 1; if self.coords > MAX_WKT_COORDS { return Err("too many coordinates".into()); } - match v.as_slice() { - [x, y, ..] if v.len() <= 4 && x.is_finite() && y.is_finite() => Ok(Coord::new(*x, *y)), - _ => Err("a coordinate needs two to four numbers".into()), + let (x, y) = (nums[0], nums[1]); + if len < 2 || !x.is_finite() || !y.is_finite() { + return Err("a coordinate needs two to four numbers".into()); } + Ok(Coord::new(x, y)) } /// `( a, b, … )`, each item read by `item`. @@ -351,12 +418,11 @@ impl WktParser { if matches!(self.peek(), Some(WktTok::Word(w)) if matches!(w.as_str(), "Z" | "M" | "ZM")) { self.at += 1; } - let kind = - kind.strip_suffix("ZM").or_else(|| kind.strip_suffix('Z')).or_else(|| kind.strip_suffix('M')).unwrap_or(&kind).to_string(); + let kind = kind.strip_suffix("ZM").or_else(|| kind.strip_suffix('Z')).or_else(|| kind.strip_suffix('M')).unwrap_or(&kind); if self.empty() { return Ok(None); } - Ok(Some(match kind.as_str() { + Ok(Some(match kind { "POINT" => { self.expect(WktTok::Open)?; let c = self.coord()?;