Devtools Inspect live query and mutation state in gpui-query: ClientDiagnostic snapshots and patterns for building a debug panel. gpui-query exposes runtime diagnostics through the QueryClient (/docs/api/query-client). 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 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, } - 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 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, pub started_at_ms: Option, } The fields: Field What it tells you key The stringified QueryKey (/docs/api/queries) 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 let client = cx.global::(); 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 If you only care about one type, use query_diagnostics::(). 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::(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 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 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 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 After calling invalidate_queries on the QueryClient (/docs/api/query-client), 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 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); If you are building a devtools panel in GPUI, consider polling diagnostics() on a timer. The cost is low: it reads entity state without cloning the actual cached data. Observers For real-time state tracking rather than point-in-time snapshots, see Observers (/docs/advanced/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.