Skip to content

Commit 922959f

Browse files
committed
feat(models): Replace RouteId with ArrayString for improved performance and memory efficiency
1 parent 1a2e865 commit 922959f

5 files changed

Lines changed: 36 additions & 20 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ form_urlencoded = "1"
7676
# MIME resolution
7777
mime_guess = "2"
7878

79+
# Fixed-capacity, inline (stack) string for the 64-char route id: no heap
80+
# allocation, `Copy`, and a safe zero-cost `as_str()` — the id never touches the
81+
# allocator on the delivery hot path, not even as a moka cache key.
82+
arrayvec = "0.7"
83+
7984
# Zero-copy response bodies: Bytes::from_owner wraps Arc<str> without memcpy
8085
bytes = "1"
8186

‎src/api/handlers.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ pub async fn create_snippet(
202202
let inserted = state
203203
.repo
204204
.create_route(CreateRoute {
205-
id: id.clone(),
205+
id,
206206
hash: hash.clone(),
207207
content: &req.content,
208208
content_type: &content_type,

‎src/db/models.rs‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@ use crate::crypto::ID_LEN;
1818
/// so the Data Plane can address a specific stored version directly — but that
1919
/// is an internal delivery detail, never a separate user-facing kind of route.
2020
///
21-
/// Backed by an `Arc<str>` rather than a `String`. The Data Plane allocates the
22-
/// id exactly once (at [`parse`](Self::parse)); the cache must then own the key
23-
/// for longer than the request, and the loader closure needs its own copy. With
24-
/// shared ownership those follow-on copies — `clone` for the closure and moka's
25-
/// internal key clone on insert — are atomic refcount bumps, never fresh 64-byte
26-
/// heap allocations. Borrowing stays a plain, safe deref via [`as_str`].
21+
/// Backed by a fixed-capacity, inline [`ArrayString<ID_LEN>`] rather than a
22+
/// heap `String` — the type *is* "a validated string of exactly [`ID_LEN`]
23+
/// characters". This makes the id `Copy` and keeps it entirely off the
24+
/// allocator on the Data Plane hot path: `parse` writes the bytes into the
25+
/// stack buffer, the loader-closure copy and moka's internal key clone are
26+
/// 64-byte `memcpy`s (no refcount, no allocation), and the cache stores the key
27+
/// inline in its node instead of in a separate per-entry heap allocation.
28+
/// `ArrayString` preserves its UTF-8 invariant internally, so [`as_str`] stays
29+
/// a safe, zero-cost borrow with no `unsafe` and no re-validation.
2730
///
31+
/// [`ArrayString<ID_LEN>`]: arrayvec::ArrayString
2832
/// [`as_str`]: Self::as_str
29-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30-
pub struct RouteId(std::sync::Arc<str>);
33+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34+
pub struct RouteId(arrayvec::ArrayString<ID_LEN>);
3135

3236
/// Error returned when a candidate route id fails validation.
3337
#[derive(Debug, thiserror::Error)]
@@ -41,9 +45,10 @@ pub enum RouteIdError {
4145
impl RouteId {
4246
/// Parse and validate an untrusted id (e.g. from a request path).
4347
///
44-
/// The structural checks (length, then charset) run before the id is
45-
/// materialized, so malformed input is rejected without any allocation;
46-
/// only a well-formed id is interned into the single shared `Arc<str>`.
48+
/// The structural checks (length, then charset) run first, so malformed
49+
/// input is rejected up front; a well-formed id is then copied into the
50+
/// inline buffer without ever touching the heap. The length check
51+
/// guarantees the id fits the fixed [`ID_LEN`] capacity exactly.
4752
pub fn parse(raw: &str) -> Result<Self, RouteIdError> {
4853
if raw.len() != ID_LEN {
4954
return Err(RouteIdError::WrongLength(raw.len()));
@@ -54,7 +59,10 @@ impl RouteId {
5459
{
5560
return Err(RouteIdError::InvalidCharacter);
5661
}
57-
Ok(Self(std::sync::Arc::from(raw)))
62+
// Infallible: `raw.len() == ID_LEN` equals the buffer capacity.
63+
Ok(Self(
64+
arrayvec::ArrayString::from(raw).expect("len == ID_LEN"),
65+
))
5866
}
5967

6068
/// Adopt a freshly-minted, already-signed id from [`crypto::IdSigner`].
@@ -64,26 +72,26 @@ impl RouteId {
6472
#[must_use]
6573
pub fn from_signed(id: String) -> Self {
6674
debug_assert_eq!(id.len(), ID_LEN, "signer must emit {ID_LEN}-char ids");
67-
Self(std::sync::Arc::from(id))
75+
Self(arrayvec::ArrayString::from(&id).expect("signer emits ID_LEN-char ids"))
6876
}
6977

7078
/// Borrow the id as a string slice.
7179
#[must_use]
7280
pub fn as_str(&self) -> &str {
73-
&self.0
81+
self.0.as_str()
7482
}
7583

76-
/// Consume the newtype, yielding an owned string. Used only on cold Control
77-
/// Plane response paths, never on the Data Plane hot path.
84+
/// Yield an owned string. Used only on cold Control Plane response paths,
85+
/// never on the Data Plane hot path.
7886
#[must_use]
7987
pub fn into_inner(self) -> String {
80-
self.0.as_ref().to_owned()
88+
self.0.as_str().to_owned()
8189
}
8290
}
8391

8492
impl std::fmt::Display for RouteId {
8593
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86-
f.write_str(&self.0)
94+
f.write_str(self.0.as_str())
8795
}
8896
}
8997

‎src/delivery/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ async fn deliver(
136136
// coalesced into a single 1-RTT load. A present entry is always current
137137
// because the Control Plane invalidates on every write.
138138
let repo = state.repo.clone();
139-
let load_id = id.clone();
139+
// `RouteId` is `Copy`, so the loader closure captures its own inline copy
140+
// (a 64-byte stack memcpy) with no allocation or refcount.
141+
let load_id = id;
140142
let loaded = state
141143
.cache
142144
.get_or_load(&id, move || async move {

0 commit comments

Comments
 (0)