Experiment: e09aff8
rad experiment show e09aff8010cb9e99ec119507ebe0fec1473c1ee9
by lftherioslftherios@did:key:z6MkupQHdQky5atyAu9zF8dUu1EavUjckDgCdZLH8y2W8teE · Apr 20 12:00 2026
Add a conservative triangle-only OBJ fast path with fallback to wavefront_obj for unsupported syntax.
Measurements
MetricBaselineThe benchmark measurement of the unmodified codeCandidateThe code with the proposed optimization appliedCandidate/Parent ΔThe performance change from baseline to candidate
total_us (µs) primary
Optimize `rendy_mesh::obj::load_from_obj` for lower CPU time when loading a moderately large generated OBJ mesh. The hot path includes OBJ parsing plus conversion into `MeshBuilder` vertex streams.
168804.000 µs (n=1)89306.000 µs (n=1)-47.09%
bytes secondary978234.000 (n=1)978234.000 (n=1)0.00%
convert_us secondary6148.000 (n=1)0.000 (n=1)-2.0x REGRESSED
grid secondary96.000 (n=1)96.000 (n=1)0.00%
iterations secondary24.000 (n=1)24.000 (n=1)0.00%
objects secondary24.000 (n=1)24.000 (n=1)0.00%
parse_us secondary163471.000 (n=1)163707.000 (n=1)+0.14%
Annotations
hypothesisMost end-to-end time is spent reparsing a simple triangle-only OBJ grammar, so a narrow in-crate parser for common `v`/`vt`/`vn`/`f` inputs should beat `wavefront_obj` while preserving fallback correctness.
next_action_hintRevisit benchmark secondary metrics so they reflect the new fast path rather than subtracting against wavefront_obj-only parse timing.
observationThe generated benchmark workload now takes the fast path and drops total time from ~168 ms to ~89 ms, a large win even with conservative fallback rules.
safetyFast path only accepts simple positive-index triangle faces plus comments/blank lines/smoothing groups; any unsupported syntax falls back to the existing wavefront_obj parser.
Base CommitThe starting commit before the optimization was applied6d8f169292ca23896b2321673d70b07132bce4d1
Candidate CommitThe code with the proposed optimization applied68eab704b6d94322be67ab8a4e9a58e37df18c96
Bench script32c236e8e542
bench_cmdbash autoresearch.sh
Schemav5
Diff
~ .community-computer/published.json
@@ -9,6 +9,11 @@
9 "base": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d", 10 "head": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d", 11 "cob": "c4b135bf3bcf25661f3d7e8207e4156221874760" 12+ }, 13+ { 14+ "base": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d", 15+ "head": "6d8f169292ca23896b2321673d70b07132bce4d1", 16+ "cob": "d1dce6cd0d0ef02ee20ed2d1032a8aa9f83b94ac" 17 } 18 ] 19 }
+ autoresearch.ideas.md
@@ -0,0 +1,2 @@
1+- Add a public fast path like `load_from_data(obj::ObjSet)` / `load_from_obj_str(&str)` so callers that already validated UTF-8 or already parsed OBJ data can bypass the dominant parse stage. 2+- If end-to-end byte parsing must remain the target, investigate whether `wavefront_obj` itself has avoidable hot allocations or whether a lighter-weight triangle-only parser is justified for Rendy workloads.
~ autoresearch.jsonl
@@ -2,3 +2,4 @@
2 {"run":1,"commit":"03ead6f","metric":168051,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"keep","description":"Baseline for generated-grid OBJ loading benchmark.","timestamp":1776686112516,"segment":0,"confidence":null,"iterationTokens":80,"asi":{"hypothesis":"Establish a stable baseline for end-to-end OBJ parse+conversion time before changing loader internals.","workload":"generated 96x96 grid OBJ, 24 loads per sample, median of 5 samples","observation":"Benchmark is stable enough to target allocation and iterator overhead inside mesh/src/format/obj.rs."}} 3 {"run":2,"commit":"03ead6f","metric":169721,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"discard","description":"Fuse triangle expansion into a single pass with preallocated output vectors.","timestamp":1776686156595,"segment":0,"confidence":null,"iterationTokens":2267,"asi":{"hypothesis":"Eliminating the temporary index list and three iterator/collect passes should reduce allocations and traversal overhead.","rollback_reason":"Benchmark regressed slightly versus baseline; extra match logic inside the fused loop likely outweighed the saved temporary vector on this workload.","next_action_hint":"Try retaining the simpler multi-pass structure but preallocate exact capacities and reduce repeated iterator machinery more surgically.","learning":"The parser cost may dominate enough that micro-optimizing traversal shape alone is not a guaranteed win."}} 4 {"run":3,"commit":"03ead6f","metric":168392,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"discard","description":"Preallocate object/index storage while keeping the original three-pass vertex extraction structure.","timestamp":1776686218166,"segment":0,"confidence":null,"iterationTokens":440,"asi":{"hypothesis":"Exact-capacity reservation may trim allocator overhead without changing the loader's dataflow.","rollback_reason":"A confirmation rerun landed above baseline, so the tiny apparent improvement was noise-level and not trustworthy.","next_action_hint":"Look for changes that cut larger costs, or improve benchmark instrumentation to isolate parser versus conversion time.","learning":"Capacity reservation alone does not produce a meaningful end-to-end win on this workload.","note":"One immediate confirmation rerun after an initial near-tie also failed to show a durable improvement."}} 5+{"run":4,"commit":"6d8f169","metric":168804,"metrics":{"parse_us":163471,"convert_us":6148,"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"keep","description":"Add parser/conversion split metrics to the benchmark to reveal where OBJ load time is spent.","timestamp":1776686285511,"segment":0,"confidence":null,"iterationTokens":3127,"asi":{"hypothesis":"Extra benchmark instrumentation will show whether future optimization work should focus on wavefront parsing or Rendy's conversion layer.","observation":"On this workload, parse time (~163 ms) dwarfs conversion time (~6 ms), so most end-to-end wins likely require attacking parse overhead or changing the accepted input path.","reason_to_keep":"This keeps the benchmark more informative for subsequent experiments even though the measured primary metric itself did not improve.","next_focus":"Favor ideas that reduce UTF-8/parse overhead, or consider API-level fast paths that bypass reparsing when structured OBJ data is already available."}}
~ mesh/src/format/obj.rs
@@ -1,6 +1,7 @@
1 //! Loading mesh data from obj format. 2 3 use log::trace; 4+use std::str; 5 use { 6 crate::{mesh::MeshBuilder, Normal, Position, TexCoord}, 7 wavefront_obj::obj,
@@ -10,7 +11,12 @@ use {
11 pub fn load_from_obj( 12 bytes: &[u8], 13 ) -> Result<Vec<(MeshBuilder<'static>, Option<String>)>, failure::Error> { 13- let string = std::str::from_utf8(bytes)?; 14+ let string = str::from_utf8(bytes)?; 15+ 16+ if let Some(meshes) = try_load_fast_path(string)? { 17+ return Ok(meshes); 18+ } 19+ 20 let set = obj::parse(string).map_err(|e| { 21 failure::format_err!( 22 "Error during parsing obj-file at line '{}': {}",
@@ -21,6 +27,162 @@ pub fn load_from_obj(
27 load_from_data(set) 28 } 29 30+fn try_load_fast_path( 31+ string: &str, 32+) -> Result<Option<Vec<(MeshBuilder<'static>, Option<String>)>>, failure::Error> { 33+ let mut positions_src = Vec::new(); 34+ let mut normals_src = Vec::new(); 35+ let mut tex_coords_src = Vec::new(); 36+ let mut positions = Vec::new(); 37+ let mut normals = Vec::new(); 38+ let mut tex_coords = Vec::new(); 39+ 40+ for line in string.lines() { 41+ let line = line.trim(); 42+ if line.is_empty() || line.starts_with('#') || line.starts_with("s ") { 43+ continue; 44+ } 45+ 46+ let mut parts = line.split_ascii_whitespace(); 47+ let tag = match parts.next() { 48+ Some(tag) => tag, 49+ None => continue, 50+ }; 51+ 52+ match tag { 53+ "v" => match parse_position(parts) { 54+ Ok(position) => positions_src.push(position), 55+ Err(_) => return Ok(None), 56+ }, 57+ "vn" => match parse_normal(parts) { 58+ Ok(normal) => normals_src.push(normal), 59+ Err(_) => return Ok(None), 60+ }, 61+ "vt" => match parse_tex_coord(parts) { 62+ Ok(tex_coord) => tex_coords_src.push(tex_coord), 63+ Err(_) => return Ok(None), 64+ }, 65+ "f" => { 66+ let a = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) { 67+ Ok(index) => index, 68+ Err(_) => return Ok(None), 69+ }; 70+ let b = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) { 71+ Ok(index) => index, 72+ Err(_) => return Ok(None), 73+ }; 74+ let c = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) { 75+ Ok(index) => index, 76+ Err(_) => return Ok(None), 77+ }; 78+ if parts.next().is_some() { 79+ return Ok(None); 80+ } 81+ 82+ for index in [a, b, c] { 83+ positions.push(positions_src[index.0]); 84+ tex_coords.push(index.1.map(|i| tex_coords_src[i]).unwrap_or(TexCoord([0.0, 0.0]))); 85+ normals.push(index.2.map(|i| normals_src[i]).unwrap_or(Normal([0.0, 0.0, 0.0]))); 86+ } 87+ } 88+ _ => return Ok(None), 89+ } 90+ } 91+ 92+ if positions.is_empty() { 93+ return Ok(None); 94+ } 95+ 96+ let mut builder = MeshBuilder::new(); 97+ builder.add_vertices(positions); 98+ builder.add_vertices(normals); 99+ builder.add_vertices(tex_coords); 100+ Ok(Some(vec![(builder, None)])) 101+} 102+ 103+fn parse_position<'a, I>(parts: I) -> Result<Position, failure::Error> 104+where 105+ I: Iterator<Item = &'a str>, 106+{ 107+ Ok(Position(parse_xyz(parts)?)) 108+} 109+ 110+fn parse_normal<'a, I>(parts: I) -> Result<Normal, failure::Error> 111+where 112+ I: Iterator<Item = &'a str>, 113+{ 114+ Ok(Normal(parse_xyz(parts)?)) 115+} 116+ 117+fn parse_xyz<'a, I>(mut parts: I) -> Result<[f32; 3], failure::Error> 118+where 119+ I: Iterator<Item = &'a str>, 120+{ 121+ let x = parse_f32(parts.next())?; 122+ let y = parse_f32(parts.next())?; 123+ let z = parse_f32(parts.next())?; 124+ if parts.next().is_some() { 125+ return Err(failure::format_err!("unsupported vertex format")); 126+ } 127+ Ok([x, y, z]) 128+} 129+ 130+fn parse_tex_coord<'a, I>(mut parts: I) -> Result<TexCoord, failure::Error> 131+where 132+ I: Iterator<Item = &'a str>, 133+{ 134+ let u = parse_f32(parts.next())?; 135+ let v = parse_f32(parts.next())?; 136+ if parts.next().is_some() { 137+ return Err(failure::format_err!("unsupported texture vertex format")); 138+ } 139+ Ok(TexCoord([u, v])) 140+} 141+ 142+fn parse_f32(value: Option<&str>) -> Result<f32, failure::Error> { 143+ value 144+ .ok_or_else(|| failure::format_err!("missing float"))? 145+ .parse::<f32>() 146+ .map_err(Into::into) 147+} 148+ 149+fn parse_face_index( 150+ value: Option<&str>, 151+ positions_len: usize, 152+ tex_coords_len: usize, 153+ normals_len: usize, 154+) -> Result<(usize, Option<usize>, Option<usize>), failure::Error> { 155+ let value = value.ok_or_else(|| failure::format_err!("missing face index"))?; 156+ let mut parts = value.split('/'); 157+ let position = parse_obj_index(parts.next(), positions_len)?; 158+ let tex_coord = match parts.next() { 159+ Some("") | None => None, 160+ Some(value) => Some(parse_obj_index(Some(value), tex_coords_len)?), 161+ }; 162+ let normal = match parts.next() { 163+ Some("") | None => None, 164+ Some(value) => Some(parse_obj_index(Some(value), normals_len)?), 165+ }; 166+ if parts.next().is_some() { 167+ return Err(failure::format_err!("unsupported face format")); 168+ } 169+ Ok((position, tex_coord, normal)) 170+} 171+ 172+fn parse_obj_index(value: Option<&str>, len: usize) -> Result<usize, failure::Error> { 173+ let raw = value 174+ .ok_or_else(|| failure::format_err!("missing index"))? 175+ .parse::<isize>()?; 176+ if raw <= 0 { 177+ return Err(failure::format_err!("relative or zero indices are unsupported in fast path")); 178+ } 179+ let index = raw as usize - 1; 180+ if index >= len { 181+ return Err(failure::format_err!("index out of bounds")); 182+ } 183+ Ok(index) 184+} 185+ 186 fn load_from_data( 187 obj_set: obj::ObjSet, 188 ) -> Result<Vec<(MeshBuilder<'static>, Option<String>)>, failure::Error> {
Environment
Archaarch64
OSDarwin 26.3.1
CPUApple M1 Max
Agentpi-autoresearch / pi
Files.community-computer/published.json, autoresearch.ideas.md, autoresearch.jsonl, mesh/src/format/obj.rs