perf: round 18 — calendar-event in-place iCal edit, ResourceList incremental id-index
Two items from the ROUND17 deferred list, each benchmark-gated with a
BEFORE/AFTER equivalence gate and a rollback-on-regression check (ROUND2–17
discipline). See benches/ROUND18.md.
[C1] backend — CalendarEvent::update_ical_property / remove_ical_property
rewrote the ENTIRE ical_data body with format!("{}{}{}") on every call and
allocated two search needles per call. calendar_storage_adapter::update_event
fans a multi-field edit out into one call per changed field, so a full REST
edit paid one full-body (up to ~11 KB) allocation per property. The body is
now mutated in place (replace_range for an existing property, four insert/
insert_str for a new one, byte-identical spans) and the single "\nNAME:"
needle is built on the stack (the "\r\nNAME:" needle was redundant — the LF
form is its suffix). bench_round18_micro [C1]: 70 -> 2 allocs/op (68 fewer),
2.46x wall, emitted body byte-identical.
[F1] frontend — ResourceList itemIndexById rebuilt a fresh Map over the whole
accumulated list every infinite-scroll page (O(N)/page, O(N^2) drain) and,
being a new instance each page, re-fired the reap-stale effect (another O(N)
id Set/page). New ItemIndexBuilder extends a persistent Map with the fresh
page only and reuses the reference across appends; the reap-stale effect now
tests membership against it. round18.bench.test.ts [F1]: 40x50 drain 74.1 ->
6.4 ms (11.5x), deep-equal to the reference at every page, reference-contract
gate (same-ref append / new-ref rebuild).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoxFtikahM1N4PE5s3ZVH3
This commit is contained in:
@@ -1092,40 +1092,70 @@ impl CalendarEvent {
|
||||
* @param property_name The name of the property to update
|
||||
* @param value The new value for the property
|
||||
*/
|
||||
/// Write `\n{name}:` into `buf` and return it as `&str`, or `None` if the
|
||||
/// name is too long to fit (unreachable for RFC 5545 property names, the
|
||||
/// longest of which — `LAST-MODIFIED`, `RECURRENCE-ID` — are 13 bytes).
|
||||
///
|
||||
/// The bare-LF form is deliberate: `\n{name}:` is a suffix of the CRLF form
|
||||
/// `\r\n{name}:`, so a single search for it matches a property line whether
|
||||
/// the body is LF- or CRLF-terminated and returns the LF offset either way
|
||||
/// — behaviour-identical to the old `find("\n..").or(find("\r\n.."))` (the
|
||||
/// CRLF needle could never match where the LF one didn't). Built on the
|
||||
/// stack: no per-call heap needle.
|
||||
fn line_needle<'a>(buf: &'a mut [u8; 64], name: &str) -> Option<&'a str> {
|
||||
let n = name.len();
|
||||
if n + 2 > buf.len() {
|
||||
return None;
|
||||
}
|
||||
buf[0] = b'\n';
|
||||
buf[1..1 + n].copy_from_slice(name.as_bytes());
|
||||
buf[1 + n] = b':';
|
||||
// `name` is valid UTF-8 and only ASCII bytes were added around it.
|
||||
std::str::from_utf8(&buf[..n + 2]).ok()
|
||||
}
|
||||
|
||||
fn update_ical_property(&mut self, property_name: &str, value: &str) {
|
||||
let search_str = format!("\n{}:", property_name);
|
||||
let search_str_alt = format!("\r\n{}:", property_name);
|
||||
let mut buf = [0u8; 64];
|
||||
let needle_owned;
|
||||
let needle: &str = match Self::line_needle(&mut buf, property_name) {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
needle_owned = format!("\n{property_name}:");
|
||||
&needle_owned
|
||||
}
|
||||
};
|
||||
|
||||
// Check if property exists
|
||||
let pos = self
|
||||
.ical_data
|
||||
.find(&search_str)
|
||||
.or_else(|| self.ical_data.find(&search_str_alt));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
// Find the start of the value
|
||||
let value_start = pos + search_str.len();
|
||||
|
||||
// Find the end of the value (next line or end of string)
|
||||
if let Some(pos) = self.ical_data.find(needle) {
|
||||
// Value spans from just after `\n{NAME}:` to the next LF (or EOF).
|
||||
let value_start = pos + needle.len();
|
||||
let value_end = self.ical_data[value_start..]
|
||||
.find('\n')
|
||||
.map(|p| value_start + p)
|
||||
.unwrap_or_else(|| self.ical_data.len());
|
||||
.map_or(self.ical_data.len(), |p| value_start + p);
|
||||
|
||||
// Replace the value
|
||||
let before = &self.ical_data[..value_start];
|
||||
let after = &self.ical_data[value_end..];
|
||||
self.ical_data = format!("{}{}{}", before, value, after);
|
||||
// In place: reuses the body's own buffer (growing it once only when
|
||||
// the new value is longer) instead of allocating a whole fresh body
|
||||
// String per property, as the old `format!("{}{}{}")` did — on a
|
||||
// multi-field edit that was one full-body (up to ~11 KB) allocation
|
||||
// per changed property.
|
||||
self.ical_data.replace_range(value_start..value_end, value);
|
||||
} else {
|
||||
// Property doesn't exist, add it before END:VEVENT
|
||||
// Property absent: insert `{NAME}:{value}\n` before END:VEVENT.
|
||||
let end_pos = self
|
||||
.ical_data
|
||||
.find("END:VEVENT")
|
||||
.unwrap_or(self.ical_data.len());
|
||||
|
||||
let before = &self.ical_data[..end_pos];
|
||||
let after = &self.ical_data[end_pos..];
|
||||
self.ical_data = format!("{}{}:{}\n{}", before, property_name, value, after);
|
||||
// Insert the four pieces at one point in reverse order so the result
|
||||
// is `{NAME}:{value}\n` before END:VEVENT — byte-identical to the old
|
||||
// `format!("{}{}:{}\n{}")` — without allocating a fresh body String
|
||||
// (nor a value-sized fragment). One `reserve` caps it at a single
|
||||
// grow; the shifted tail is just the trailing END:VEVENT/VCALENDAR.
|
||||
self.ical_data
|
||||
.reserve(property_name.len() + value.len() + 2);
|
||||
self.ical_data.insert(end_pos, '\n');
|
||||
self.ical_data.insert_str(end_pos, value);
|
||||
self.ical_data.insert(end_pos, ':');
|
||||
self.ical_data.insert_str(end_pos, property_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,26 +1165,25 @@ impl CalendarEvent {
|
||||
* @param property_name The name of the property to remove
|
||||
*/
|
||||
fn remove_ical_property(&mut self, property_name: &str) {
|
||||
let search_str = format!("\n{}:", property_name);
|
||||
let search_str_alt = format!("\r\n{}:", property_name);
|
||||
let mut buf = [0u8; 64];
|
||||
let needle_owned;
|
||||
let needle: &str = match Self::line_needle(&mut buf, property_name) {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
needle_owned = format!("\n{property_name}:");
|
||||
&needle_owned
|
||||
}
|
||||
};
|
||||
|
||||
// Check if property exists
|
||||
let pos = self
|
||||
.ical_data
|
||||
.find(&search_str)
|
||||
.or_else(|| self.ical_data.find(&search_str_alt));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
// Find the end of the value (next line or end of string)
|
||||
if let Some(pos) = self.ical_data.find(needle) {
|
||||
// Delete from the property's leading LF (`pos`) through the end of
|
||||
// its value line (exclusive of the next line's LF) — byte-identical
|
||||
// to the old `format!("{}{}", &data[..pos], &data[value_end..])`,
|
||||
// but in place, with no fresh body String.
|
||||
let value_end = self.ical_data[pos + 1..]
|
||||
.find('\n')
|
||||
.map(|p| pos + 1 + p)
|
||||
.unwrap_or_else(|| self.ical_data.len());
|
||||
|
||||
// Remove the property
|
||||
let before = &self.ical_data[..pos];
|
||||
let after = &self.ical_data[value_end..];
|
||||
self.ical_data = format!("{}{}", before, after);
|
||||
.map_or(self.ical_data.len(), |p| pos + 1 + p);
|
||||
self.ical_data.replace_range(pos..value_end, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user