137 // Single pass over all_exps: collect ui-visible experiment oids
138 // (for the per-peer branch tip prefilter below) while simultaneously
139 // counting experiments, reproductions, and building ticker rows.
140+ //
141+ // The ticker only needs `delta_str` + `is_improvement` from each
142+ // experiment's primary metric, so inline the minimal computation
143+ // instead of calling `PrimaryMetricView::from_experiment`, which
144+ // always allocates `name` and `unit` Strings that this call site
145+ // discards.
146 let mut visible_tip_oids: std::collections::HashSet<radicle::git::Oid> =
147 std::collections::HashSet::with_capacity(all_exps.len());
148 for (_id, exp) in &all_exps {
153 experiment_count += 1;
154 reproduction_count += exp.reproductions.len();
155
150- let primary = PrimaryMetricView::from_experiment(exp, &catalog);
151-
152- if primary.is_improvement {
156+ let Some(primary_metric) = exp.metrics.first() else {
157+ continue;
158+ };
159+ let primary_criteria = catalog.criteria_for(primary_metric);
160+ let is_improvement =
161+ radicle_experiment::is_improvement(primary_metric.delta_pct_x100, primary_criteria);
162+ if is_improvement {
163 improved_count += 1;
164 }
165
156- if primary.has_metric {
157- ticker_items.push(TickerItem {
158- repo_name: name.clone(),
159- description: exp.description.clone().unwrap_or_default(),
160- delta_str: primary.delta_str,
161- improved: primary.is_improvement,
162- created_at: exp.created_at.as_secs(),
163- rid_short: rid_short.clone(),
164- exp_id: _id.to_string(),
165- });
166- }
166+ let (delta_str, _) = fmt_delta(primary_metric.delta_pct_x100, primary_criteria);
167+ ticker_items.push(TickerItem {
168+ repo_name: name.clone(),
169+ description: exp.description.clone().unwrap_or_default(),
170+ delta_str,
171+ improved: is_improvement,
172+ created_at: exp.created_at.as_secs(),
173+ rid_short: rid_short.clone(),
174+ exp_id: _id.to_string(),
175+ });
176 }
177
178 if experiment_count == 0 {