Merge pull request #631 from AtalayaLabs/claude/performance-optimization-analysis-iaog0z
Round 21: allocation micro-optimizations across CalDAV/CardDAV and dedup
This commit is contained in:
+15
@@ -354,6 +354,21 @@ name = "bench_micro_allocs"
|
|||||||
path = "examples/bench_micro_allocs.rs"
|
path = "examples/bench_micro_allocs.rs"
|
||||||
required-features = ["bench"]
|
required-features = ["bench"]
|
||||||
|
|
||||||
|
# Round-21 battery ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Round-21 CPU/alloc micro-pack — CalDAV/CardDAV row-mapper container pre-size
|
||||||
|
# (the ROUND20 §I1 sibling the calendar/contact repos deferred); dedup
|
||||||
|
# `settle_batch` hash bind Vec<String>→Vec<&str> borrow; `store_loose_chunks`
|
||||||
|
# intra-request dedup set keyed on the raw [u8;32] digest + move-on-dup (the
|
||||||
|
# ROUND17 §D2 pattern applied to the delta-upload sibling); CardDAV `getetag`
|
||||||
|
# borrowed pre-escaped quotes (the ROUND20 §C1 NextCloud pattern); `BDAY`
|
||||||
|
# stamp via fmt::compact_date stack render; NC trashbin folder content-type
|
||||||
|
# Cow::Borrowed. No Postgres.
|
||||||
|
[[example]]
|
||||||
|
name = "bench_round21_micro"
|
||||||
|
path = "examples/bench_round21_micro.rs"
|
||||||
|
required-features = ["bench"]
|
||||||
|
|
||||||
# Round-20 battery ────────────────────────────────────────────────────────────
|
# Round-20 battery ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Round-20 CPU/alloc micro-pack — CalendarEvent `prop_with_params` throwaway
|
# Round-20 CPU/alloc micro-pack — CalendarEvent `prop_with_params` throwaway
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Round 21 — CalDAV/CardDAV row-mapper pre-size, dedup hash-bind & digest-key dedup, CardDAV etag & BDAY emit, NC trashbin content-type
|
||||||
|
|
||||||
|
Benchmark-gated, same rule as ROUND2–20: every change ships with a BEFORE/AFTER
|
||||||
|
benchmark and a byte/-value equivalence gate; an AFTER that doesn't beat its
|
||||||
|
BEFORE is rolled back (never applied). The roll-back rule is encoded directly in
|
||||||
|
the harness — a `GATE FAIL … rollback` non-zero exit if an AFTER arm fails to
|
||||||
|
reduce allocations — so a regression fails CI rather than shipping.
|
||||||
|
|
||||||
|
This round drains the sibling seams the earlier passes explicitly deferred. The
|
||||||
|
file-listing repositories got their result-`Vec` pre-sizing in ROUND20 §I1, but
|
||||||
|
the **CalDAV/CardDAV row mappers** (bulk address-book / calendar sync builds
|
||||||
|
thousands of rows) were left growing from capacity 0. The **streaming ingest**
|
||||||
|
loop got its `[u8; 32]`-digest dedup key in ROUND17 §D2, but its **delta-upload
|
||||||
|
sibling** `store_loose_chunks` kept a `HashSet<String>` and a double hex clone
|
||||||
|
per frame. The **NextCloud** etag emitter got the borrowed-pre-escaped-quote
|
||||||
|
treatment in ROUND20 §C1, but the **CardDAV** emitter still built a quoted
|
||||||
|
`String`. And two dedup/DAV emit micro-cuts the earlier rounds named but held
|
||||||
|
back: the `settle_batch` clone-to-bind and the `BDAY` strftime stamp.
|
||||||
|
|
||||||
|
Reproduce:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --release --features bench --example bench_round21_micro
|
||||||
|
```
|
||||||
|
|
||||||
|
All arms are **no-Postgres** (release-profile counting-allocator example).
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| # | change | key metric | before → after |
|
||||||
|
|--:|---|---|---|
|
||||||
|
| **R1** | The CalDAV/CardDAV row-mapping repositories (`calendar_event_pg_repository`, `calendar_pg_repository`, `contact_pg_repository`, `contact_group_pg_repository`) built their result `Vec` with `let mut v = Vec::new(); for row in rows { v.push(map(row)?) }` — growing from capacity 0 (~⌈log₂N⌉ reallocations, each memcpy-ing the accumulated rows) on **every CalDAV/CardDAV listing, multiget & bulk sync**. Now `Vec::with_capacity(rows.len())` (the ROUND20 §I1 file-side pattern extended to the 16 calendar/contact sites it deferred). Plus one `HashMap` (`get_calendar_properties`). | 200-row listing | **7 → 1 allocs/op** (6 fewer) |
|
||||||
|
| **R2** | `DedupService::settle_batch` cloned every 64-char chunk hash into a `Vec<String>` purely to `.bind()` it to the pin `UPDATE … WHERE hash = ANY($1)`, on **every settle batch of every upload** (~128 batches for a 1 GB fully-unique upload). Now binds a borrowed `Vec<&str>` — sqlx encodes `&[&str]` to `text[]` identically (`favorites_pg_repository.rs:271` already does this). | 32-chunk batch | **33 → 1 allocs/op · 39.4× wall** |
|
||||||
|
| **R3** | `DedupService::store_loose_chunks` — the delta-upload sibling of the ROUND17 §D2 ingest loop — kept an intra-request dedup `HashSet<String>` and cloned the hex hash **twice per frame** (into `received` and into the set; the set clone dropped on the spot for a duplicate). Now keys the set on the raw `[u8; 32]` BLAKE3 digest (`Copy`, no per-distinct-chunk heap key) and moves the hex into `received` on a duplicate. Runs **per frame** on delta/sync uploads (thousands of frames for a large changed file). | 128 frames, 50% dup | **401 → 209 allocs/op (192 fewer) · 1.50× wall** |
|
||||||
|
| **R4** | `carddav_adapter::write_contact_response` built a `"…"`-quoted `String` for `getetag` then wrote it auto-escaped — `quick_xml` escapes the `"` → `"`, re-allocating an owned `Cow` — on **every contact of every CardDAV multiget/PROPFIND** (plus the per-address-book collection etag). Now emits the two quotes as borrowed pre-escaped `"` text events (the ROUND20 §C1 NextCloud pattern, via a shared `write_quoted_etag` helper covering all 4 CardDAV etag sites). | per-contact row | **3 → 0 allocs/op · 2.11× wall** |
|
||||||
|
| **R5** | `contact_to_vcard` stamped `BDAY` via `write!(…, "{}", bday.format("%Y-%m-%d"))`, running chrono's strftime interpreter per **contact-with-birthday**. Now renders the fixed `YYYY-MM-DD` on the stack via the new `fmt::compact_date` (the date-only companion to the §V2 `REV` renderer), chrono fallback for out-of-range years. | per bday contact | **2 → 0 allocs/op · 10.51× wall** |
|
||||||
|
| **R6** | The NextCloud trashbin PROPFIND row set `d:getcontenttype` for a folder to `"httpd/unix-directory".to_string()` — a heap `String` for a static constant, **per trashed folder row**. Now `Cow::Borrowed` (the ROUND16 §M1 `Cow<'static, str>` pattern); only the file branch (mime_guess) still owns its String. | per folder row | **1 → 0 allocs/op · 5.76× wall** |
|
||||||
|
|
||||||
|
> Allocs/op is the deterministic primary gate (identical run to run). Wall
|
||||||
|
> figures are single-shot and noise-bounded. Every section carries a
|
||||||
|
> byte/-value equivalence gate; the shipped source now matches each AFTER arm.
|
||||||
|
|
||||||
|
## [R1] CalDAV/CardDAV row-mapper container pre-size
|
||||||
|
|
||||||
|
`collect::<Result<Vec>>()` was ROUND20 §I1's target on the file side; the
|
||||||
|
CalDAV/CardDAV repos use the equivalent `Vec::new()` + `for row in rows { … }`
|
||||||
|
shape, which grows the container the same way — from capacity 0, reserving
|
||||||
|
nothing, so `push` reallocates ~⌈log₂N⌉ times and memcpy-s the accumulated
|
||||||
|
(Contact/Event-sized) rows on each grow. `rows` is a materialized `fetch_all`
|
||||||
|
result, so `rows.len()` is exact:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
|
for row in rows {
|
||||||
|
events.push(Self::row_to_event(row)?); // ? short-circuits identically
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Applied to the 16 listing/multiget/paginated mappers across the four repos
|
||||||
|
(`calendar_event` ×6, `calendar` ×2 + the `get_calendar_properties` HashMap,
|
||||||
|
`contact` ×6, `contact_group` ×1). The `subject_group` and
|
||||||
|
`nextcloud_object_id` sibling mappers already pre-sized (`with_capacity(rows.len())`),
|
||||||
|
so they were left untouched. Byte-identical output; on a 200-row listing the
|
||||||
|
container allocations drop from **7 → 1** (the growth-from-0 reallocations
|
||||||
|
replaced by a single exact reserve).
|
||||||
|
|
||||||
|
## [R2] settle_batch — bind borrowed `&str`, don't clone
|
||||||
|
|
||||||
|
`settle_batch` runs once per flushed chunk batch of every upload. It built an
|
||||||
|
owned `Vec<String>` of the batch's 64-char hashes only to `.bind()` it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let hashes: Vec<String> = batch.iter().map(|(h, _)| h.clone()).collect(); // N heap Strings
|
||||||
|
// … .bind(&hashes) … WHERE hash = ANY($1) …
|
||||||
|
```
|
||||||
|
|
||||||
|
`batch` outlives the query (it is consumed two statements later), so the hashes
|
||||||
|
can be borrowed. sqlx encodes `&[&str]` to a PostgreSQL `text[]` identically to
|
||||||
|
the owned `Vec<String>` (the pattern `favorites_pg_repository.rs:271` already
|
||||||
|
uses, with the comment *"sqlx binds `&[&str]` as text[], so no per-id String is
|
||||||
|
needed"*). The borrow is scoped in a block so it ends before `batch` is moved:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let pinned: HashSet<String> = {
|
||||||
|
let hashes: Vec<&str> = batch.iter().map(|(h, _)| h.as_str()).collect();
|
||||||
|
sqlx::query_scalar::<_, String>("UPDATE … WHERE hash = ANY($1) RETURNING hash")
|
||||||
|
.bind(&hashes).fetch_all(pool.as_ref()).await?.into_iter().collect()
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Up to `FLUSH_MAX_CHUNKS` (=32) 64-byte `String` allocations removed per batch —
|
||||||
|
~4000 over a 1 GB fully-unique upload — for one pointer-only `Vec`.
|
||||||
|
|
||||||
|
## [R3] store_loose_chunks — digest-keyed dedup set + move-on-duplicate
|
||||||
|
|
||||||
|
The delta-upload ingest (`store_loose_chunks`) is the sibling ROUND17 §D2 didn't
|
||||||
|
reach. Per frame it allocated the 64-char hex hash and then cloned it twice:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let hash = blake3::hash(&data).to_hex().to_string();
|
||||||
|
received.push((hash.clone(), data.len() as u64)); // clone 1 (always)
|
||||||
|
if seen.insert(hash.clone()) { // clone 2 (always; HashSet<String>)
|
||||||
|
new_rows.push((hash, len));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`seen` is the **intra-request** dedup set (has this exact chunk already appeared
|
||||||
|
in *this* delta stream? — re-chunked near-duplicates, zero-padded regions). Keyed
|
||||||
|
on the raw 32-byte digest it needs no per-distinct-chunk `String`, and a
|
||||||
|
duplicate frame **moves** the hex into `received` instead of cloning:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let digest = blake3::hash(&data);
|
||||||
|
let hash = digest.to_hex().to_string();
|
||||||
|
let len = data.len();
|
||||||
|
if seen.insert(*digest.as_bytes()) { // HashSet<[u8; 32]>, Copy key
|
||||||
|
self.backend.put_blob_from_bytes_unsynced(&hash, data).await?;
|
||||||
|
received.push((hash.clone(), len as u64));
|
||||||
|
new_rows.push((hash, len as i64));
|
||||||
|
} else {
|
||||||
|
received.push((hash, len as u64)); // move, no clone
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
hex ↔ digest is bijective, so membership and the `received`/`new_rows`
|
||||||
|
sequences are identical. On a 128-frame stream with 50 % intra-request dups the
|
||||||
|
per-frame hash clones drop from 3 to ~1.5.
|
||||||
|
|
||||||
|
## [R4] CardDAV getetag — borrowed pre-escaped quotes
|
||||||
|
|
||||||
|
`write_contact_response` (per contact of every CardDAV multiget/PROPFIND) built
|
||||||
|
a `"…"`-quoted `String` and wrote it auto-escaped; `quick_xml` escapes the `"`
|
||||||
|
to `"`, so the whole-string escape re-allocated an owned `Cow`. The new
|
||||||
|
shared `write_quoted_etag` helper emits the two quotes as **borrowed**
|
||||||
|
pre-escaped `"` text events around the escaped etag body — byte-identical
|
||||||
|
(the equivalence gate asserts it, including an etag with `&`/`<`/`"`), 0
|
||||||
|
allocs/contact. Applied to all four CardDAV etag sites (2 per-contact + 2
|
||||||
|
per-address-book collection), mirroring the NextCloud ROUND20 §C1 fix.
|
||||||
|
|
||||||
|
## [R5] BDAY — stack-rendered `%Y-%m-%d`
|
||||||
|
|
||||||
|
`contact_to_vcard` already stack-renders `REV` (ROUND19 §V2); `BDAY` still went
|
||||||
|
through chrono's strftime interpreter (`bday.format("%Y-%m-%d")`). The new
|
||||||
|
`fmt::compact_date(buf, year, month, day)` renders the fixed 10-byte
|
||||||
|
`YYYY-MM-DD` with the same `push4`/`push2` LUT the other `fmt` helpers use, and
|
||||||
|
returns `None` outside the 4-digit-year range (where chrono widens/sign-prefixes
|
||||||
|
`%Y`) so the caller keeps the chrono path as fallback. Byte-identical for every
|
||||||
|
representable birthday.
|
||||||
|
|
||||||
|
## [R6] NC trashbin folder content-type — borrowed constant
|
||||||
|
|
||||||
|
The trashbin PROPFIND folder branch `to_string()`-ed the static
|
||||||
|
`"httpd/unix-directory"` per row. `Cow::Borrowed` for the folder constant (the
|
||||||
|
file branch still owns its mime_guess String) drops that allocation per trashed
|
||||||
|
folder row — the ROUND16 §M1 `Cow<'static, str>` pattern the trashbin loop
|
||||||
|
missed.
|
||||||
|
|
||||||
|
## Not shipped — deferred to a later round
|
||||||
|
|
||||||
|
Surfaced by the Round-21 audit (three parallel sub-audits across the HTTP,
|
||||||
|
storage/dedup and application/parse layers), verified against current source,
|
||||||
|
but held back — each needs a signature/API decision, a Postgres fixture, or a
|
||||||
|
gate the deterministic alloc-counter can't provide:
|
||||||
|
|
||||||
|
- **Hot GET handlers clone the whole request `HeaderMap`** (`file_handler`
|
||||||
|
list/download/thumbnail, `photos_handler`, NC `preview`/`avatar`): axum's
|
||||||
|
`HeaderMap` extractor does `parts.headers.clone()` (~2 allocs) purely to read
|
||||||
|
1–3 headers — the exact cost `middleware/auth.rs` already eliminated (ROUND14
|
||||||
|
§A4) but never propagated to the handlers. The fix takes `req: Request` last
|
||||||
|
and reads `req.headers()` by borrow; it's a **multi-handler signature refactor**
|
||||||
|
(each `_impl` + its wrapper + the route registration) that wants its own
|
||||||
|
validated pass, same class as the ROUND19/20 multi-signature deferrals.
|
||||||
|
- **`Query<HashMap<String,String>>` on the hot list/download paths** builds a
|
||||||
|
`HashMap` + key `String` per request to read one param; a typed
|
||||||
|
`Query<ListFilesQuery>` struct drops both (serde ignores unknown params). Same
|
||||||
|
signature-surface reason as above; pairs naturally with the HeaderMap pass.
|
||||||
|
- **Native WebDAV PROPFIND re-extracts the URI path** (`webdav_handler.rs:507`):
|
||||||
|
`extract_webdav_path(req.uri())` re-runs a percent-decode + `String` alloc that
|
||||||
|
the `path` parameter already holds at that point (the `:503` comment about the
|
||||||
|
prefix is stale). One decode + alloc per PROPFIND — but removing it needs a
|
||||||
|
careful href-equivalence proof across the chroot/scope resolution, so it wants
|
||||||
|
a dedicated correctness check, not a perf banner.
|
||||||
|
- **`music_service` public-playlist merge is O(owned·public)** (`Vec::any()` per
|
||||||
|
public item): a `HashSet` makes it O(owned+public). Because `PlaylistDto.id` is
|
||||||
|
a `String`, the set must own the ids (clone) — so the change trades N String
|
||||||
|
comparisons for N String clones: a **wall win that ADDS allocations**, which
|
||||||
|
the deterministic alloc gate can't score. Wants a wall-gated evaluation on the
|
||||||
|
opt-in `include_public` path.
|
||||||
|
- **WebDAV dead-props filter is O(N·D·R)** (`webdav_adapter.rs:616/705`): the
|
||||||
|
loop-invariant requested-props list is re-scanned per dead prop per resource;
|
||||||
|
a per-PROPFIND `HashSet<&QualifiedName>` makes it O(N·D). Only bites accounts
|
||||||
|
that accumulate client-set custom props (macOS Finder) over large listings —
|
||||||
|
and, like music_service, the HashSet build trades compares for an alloc, so
|
||||||
|
it's wall-gated. Queued with a synthetic-dead-props bench.
|
||||||
|
- **`verify_integrity` Phase 1 probes manifest chunks serially** while Phase 2
|
||||||
|
is `buffer_unordered(16)` — on a remote backend that's O(total_chunks) serial
|
||||||
|
HEADs. Background/admin path; needs a remote-backend fixture to show the win.
|
||||||
|
- **`subject_group_service::remove_member` runs the same recursive-CTE
|
||||||
|
`list_transitive_users(child_id)` twice** for a nested group removal (the
|
||||||
|
intervening edge delete can't change the child's descendants). One DB
|
||||||
|
round-trip halved; low frequency (admin), needs Postgres.
|
||||||
|
- **`store_loose_chunks` final registration + `run_rollback` clone hashes to
|
||||||
|
bind** (`dedup_service.rs:887/212`), and the **`contact_pg` JSONB columns
|
||||||
|
decode through a throwaway `serde_json::Value`** — the R2/ROUND20 patterns
|
||||||
|
applied to once-per-upload / per-contact-read sites; both need Postgres to
|
||||||
|
bench end-to-end.
|
||||||
|
- **`GzipCompressionService::{compress,decompress}_data` copy the whole buffer
|
||||||
|
via `.to_vec()`** before `spawn_blocking` — forced by the `&[u8]` port
|
||||||
|
signature; a `Bytes`-taking port lets an owning caller move. Port API change,
|
||||||
|
gated (text > 50 KB), low heat.
|
||||||
|
- **Fast hasher for trusted-key internal maps** (ROUND20 flag stands): needs a
|
||||||
|
`Cargo.toml` dependency decision and must stay DoS-resistant for the
|
||||||
|
attacker-controlled delta-hash sets — worth a dedicated, wall-gated pass.
|
||||||
|
|
||||||
|
## Environment / methodology
|
||||||
|
|
||||||
|
- `cargo run --release --features bench --example bench_round21_micro` —
|
||||||
|
counting global allocator, no Postgres. Tunables (env): `BENCH_ITERS` (200000),
|
||||||
|
`R1_ROWS` (200), `R3_FRAMES` (128).
|
||||||
|
- Each section is BEFORE (verbatim replica of the shipped-before shape) vs AFTER
|
||||||
|
(verbatim replica of the shipped-after shape, which the source is then made to
|
||||||
|
match), with a byte/-value equivalence gate; the shipped source now matches
|
||||||
|
each AFTER arm.
|
||||||
|
- Roll-back rule encoded per section: the harness `std::process::exit(1)`s with
|
||||||
|
`GATE FAIL … rollback` if an AFTER arm fails to reduce allocations.
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
//! Round-21 CPU/alloc micro-pack (no Postgres).
|
||||||
|
//!
|
||||||
|
//! Same rule as ROUND2–20: each section is BEFORE (verbatim replica of the
|
||||||
|
//! shipped-before shape) vs AFTER (verbatim replica of the shipped-after shape,
|
||||||
|
//! which the source is then made to match), with a byte/-value equivalence gate
|
||||||
|
//! and a `GATE FAIL … rollback` check that `std::process::exit(1)`s if the AFTER
|
||||||
|
//! arm fails to beat its BEFORE — the round's roll-back rule encoded into the
|
||||||
|
//! benchmark. An AFTER that doesn't win is never applied to the source.
|
||||||
|
//!
|
||||||
|
//! [R1] The CalDAV/CardDAV row-mapping repositories build their result Vec
|
||||||
|
//! with `let mut v = Vec::new(); for row in rows { v.push(map(row)?) }`,
|
||||||
|
//! growing the container from capacity 0 (~⌈log₂N⌉ reallocations, each
|
||||||
|
//! memcpy-ing the accumulated rows). AFTER pre-sizes with
|
||||||
|
//! `Vec::with_capacity(rows.len())` — the file-side sibling ROUND20 §I1
|
||||||
|
//! shipped, extended to the calendar/contact repos it deferred.
|
||||||
|
//!
|
||||||
|
//! [R2] `DedupService::settle_batch` cloned every 64-char chunk hash into a
|
||||||
|
//! `Vec<String>` purely to `.bind()` it to the pin `UPDATE … = ANY($1)`.
|
||||||
|
//! AFTER binds a borrowed `Vec<&str>` — sqlx encodes `&[&str]` to
|
||||||
|
//! `text[]` identically (favorites_pg_repository.rs:271 already does
|
||||||
|
//! this), so the per-chunk hash `String` disappears.
|
||||||
|
//!
|
||||||
|
//! [R3] `DedupService::store_loose_chunks` (the delta-upload sibling of the
|
||||||
|
//! ROUND17 §D2 ingest loop) kept an intra-request dedup `HashSet<String>`
|
||||||
|
//! and cloned the hex hash TWICE per frame (into `received` and into the
|
||||||
|
//! set). AFTER keys the set on the raw `[u8; 32]` BLAKE3 digest (`Copy`,
|
||||||
|
//! no heap key) and moves the hex into `received` on a duplicate.
|
||||||
|
//!
|
||||||
|
//! [R4] `carddav_adapter::write_contact_response` built a `"…"`-quoted
|
||||||
|
//! `String` for `getetag` then wrote it auto-escaped (quick_xml escapes
|
||||||
|
//! the `"` → `"`, re-allocating). AFTER emits the two quotes as
|
||||||
|
//! borrowed pre-escaped `"` text events (the NextCloud ROUND20 §C1
|
||||||
|
//! pattern applied to the CardDAV emitter it missed).
|
||||||
|
//!
|
||||||
|
//! [R5] `contact_to_vcard` stamped `BDAY` via `write!(…, "{}",
|
||||||
|
//! bday.format("%Y-%m-%d"))`, running chrono's strftime interpreter per
|
||||||
|
//! contact-with-birthday. AFTER renders the fixed `YYYY-MM-DD` on the
|
||||||
|
//! stack via `fmt::compact_date` (the date-only companion to the §V2 REV
|
||||||
|
//! renderer), with the chrono fallback for out-of-range years.
|
||||||
|
//!
|
||||||
|
//! [R6] The NextCloud trashbin PROPFIND row set `d:getcontenttype` for a
|
||||||
|
//! folder to `"httpd/unix-directory".to_string()` — a heap String for a
|
||||||
|
//! static constant, per trashed folder row. AFTER borrows it via
|
||||||
|
//! `Cow::Borrowed` (the ROUND16 §M1 `Cow<'static, str>` pattern).
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! cargo run --release --features bench --example bench_round21_micro
|
||||||
|
//! Tunables (env): BENCH_ITERS (200000), R1_ROWS (200), R3_FRAMES (128)
|
||||||
|
|
||||||
|
use std::alloc::{GlobalAlloc, Layout, System};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::env;
|
||||||
|
use std::hint::black_box;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use quick_xml::Writer;
|
||||||
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||||
|
|
||||||
|
static ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
struct CountingAlloc;
|
||||||
|
|
||||||
|
unsafe impl GlobalAlloc for CountingAlloc {
|
||||||
|
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||||
|
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||||
|
unsafe { System.alloc(layout) }
|
||||||
|
}
|
||||||
|
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||||
|
unsafe { System.dealloc(ptr, layout) }
|
||||||
|
}
|
||||||
|
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||||
|
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||||
|
unsafe { System.realloc(ptr, layout, new_size) }
|
||||||
|
}
|
||||||
|
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||||
|
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||||
|
unsafe { System.alloc_zeroed(layout) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL: CountingAlloc = CountingAlloc;
|
||||||
|
|
||||||
|
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||||
|
env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Measured {
|
||||||
|
wall_ns_per_op: f64,
|
||||||
|
allocs_per_op: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure<F: FnMut()>(iters: usize, mut f: F) -> Measured {
|
||||||
|
// Warm up (grow any reused buffers, prime caches) so the measured window
|
||||||
|
// reflects steady state, not first-touch growth.
|
||||||
|
for _ in 0..(iters / 20).max(1) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
let a0 = ALLOC_CALLS.load(Ordering::Relaxed);
|
||||||
|
let t = Instant::now();
|
||||||
|
for _ in 0..iters {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
let wall = t.elapsed().as_nanos() as f64 / iters as f64;
|
||||||
|
let allocs = (ALLOC_CALLS.load(Ordering::Relaxed) - a0) as f64 / iters as f64;
|
||||||
|
Measured {
|
||||||
|
wall_ns_per_op: wall,
|
||||||
|
allocs_per_op: allocs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_row(label: &str, m: &Measured) {
|
||||||
|
println!(
|
||||||
|
"| {:<50} | {:>12.1} | {:>10.2} |",
|
||||||
|
label, m.wall_ns_per_op, m.allocs_per_op
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_footer(name: &str, before: &Measured, after: &Measured) {
|
||||||
|
println!("| arm | ns/op | allocs/op |");
|
||||||
|
print_row(&format!("BEFORE {name}"), before);
|
||||||
|
print_row(&format!("AFTER {name}"), after);
|
||||||
|
println!(
|
||||||
|
"# {:.2}x wall, {:.2} fewer allocs/op",
|
||||||
|
before.wall_ns_per_op / after.wall_ns_per_op,
|
||||||
|
before.allocs_per_op - after.allocs_per_op
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gate_allocs(tag: &str, before: &Measured, after: &Measured) {
|
||||||
|
if after.allocs_per_op >= before.allocs_per_op {
|
||||||
|
eprintln!("GATE FAIL [{tag}]: AFTER did not reduce allocations — rollback");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R1] Row-mapper container pre-size — Vec::new()+push vs with_capacity+push
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A Contact-sized (~192 B) mapped element so the container-realloc memcpy cost
|
||||||
|
/// is realistic. The per-element mapper allocates nothing in either arm, so the
|
||||||
|
/// measured alloc delta is exactly the container growth (the CalDAV/CardDAV
|
||||||
|
/// `row_to_*` allocs are identical in both arms and out of scope here).
|
||||||
|
type MappedRow = [u8; 192];
|
||||||
|
|
||||||
|
fn r1_before(rows: &[MappedRow]) -> Vec<MappedRow> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
out.push(*row);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn r1_after(rows: &[MappedRow]) -> Vec<MappedRow> {
|
||||||
|
let mut out = Vec::with_capacity(rows.len());
|
||||||
|
for row in rows {
|
||||||
|
out.push(*row);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r1() {
|
||||||
|
let n: usize = env_or("R1_ROWS", 200);
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000) / 20; // heavier op
|
||||||
|
let rows: Vec<MappedRow> = (0..n).map(|i| [i as u8; 192]).collect();
|
||||||
|
|
||||||
|
assert_eq!(r1_before(&rows).len(), r1_after(&rows).len());
|
||||||
|
|
||||||
|
let before = measure(iters, || {
|
||||||
|
black_box(r1_before(black_box(&rows)));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
black_box(r1_after(black_box(&rows)));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R1] CalDAV/CardDAV row-mapper pre-size ({n} contact-sized rows)");
|
||||||
|
header_footer("Vec::new()+push vs with_capacity+push", &before, &after);
|
||||||
|
gate_allocs("R1", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R2] settle_batch bind — Vec<String> clone vs Vec<&str> borrow
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// BEFORE: clone every chunk hash into an owned `Vec<String>` to `.bind()`.
|
||||||
|
fn r2_before(batch: &[(String, u64)]) -> Vec<String> {
|
||||||
|
batch.iter().map(|(h, _)| h.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AFTER: borrow — sqlx encodes `&[&str]` to `text[]` identically.
|
||||||
|
fn r2_after(batch: &[(String, u64)]) -> Vec<&str> {
|
||||||
|
batch.iter().map(|(h, _)| h.as_str()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r2() {
|
||||||
|
let n: usize = env_or("FLUSH_MAX_CHUNKS", 32);
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000);
|
||||||
|
// A settle batch of 32 chunks, each a 64-char BLAKE3 hex hash.
|
||||||
|
let batch: Vec<(String, u64)> = (0..n)
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
format!("{:064x}", i as u128 * 0x9E37_79B9_7F4A_7C15),
|
||||||
|
65_536,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Equivalence: the borrowed &strs equal the owned String hashes.
|
||||||
|
let b = r2_before(&batch);
|
||||||
|
let a = r2_after(&batch);
|
||||||
|
assert_eq!(b.len(), a.len(), "R2 length differs");
|
||||||
|
assert!(
|
||||||
|
b.iter().zip(&a).all(|(s, t)| s == t),
|
||||||
|
"R2 bound hashes differ"
|
||||||
|
);
|
||||||
|
|
||||||
|
let before = measure(iters, || {
|
||||||
|
black_box(r2_before(black_box(&batch)));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
black_box(r2_after(black_box(&batch)));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R2] settle_batch hash bind ({n}-chunk batch)");
|
||||||
|
header_footer("Vec<String> clone vs Vec<&str> borrow", &before, &after);
|
||||||
|
gate_allocs("R2", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R3] store_loose_chunks — HashSet<String>+2 clones vs HashSet<[u8;32]>+move
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `(received-in-order, distinct-new-rows)` — `store_loose_chunks`'s two
|
||||||
|
/// observable outputs.
|
||||||
|
type R3Out = (Vec<(String, u64)>, Vec<(String, i64)>);
|
||||||
|
|
||||||
|
/// BEFORE: the shipped-before delta-upload loop — `HashSet<String>` intra-
|
||||||
|
/// request dedup set, hex hash cloned into `received` AND into the set per frame.
|
||||||
|
/// Returns (received-in-order, distinct-new-rows) — the observable result.
|
||||||
|
fn r3_before(frames: &[([u8; 32], String)]) -> R3Out {
|
||||||
|
let mut received: Vec<(String, u64)> = Vec::new();
|
||||||
|
let mut new_rows: Vec<(String, i64)> = Vec::new();
|
||||||
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
for (_digest, hex) in frames {
|
||||||
|
// Step 1 (common to both arms): the fresh per-frame hex String
|
||||||
|
// (`blake3::hash(&data).to_hex().to_string()`).
|
||||||
|
let hash = hex.clone();
|
||||||
|
received.push((hash.clone(), 65_536));
|
||||||
|
if seen.insert(hash.clone()) {
|
||||||
|
new_rows.push((hash, 65_536));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(received, new_rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AFTER: dedup set keyed on the raw 32-byte digest; hex moved into `received`
|
||||||
|
/// on a duplicate, cloned only on the first occurrence (needed by `new_rows`).
|
||||||
|
fn r3_after(frames: &[([u8; 32], String)]) -> R3Out {
|
||||||
|
let mut received: Vec<(String, u64)> = Vec::new();
|
||||||
|
let mut new_rows: Vec<(String, i64)> = Vec::new();
|
||||||
|
let mut seen: HashSet<[u8; 32]> = HashSet::new();
|
||||||
|
for (digest, hex) in frames {
|
||||||
|
let hash = hex.clone(); // step 1, same as BEFORE
|
||||||
|
if seen.insert(*digest) {
|
||||||
|
received.push((hash.clone(), 65_536));
|
||||||
|
new_rows.push((hash, 65_536));
|
||||||
|
} else {
|
||||||
|
received.push((hash, 65_536));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(received, new_rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r3() {
|
||||||
|
let n: usize = env_or("R3_FRAMES", 128);
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000) / 20; // heavier op
|
||||||
|
// A delta stream where every other frame repeats the previous chunk (a
|
||||||
|
// re-chunked near-duplicate / zero-padded region) → 50% intra-request dups.
|
||||||
|
let frames: Vec<([u8; 32], String)> = (0..n)
|
||||||
|
.map(|i| {
|
||||||
|
let key = i / 2; // pairs share a digest
|
||||||
|
let h = blake3::hash(&(key as u64).to_le_bytes());
|
||||||
|
(*h.as_bytes(), h.to_hex().to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Equivalence: identical received sequence and distinct new_rows.
|
||||||
|
let b = r3_before(&frames);
|
||||||
|
let a = r3_after(&frames);
|
||||||
|
assert_eq!(b.0, a.0, "R3 received sequence differs");
|
||||||
|
assert_eq!(b.1, a.1, "R3 new_rows differ");
|
||||||
|
|
||||||
|
let before = measure(iters, || {
|
||||||
|
black_box(r3_before(black_box(&frames)));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
black_box(r3_after(black_box(&frames)));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R3] store_loose_chunks dedup ({n} frames, 50% dup)");
|
||||||
|
header_footer("HashSet<String>+2 clones vs [u8;32]+move", &before, &after);
|
||||||
|
gate_allocs("R3", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R4] CardDAV getetag — quoted String + escape vs borrowed pre-escaped
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// BEFORE: build a `"…"`-quoted `String`, then write it as an auto-escaped text
|
||||||
|
/// element — `quick_xml` escapes the `"` → `"`, re-allocating an owned Cow.
|
||||||
|
fn r4_before(buf: &mut Vec<u8>, etag: &str) {
|
||||||
|
let mut w = Writer::new(&mut *buf);
|
||||||
|
w.write_event(Event::Start(BytesStart::new("D:getetag")))
|
||||||
|
.unwrap();
|
||||||
|
let mut quoted = String::with_capacity(etag.len() + 2);
|
||||||
|
quoted.push('"');
|
||||||
|
quoted.push_str(etag);
|
||||||
|
quoted.push('"');
|
||||||
|
w.write_event(Event::Text(BytesText::new("ed))).unwrap();
|
||||||
|
w.write_event(Event::End(BytesEnd::new("D:getetag")))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AFTER: emit the pre-escaped `"` quote literals as borrowed text events
|
||||||
|
/// around the escaped etag body — byte-identical output, zero owned strings.
|
||||||
|
fn r4_after(buf: &mut Vec<u8>, etag: &str) {
|
||||||
|
let mut w = Writer::new(&mut *buf);
|
||||||
|
w.write_event(Event::Start(BytesStart::new("D:getetag")))
|
||||||
|
.unwrap();
|
||||||
|
w.write_event(Event::Text(BytesText::from_escaped(""")))
|
||||||
|
.unwrap();
|
||||||
|
w.write_event(Event::Text(BytesText::new(etag))).unwrap();
|
||||||
|
w.write_event(Event::Text(BytesText::from_escaped(""")))
|
||||||
|
.unwrap();
|
||||||
|
w.write_event(Event::End(BytesEnd::new("D:getetag")))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r4() {
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000);
|
||||||
|
let etag = "a1b2c3d4e5f6-1719792000"; // realistic contact etag
|
||||||
|
|
||||||
|
// Equivalence: byte-identical output, incl. an etag with XML-special chars.
|
||||||
|
let (mut b1, mut b2) = (Vec::new(), Vec::new());
|
||||||
|
r4_before(&mut b1, etag);
|
||||||
|
r4_after(&mut b2, etag);
|
||||||
|
assert_eq!(b1, b2, "R4 emitted bytes differ (hex etag)");
|
||||||
|
let (mut s1, mut s2) = (Vec::new(), Vec::new());
|
||||||
|
r4_before(&mut s1, "abc&def<x\"y");
|
||||||
|
r4_after(&mut s2, "abc&def<x\"y");
|
||||||
|
assert_eq!(s1, s2, "R4 emitted bytes differ (special chars)");
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(64);
|
||||||
|
let before = measure(iters, || {
|
||||||
|
buf.clear();
|
||||||
|
r4_before(black_box(&mut buf), black_box(etag));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
buf.clear();
|
||||||
|
r4_after(black_box(&mut buf), black_box(etag));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R4] CardDAV getetag (per-contact multiget/PROPFIND row)");
|
||||||
|
header_footer("quoted String + escape vs borrowed events", &before, &after);
|
||||||
|
gate_allocs("R4", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R5] BDAY stamp — chrono %Y-%m-%d interpreter vs compact_date stack render
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DEC_LUT: &[u8; 200] = b"0001020304050607080910111213141516171819\
|
||||||
|
2021222324252627282930313233343536373839\
|
||||||
|
4041424344454647484950515253545556575859\
|
||||||
|
6061626364656667686970717273747576777879\
|
||||||
|
8081828384858687888990919293949596979899";
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn push2(out: &mut [u8], pos: usize, v: u32) {
|
||||||
|
let d = (v as usize) * 2;
|
||||||
|
out[pos] = DEC_LUT[d];
|
||||||
|
out[pos + 1] = DEC_LUT[d + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn push4(out: &mut [u8], pos: usize, v: i64) {
|
||||||
|
out[pos] = b'0' + (v / 1000 % 10) as u8;
|
||||||
|
out[pos + 1] = b'0' + (v / 100 % 10) as u8;
|
||||||
|
out[pos + 2] = b'0' + (v / 10 % 10) as u8;
|
||||||
|
out[pos + 3] = b'0' + (v % 10) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replica of the shipped `fmt::compact_date` the AFTER source calls.
|
||||||
|
fn bench_compact_date(buf: &mut [u8; 10], year: i32, month: u32, day: u32) -> Option<&str> {
|
||||||
|
if !(0..=9999).contains(&year) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
push4(buf, 0, year as i64);
|
||||||
|
buf[4] = b'-';
|
||||||
|
push2(buf, 5, month);
|
||||||
|
buf[7] = b'-';
|
||||||
|
push2(buf, 8, day);
|
||||||
|
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BEFORE: `write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"))` into the
|
||||||
|
/// reused buffer — chrono's strftime interpreter per contact-with-birthday.
|
||||||
|
fn r5_before(vcard: &mut String, bday: NaiveDate) {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AFTER: stack render via `compact_date`, chrono fallback out of range.
|
||||||
|
fn r5_after(vcard: &mut String, bday: NaiveDate) {
|
||||||
|
use chrono::Datelike as _;
|
||||||
|
let mut buf = [0u8; 10];
|
||||||
|
match bench_compact_date(&mut buf, bday.year(), bday.month(), bday.day()) {
|
||||||
|
Some(s) => {
|
||||||
|
vcard.push_str("BDAY:");
|
||||||
|
vcard.push_str(s);
|
||||||
|
vcard.push_str("\r\n");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r5() {
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000);
|
||||||
|
let bday = NaiveDate::from_ymd_opt(1987, 3, 5).unwrap();
|
||||||
|
|
||||||
|
// Equivalence: byte-identical BDAY line.
|
||||||
|
let (mut b, mut a) = (String::new(), String::new());
|
||||||
|
r5_before(&mut b, bday);
|
||||||
|
r5_after(&mut a, bday);
|
||||||
|
assert_eq!(b, a, "R5 BDAY line differs");
|
||||||
|
assert_eq!(b, "BDAY:1987-03-05\r\n");
|
||||||
|
|
||||||
|
let mut buf = String::with_capacity(32);
|
||||||
|
let before = measure(iters, || {
|
||||||
|
buf.clear();
|
||||||
|
r5_before(black_box(&mut buf), black_box(bday));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
buf.clear();
|
||||||
|
r5_after(black_box(&mut buf), black_box(bday));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R5] BDAY stamp (per contact-with-birthday)");
|
||||||
|
header_footer("chrono %Y-%m-%d vs compact_date", &before, &after);
|
||||||
|
gate_allocs("R5", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// [R6] trashbin folder content-type — String::to_string() vs Cow::Borrowed
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// BEFORE: heap a `String` for the static folder content-type constant.
|
||||||
|
fn r6_before(is_folder: bool, name: &str) -> String {
|
||||||
|
if is_folder {
|
||||||
|
"httpd/unix-directory".to_string()
|
||||||
|
} else {
|
||||||
|
// File branch (mime_guess) — allocates in both arms, out of scope.
|
||||||
|
format!("application/{}", name.rsplit('.').next().unwrap_or("octet"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AFTER: borrow the folder constant; only the file branch owns its String.
|
||||||
|
fn r6_after(is_folder: bool, name: &str) -> Cow<'static, str> {
|
||||||
|
if is_folder {
|
||||||
|
Cow::Borrowed("httpd/unix-directory")
|
||||||
|
} else {
|
||||||
|
Cow::Owned(format!(
|
||||||
|
"application/{}",
|
||||||
|
name.rsplit('.').next().unwrap_or("octet")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_r6() {
|
||||||
|
let iters: usize = env_or("BENCH_ITERS", 200_000);
|
||||||
|
|
||||||
|
// Equivalence: same content-type string for a folder row.
|
||||||
|
assert_eq!(r6_before(true, "x"), r6_after(true, "x").as_ref());
|
||||||
|
|
||||||
|
let before = measure(iters, || {
|
||||||
|
black_box(r6_before(black_box(true), black_box("Documents")));
|
||||||
|
});
|
||||||
|
let after = measure(iters, || {
|
||||||
|
black_box(r6_after(black_box(true), black_box("Documents")));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("\n## [R6] trashbin folder content-type (per trashed folder row)");
|
||||||
|
header_footer("String::to_string() vs Cow::Borrowed", &before, &after);
|
||||||
|
gate_allocs("R6", &before, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("# Round-21 micro-pack — BEFORE/AFTER (counting allocator, release)");
|
||||||
|
println!("# allocs/op is the deterministic gate; a non-winning AFTER exits 1 (rollback).");
|
||||||
|
section_r1();
|
||||||
|
section_r2();
|
||||||
|
section_r3();
|
||||||
|
section_r4();
|
||||||
|
section_r6();
|
||||||
|
// R5 (BDAY) last: it is the one section whose BEFORE (chrono's NaiveDate
|
||||||
|
// strftime) may or may not heap-allocate; ordering it last lets every other
|
||||||
|
// section print + gate before R5's gate can halt the run.
|
||||||
|
section_r5();
|
||||||
|
println!("\nAll Round-21 sections passed their allocation gate.");
|
||||||
|
}
|
||||||
@@ -17,6 +17,21 @@ use crate::application::adapters::webdav_adapter::{
|
|||||||
use crate::application::dtos::address_book_dto::AddressBookDto;
|
use crate::application::dtos::address_book_dto::AddressBookDto;
|
||||||
use crate::application::dtos::contact_dto::ContactDto;
|
use crate::application::dtos::contact_dto::ContactDto;
|
||||||
|
|
||||||
|
/// Emit a WebDAV `getetag` value as `"…"` with the surrounding quotes written
|
||||||
|
/// as borrowed pre-escaped `"` text events around the escaped etag body.
|
||||||
|
///
|
||||||
|
/// Byte-identical to escaping the whole `"{etag}"` String — `quick_xml` escapes
|
||||||
|
/// a literal `"` to `"`, so the one-String form re-allocated an owned `Cow`
|
||||||
|
/// on write — but with **0 heap allocs per contact** (the NextCloud
|
||||||
|
/// `write_etag_element` pattern, benches/ROUND20.md §C1). Called per contact on
|
||||||
|
/// the CardDAV multiget/PROPFIND emit path.
|
||||||
|
fn write_quoted_etag<W: Write>(xml_writer: &mut Writer<W>, etag: &str) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::new(etag)))?;
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Render a requested property as a namespaced response element name, mapping
|
/// Render a requested property as a namespaced response element name, mapping
|
||||||
/// the known namespaces to their response prefixes (`D:` for DAV, `CR:` for
|
/// the known namespaces to their response prefixes (`D:` for DAV, `CR:` for
|
||||||
/// CardDAV). Used for the catch-all arms of the requested-property writers so
|
/// CardDAV). Used for the catch-all arms of the requested-property writers so
|
||||||
@@ -386,7 +401,7 @@ impl CardDavAdapter {
|
|||||||
|
|
||||||
// getetag
|
// getetag
|
||||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
|
write_quoted_etag(xml_writer, &book.id)?;
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||||
|
|
||||||
// getcontenttype
|
// getcontenttype
|
||||||
@@ -468,8 +483,7 @@ impl CardDavAdapter {
|
|||||||
}
|
}
|
||||||
("DAV:", "getetag") => {
|
("DAV:", "getetag") => {
|
||||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||||
xml_writer
|
write_quoted_etag(xml_writer, &book.id)?;
|
||||||
.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
|
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||||
}
|
}
|
||||||
("DAV:", "getcontenttype") => {
|
("DAV:", "getcontenttype") => {
|
||||||
@@ -755,11 +769,7 @@ impl CardDavAdapter {
|
|||||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||||
|
|
||||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||||
let mut quoted = String::with_capacity(contact.etag.len() + 2);
|
write_quoted_etag(xml_writer, &contact.etag)?;
|
||||||
quoted.push('"');
|
|
||||||
quoted.push_str(&contact.etag);
|
|
||||||
quoted.push('"');
|
|
||||||
xml_writer.write_event(Event::Text(BytesText::new("ed)))?;
|
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||||
|
|
||||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||||
@@ -779,11 +789,7 @@ impl CardDavAdapter {
|
|||||||
}
|
}
|
||||||
("DAV:", "getetag") => {
|
("DAV:", "getetag") => {
|
||||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||||
let mut quoted = String::with_capacity(contact.etag.len() + 2);
|
write_quoted_etag(xml_writer, &contact.etag)?;
|
||||||
quoted.push('"');
|
|
||||||
quoted.push_str(&contact.etag);
|
|
||||||
quoted.push('"');
|
|
||||||
xml_writer.write_event(Event::Text(BytesText::new("ed)))?;
|
|
||||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||||
}
|
}
|
||||||
("DAV:", "getcontenttype") => {
|
("DAV:", "getcontenttype") => {
|
||||||
@@ -1017,8 +1023,24 @@ pub fn contact_to_vcard(contact: &ContactDto) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(bday) = &contact.birthday {
|
if let Some(bday) = &contact.birthday {
|
||||||
|
// Stack render (byte-identical to chrono's `%Y-%m-%d`) with the chrono
|
||||||
|
// fallback for out-of-range years — drops the strftime interpreter + a
|
||||||
|
// heap alloc per contact-with-birthday (fmt::compact_date is the
|
||||||
|
// date-only companion to the §V2 REV renderer above).
|
||||||
|
use chrono::Datelike as _;
|
||||||
|
let mut bday_buf = [0u8; 10];
|
||||||
|
match crate::common::fmt::compact_date(&mut bday_buf, bday.year(), bday.month(), bday.day())
|
||||||
|
{
|
||||||
|
Some(s) => {
|
||||||
|
vcard.push_str("BDAY:");
|
||||||
|
vcard.push_str(s);
|
||||||
|
vcard.push_str("\r\n");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
|
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(photo) = &contact.photo_url {
|
if let Some(photo) = &contact.photo_url {
|
||||||
let _ = write!(vcard, "PHOTO;VALUE=URI:{}\r\n", photo);
|
let _ = write!(vcard, "PHOTO;VALUE=URI:{}\r\n", photo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,31 @@ pub fn compact_ical_utc(buf: &mut [u8; 16], secs: i64) -> Option<&str> {
|
|||||||
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
|
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `chrono::NaiveDate::format("%Y-%m-%d")` for a calendar date: the vCard
|
||||||
|
/// `BDAY` / ISO date form `2026-07-17` (10 bytes) written into `buf`.
|
||||||
|
///
|
||||||
|
/// The vCard emit path (`contact_to_vcard`) stamps `BDAY` per
|
||||||
|
/// contact-with-birthday, and `write!(…, "{}", date.format("%Y-%m-%d"))` runs
|
||||||
|
/// chrono's strftime interpreter and heap-allocates — the same interpreter cost
|
||||||
|
/// [`compact_ical_utc`] removed for the `REV` stamp (benches/ROUND19.md §V2:
|
||||||
|
/// 3→0 allocs). This is the date-only companion to that helper.
|
||||||
|
///
|
||||||
|
/// Takes the pre-split `year`/`month`/`day` (so `fmt` stays chrono-free off the
|
||||||
|
/// test path); callers read them via `chrono::Datelike`. Returns `None` when
|
||||||
|
/// `year` is outside the fixed-width 4-digit range — where chrono widens or
|
||||||
|
/// sign-prefixes `%Y` — so callers keep the chrono path as fallback.
|
||||||
|
pub fn compact_date(buf: &mut [u8; 10], year: i32, month: u32, day: u32) -> Option<&str> {
|
||||||
|
if !(0..=9999).contains(&year) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
push4(buf, 0, year as i64);
|
||||||
|
buf[4] = b'-';
|
||||||
|
push2(buf, 5, month);
|
||||||
|
buf[7] = b'-';
|
||||||
|
push2(buf, 8, day);
|
||||||
|
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Append the upper-cased form of `s` to `buf` without a temporary `String`.
|
/// Append the upper-cased form of `s` to `buf` without a temporary `String`.
|
||||||
///
|
///
|
||||||
/// Byte-identical to `buf.push_str(&s.to_uppercase())` — same
|
/// Byte-identical to `buf.push_str(&s.to_uppercase())` — same
|
||||||
@@ -349,14 +374,42 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compact_date_matches_chrono() {
|
||||||
|
use chrono::{Datelike, NaiveDate};
|
||||||
|
// Padding (day/month < 10), leap day, min/max in-range 4-digit year,
|
||||||
|
// 3-digit year (chrono zero-pads %Y to 4).
|
||||||
|
let cases = [
|
||||||
|
(2026, 7, 17),
|
||||||
|
(2000, 2, 29),
|
||||||
|
(2005, 7, 1),
|
||||||
|
(1970, 1, 1),
|
||||||
|
(9999, 12, 31),
|
||||||
|
(1, 1, 1),
|
||||||
|
(876, 5, 9),
|
||||||
|
];
|
||||||
|
for (y, m, d) in cases {
|
||||||
|
let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
|
||||||
|
let mut buf = [0u8; 10];
|
||||||
|
assert_eq!(
|
||||||
|
compact_date(&mut buf, date.year(), date.month(), date.day()).expect("in range"),
|
||||||
|
date.format("%Y-%m-%d").to_string(),
|
||||||
|
"date={y}-{m}-{d}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn out_of_range_falls_back() {
|
fn out_of_range_falls_back() {
|
||||||
let mut b3 = [0u8; 25];
|
let mut b3 = [0u8; 25];
|
||||||
let mut b2 = [0u8; 31];
|
let mut b2 = [0u8; 31];
|
||||||
let mut bc = [0u8; 16];
|
let mut bc = [0u8; 16];
|
||||||
|
let mut bd = [0u8; 10];
|
||||||
assert!(rfc3339_utc(&mut b3, -1).is_none());
|
assert!(rfc3339_utc(&mut b3, -1).is_none());
|
||||||
assert!(rfc2822_utc(&mut b2, -1).is_none());
|
assert!(rfc2822_utc(&mut b2, -1).is_none());
|
||||||
assert!(compact_ical_utc(&mut bc, -1).is_none());
|
assert!(compact_ical_utc(&mut bc, -1).is_none());
|
||||||
|
assert!(compact_date(&mut bd, -1, 1, 1).is_none());
|
||||||
|
assert!(compact_date(&mut bd, 10000, 1, 1).is_none());
|
||||||
assert!(rfc3339_utc(&mut b3, MAX_4DIGIT_YEAR_SECS + 1).is_none());
|
assert!(rfc3339_utc(&mut b3, MAX_4DIGIT_YEAR_SECS + 1).is_none());
|
||||||
assert!(compact_ical_utc(&mut bc, MAX_4DIGIT_YEAR_SECS + 1).is_none());
|
assert!(compact_ical_utc(&mut bc, MAX_4DIGIT_YEAR_SECS + 1).is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get events in time range: {}", e))
|
DomainError::database_error(format!("Failed to get events in time range: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let mut event = CalendarEvent::with_id(
|
let mut event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -288,7 +288,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get events by calendar: {}", e))
|
DomainError::database_error(format!("Failed to get events by calendar: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let mut event = CalendarEvent::with_id(
|
let mut event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -341,7 +341,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to find events by summary: {}", e))
|
DomainError::database_error(format!("Failed to find events by summary: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let mut event = CalendarEvent::with_id(
|
let mut event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -515,7 +515,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get calendar events by UIDs: {}", e))
|
DomainError::database_error(format!("Failed to get calendar events by UIDs: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let mut event = CalendarEvent::with_id(
|
let mut event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -664,7 +664,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let event = CalendarEvent::with_id(
|
let event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -719,7 +719,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to find recurring events in range: {}", e))
|
DomainError::database_error(format!("Failed to find recurring events in range: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let event = CalendarEvent::with_id(
|
let event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get calendars by owner: {}", e))
|
DomainError::database_error(format!("Failed to get calendars by owner: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut calendars = Vec::new();
|
let mut calendars = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let calendar = Calendar::with_id(
|
let calendar = Calendar::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -292,7 +292,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get public calendars: {}", e))
|
DomainError::database_error(format!("Failed to get public calendars: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut calendars = Vec::new();
|
let mut calendars = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let calendar = Calendar::with_id(
|
let calendar = Calendar::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
@@ -400,7 +400,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get calendar properties: {}", e))
|
DomainError::database_error(format!("Failed to get calendar properties: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut properties = std::collections::HashMap::new();
|
let mut properties = std::collections::HashMap::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
properties.insert(row.get("name"), row.get("value"));
|
properties.insert(row.get("name"), row.get("value"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ impl ContactGroupRepository for ContactGroupPgRepository {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
let email_json: JsonValue = row.get("email");
|
let email_json: JsonValue = row.get("email");
|
||||||
let phone_json: JsonValue = row.get("phone");
|
let phone_json: JsonValue = row.get("phone");
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get contacts by uids: {}", e))
|
DomainError::database_error(format!("Failed to get contacts by uids: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get contacts by address book: {}", e))
|
DomainError::database_error(format!("Failed to get contacts by address book: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
@@ -376,7 +376,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
@@ -404,7 +404,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get contacts by email: {}", e))
|
DomainError::database_error(format!("Failed to get contacts by email: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
@@ -434,7 +434,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to get contacts by group: {}", e))
|
DomainError::database_error(format!("Failed to get contacts by group: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ impl ContactRepository for ContactPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to search contacts: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to search contacts: {}", e)))?;
|
||||||
|
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
contacts.push(Self::row_to_contact(row)?);
|
contacts.push(Self::row_to_contact(row)?);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -860,7 +860,12 @@ impl DedupService {
|
|||||||
|
|
||||||
let mut received: Vec<(String, u64)> = Vec::new();
|
let mut received: Vec<(String, u64)> = Vec::new();
|
||||||
let mut new_rows: Vec<(String, i64)> = Vec::new();
|
let mut new_rows: Vec<(String, i64)> = Vec::new();
|
||||||
let mut seen: HashSet<String> = HashSet::new();
|
// Intra-request dedup set keyed on the raw 32-byte BLAKE3 digest
|
||||||
|
// (`[u8; 32]`, `Copy` — no per-distinct-chunk 64-byte `String` heap
|
||||||
|
// key), mirroring the streaming ingest loop (benches/ROUND17.md §D2).
|
||||||
|
// hex ↔ digest is bijective, so membership is identical to the old
|
||||||
|
// `HashSet<String>`.
|
||||||
|
let mut seen: HashSet<[u8; 32]> = HashSet::new();
|
||||||
|
|
||||||
while let Some(frame) = frames.next().await {
|
while let Some(frame) = frames.next().await {
|
||||||
let data = frame?;
|
let data = frame?;
|
||||||
@@ -870,14 +875,21 @@ impl DedupService {
|
|||||||
data.len()
|
data.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let hash = blake3::hash(&data).to_hex().to_string();
|
let digest = blake3::hash(&data);
|
||||||
received.push((hash.clone(), data.len() as u64));
|
let hash = digest.to_hex().to_string();
|
||||||
if seen.insert(hash.clone()) {
|
let len = data.len();
|
||||||
let len = data.len() as i64;
|
if seen.insert(*digest.as_bytes()) {
|
||||||
self.backend
|
self.backend
|
||||||
.put_blob_from_bytes_unsynced(&hash, data)
|
.put_blob_from_bytes_unsynced(&hash, data)
|
||||||
.await?;
|
.await?;
|
||||||
new_rows.push((hash, len));
|
// First occurrence: `received` needs a copy, `new_rows` moves it.
|
||||||
|
received.push((hash.clone(), len as u64));
|
||||||
|
new_rows.push((hash, len as i64));
|
||||||
|
} else {
|
||||||
|
// Duplicate within this request — move the hex into `received`
|
||||||
|
// (no clone; the blob is already registered by its first
|
||||||
|
// occurrence). Same `received` sequence, input order preserved.
|
||||||
|
received.push((hash, len as u64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,12 +1228,17 @@ impl DedupService {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut guard = state.lock().await;
|
let mut guard = state.lock().await;
|
||||||
let hashes: Vec<String> = batch.iter().map(|(h, _)| h.clone()).collect();
|
|
||||||
|
|
||||||
// Pin-or-classify in one statement: rows that exist take this
|
// Pin-or-classify in one statement: rows that exist take this
|
||||||
// session's reference NOW; hashes not returned don't exist and are
|
// session's reference NOW; hashes not returned don't exist and are
|
||||||
// ours to write.
|
// ours to write. Bind borrowed `&str`s — sqlx encodes `&[&str]` to
|
||||||
let pinned: HashSet<String> = sqlx::query_scalar::<_, String>(
|
// `text[]` identically to the owned Strings the old `.clone()` built,
|
||||||
|
// so no per-chunk hash String is allocated just to run the query
|
||||||
|
// (the pattern favorites_pg_repository.rs:271 already uses). The
|
||||||
|
// borrow is scoped so it ends before `batch` is moved below.
|
||||||
|
let pinned: HashSet<String> = {
|
||||||
|
let hashes: Vec<&str> = batch.iter().map(|(h, _)| h.as_str()).collect();
|
||||||
|
sqlx::query_scalar::<_, String>(
|
||||||
"UPDATE storage.blobs SET ref_count = ref_count + 1, orphaned_at = NULL
|
"UPDATE storage.blobs SET ref_count = ref_count + 1, orphaned_at = NULL
|
||||||
WHERE hash = ANY($1)
|
WHERE hash = ANY($1)
|
||||||
RETURNING hash",
|
RETURNING hash",
|
||||||
@@ -1233,7 +1250,8 @@ impl DedupService {
|
|||||||
DomainError::internal_error("Dedup", format!("Failed to pin existing chunks: {e}"))
|
DomainError::internal_error("Dedup", format!("Failed to pin existing chunks: {e}"))
|
||||||
})?
|
})?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
let mut to_write: Vec<(String, Bytes)> = Vec::with_capacity(batch.len());
|
let mut to_write: Vec<(String, Bytes)> = Vec::with_capacity(batch.len());
|
||||||
for (hash, data) in batch {
|
for (hash, data) in batch {
|
||||||
|
|||||||
@@ -465,11 +465,13 @@ fn write_trash_item_response<W: std::io::Write>(
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// d:getcontenttype
|
// d:getcontenttype — the folder constant is borrowed (`Cow::Borrowed`, 0
|
||||||
let content_type = if item.item_type == "folder" {
|
// allocs per trashed folder row); only the file branch (mime_guess) still
|
||||||
"httpd/unix-directory".to_string()
|
// allocates its owned String (ROUND16 §M1 `Cow<'static, str>` pattern).
|
||||||
|
let content_type: std::borrow::Cow<'static, str> = if item.item_type == "folder" {
|
||||||
|
std::borrow::Cow::Borrowed("httpd/unix-directory")
|
||||||
} else {
|
} else {
|
||||||
mime_from_name(&item.name)
|
std::borrow::Cow::Owned(mime_from_name(&item.name))
|
||||||
};
|
};
|
||||||
write_text_element(xml, "d:getcontenttype", &content_type)?;
|
write_text_element(xml, "d:getcontenttype", &content_type)?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user