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())