perf(round27): NextCloud PROPFIND oc:id per-row buffer, contact JSONB write direct-serialize

Two behaviour-preserving allocation cuts (benches/ROUND27.md), each with a
counting-allocator BEFORE/AFTER gate that rolls back if AFTER does not allocate
fewer than BEFORE:

- H1 NextCloud PROPFIND: the streaming page loops built oc:id as a fresh String
  per child (format_oc_id -> format!("{:08}{}", id, instance)). Add
  format_oc_id_into(&mut buf, id, svc) and compute into one oc_buf reused across
  the page (next to the existing href buffer) — 1 String/row -> 0. 998->0
  per-row allocs on a 500-row page, 2.16x wall. The write fns still take
  Option<&str>, so no signature change; oc:id bytes identical. Scoped to the two
  PROPFIND page loops (the hot directory-listing path); REPORT/trashbin deferred.
- P2 contact create/update: bind sqlx::types::Json(&dtos) (Encode runs to_writer
  straight into the JSONB buffer) instead of serde_json::to_value(&dtos) + bind,
  which built a throwaway Value DOM per JSONB column. Write-side twin of ROUND23
  J1. 21->2 allocs, 4.68x wall for a 3-entry column. Behaviour-preserving:
  to_value sorts keys and direct serialize keeps struct order, but Postgres
  normalizes JSONB key order so the stored value is identical (verified via psql:
  '{...alpha...}'::jsonb = '{...struct...}'::jsonb -> t), and reads decode by
  field name; the etag comes from the domain entity, not the stored JSONB.

Adds bench_round27_micro. Verified: cargo fmt clean, cargo clippy --features
bench -D warnings clean (real exit), cargo test --lib --features bench = 529
passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8gs91AhmazoxMsDcNk3KT
This commit is contained in:
Claude
2026-07-21 01:30:53 +00:00
parent eeb41c9c28
commit 8c936de50d
5 changed files with 358 additions and 21 deletions
@@ -1,5 +1,4 @@
use chrono::Utc;
use serde_json::Value as JsonValue;
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
@@ -98,10 +97,6 @@ impl ContactRepository for ContactPgRepository {
let phone_dtos = phones_to_persistence(contact.phone());
let address_dtos = addresses_to_persistence(contact.address());
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
let row = sqlx::query(
r#"
INSERT INTO carddav.contacts (
@@ -126,9 +121,9 @@ impl ContactRepository for ContactPgRepository {
.bind(contact.first_name_owned())
.bind(contact.last_name_owned())
.bind(contact.nickname_owned())
.bind(email_json)
.bind(phone_json)
.bind(address_json)
.bind(sqlx::types::Json(&email_dtos))
.bind(sqlx::types::Json(&phone_dtos))
.bind(sqlx::types::Json(&address_dtos))
.bind(contact.organization_owned())
.bind(contact.title_owned())
.bind(contact.notes_owned())
@@ -153,10 +148,6 @@ impl ContactRepository for ContactPgRepository {
let phone_dtos = phones_to_persistence(contact.phone());
let address_dtos = addresses_to_persistence(contact.address());
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
// Create a clone of the contact with the updated timestamp
let mut updated_contact = contact.clone();
updated_contact.set_updated_at(now);
@@ -192,9 +183,9 @@ impl ContactRepository for ContactPgRepository {
.bind(updated_contact.first_name_owned())
.bind(updated_contact.last_name_owned())
.bind(updated_contact.nickname_owned())
.bind(email_json)
.bind(phone_json)
.bind(address_json)
.bind(sqlx::types::Json(&email_dtos))
.bind(sqlx::types::Json(&phone_dtos))
.bind(sqlx::types::Json(&address_dtos))
.bind(updated_contact.organization_owned())
.bind(updated_contact.title_owned())
.bind(updated_contact.notes_owned())
+33 -6
View File
@@ -1610,8 +1610,10 @@ fn build_nc_streaming_propfind(
{
let mut xml = Writer::new(&mut chunk);
// One href buffer reused across the page instead of a fresh
// format! String per child (benches/ROUND19.md §M6).
// format! String per child (benches/ROUND19.md §M6); likewise
// one oc:id buffer (benches/ROUND27.md §H1).
let mut href = String::new();
let mut oc_buf = String::new();
for file in batch.iter() {
let dead = dead_props_for(&file.id, &file_deads);
// Only the name varies per row — the encoded
@@ -1622,8 +1624,14 @@ fn build_nc_streaming_propfind(
href.push_str(&child_href_prefix);
href.push_str(&urlencoding::encode(&file.name));
let fid = nc_id_of(&file_id_map, &file.id);
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
write_file_response(&mut xml, file, &href, (fid, oc_id.as_deref()), &username, &favs, dead)
let oc_id: Option<&str> = match fid {
Some(id) => {
format_oc_id_into(&mut oc_buf, id, file_id_svc);
Some(oc_buf.as_str())
}
None => None,
};
write_file_response(&mut xml, file, &href, (fid, oc_id), &username, &favs, dead)
.map_err(std::io::Error::other)?;
}
}
@@ -1675,8 +1683,10 @@ fn build_nc_streaming_propfind(
let mut chunk = Vec::with_capacity(batch.len() * 1024);
{
let mut xml = Writer::new(&mut chunk);
// One href buffer reused across the page (benches/ROUND19.md §M6).
// One href buffer reused across the page (benches/ROUND19.md
// §M6); likewise one oc:id buffer (benches/ROUND27.md §H1).
let mut href = String::new();
let mut oc_buf = String::new();
for sf in batch.iter() {
let dead = dead_props_for(&sf.id, &sub_deads);
// Collections carry the trailing slash; prefix
@@ -1686,8 +1696,14 @@ fn build_nc_streaming_propfind(
href.push_str(&urlencoding::encode(&sf.name));
href.push('/');
let fid = nc_id_of(&sub_id_map, &sf.id);
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
write_folder_response(&mut xml, sf, &href, (fid, oc_id.as_deref()), &username, &favs, quota, dead)
let oc_id: Option<&str> = match fid {
Some(id) => {
format_oc_id_into(&mut oc_buf, id, file_id_svc);
Some(oc_buf.as_str())
}
None => None,
};
write_folder_response(&mut xml, sf, &href, (fid, oc_id), &username, &favs, quota, dead)
.map_err(std::io::Error::other)?;
}
}
@@ -2062,6 +2078,17 @@ pub fn format_oc_id(id: i64, svc: Option<&Arc<NextcloudFileIdService>>) -> Strin
}
}
/// Write `oc:id` (`{:08}{instance_id}`) into a caller-provided buffer reused
/// across a PROPFIND/REPORT page — the 0-alloc form of [`format_oc_id`] for the
/// emit loops, replacing a fresh `String` per child (benches/ROUND27.md §H1).
/// Output is byte-identical to `format_oc_id`.
pub fn format_oc_id_into(out: &mut String, id: i64, svc: Option<&Arc<NextcloudFileIdService>>) {
use std::fmt::Write as _;
out.clear();
let _ = write!(out, "{id:08}");
out.push_str(svc.map(|s| s.instance_id()).unwrap_or("ocnca"));
}
#[cfg(test)]
mod tests {
use super::*;