Important
Picante now lives in the Facet monorepo.
Use picante and
picante-macros
from facet-rs/facet for new issues,
pull requests, releases, and documentation.
After this redirect lands, this repository will be archived.
picante is an async incremental query runtime for Rust, inspired by Salsa but built for Tokio-first pipelines (like Dodeca).
What it has today
- Inputs (
InputIngredient<K, V>), interning (InternedIngredient<K>), and derived async queries (DerivedIngredient<DB, K, V>) - Dependency tracking via Tokio task-locals
- Per-task cycle detection (fast path)
- Async single-flight memoization per
(kind, key) - In-flight query deduplication across database snapshots (see below)
- Cache persistence to disk (snapshot file) using
facet+facet-postcard(no serde) - Runtime notifications for live reload (
Runtime::subscribe_revisions,Runtime::subscribe_events) - Debugging and observability tools (
picante::debug): dependency graph visualization, query execution tracing, cache statistics, enhanced cycle diagnostics
use picante::{DerivedIngredient, DynIngredient, HasRuntime, IngredientLookup, IngredientRegistry, InputIngredient, QueryKindId, Runtime};
use std::sync::Arc;
#[derive(Default)]
struct Db {
runtime: Runtime,
ingredients: IngredientRegistry<Db>,
}
impl HasRuntime for Db {
fn runtime(&self) -> &Runtime {
&self.runtime
}
}
impl IngredientLookup for Db {
fn ingredient(&self, kind: QueryKindId) -> Option<&dyn DynIngredient<Self>> {
self.ingredients.ingredient(kind)
}
}
fn main() -> picante::PicanteResult<()> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let db = Db::default();
let text: Arc<InputIngredient<String, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "Text"));
let len: Arc<DerivedIngredient<Db, String, u64>> = {
let text = text.clone();
Arc::new(DerivedIngredient::new(QueryKindId(2), "Len", move |db, key| {
let text = text.clone();
Box::pin(async move {
let s = text.get(db, &key)?.unwrap_or_default();
Ok(s.len() as u64)
})
}))
};
text.set(&db, "a".into(), "hello".into());
assert_eq!(len.get(&db, "a".into()).await?, 5);
Ok(())
})
}picante can save/load inputs and memoized derived values (including dependency lists):
use picante::persist::{load_cache, save_cache};
// save_cache(path, db.runtime(), &[&*text, &*len]).await?;
// load_cache(path, db.runtime(), &[&*text, &*len]).await?;If you need cache size limits or custom corruption handling, use:
save_cache_with_optionsload_cache_with_options
When multiple concurrent async tasks request the same tracked query with identical parameters, picante automatically coalesces these into a single computation:
- First caller becomes the leader and starts computing the value
- Concurrent callers become followers and await the leader's result
- All callers receive the same result once computation completes
- Each caller still caches the result in its own memo table, keeping snapshot caches independent
This deduplication works across database snapshots from the same parent database. Coalescing is scoped to:
- The same database identity (parent database and all its snapshots share an identity)
- The same revision (different revisions compute independently)
- The same query kind and key
This is particularly useful for request-per-snapshot patterns where many concurrent requests may query the same data.
picante provides comprehensive debugging tools in the picante::debug module:
Export the dependency graph in Graphviz DOT format for visualization:
# use picante::{Runtime, HasRuntime};
# struct Db { runtime: Runtime }
# impl HasRuntime for Db { fn runtime(&self) -> &Runtime { &self.runtime } }
# fn main() -> std::io::Result<()> {
# let db = Db { runtime: Runtime::new() };
use picante::debug::DependencyGraph;
let graph = DependencyGraph::from_runtime(db.runtime());
graph.write_dot("deps.dot")?;
// Visualize with: dot -Tpng deps.dot -o deps.png
# Ok(())
# }Record and analyze query execution events:
# use picante::{Runtime, HasRuntime};
# struct Db { runtime: Runtime }
# impl HasRuntime for Db { fn runtime(&self) -> &Runtime { &self.runtime } }
# fn main() {
# let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
# rt.block_on(async {
# let db = Db { runtime: Runtime::new() };
use picante::debug::{TraceCollector, TraceAnalysis};
let collector = TraceCollector::start(db.runtime());
// ... perform queries ...
let trace = collector.stop().await;
let analysis = TraceAnalysis::from_trace(&trace);
println!("{}", analysis.format());
// Shows: total events, input changes, invalidations, recomputations, duration
# });
# }Get insights into cache usage and dependency structure:
# use picante::{Runtime, HasRuntime};
# struct Db { runtime: Runtime }
# impl HasRuntime for Db { fn runtime(&self) -> &Runtime { &self.runtime } }
# fn main() {
# let db = Db { runtime: Runtime::new() };
use picante::debug::CacheStats;
let stats = CacheStats::collect(db.runtime());
println!("{}", stats.format());
// Shows: forward deps, reverse deps, total edges, root queries, distribution
# }When a dependency cycle is detected, picante shows the full path:
dependency cycle detected
→ kind_1, key_0000000000000001 (initial)
→ kind_2, key_0000000000000002
→ kind_3, key_0000000000000003
→ kind_1, key_0000000000000001 ← cycle (already in stack)
- Tokio-only: query execution uses Tokio task-local context.
- Global invalidation v1: changing any input bumps a single global
Revision.