Speed up TUI startup by reusing plugin discovery (#26469)

## Summary

TUI startup loads related plugin data from `hooks/list`, session MCP
initialization, and plugin skill warmup. These paths repeated filesystem
discovery and emitted the same plugin warnings, while `hooks/list` and
account/model bootstrap ran serially.

This change:

- Reuses one immutable plugin load outcome across startup consumers.
- Keys the cache only on plugin-relevant configuration.
- Single-flights concurrent plugin loads and prevents invalidated loads
from repopulating the cache.
- Runs hook discovery and account/model bootstrap concurrently.
- Preserves configuration-migration ordering, hook review behavior, and
accurate startup telemetry.

In 10 alternating release-build launches in the Ruff repository with the
existing `~/.codex` configuration, median time to the first editable
composer decreased from 833ms to 504ms. The branch was faster in 9 of 10
pairs, with a paired median improvement of 312ms.
This commit is contained in:
Charlie Marsh authored and GitHub committed 2026-06-05 15:32:43 -04:00
1 parent 345cf6e8d0
commit 055c7a7c53
9 files changed
+303 -37

No files matched your search

@@ -1300,6 +1300,97 @@ async fn load_plugins_returns_empty_when_feature_disabled() {
assert_eq!(outcome, PluginLoadOutcome::default());
}
#[tokio::test]
async fn plugin_cache_ignores_unrelated_session_overrides() {
let codex_home = TempDir::new().unwrap();
let plugin_root = codex_home
.path()
.join("plugins/cache")
.join("test/sample/local");
write_plugin(
codex_home.path().join("plugins/cache/test").as_path(),
"sample/local",
"sample",
);
write_file(
&plugin_root.join(".mcp.json"),
r#"{
"mcpServers": {
"sample": {
"url": "https://sample.example/mcp"
}
}
}"#,
);
let user_file = codex_home.path().join(CONFIG_TOML_FILE).abs();
let user_config: toml::Value = toml::from_str(&plugin_config_toml(
/*enabled*/ true, /*plugins_feature_enabled*/ true,
))
.expect("user config should parse");
let stack = |session_config: &str| {
ConfigLayerStack::new(
vec![
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: user_file.clone(),
profile: None,
},
user_config.clone(),
),
ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::from_str(session_config).expect("session config should parse"),
),
],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("config layer stack should build")
};
let config = |session_config| {
PluginsConfigInput::new(
stack(session_config),
/*plugins_enabled*/ true,
/*remote_plugin_enabled*/ false,
"https://chatgpt.com".to_string(),
)
};
let manager = PluginsManager::new(codex_home.path().to_path_buf());
let first = manager
.plugins_for_config(&config(r#"model = "first""#))
.await;
std::fs::remove_file(plugin_root.join(".mcp.json")).unwrap();
let second = manager
.plugins_for_config(&config(r#"model = "second""#))
.await;
assert_eq!(second, first);
assert_eq!(second.plugins()[0].mcp_servers.len(), 1);
}
#[test]
fn plugin_cache_invalidation_rejects_stale_load_completion() {
let codex_home = TempDir::new().unwrap();
let manager = PluginsManager::new(codex_home.path().to_path_buf());
let cache_key = PluginLoadCacheKey {
configured_plugins: HashMap::new(),
skill_config_rules: SkillConfigRules::default(),
remote_plugin_enabled: false,
};
let stale_generation = manager.enabled_outcome_cache_generation();
manager.clear_enabled_outcome_cache();
manager.cache_enabled_outcome_if_current(
stale_generation,
cache_key.clone(),
PluginLoadOutcome::default(),
);
assert_eq!(manager.cached_enabled_outcome(&cache_key), None);
}
#[tokio::test]
async fn load_plugins_rejects_invalid_plugin_keys() {
let codex_home = TempDir::new().unwrap();