Skip to main content

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

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

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:

FieldWhat it tells you
keyThe stringified QueryKey for this resource.
statusHuman-readable label from QueryStatus: "Idle", "Loading empty", "Loading with data", "Success", "Failure", or "Cancelled".
has_dataWhether the resource currently holds cached data.
has_errorWhether the resource currently holds an error.
cache_policyLabel from the configured CachePolicy: "No cache", "Cache TTL 60s", or "Stale-while-revalidate 60s".
request_policyLabel from the configured RequestPolicy: "Latest wins" or "Ignore while loading".
cache_hitsHow many times this resource returned cached data instead of fetching.
cancelled_countHow many in-flight requests were cancelled (for example, by a LatestWins policy replacing them).
ignored_resultsHow many stale or out-of-order responses were discarded.
last_updated_at_msUNIX timestamp in milliseconds of the last successful data update, if any.
started_at_msUNIX timestamp in milliseconds of the current or most recent request start, if any.

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

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

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, 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);
tip

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. 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.