Skip to content

Commit e3a3454

Browse files
committed
Various performance improvements (#164):
- Add caching of references using ResolveCache - Introduced GraphQLSchemaChangeListener to only clear schema caches when editing SDL, improving performance drastically while editing queries, fragments etc. that can't affect the schema - Improved use of graphql-java parser by minimal parsing and then adjusting source locations
1 parent a086500 commit e3a3454

File tree

9 files changed

+335
-40
lines changed

9 files changed

+335
-40
lines changed

resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<!-- Project services -->
5555
<projectService serviceInterface="com.intellij.lang.jsgraphql.schema.GraphQLTypeDefinitionRegistryServiceImpl" serviceImplementation="com.intellij.lang.jsgraphql.schema.GraphQLTypeDefinitionRegistryServiceImpl" />
5656
<projectService serviceInterface="com.intellij.lang.jsgraphql.schema.SchemaIDLTypeDefinitionRegistry" serviceImplementation="com.intellij.lang.jsgraphql.schema.SchemaIDLTypeDefinitionRegistry" />
57+
<projectService serviceInterface="com.intellij.lang.jsgraphql.schema.GraphQLSchemaChangeListener" serviceImplementation="com.intellij.lang.jsgraphql.schema.GraphQLSchemaChangeListener" />
5758
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.project.GraphQLPsiSearchHelper" serviceImplementation="com.intellij.lang.jsgraphql.ide.project.GraphQLPsiSearchHelper" />
5859
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.references.GraphQLReferenceService" serviceImplementation="com.intellij.lang.jsgraphql.ide.references.GraphQLReferenceService" />
5960
<projectService serviceInterface="com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService" serviceImplementation="com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService" />

src/main/com/intellij/lang/jsgraphql/endpoint/ide/project/JSGraphQLEndpointNamedTypeRegistry.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import com.google.common.collect.Lists;
1111
import com.google.common.collect.Maps;
1212
import com.intellij.lang.jsgraphql.endpoint.psi.*;
13+
import com.intellij.lang.jsgraphql.schema.GraphQLSchemaChangeListener;
14+
import com.intellij.lang.jsgraphql.schema.GraphQLSchemaEventListener;
1315
import com.intellij.lang.jsgraphql.schema.TypeDefinitionRegistryWithErrors;
1416
import com.intellij.lang.jsgraphql.v1.ide.configuration.JSGraphQLConfigurationProvider;
1517
import com.intellij.lang.jsgraphql.v1.psi.JSGraphQLElementType;
@@ -20,8 +22,6 @@
2022
import com.intellij.openapi.project.Project;
2123
import com.intellij.openapi.vfs.VirtualFile;
2224
import com.intellij.psi.*;
23-
import com.intellij.psi.impl.AnyPsiChangeListener;
24-
import com.intellij.psi.impl.PsiManagerImpl;
2525
import com.intellij.psi.util.PsiTreeUtil;
2626
import graphql.GraphQLException;
2727
import graphql.introspection.Introspection;
@@ -53,10 +53,9 @@ public static JSGraphQLEndpointNamedTypeRegistry getService(@NotNull Project pro
5353
public JSGraphQLEndpointNamedTypeRegistry(Project project) {
5454
this.project = project;
5555
this.configurationProvider = JSGraphQLConfigurationProvider.getService(project);
56-
project.getMessageBus().connect().subscribe(PsiManagerImpl.ANY_PSI_CHANGE_TOPIC, new AnyPsiChangeListener.Adapter() {
56+
project.getMessageBus().connect().subscribe(GraphQLSchemaChangeListener.TOPIC, new GraphQLSchemaEventListener() {
5757
@Override
58-
public void beforePsiChanged(boolean isPhysical) {
59-
// clear the cache on each PSI change
58+
public void onGraphQLSchemaChanged() {
6059
endpointTypesByName.clear();
6160
endpointEntryPsiFile.clear();
6261
projectToRegistry.clear();

src/main/com/intellij/lang/jsgraphql/endpoint/ide/startup/GraphQLStartupActivity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
package com.intellij.lang.jsgraphql.endpoint.ide.startup;
99

10+
import com.intellij.lang.jsgraphql.schema.GraphQLSchemaChangeListener;
1011
import com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService;
1112
import com.intellij.openapi.application.ApplicationManager;
1213
import com.intellij.openapi.project.DumbAware;
@@ -21,6 +22,10 @@ public class GraphQLStartupActivity implements StartupActivity, DumbAware {
2122

2223
@Override
2324
public void runActivity(@NotNull Project project) {
25+
26+
// startup schema change listener
27+
GraphQLSchemaChangeListener.getService(project);
28+
2429
if(ApplicationManager.getApplication().isUnitTestMode()) {
2530
// don't create the UI when unit testing
2631
return;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2018-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
* <p>
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.ide.references;
9+
10+
import com.intellij.lang.jsgraphql.psi.impl.GraphQLReferencePsiElement;
11+
import com.intellij.psi.PsiElement;
12+
import com.intellij.psi.PsiReference;
13+
import com.intellij.psi.PsiReferenceBase;
14+
import com.intellij.psi.impl.source.resolve.ResolveCache;
15+
import com.intellij.util.ArrayUtil;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.jetbrains.annotations.Nullable;
18+
19+
import java.util.function.Function;
20+
21+
22+
/**
23+
* Adds a layer of GraphQL Reference Caching based on ResolveCache
24+
*/
25+
public class GraphQLCachingReference extends PsiReferenceBase<GraphQLReferencePsiElement> {
26+
27+
private final Function<GraphQLReferencePsiElement, PsiElement> innerResolver;
28+
29+
public GraphQLCachingReference(@NotNull GraphQLReferencePsiElement element, Function<GraphQLReferencePsiElement, PsiElement> innerResolver) {
30+
super(element);
31+
this.innerResolver = innerResolver;
32+
}
33+
34+
@Nullable
35+
@Override
36+
public PsiElement resolve() {
37+
return ResolveCache.getInstance(getElement().getProject()).resolveWithCaching(this, GraphQLCachingReference.MyResolver.INSTANCE, false, false);
38+
}
39+
40+
@Nullable
41+
private PsiElement resolveInner() {
42+
return innerResolver.apply(myElement);
43+
}
44+
45+
@NotNull
46+
@Override
47+
public Object[] getVariants() {
48+
return ArrayUtil.EMPTY_OBJECT_ARRAY;
49+
}
50+
51+
private static class MyResolver implements ResolveCache.Resolver {
52+
private static final GraphQLCachingReference.MyResolver INSTANCE = new GraphQLCachingReference.MyResolver();
53+
54+
@Nullable
55+
public PsiElement resolve(@NotNull PsiReference ref, boolean incompleteCode) {
56+
return ((GraphQLCachingReference) ref).resolveInner();
57+
}
58+
}
59+
60+
}

src/main/com/intellij/lang/jsgraphql/ide/references/GraphQLReferenceService.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
package com.intellij.lang.jsgraphql.ide.references;
99

10+
import com.google.common.collect.Maps;
1011
import com.intellij.lang.jsgraphql.endpoint.ide.project.JSGraphQLEndpointNamedTypeRegistry;
1112
import com.intellij.lang.jsgraphql.endpoint.psi.*;
1213
import com.intellij.lang.jsgraphql.ide.project.GraphQLPsiSearchHelper;
@@ -22,23 +23,47 @@
2223
import com.intellij.openapi.util.Ref;
2324
import com.intellij.openapi.util.TextRange;
2425
import com.intellij.psi.*;
26+
import com.intellij.psi.impl.AnyPsiChangeListener;
27+
import com.intellij.psi.impl.PsiManagerImpl;
2528
import com.intellij.psi.util.PsiTreeUtil;
2629
import graphql.schema.GraphQLType;
2730
import graphql.schema.SchemaUtil;
2831
import org.jetbrains.annotations.NotNull;
2932
import org.jetbrains.annotations.Nullable;
3033

3134
import java.util.Collection;
35+
import java.util.Map;
3236
import java.util.Objects;
3337
import java.util.function.Predicate;
3438

3539
public class GraphQLReferenceService {
3640

41+
private final Map<String, PsiReference> logicalTypeNameToReference = Maps.newConcurrentMap();
42+
3743
public static GraphQLReferenceService getService(@NotNull Project project) {
3844
return ServiceManager.getService(project, GraphQLReferenceService.class);
3945
}
4046

47+
public GraphQLReferenceService(@NotNull final Project project) {
48+
project.getMessageBus().connect().subscribe(PsiManagerImpl.ANY_PSI_CHANGE_TOPIC, new AnyPsiChangeListener.Adapter() {
49+
@Override
50+
public void beforePsiChanged(boolean isPhysical) {
51+
// clear the cache on each PSI change
52+
logicalTypeNameToReference.clear();
53+
}
54+
});
55+
}
56+
4157
public PsiReference resolveReference(GraphQLReferencePsiElement element) {
58+
return new GraphQLCachingReference(element, this::doResolveReference);
59+
}
60+
61+
private PsiElement doResolveReference(GraphQLReferencePsiElement element) {
62+
PsiReference reference = innerResolveReference(element);
63+
return reference != null ? reference.resolve() : null;
64+
}
65+
66+
private PsiReference innerResolveReference(GraphQLReferencePsiElement element) {
4267
if (element != null) {
4368
final PsiElement parent = element.getParent();
4469
if (parent instanceof GraphQLField) {
@@ -259,16 +284,19 @@ public Object[] getVariants() {
259284

260285

261286
PsiReference resolveTypeName(GraphQLReferencePsiElement element) {
262-
PsiReference psiReference = resolveUsingIndex(element, psiNamedElement -> psiNamedElement instanceof GraphQLIdentifier && psiNamedElement.getParent() instanceof GraphQLTypeNameDefinition);
263-
if (psiReference == null) {
264-
// Endpoint language
265-
final JSGraphQLEndpointNamedTypeRegistry endpointNamedTypeRegistry = JSGraphQLEndpointNamedTypeRegistry.getService(element.getProject());
266-
final JSGraphQLNamedType namedType = endpointNamedTypeRegistry.getNamedType(element.getName());
267-
if (namedType != null) {
268-
return createReference(element, namedType.nameElement);
287+
final String logicalTypeName = GraphQLPsiSearchHelper.getFileName(element.getContainingFile()) + ":" + element.getName();
288+
return logicalTypeNameToReference.computeIfAbsent(logicalTypeName, logicalTypeNameKey -> {
289+
PsiReference psiReference = resolveUsingIndex(element, psiNamedElement -> psiNamedElement instanceof GraphQLIdentifier && psiNamedElement.getParent() instanceof GraphQLTypeNameDefinition);
290+
if (psiReference == null) {
291+
// Endpoint language
292+
final JSGraphQLEndpointNamedTypeRegistry endpointNamedTypeRegistry = JSGraphQLEndpointNamedTypeRegistry.getService(element.getProject());
293+
final JSGraphQLNamedType namedType = endpointNamedTypeRegistry.getNamedType(element.getName());
294+
if (namedType != null) {
295+
return createReference(element, namedType.nameElement);
296+
}
269297
}
270-
}
271-
return psiReference;
298+
return psiReference;
299+
});
272300
}
273301

274302

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2018-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
* <p>
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.schema;
9+
10+
import com.google.common.collect.Lists;
11+
import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointFile;
12+
import com.intellij.lang.jsgraphql.psi.GraphQLFile;
13+
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentDefinition;
14+
import com.intellij.lang.jsgraphql.psi.GraphQLOperationDefinition;
15+
import com.intellij.lang.jsgraphql.psi.GraphQLTemplateDefinition;
16+
import com.intellij.openapi.components.ServiceManager;
17+
import com.intellij.openapi.project.Project;
18+
import com.intellij.psi.PsiElement;
19+
import com.intellij.psi.PsiManager;
20+
import com.intellij.psi.PsiTreeChangeAdapter;
21+
import com.intellij.psi.PsiTreeChangeEvent;
22+
import com.intellij.psi.util.PsiTreeUtil;
23+
import com.intellij.util.messages.Topic;
24+
import org.jetbrains.annotations.NotNull;
25+
26+
import java.util.List;
27+
28+
/**
29+
* Tracks PSI changes that can affect declared GraphQL schemas
30+
*/
31+
public class GraphQLSchemaChangeListener {
32+
33+
public final static Topic<GraphQLSchemaEventListener> TOPIC = new Topic<>(
34+
"GraphQL Schema Change Events",
35+
GraphQLSchemaEventListener.class,
36+
Topic.BroadcastDirection.TO_PARENT
37+
);
38+
39+
public static GraphQLSchemaChangeListener getService(@NotNull Project project) {
40+
return ServiceManager.getService(project, GraphQLSchemaChangeListener.class);
41+
}
42+
43+
44+
private final Project myProject;
45+
private final PsiTreeChangeAdapter listener;
46+
private final PsiManager psiManager;
47+
48+
public GraphQLSchemaChangeListener(Project project) {
49+
myProject = project;
50+
psiManager = PsiManager.getInstance(myProject);
51+
listener = new PsiTreeChangeAdapter() {
52+
53+
private void checkForSchemaChange(PsiTreeChangeEvent event) {
54+
if (myProject.isDisposed()) {
55+
psiManager.removePsiTreeChangeListener(listener);
56+
return;
57+
}
58+
if (event.getFile() instanceof GraphQLFile) {
59+
if (affectsGraphQLSchema(event)) {
60+
signalSchemaChanged();
61+
}
62+
}
63+
if (event.getFile() instanceof JSGraphQLEndpointFile) {
64+
// always consider the schema changed when editing an endpoint file
65+
signalSchemaChanged();
66+
}
67+
}
68+
69+
private void signalSchemaChanged() {
70+
myProject.getMessageBus().syncPublisher(GraphQLSchemaChangeListener.TOPIC).onGraphQLSchemaChanged();
71+
}
72+
73+
@Override
74+
public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
75+
checkForSchemaChange(event);
76+
}
77+
78+
@Override
79+
public void childAdded(@NotNull PsiTreeChangeEvent event) {
80+
checkForSchemaChange(event);
81+
}
82+
83+
@Override
84+
public void childRemoved(@NotNull PsiTreeChangeEvent event) {
85+
checkForSchemaChange(event);
86+
}
87+
88+
@Override
89+
public void childMoved(@NotNull PsiTreeChangeEvent event) {
90+
checkForSchemaChange(event);
91+
}
92+
93+
@Override
94+
public void childReplaced(@NotNull PsiTreeChangeEvent event) {
95+
checkForSchemaChange(event);
96+
}
97+
98+
};
99+
psiManager.addPsiTreeChangeListener(listener);
100+
}
101+
102+
/**
103+
* Evaluates whether the change event can affect the associated GraphQL schema
104+
*
105+
* @param event the event that occurred
106+
*
107+
* @return true if the change can affect the declared schema
108+
*/
109+
private boolean affectsGraphQLSchema(PsiTreeChangeEvent event) {
110+
if (PsiTreeChangeEvent.PROP_FILE_NAME.equals(event.getPropertyName()) || PsiTreeChangeEvent.PROP_DIRECTORY_NAME.equals(event.getPropertyName())) {
111+
// renamed and moves are likely to affect schema blobs etc.
112+
return true;
113+
}
114+
final List<PsiElement> elements = Lists.newArrayList(event.getParent(), event.getChild(), event.getNewChild(), event.getOldChild());
115+
for (PsiElement element : elements) {
116+
if (element == null) {
117+
continue;
118+
}
119+
if (PsiTreeUtil.findFirstParent(element, parent -> parent instanceof GraphQLOperationDefinition || parent instanceof GraphQLFragmentDefinition || parent instanceof GraphQLTemplateDefinition) != null) {
120+
// edits inside query, mutation, subscription, fragment etc. don't affect the schema
121+
return false;
122+
}
123+
}
124+
// fallback to assume the schema can be affected by the edit
125+
return true;
126+
}
127+
128+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.intellij.lang.jsgraphql.schema;
2+
3+
import java.util.EventListener;
4+
5+
/**
6+
* Events relating to GraphQL schemas
7+
*/
8+
public interface GraphQLSchemaEventListener extends EventListener {
9+
10+
/**
11+
* One or more GraphQL schema changes are likely based on changed to the PSI trees
12+
*/
13+
void onGraphQLSchemaChanged();
14+
}

src/main/com/intellij/lang/jsgraphql/schema/GraphQLTypeDefinitionRegistryServiceImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public static GraphQLTypeDefinitionRegistryServiceImpl getService(@NotNull Proje
4040

4141
public GraphQLTypeDefinitionRegistryServiceImpl(Project project) {
4242
this.project = project;
43-
project.getMessageBus().connect().subscribe(PsiManagerImpl.ANY_PSI_CHANGE_TOPIC, new AnyPsiChangeListener.Adapter() {
43+
project.getMessageBus().connect().subscribe(GraphQLSchemaChangeListener.TOPIC, new GraphQLSchemaEventListener() {
4444
@Override
45-
public void beforePsiChanged(boolean isPhysical) {
45+
public void onGraphQLSchemaChanged() {
4646
// clear the cache on each PSI change
4747
fileNameToRegistry.clear();
4848
fileNameToSchema.clear();

0 commit comments

Comments
 (0)