Repository: haxefoundation/intellij-haxe
Issue: Performance issues with indexing and syntax inspections

Found performance issues that was introduced by this commit 0f734746972da141fe57846b303fed8e110aa89f

On project with multiple roots it drastically slowing indexing and syntax annotation performance. 
Caching with invalidating listeners are required.

Changed files:
src/common/com/intellij/plugins/haxe/model/HaxePackageModel.java, src/common/com/intellij/plugins/haxe/model/HaxeStdPackageModel.java, src/common/com/intellij/plugins/haxe/util/HaxeResolveUtil.java, src/common/com/intellij/plugins/haxe/model/HaxeProjectModel.java, src/common/com/intellij/plugins/haxe/model/HaxeSourceRootModel.java

diff --git a/src/common/com/intellij/plugins/haxe/model/HaxePackageModel.java b/src/common/com/intellij/plugins/haxe/model/HaxePackageModel.java
index 6a1ff398..68af795d 100644
--- a/src/common/com/intellij/plugins/haxe/model/HaxePackageModel.java
+++ b/src/common/com/intellij/plugins/haxe/model/HaxePackageModel.java
@@ -39,11 +39,10 @@ public class HaxePackageModel implements HaxeExposableModel {
   private final FullyQualifiedInfo qualifiedInfo;
 
 
-  public HaxePackageModel(@NotNull HaxeProjectModel project,
-                          @NotNull HaxeSourceRootModel root,
+  public HaxePackageModel(@NotNull HaxeSourceRootModel root,
                           @NotNull String name,
                           @Nullable HaxePackageModel parent) {
-    this.project = project;
+    this.project = root.project;
     this.name = name;
     this.root = root;
     this.parent = parent;
@@ -102,7 +101,7 @@ public class HaxePackageModel implements HaxeExposableModel {
     } else {
       PsiDirectory directory = root.access(path.isEmpty() ? name : path + '.' + name);
       if (directory != null) {
-        return new HaxePackageModel(project, root, name, this);
+        return new HaxePackageModel(root, name, this);
       }
     }
 
@@ -114,7 +113,7 @@ public class HaxePackageModel implements HaxeExposableModel {
     PsiDirectory directory = root.access(path);
     if (directory != null) {
       return Arrays.stream(directory.getSubdirectories())
-        .map(subDirectory -> new HaxePackageModel(project, root, subDirectory.getName(), this))
+        .map(subDirectory -> new HaxePackageModel(root, subDirectory.getName(), this))
         .collect(Collectors.toList());
     }
     return Collections.emptyList();
diff --git a/src/common/com/intellij/plugins/haxe/model/HaxeProjectModel.java b/src/common/com/intellij/plugins/haxe/model/HaxeProjectModel.java
index 6fc68485..1b11e96f 100644
--- a/src/common/com/intellij/plugins/haxe/model/HaxeProjectModel.java
+++ b/src/common/com/intellij/plugins/haxe/model/HaxeProjectModel.java
@@ -15,9 +15,13 @@
  */
 package com.intellij.plugins.haxe.model;
 
+import com.intellij.ProjectTopics;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootEvent;
+import com.intellij.openapi.roots.ModuleRootListener;
 import com.intellij.openapi.roots.OrderEnumerator;
+import com.intellij.openapi.util.Key;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiDirectory;
 import com.intellij.psi.PsiElement;
@@ -25,59 +29,37 @@ import com.intellij.psi.search.GlobalSearchScope;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static com.intellij.plugins.haxe.model.HaxeStdTypesFileModel.STD_TYPES_HX;
 
 public class HaxeProjectModel {
+  private static final Key<HaxeProjectModel> HAXE_PROJECT_MODEL_KEY = new Key<>("HAXE_PROJECT_MODEL");
   private final Project project;
-  private final HaxePackageModel stdPackage;
-  private final HaxeSourceRootModel sdkRoot;
+
+  private RootsCache rootsCache;
 
   private HaxeProjectModel(Project project) {
     this.project = project;
-    this.sdkRoot = resolveSdkRoot();
-    this.stdPackage = new HaxeStdPackageModel(this, getSdkRoot());
-  }
-
-  private HaxeSourceRootModel resolveSdkRoot() {
-    if (ApplicationManager.getApplication().isUnitTestMode()) {
-      final VirtualFile[] roots;
-
-      roots = OrderEnumerator.orderEntries(project).getAllSourceRoots();
-      if (roots.length > 0) {
-        VirtualFile stdRootForTests = roots[0].findChild("std");
-        if (stdRootForTests != null) {
-          return new HaxeSourceRootModel(this, stdRootForTests);
-        }
-      }
-    } else {
-      VirtualFile root = detectProperSDKSourceRoot(OrderEnumerator.orderEntries(project).sdkOnly().getAllSourceRoots());
-
-      if (root != null) {
-        return new HaxeSourceRootModel(this, root);
-      }
-    }
-    return HaxeSourceRootModel.DUMMY;
+    addProjectListeners();
   }
 
-  private VirtualFile detectProperSDKSourceRoot(VirtualFile[] roots) {
-    for (VirtualFile root : roots) {
-      if (root.findChild(STD_TYPES_HX) != null) return root;
-    }
-
-    return null;
-  }
-
-
   public static HaxeProjectModel fromElement(PsiElement element) {
     return fromProject(element.getProject());
   }
 
   public static HaxeProjectModel fromProject(Project project) {
-    return new HaxeProjectModel(project);
+    HaxeProjectModel model = project.getUserData(HAXE_PROJECT_MODEL_KEY);
+    if (model == null) {
+      model = new HaxeProjectModel(project);
+      project.putUserData(HAXE_PROJECT_MODEL_KEY, model);
+    }
+
+    return model;
   }
 
   public Project getProject() {
@@ -89,25 +71,17 @@ public class HaxeProjectModel {
   }
 
   public List<HaxeSourceRootModel> getRoots() {
-    OrderEnumerator enumerator = OrderEnumerator.orderEntries(project).withoutSdk();
-
-    return Stream.concat(
-        Arrays.stream(enumerator.getClassesRoots()),
-        Arrays.stream(enumerator.getSourceRoots())
-      )
-      .distinct()
-      .map(root -> new HaxeSourceRootModel(this, root))
-      .collect(Collectors.toList());
+    return getRootsCache().roots;
   }
 
   @NotNull
   public HaxeSourceRootModel getSdkRoot() {
-    return sdkRoot;
+    return getRootsCache().sdkRoot;
   }
 
   @NotNull
   public HaxePackageModel getStdPackage() {
-    return stdPackage;
+    return getRootsCache().stdPackageModel;
   }
 
   @Nullable
@@ -128,8 +102,8 @@ public class HaxeProjectModel {
       if (resolvedValue != null) result.add(resolvedValue);
     }
 
-    if (result.isEmpty() && stdPackage != null) {
-      resolvedValue = stdPackage.resolve(info);
+    if (result.isEmpty()) {
+      resolvedValue = getStdPackage().resolve(info);
       if (resolvedValue != null) result.add(resolvedValue);
     }
 
@@ -160,4 +134,69 @@ public class HaxeProjectModel {
     }
     return null;
   }
+
+  private void addProjectListeners() {
+    project.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
+      @Override
+      public void rootsChanged(ModuleRootEvent event) {
+        rootsCache = null;
+      }
+    });
+  }
+
+  private RootsCache getRootsCache() {
+    if (rootsCache == null) {
+      rootsCache = RootsCache.fromProjectModel(this);
+    }
+    return rootsCache;
+  }
+}
+
+class RootsCache {
+  final List<HaxeSourceRootModel> roots;
+  final HaxeSourceRootModel sdkRoot;
+  final HaxeStdPackageModel stdPackageModel;
+
+  private RootsCache(List<HaxeSourceRootModel> roots, HaxeSourceRootModel sdkRoot) {
+    this.roots = roots;
+    this.sdkRoot = sdkRoot;
+    this.stdPackageModel = new HaxeStdPackageModel(sdkRoot);
+  }
+
+  static RootsCache fromProjectModel(HaxeProjectModel model) {
+    return new RootsCache(getProjectRoots(model), getSdkRoot(model));
+  }
+
+  private static List<HaxeSourceRootModel> getProjectRoots(final HaxeProjectModel model) {
+    final OrderEnumerator enumerator = OrderEnumerator.orderEntries(model.getProject()).withoutSdk();
+
+    return Stream.concat(
+      Arrays.stream(enumerator.getClassesRoots()),
+      Arrays.stream(enumerator.getSourceRoots())
+    )
+      .distinct()
+      .map(root -> new HaxeSourceRootModel(model, root))
+      .collect(Collectors.toList());
+  }
+
+  private static HaxeSourceRootModel getSdkRoot(final HaxeProjectModel model) {
+    final VirtualFile[] roots;
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      roots = OrderEnumerator.orderEntries(model.getProject()).getAllSourceRoots();
+      if (roots.length > 0) {
+        VirtualFile stdRootForTests = roots[0].findChild("std");
+        if (stdRootForTests != null) {
+          return new HaxeSourceRootModel(model, stdRootForTests);
+        }
+      }
+    } else {
+      roots = OrderEnumerator.orderEntries(model.getProject()).sdkOnly().getAllSourceRoots();
+      for (VirtualFile root : roots) {
+        if (root.findChild(STD_TYPES_HX) != null) {
+          return new HaxeSourceRootModel(model, root);
+        }
+      }
+    }
+    return HaxeSourceRootModel.DUMMY;
+  }
 }
\\ No newline at end of file
diff --git a/src/common/com/intellij/plugins/haxe/model/HaxeSourceRootModel.java b/src/common/com/intellij/plugins/haxe/model/HaxeSourceRootModel.java
index 25d575e3..bc97de1a 100644
--- a/src/common/com/intellij/plugins/haxe/model/HaxeSourceRootModel.java
+++ b/src/common/com/intellij/plugins/haxe/model/HaxeSourceRootModel.java
@@ -37,7 +37,7 @@ public class HaxeSourceRootModel {
       rootPackage = null;
     }
     else {
-      rootPackage = new HaxePackageModel(project, this, "", null);
+      rootPackage = new HaxePackageModel(this, "", null);
     }
   }
 
diff --git a/src/common/com/intellij/plugins/haxe/model/HaxeStdPackageModel.java b/src/common/com/intellij/plugins/haxe/model/HaxeStdPackageModel.java
index f4237e89..88890a6d 100644
--- a/src/common/com/intellij/plugins/haxe/model/HaxeStdPackageModel.java
+++ b/src/common/com/intellij/plugins/haxe/model/HaxeStdPackageModel.java
@@ -23,9 +23,8 @@ public class HaxeStdPackageModel extends HaxePackageModel {
   private static final String STD_TYPES = "StdTypes";
   private final HaxeFileModel stdTypesModel;
 
-  HaxeStdPackageModel(@NotNull HaxeProjectModel project,
-                      @NotNull HaxeSourceRootModel root) {
-    super(project, root, "", null);
+  HaxeStdPackageModel(@NotNull HaxeSourceRootModel root) {
+    super(root, "", null);
     this.stdTypesModel = this.getStdFileModel();
   }
 
diff --git a/src/common/com/intellij/plugins/haxe/util/HaxeResolveUtil.java b/src/common/com/intellij/plugins/haxe/util/HaxeResolveUtil.java
index 2df26f25..2f6423e1 100644
--- a/src/common/com/intellij/plugins/haxe/util/HaxeResolveUtil.java
+++ b/src/common/com/intellij/plugins/haxe/util/HaxeResolveUtil.java
@@ -617,12 +617,12 @@ public class HaxeResolveUtil {
     List<HaxeClassModel> models = file.getClassModels();
     final Stream<HaxeClassModel> classesStream = models.stream().filter(model -> name.equals(model.getName()));
     final Stream<HaxeEnumValueModel> enumsStream = models.stream().filter(model -> model instanceof HaxeEnumModel)
-                                                  .map(model -> ((HaxeEnumModel)model).getValue(name))
-                                                  .filter(Objects::nonNull);
+      .map(model -> ((HaxeEnumModel)model).getValue(name))
+      .filter(Objects::nonNull);
 
     final HaxeModel result = Stream.concat(classesStream, enumsStream)
-                                .findFirst()
-                                .orElse(null);
+      .findFirst()
+      .orElse(null);
 
     return result != null ? result.getBasePsi() : null;
   }
@@ -658,6 +658,15 @@ public class HaxeResolveUtil {
   }
 
   public static String getQName(PsiElement[] fileChildren, final String result, boolean searchInSamePackage) {
+    final HaxeClass classForType = (HaxeClass)Arrays.stream(fileChildren)
+      .filter(child -> child instanceof HaxeClass && result.equals(((HaxeClass)child).getName()))
+      .findFirst()
+      .orElse(null);
+
+    if (classForType != null) {
+      return classForType.getQualifiedName();
+    }
+
     final HaxeImportStatement importStatement =
       (HaxeImportStatement)(StreamUtil.reverse(Arrays.stream(fileChildren))
                               .filter(element ->
@@ -667,27 +676,23 @@ public class HaxeResolveUtil {
                               .orElse(null));
 
     final HaxeExpression importStatementExpression = importStatement == null ? null : importStatement.getReferenceExpression();
-    final String packageName = getPackageName(
-      (HaxePackageStatement)Arrays.stream(fileChildren)
-        .filter(element -> element instanceof HaxePackageStatement)
-        .findFirst()
-        .orElse(null)
-    );
+    if (importStatementExpression != null) {
+      return importStatementExpression.getText();
+    }
 
-    final HaxeClass classForType = (HaxeClass)ContainerUtil.find(fileChildren, new Condition<PsiElement>() {
-      @Override
-      public boolean value(PsiElement element) {
-        return element instanceof HaxeClass && result.equals(((HaxeClass)element).getName());
+    if (searchInSamePackage && fileChildren.length > 0) {
+      final HaxeFileModel fileModel = HaxeFileModel.fromElement(fileChildren[0]);
+      if (fileModel != null) {
+        final HaxePackageModel packageModel = fileModel.getPackageModel();
+        if (packageModel != null) {
+          final HaxeClassModel classModel = packageModel.getClassModel(result);
+          if (classModel != null) {
+            return classModel.haxeClass.getQualifiedName();
+          }
+        }
       }
-    });
-
-    if (classForType != null) {
-      return classForType.getQualifiedName();
-    } else if (importStatement != null && importStatementExpression != null) {
-      return importStatementExpression.getText();
-    } else if (searchInSamePackage && !packageName.isEmpty()) {
-      return packageName + "." + result;
     }
+
     return result;
   }