Skip to content

Commit 102bb92

Browse files
committed
feat(gateway): Phase 0 search_tools latency instrumentation
Close enrichment and rank timing blind spots, and log include_inactive/scope usage so cold vs warm baselines can drive the next perf pass. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent df0f7b8 commit 102bb92

6 files changed

Lines changed: 364 additions & 29 deletions

File tree

crates/mcpmux-gateway/src/services/embedding.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ fn load_text_embedding(cache_dir: &Path) -> anyhow::Result<TextEmbedding> {
329329
TextEmbedding::try_new(options)
330330
}
331331

332-
fn model_state_label(state: &EmbeddingState) -> &'static str {
332+
/// Wire/log label for embedding lifecycle state (`absent` | `downloading` | `ready` | `failed`).
333+
pub(crate) fn model_state_label(state: &EmbeddingState) -> &'static str {
333334
match state {
334335
EmbeddingState::NotDownloaded => "absent",
335336
EmbeddingState::Downloading => "downloading",

crates/mcpmux-gateway/src/services/meta_tools/search_tools.rs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use std::time::Instant;
88
use tracing::{debug, info};
99
use uuid::Uuid;
1010

11+
use crate::services::embedding::model_state_label;
12+
1113
use super::meta_tool_common::{
1214
build_installed_server_meta_maps, build_server_readiness_map, caller_resolution,
1315
is_query_empty, text_result,
@@ -141,6 +143,7 @@ impl MetaTool for SearchToolsTool {
141143

142144
let fingerprint = feature_set_ids_fingerprint(&resolved.feature_set_ids);
143145

146+
let server_id_set = server_id_filter.is_some();
144147
info!(
145148
query_id = %query_id,
146149
session_id = ?call.session_id,
@@ -150,15 +153,22 @@ impl MetaTool for SearchToolsTool {
150153
limit,
151154
is_browse,
152155
include_inactive,
156+
scope_all,
157+
server_id_set,
153158
"[search] call entry"
154159
);
155160
if let Some(query) = effective_query {
156161
debug!(query_id = %query_id, query, "[search] query text");
157162
}
158163

164+
let readiness_started = Instant::now();
159165
let readiness_map = build_server_readiness_map(&call, &space_id, &resolved).await?;
166+
let readiness_ms = readiness_started.elapsed().as_millis() as u64;
167+
168+
let installed_meta_started = Instant::now();
160169
let (server_display_names, prefilled_params_by_server) =
161170
build_installed_server_meta_maps(&call, &space_id).await?;
171+
let installed_meta_ms = installed_meta_started.elapsed().as_millis() as u64;
162172

163173
let mut index_cache_hit = false;
164174
let active_index_started = Instant::now();
@@ -255,10 +265,13 @@ impl MetaTool for SearchToolsTool {
255265
);
256266
}
257267

258-
let hydrate_ms = if effective_query.is_some() {
259-
hydrate_active_embeddings(&call, query_id.as_str(), active_index.as_slice()).await?
268+
let (hydrate_ms, hydrated_missing_count) = if effective_query.is_some() {
269+
let hydrate =
270+
hydrate_active_embeddings(&call, query_id.as_str(), active_index.as_slice())
271+
.await?;
272+
(hydrate.hydrate_ms, hydrate.hydrated_missing_count)
260273
} else {
261-
0
274+
(0, 0)
262275
};
263276

264277
let hybrid = effective_query.map(|_| crate::services::tool_discovery::SearchContext {
@@ -284,6 +297,8 @@ impl MetaTool for SearchToolsTool {
284297
is_browse,
285298
);
286299
let rank_ms = rank_started.elapsed().as_millis() as u64;
300+
// Prefer live model state so browse / lexical-only calls still report readiness.
301+
let embedding_state = model_state_label(&call.ctx.embeddings.state());
287302

288303
let top_qualified_name = result
289304
.tools
@@ -293,6 +308,8 @@ impl MetaTool for SearchToolsTool {
293308
.unwrap_or("");
294309

295310
let post_started = Instant::now();
311+
let mut zero_result_inactive_preview = false;
312+
let mut zero_result_catalog_scan = false;
296313
let mut payload = json!({
297314
"tools": result.tools,
298315
"next_cursor": result.next_cursor,
@@ -310,7 +327,7 @@ impl MetaTool for SearchToolsTool {
310327
}
311328

312329
if !include_inactive && result.total == 0 && effective_query.is_some() {
313-
let inactive_started = Instant::now();
330+
zero_result_inactive_preview = true;
314331
let inactive = call
315332
.ctx
316333
.feature_service
@@ -364,18 +381,17 @@ impl MetaTool for SearchToolsTool {
364381
the bindable_feature_set_id shown on each preview entry to activate them."
365382
);
366383
}
367-
inactive_widen_ms = inactive_started.elapsed().as_millis() as u64;
368384
debug!(
369385
query_id = %query_id,
370386
ready_inactive_preview = payload
371387
.get("inactive_preview")
372388
.and_then(|v| v.as_array())
373389
.map(|a| a.len())
374390
.unwrap_or(0),
375-
inactive_widen_ms,
376391
"[search] zero-result inactive preview"
377392
);
378393
} else if include_inactive && result.total == 0 {
394+
zero_result_catalog_scan = true;
379395
let catalog = call
380396
.ctx
381397
.tool_discovery
@@ -409,6 +425,8 @@ impl MetaTool for SearchToolsTool {
409425

410426
let total_ms = started.elapsed().as_millis() as u64;
411427
let accounted_ms = resolve_ms
428+
+ readiness_ms
429+
+ installed_meta_ms
412430
+ active_index_ms
413431
+ index_clone_ms
414432
+ inactive_widen_ms
@@ -423,21 +441,44 @@ impl MetaTool for SearchToolsTool {
423441
returned = result.tools.len(),
424442
top_qualified_name,
425443
top_fused_score = ?result.top_fused_score,
444+
index_cache_hit,
445+
include_inactive,
446+
scope_all,
447+
server_id_set,
448+
inactive_tool_count,
449+
zero_result_inactive_preview,
450+
zero_result_catalog_scan,
451+
embedding_state,
452+
hydrated_missing_count,
426453
total_ms,
427454
"[search] result summary"
428455
);
429456
info!(
430457
query_id = %query_id,
431458
resolve_ms,
459+
readiness_ms,
460+
installed_meta_ms,
432461
active_index_ms,
462+
index_cache_hit,
433463
index_clone_ms,
434464
inactive_widen_ms,
435465
hydrate_ms,
466+
hydrated_missing_count,
436467
rank_ms,
468+
rank_lexical_ms = result.rank_lexical_ms,
469+
rank_embed_query_ms = result.rank_embed_query_ms,
470+
rank_semantic_ms = result.rank_semantic_ms,
471+
embedding_state,
437472
post_ms,
438473
accounted_ms,
439474
unaccounted_ms = total_ms.saturating_sub(accounted_ms),
440475
merged_index = index.len(),
476+
include_inactive,
477+
scope_all,
478+
server_id_set,
479+
inactive_tool_count,
480+
zero_result_inactive_preview,
481+
zero_result_catalog_scan,
441482
"[search] timing breakdown"
442483
);
443484

crates/mcpmux-gateway/src/services/meta_tools/search_tools_index.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,19 @@ pub(crate) async fn build_and_cache_active_index(
6262
Ok(index)
6363
}
6464

65+
/// Timing + miss count from [`hydrate_active_embeddings`].
66+
pub(crate) struct HydrateEmbeddingsTiming {
67+
pub hydrate_ms: u64,
68+
/// Content hashes absent from the in-memory store before this hydrate attempt.
69+
pub hydrated_missing_count: usize,
70+
}
71+
6572
/// Load missing active-tool vectors from persistent storage into the global embedding map.
6673
pub(crate) async fn hydrate_active_embeddings(
6774
call: &MetaToolCall<'_>,
6875
query_id: &str,
6976
active_index: &[crate::services::ToolIndexEntry],
70-
) -> Result<u64, MetaToolError> {
77+
) -> Result<HydrateEmbeddingsTiming, MetaToolError> {
7178
let hydrate_started = Instant::now();
7279
let missing_hashes: HashSet<String> = active_index
7380
.iter()
@@ -91,7 +98,10 @@ pub(crate) async fn hydrate_active_embeddings(
9198
hydrate_ms,
9299
"[embed] store hydrate"
93100
);
94-
return Ok(hydrate_ms);
101+
return Ok(HydrateEmbeddingsTiming {
102+
hydrate_ms,
103+
hydrated_missing_count: 0,
104+
});
95105
}
96106

97107
let missing_hashes: Vec<String> = missing_hashes.into_iter().collect();
@@ -124,5 +134,8 @@ pub(crate) async fn hydrate_active_embeddings(
124134
"[embed] store hydrate"
125135
);
126136

127-
Ok(hydrate_ms)
137+
Ok(HydrateEmbeddingsTiming {
138+
hydrate_ms,
139+
hydrated_missing_count: hashes_requested,
140+
})
128141
}

0 commit comments

Comments
 (0)