Devtools
gpui-query exposes runtime diagnostics through the QueryClient. You get a serializable snapshot of every active query and mutation, which you can log, render in a debug panel, or serialize to JSON for later analysis.
ClientDiagnostic
Section titled “ClientDiagnostic”Calling client.diagnostics(cx) returns a ClientDiagnostic struct. It is a point-in-time snapshot of the client internals.
pub struct ClientDiagnostic { pub total_resources: usize, pub bucket_count: usize, pub mutation_count: usize, pub queries: Vec<QueryDiagnostic>,}- total_resources: number of active query resources across all type buckets.
- bucket_count: number of type-partitioned buckets. Each unique
(T, E)pair gets its own bucket. - mutation_count: number of active mutation resources.
- queries: per-query diagnostics, one entry per resource.
The struct derives Clone, Debug, and Serialize. You can serialize it to JSON and ship it to a log aggregator or render it in a GPUI debug panel.
QueryDiagnostic
Section titled “QueryDiagnostic”Each entry in queries is a QueryDiagnostic:
pub struct QueryDiagnostic { pub key: String, pub status: String, pub has_data: bool, pub has_error: bool, pub cache_policy: String, pub request_policy: String, pub cache_hits: u64, pub cancelled_count: u64, pub ignored_results: u64, pub last_updated_at_ms: Option<u128>, pub started_at_ms: Option<u128>,}The fields:
| Field | What it tells you |
|---|---|
key |
The stringified QueryKey for this resource. |
status |
Human-readable label from QueryStatus: "Idle", "Loading empty", "Loading with data", "Success", "Failure", or "Cancelled". |
has_data |
Whether the resource currently holds cached data. |
has_error |
Whether the resource currently holds an error. |
cache_policy |
Label from the configured CachePolicy: "No cache", "Cache TTL 60s", or "Stale-while-revalidate 60s". |
request_policy |
Label from the configured RequestPolicy: "Latest wins" or "Ignore while loading". |
cache_hits |
How many times this resource returned cached data instead of fetching. |
cancelled_count |
How many in-flight requests were cancelled (for example, by a LatestWins policy replacing them). |
ignored_results |
How many stale or out-of-order responses were discarded. |
last_updated_at_ms |
UNIX timestamp in milliseconds of the last successful data update, if any. |
started_at_ms |
UNIX timestamp in milliseconds of the current or most recent request start, if any. |
Basic usage
Section titled “Basic usage”let client = cx.global::<QueryClient>();let diag = client.diagnostics(cx);
println!("Active queries: {}", diag.total_resources);println!("Buckets: {}", diag.bucket_count);println!("Mutations: {}", diag.mutation_count);
for q in &diag.queries { println!( " {} - {} (data: {}, error: {}, hits: {})", q.key, q.status, q.has_data, q.has_error, q.cache_hits );}Filtering by type
Section titled “Filtering by type”If you only care about one type, use query_diagnostics::<T, E>(). It returns diagnostics for a single (T, E) bucket, or an empty vec if no bucket exists for that type pair.
let user_diags = client.query_diagnostics::<User, QueryError>(cx);for q in &user_diags { println!("user query: {} - {}", q.key, q.status);}This avoids iterating over unrelated type buckets when you are debugging a specific domain type.
Practical debugging patterns
Section titled “Practical debugging patterns”Check whether your TTL is right
Section titled “Check whether your TTL is right”Use last_updated_at_ms to figure out how old cached data is. Compare it to the current time. If data is consistently stale, your CachePolicy::Ttl { ttl_ms } might be too long. If cache_hits is zero across the board, your TTL might be too short and every read triggers a refetch.
let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis();
for q in &diag.queries { if let Some(updated) = q.last_updated_at_ms { let age_ms = now - updated; println!("{} is {}ms old", q.key, age_ms); }}Verify deduplication is working
Section titled “Verify deduplication is working”A high cache_hits count means the cache is doing its job. A count of zero on a resource that should be shared means you might be creating duplicate resources with slightly different keys. Check that your key construction is consistent.
for q in &diag.queries { if q.cache_hits == 0 && q.status == "Success" { println!("warning: {} has data but zero cache hits", q.key); }}Spot flaky endpoints
Section titled “Spot flaky endpoints”Look at cancelled_count and ignored_results. A high cancelled count on a resource suggests rapid successive fetches are replacing each other (normal under RequestPolicy::LatestWins, but worth investigating if unexpected). High ignored results mean responses are arriving out of order relative to newer requests.
for q in &diag.queries { if q.cancelled_count > 5 { println!("{}: {} cancelled requests", q.key, q.cancelled_count); }}Verify invalidation hit the right keys
Section titled “Verify invalidation hit the right keys”After calling invalidate_queries on the QueryClient, run diagnostics again to confirm the target resources moved to the expected status. If a key is still "Success" with stale data, your invalidation filter did not match it.
client.invalidate_queries(&filter, cx);
let diag = client.diagnostics(cx);for q in &diag.queries { println!("{} - {}", q.key, q.status);}Serialization
Section titled “Serialization”Because ClientDiagnostic and QueryDiagnostic both derive Serialize, you can dump the full client state to JSON in one call. This is useful for logging, snapshot tests, or building a debug panel that communicates over IPC.
let json = serde_json::to_string_pretty(&client.diagnostics(cx)) .expect("diagnostics are serializable");println!("{}", json);Observers
Section titled “Observers”For real-time state tracking rather than point-in-time snapshots, see Observers. Observers subscribe to individual resource state changes, while diagnostics give you the full client picture in a single call. Use both together: diagnostics for the overview, observers for reactive updates on specific keys.