+ * All types of a resource are generated with one project resource loader: the inferrer renders every
+ * method body and field initializer during emission within
+ * {@link GeneratorSupport#executeWithProjectResourceLoader(org.eclipse.core.resources.IProject, Runnable)
+ * GeneratorSupport#executeWithProjectResourceLoader}, and establishing that resource loader once for the
+ * whole resource lets those nested calls reuse it instead of building a class loader for every body.
*/
public class ExportJvmModelGenerator extends JvmModelGenerator {
@@ -50,6 +59,18 @@ public class ExportJvmModelGenerator extends JvmModelGenerator {
@Inject
private TreeAppendableUtil treeAppendableUtil;
+ @Inject
+ private GeneratorSupport generatorSupport;
+
+ @Override
+ public void doGenerate(final Resource input, final IFileSystemAccess fsa) {
+ if (input.getContents().stream().anyMatch(JvmDeclaredType.class::isInstance)) {
+ generatorSupport.executeWithProjectResourceLoaderOf(input, () -> super.doGenerate(input, fsa));
+ } else {
+ super.doGenerate(input, fsa); // no inferred type, hence no body to render: do not build a class loader
+ }
+ }
+
/**
* {@inheritDoc}
*
diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportModelTypeResolver.java b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportModelTypeResolver.java
index 60dfdb0cd..fa1ab56fb 100644
--- a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportModelTypeResolver.java
+++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportModelTypeResolver.java
@@ -26,6 +26,7 @@
import com.avaloq.tools.ddk.xtext.export.export.ExportModel;
import com.avaloq.tools.ddk.xtext.export.export.ExportPackage;
import com.avaloq.tools.ddk.xtext.export.export.Import;
+import com.avaloq.tools.ddk.xtext.expression.generator.GeneratorSupport;
import com.avaloq.tools.ddk.xtext.util.EObjectUtil;
@@ -146,7 +147,30 @@ private EClassifier findClassifier(final String name) {
*/
public static ExportModelTypeResolver forElement(final EObject element) {
final ExportModel exportModel = EcoreUtil2.getContainerOfType(element, ExportModel.class);
- return exportModel == null ? null : new ExportModelTypeResolver(exportModel);
+ return exportModel == null ? null : forModel(exportModel);
+ }
+
+ /**
+ * Returns the resolver for the given export model. The resolver is immutable and determined by the model and the EPackages
+ * visible to it, so within a generation pass it is {@link GeneratorSupport#memoize(Object, java.util.function.Supplier)
+ * memoized} and shared by all expressions of the model instead of being rebuilt - which resolves every visible EPackage -
+ * for each of them.
+ *
+ * @param model
+ * the export model, must not be {@code null}
+ * @return the resolver, never {@code null}
+ */
+ public static ExportModelTypeResolver forModel(final ExportModel model) {
+ return GeneratorSupport.memoize(new Key(model), () -> new ExportModelTypeResolver(model));
+ }
+
+ /**
+ * Key under which the resolver of an export model is memoized.
+ *
+ * @param model
+ * the export model
+ */
+ private record Key(ExportModel model) {
}
}
diff --git a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GenModelUtilX.java b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GenModelUtilX.java
index 7a18e33d8..bcbe6d6cf 100644
--- a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GenModelUtilX.java
+++ b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GenModelUtilX.java
@@ -56,10 +56,9 @@ public class GenModelUtilX {
private ResourceDescriptionsProvider resourceDescriptionsProvider;
/**
- * The current model resource used for GenPackage lookups. This utility is normally a Guice singleton, so
- * multiple Xtext workers (the editor reconciler, the workspace builder, content assist, etc.) share the same
- * instance and would otherwise race on a single field. Storing the context per-thread isolates each worker's
- * lookups from the others.
+ * The current model resource used for GenPackage lookups. This utility is not a singleton: every injection point gets
+ * its own instance, and only instances given a resource through {@link #setResource(Resource)} use one. The context is
+ * stored per thread so that callers sharing an instance on different threads do not see each other's context.
*/
private final ThreadLocal context = new ThreadLocal<>();
@@ -236,9 +235,53 @@ private String instanceClassNameFromRegistry(final EClassifier classifier) {
return counterpart.getInstanceClassName();
}
+ /**
+ * Returns the {@link GenPackage} of the {@link EPackage} containing the given element.
+ *
+ * With a context resource set, a GenPackage found is
+ * {@link GeneratorSupport#memoize(Object, java.util.function.Supplier) memoized} for the generation pass, so that the many
+ * literal and instance class names a generated file refers to do not each repeat the index lookup and the scan over all
+ * indexed GenPackages it can fall back to. Once found, the GenPackage depends only on the context resource and the
+ * EPackage. A lookup that finds nothing is not memoized: its final fallback only searches the GenModels already loaded
+ * into the resource set, so it may succeed later in the same pass.
+ *
+ *
+ * @param element
+ * the model element, must not be {@code null}
+ * @return the GenPackage, or {@code null} if none is found
+ */
public GenPackage genPackage(final EModelElement element) {
final EPackage ePackage = EcoreUtil2.getContainerOfType(element, EPackage.class);
final Resource ctx = context.get();
+ if (ctx == null || ePackage == null) {
+ return lookUpGenPackage(element, ePackage, ctx);
+ }
+ return GeneratorSupport.memoize(new GenPackageKey(ctx, ePackage), () -> lookUpGenPackage(element, ePackage, ctx));
+ }
+
+ /**
+ * Key under which the GenPackage of an EPackage is memoized for a context resource.
+ *
+ * @param context
+ * the context resource
+ * @param ePackage
+ * the EPackage
+ */
+ private record GenPackageKey(Resource context, EPackage ePackage) {
+ }
+
+ /**
+ * Looks up the {@link GenPackage} of the given EPackage.
+ *
+ * @param element
+ * the model element, must not be {@code null}
+ * @param ePackage
+ * the EPackage containing the element, may be {@code null}
+ * @param ctx
+ * the context resource, may be {@code null}
+ * @return the GenPackage, or {@code null} if none is found
+ */
+ private GenPackage lookUpGenPackage(final EModelElement element, final EPackage ePackage, final Resource ctx) {
if (globalScopeProvider != null && ctx != null) {
final IScope scope = globalScopeProvider.getScope(ctx, GenModelPackage.Literals.GEN_MODEL__GEN_PACKAGES, null);
if (scope != null && ePackage != null) {
diff --git a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorSupport.java b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorSupport.java
index decf92f15..137d641aa 100644
--- a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorSupport.java
+++ b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorSupport.java
@@ -15,14 +15,21 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.mwe.core.resources.ResourceLoader;
import org.eclipse.emf.mwe.core.resources.ResourceLoaderFactory;
import org.eclipse.emf.mwe.core.resources.ResourceLoaderImpl;
@@ -40,9 +47,38 @@ public class GeneratorSupport {
/** Class-wide logger. */
private static final Logger LOG = LogManager.getLogger(GeneratorSupport.class);
+ /**
+ * The {@link #executeWithProjectResourceLoader} call whose resource loader is installed on the current thread, if any.
+ * Nested calls for the same project and resource loader run within it and do not replace it.
+ */
+ private static final ThreadLocal CURRENT_SCOPE = new ThreadLocal<>();
+
+ /**
+ * One {@link #executeWithProjectResourceLoader} call: the project, the resource loader installed for it and the values
+ * memoized while it runs.
+ *
+ * @param project
+ * the project, may be {@code null}
+ * @param resourceLoader
+ * the resource loader installed for the project
+ * @param memoizedValues
+ * the values memoized while the call runs
+ */
+ private record ProjectScope(IProject project, ResourceLoader resourceLoader, Map