Skip to content

Commit 95f6b36

Browse files
tyao1meta-codesync[bot]
authored andcommitted
Avoid redundant cloning of executable definition ASTs
Reviewed By: scotthovestadt Differential Revision: D94779922 fbshipit-source-id: 3fb6f5130b5b69b5c46929509303b13958b5af88
1 parent 3536b2b commit 95f6b36

4 files changed

Lines changed: 43 additions & 28 deletions

File tree

compiler/crates/dependency-analyzer/src/ast.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ pub struct ReachableAst {
2323
pub base_fragment_names: FragmentDefinitionNameSet,
2424
}
2525

26-
/// Get all definitions that are reachable from project definitions
26+
/// Get all definitions that are reachable from project definitions.
27+
///
28+
/// Takes borrowed slices to avoid cloning all definitions upfront. Only clones
29+
/// into the result Vec once, saving ~222K deep clones for large projects with
30+
/// base definitions.
2731
pub fn get_reachable_ast(
28-
project_definitions: Vec<ExecutableDefinition>,
29-
base_definitions: Vec<ExecutableDefinition>,
32+
project_definitions: &[ExecutableDefinition],
33+
base_definitions: &[ExecutableDefinition],
3034
) -> ReachableAst {
3135
if base_definitions.is_empty() {
3236
return ReachableAst {
33-
definitions: project_definitions,
37+
definitions: project_definitions.to_vec(),
3438
base_fragment_names: Default::default(),
3539
};
3640
}
@@ -42,30 +46,31 @@ pub fn get_reachable_ast(
4246
// Preprocess all base fragment definitions
4347
// Skipping operations because project definitions can't reference base operations
4448
for base_definition in base_definitions {
45-
match &base_definition {
46-
ExecutableDefinition::Fragment(fragment) => {
47-
let name = FragmentDefinitionName(fragment.name.value);
48-
assert!(
49-
base_definitions_map.insert(name, base_definition).is_none(),
50-
"get_reachable_ast called on graph with duplicate definition of `{name}`"
51-
)
52-
}
53-
ExecutableDefinition::Operation(_) => {}
49+
if let ExecutableDefinition::Fragment(fragment) = base_definition {
50+
let name = FragmentDefinitionName(fragment.name.value);
51+
assert!(
52+
base_definitions_map
53+
.insert(name, base_definition.clone())
54+
.is_none(),
55+
"get_reachable_ast called on graph with duplicate definition of `{name}`"
56+
);
5457
}
5558
}
5659

57-
let mut result = project_definitions.clone();
60+
// Clone project definitions into result once. We iterate the borrowed slice
61+
// directly below for selection visiting, avoiding a second clone.
62+
let mut result = project_definitions.to_vec();
5863

5964
// Find references from project definitions -> base definitions
6065
for definition in project_definitions {
6166
let selections = match definition {
62-
ExecutableDefinition::Operation(definition) => definition.selections,
63-
ExecutableDefinition::Fragment(definition) => definition.selections,
67+
ExecutableDefinition::Operation(definition) => &definition.selections,
68+
ExecutableDefinition::Fragment(definition) => &definition.selections,
6469
};
6570
visit_selections(
6671
&base_definitions_map,
6772
&mut reachable_base_asts,
68-
&selections,
73+
selections,
6974
false,
7075
)
7176
}

compiler/crates/dependency-analyzer/tests/ast.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
1717

1818
let source_location = SourceLocationKey::standalone(fixture.file_name);
1919
let definitions = parse_executable(parts[0], source_location).unwrap();
20-
let base_definitions = parts
20+
let base_definitions: Vec<ExecutableDefinition> = parts
2121
.iter()
2222
.skip(1)
2323
.flat_map(|part| parse_executable(part, source_location).unwrap().definitions)
2424
.collect();
2525
let ReachableAst {
2626
definitions: result,
2727
base_fragment_names,
28-
} = get_reachable_ast(definitions.definitions, base_definitions);
28+
} = get_reachable_ast(&definitions.definitions, &base_definitions);
2929

3030
let mut texts = result
3131
.into_iter()

compiler/crates/relay-compiler/src/build_project/project_asts.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ pub fn get_project_asts(
3939
graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,
4040
project_config: &ProjectConfig,
4141
) -> Result<ProjectAstData, BuildProjectError> {
42+
let empty: &[ExecutableDefinition] = &[];
4243
let project_asts = graphql_asts
4344
.get(&project_config.name)
4445
.map(|asts| asts.get_all_executable_definitions())
45-
.unwrap_or_default();
46+
.unwrap_or(empty);
4647
let (base_project_asts, base_definition_names) = match project_config.base {
4748
Some(base_project_name) => {
4849
let base_project_asts = graphql_asts
4950
.get(&base_project_name)
5051
.map(|asts| asts.get_all_executable_definitions())
51-
.unwrap_or_default();
52+
.unwrap_or(empty);
5253
let base_definition_names = base_project_asts
5354
.iter()
5455
// TODO(T64459085): Figure out what to do about unnamed (anonymous) operations
@@ -61,17 +62,17 @@ pub fn get_project_asts(
6162
.collect::<ExecutableDefinitionNameSet>();
6263
(base_project_asts, base_definition_names)
6364
}
64-
None => (Vec::new(), Default::default()),
65+
None => (empty, Default::default()),
6566
};
66-
find_duplicates(&project_asts, &base_project_asts).map_err(|errors| {
67+
find_duplicates(project_asts, base_project_asts).map_err(|errors| {
6768
BuildProjectError::ValidationErrors {
6869
errors,
6970
project_name: project_config.name,
7071
}
7172
})?;
7273

7374
let mut base_resolver_fragment_asts =
74-
find_base_resolver_fragment_asts(schema, &base_definition_names, &base_project_asts);
75+
find_base_resolver_fragment_asts(schema, &base_definition_names, base_project_asts);
7576

7677
let ReachableAst {
7778
mut definitions,

compiler/crates/relay-compiler/src/graphql_asts.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ use crate::utils::get_parser_features;
3535

3636
/// A collection of GraphQL abstract syntax trees (ASTs) for a set of files.
3737
///
38-
/// This struct contains a map of file paths to their corresponding GraphQL ASTs,
39-
/// as well as sets of pending and removed definition names.
38+
/// Stores definitions both per-file (for per-file lookup) and in a pre-flattened
39+
/// Vec (for efficient access to all definitions). `get_all_executable_definitions`
40+
/// returns a borrowed slice instead of cloning ~222K definitions each time.
4041
#[derive(Debug)]
4142
pub struct GraphQLAsts {
4243
/// A map of file paths to their corresponding GraphQL ASTs.
4344
asts: FnvHashMap<PathBuf, Vec<ExecutableDefinition>>,
45+
/// Pre-flattened Vec of all definitions across all files. Avoids expensive
46+
/// per-call cloning in `get_all_executable_definitions`.
47+
all_definitions: Vec<ExecutableDefinition>,
4448
/// Names of fragments and operations that are updated or created.
4549
pub pending_definition_names: ExecutableDefinitionNameSet,
4650
/// Names of fragments and operations that are deleted.
@@ -55,8 +59,8 @@ impl GraphQLAsts {
5559
self.asts.get(file_path)
5660
}
5761

58-
pub fn get_all_executable_definitions(&self) -> Vec<ExecutableDefinition> {
59-
self.asts.values().flatten().cloned().collect()
62+
pub fn get_all_executable_definitions(&self) -> &[ExecutableDefinition] {
63+
&self.all_definitions
6064
}
6165

6266
pub fn from_graphql_sources_map(
@@ -198,8 +202,13 @@ impl GraphQLAsts {
198202
}
199203

200204
if syntax_errors.is_empty() {
205+
// Pre-flatten all definitions to avoid cloning on every call to
206+
// get_all_executable_definitions(). This trades memory for avoiding
207+
// ~222K deep clones per build on the intern project.
208+
let all_definitions = asts.values().flatten().cloned().collect();
201209
Ok(Self {
202210
asts,
211+
all_definitions,
203212
pending_definition_names,
204213
removed_definition_names,
205214
})

0 commit comments

Comments
 (0)