Files
zed/tooling/xtask/src/workspace.rs
T
Kirill Bulatov 21f16f7b5b Speed up cargo build -p zed compilation (#62059)
Splits the crate graph.
Before, the compilation graph: `editor → picker_preview → search →
project_panel → open_path_prompt → recent_projects → title_bar →
collab_ui → zed`
After, the compilation graph: `editor → picker_preview → search →
agent_ui → sidebar → zed`

With sccache disabled and project fully built, `touch
crates/editor/src/editor.rs` and `cargo build -p zed` took time
Before (5e1fd392f6): 13.19s
After: 11.85s (-10.2% speed up)

Each commit contains a separate compilation instructions, and a test in
the final commit.
The main approach is to split coupled crates and replace them with
`zed_actions` and move some shared functionality into `git_ui_core` new,
shared module.

I've also tried to add a test to prevent common pitfalls, but not sure
I'm happy with the end result — can remove it if it looks too synthetic.

Release Notes:

- N/A
2026-08-01 19:42:37 +00:00

102 lines
3.5 KiB
Rust

use anyhow::{Context as _, Result};
use cargo_metadata::{Metadata, MetadataCommand};
/// Returns the Cargo workspace.
pub fn load_workspace() -> Result<Metadata> {
MetadataCommand::new()
.exec()
.context("failed to load cargo metadata")
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use cargo_metadata::DependencyKind;
use super::load_workspace;
/// Crates that must not depend on each other, directly or transitively:
/// such edges chain large UI crates one after another and serialize the
/// build, badly hurting incremental compile times. Dev-dependencies are
/// exempt since they don't affect `cargo build`.
const FORBIDDEN_DEPENDENCIES: &[(&str, &str)] = &[
("agent_ui", "git_ui"),
("file_finder", "project_panel"),
("git_ui", "agent_ui"),
("git_ui", "search"),
("open_path_prompt", "project_panel"),
("picker", "editor"),
("project_panel", "git_ui"),
("project_panel", "search"),
("search", "git_ui"),
("search", "project_panel"),
("sidebar", "git_ui"),
("title_bar", "git_ui"),
];
#[test]
fn no_forbidden_dependencies_between_feature_crates() {
let workspace = load_workspace().expect("failed to load cargo metadata");
let packages = workspace.workspace_packages();
let member_names = packages
.iter()
.map(|package| package.name.as_str())
.collect::<BTreeSet<_>>();
let mut graph = BTreeMap::new();
for package in &packages {
let dependencies = package
.dependencies
.iter()
.filter(|dependency| dependency.kind != DependencyKind::Development)
.map(|dependency| dependency.name.as_str())
.filter(|name| member_names.contains(name))
.collect::<BTreeSet<_>>();
graph.insert(package.name.as_str(), dependencies);
}
let mut violations = Vec::new();
for &(from, to) in FORBIDDEN_DEPENDENCIES {
if let Some(path) = dependency_path(&graph, from, to) {
violations.push(path.join(" -> "));
}
}
assert_eq!(
violations,
Vec::<String>::new(),
"forbidden dependency paths between sibling feature crates; \
break the dependency (e.g. by extracting shared code into a lower-level crate) \
instead of joining these crates into one serial build chain",
);
}
fn dependency_path<'a>(
graph: &BTreeMap<&'a str, BTreeSet<&'a str>>,
from: &'a str,
to: &str,
) -> Option<Vec<&'a str>> {
let mut parents = BTreeMap::new();
let mut queue = VecDeque::from([from]);
while let Some(current) = queue.pop_front() {
if current == to {
let mut path = vec![current];
let mut node = current;
while let Some(&parent) = parents.get(node) {
path.push(parent);
node = parent;
}
path.reverse();
return Some(path);
}
for &dependency in graph.get(current).into_iter().flatten() {
if dependency != from && !parents.contains_key(dependency) {
parents.insert(dependency, current);
queue.push_back(dependency);
}
}
}
None
}
}