Skip to content

Commit dc62874

Browse files
ahaviliclaude
andcommitted
perf(index): skip store and search sync on no-op incremental reindex
Add GraphDelta::is_empty() and use it to detect when the graph is unchanged after extraction+merge+classify. When empty, skip SQLite save (2.8s), search index sync (1.1s), and PRAGMA optimize — still run localization and asset snapshots since they scan independent files. Also fix save_incremental to compute the delta only once instead of three times (was called twice via StoreWriteStats::from_graphs plus once directly), and return early with empty stats when delta is empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 01930e7 commit dc62874

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

grapha/src/app/index.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,76 @@ pub(crate) fn handle_index(
153153
.map(|prev| delta::GraphDelta::between(prev, &graph))
154154
};
155155

156+
let graph_unchanged = delta.as_ref().is_some_and(|d| d.is_empty());
157+
156158
let search_index_path = store_path.join("search_index");
157159
let index_root = path.clone();
160+
161+
if graph_unchanged {
162+
let snapshot_result = std::thread::scope(|scope| {
163+
let localization_handle = scope.spawn(|| {
164+
let t = Instant::now();
165+
let stats =
166+
localization::build_and_save_catalog_snapshot(&index_root, &store_path)?;
167+
Ok::<_, anyhow::Error>((t.elapsed(), stats))
168+
});
169+
170+
let assets_handle = scope.spawn(|| {
171+
let t = Instant::now();
172+
let stats = assets::build_and_save_snapshot(&index_root, &store_path)?;
173+
Ok::<_, anyhow::Error>((t.elapsed(), stats))
174+
});
175+
176+
let localization = localization_handle
177+
.join()
178+
.expect("localization thread panicked")?;
179+
let assets = assets_handle.join().expect("assets thread panicked")?;
180+
Ok::<_, anyhow::Error>((localization, assets))
181+
});
182+
let ((localize_elapsed, localize_stats), (assets_elapsed, assets_stats)) = snapshot_result?;
183+
184+
extraction_cache
185+
.save_entries(&pipeline.extraction_cache_entries)
186+
.with_context(|| "failed to save extraction cache".to_string())?;
187+
188+
eprintln!(" \x1b[32m✓\x1b[0m no graph changes detected, skipping store and search sync");
189+
progress::done_elapsed(
190+
&format!(
191+
"saved localization snapshot ({} records)",
192+
localize_stats.record_count
193+
),
194+
localize_elapsed,
195+
);
196+
for warning in &localize_stats.warnings {
197+
eprintln!(
198+
" \x1b[33m!\x1b[0m skipped invalid localization catalog {}: {}",
199+
warning.catalog_file, warning.reason
200+
);
201+
}
202+
progress::done_elapsed(
203+
&format!(
204+
"saved asset snapshot ({} images)",
205+
assets_stats.record_count
206+
),
207+
assets_elapsed,
208+
);
209+
for warning in &assets_stats.warnings {
210+
eprintln!(
211+
" \x1b[33m!\x1b[0m skipped invalid asset catalog {}: {}",
212+
warning.catalog_path, warning.reason
213+
);
214+
}
215+
216+
progress::summary(&format!(
217+
"\n {} nodes, {} edges indexed in {:.1}s",
218+
graph.nodes.len(),
219+
graph.edges.len(),
220+
total_start.elapsed().as_secs_f64(),
221+
));
222+
223+
return Ok(());
224+
}
225+
158226
let save_result = std::thread::scope(|scope| {
159227
let save_handle = scope.spawn(|| {
160228
let t = Instant::now();

grapha/src/delta.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ impl<'a> GraphDelta<'a> {
132132
}
133133
}
134134

135+
pub fn is_empty(&self) -> bool {
136+
self.added_nodes.is_empty()
137+
&& self.updated_nodes.is_empty()
138+
&& self.deleted_node_ids.is_empty()
139+
&& self.added_edges.is_empty()
140+
&& self.updated_edges.is_empty()
141+
&& self.deleted_edge_ids.is_empty()
142+
}
143+
135144
pub fn node_stats(&self) -> EntitySyncStats {
136145
EntitySyncStats {
137146
added: self.added_nodes.len(),

grapha/src/store/sqlite/write.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,29 @@ pub(super) fn save_incremental(
154154
previous: Option<&grapha_core::graph::Graph>,
155155
graph: &grapha_core::graph::Graph,
156156
) -> anyhow::Result<StoreWriteStats> {
157-
let current_stats =
158-
StoreWriteStats::from_graphs(previous, graph, crate::delta::SyncMode::Incremental);
159-
let full_stats =
160-
StoreWriteStats::from_graphs(previous, graph, crate::delta::SyncMode::FullRebuild);
161-
162157
let conn = store.open_for_write()?;
163158
let schema_version = schema::schema_version(&conn)?;
164159
if previous.is_none() || schema_version.as_deref() != Some(schema::STORE_SCHEMA_VERSION) {
160+
let full_stats =
161+
StoreWriteStats::from_graphs(previous, graph, crate::delta::SyncMode::FullRebuild);
165162
drop(conn);
166163
save_full(store, graph)?;
167164
return Ok(full_stats);
168165
}
169166

170167
let previous_graph = previous.expect("checked is_some above");
171168
let delta = GraphDelta::between(previous_graph, graph);
169+
170+
let stats = StoreWriteStats {
171+
mode: crate::delta::SyncMode::Incremental,
172+
nodes: delta.node_stats(),
173+
edges: delta.edge_stats(),
174+
};
175+
176+
if delta.is_empty() {
177+
return Ok(stats);
178+
}
179+
172180
let tx = conn.unchecked_transaction()?;
173181
schema::write_meta(&tx, graph)?;
174182

@@ -209,5 +217,5 @@ pub(super) fn save_incremental(
209217
tx.commit()?;
210218
conn.execute_batch("PRAGMA optimize;")?;
211219

212-
Ok(current_stats)
220+
Ok(stats)
213221
}

0 commit comments

Comments
 (0)