Cache necessary info about classpaths for improved completion speed

This commit is contained in:
Akash Yadav
2022-08-09 22:54:27 +05:30
parent c269dca911
commit 24e6755dde
9 changed files with 171 additions and 99 deletions
@@ -26,8 +26,8 @@ import com.itsaky.androidide.javac.services.NBMemberEnter
import com.itsaky.androidide.javac.services.NBParserFactory
import com.itsaky.androidide.javac.services.NBResolve
import com.itsaky.androidide.javac.services.NBTreeMaker
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
import com.itsaky.androidide.javac.services.fs.JarPackageProviderImpl
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.zipfs2.JarPackageProvider
import com.sun.source.util.JavacTask
import com.sun.source.util.TaskEvent
@@ -41,6 +41,7 @@ import com.sun.tools.javac.comp.Check
import com.sun.tools.javac.comp.CompileStates
import com.sun.tools.javac.comp.Enter
import com.sun.tools.javac.comp.Modules
import com.sun.tools.javac.file.FSInfo
import com.sun.tools.javac.main.Arguments
import com.sun.tools.javac.main.JavaCompiler
import com.sun.tools.javac.model.JavacElements
@@ -59,24 +60,24 @@ import javax.tools.JavaFileObject
* @author Akash Yadav
*/
class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
private val flowCompleted = mutableSetOf<URI>()
private val log = ILogger.newInstance(javaClass.simpleName)
init {
put(Log.logKey, ReusableLog.factory)
put(FSInfo::class.java, CacheFSInfoSingleton)
put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory)
put(JavacFlowListener.flowListenerKey, JavacFlowListener { this.hasFlowCompleted(it) })
put(JarPackageProvider::class.java, JarPackageProviderImpl)
registerNBServices(cancelService)
}
@DefinedBy(COMPILER_TREE)
override fun started(e: TaskEvent) {
log.debug("Started: $e")
// log.debug("Started: $e")
// Do nothing
}
@DefinedBy(COMPILER_TREE)
override fun finished(e: TaskEvent) {
if (e.kind == ANALYZE) {
@@ -86,7 +87,7 @@ class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
}
}
}
fun clear() {
drop(Arguments.argsKey)
drop(DiagnosticListener::class.java)
@@ -96,7 +97,7 @@ class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
drop(JavacTask::class.java)
drop(JavacTrees::class.java)
drop(JavacElements::class.java)
if (ht[Log.logKey] is ReusableLog) {
// log already init-ed - not first round
(Log.instance(this) as ReusableLog).clear()
@@ -110,15 +111,15 @@ class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
MultiTaskListener.instance(this).clear()
}
}
private fun <T> drop(k: Key<T>?) {
ht.remove(k)
}
private fun <T> drop(c: Class<T>?) {
ht.remove(key(c))
}
private fun hasFlowCompleted(fo: JavaFileObject?): Boolean {
return if (fo == null) {
false
@@ -130,7 +131,7 @@ class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
}
}
}
private fun registerNBServices(cancelService: CancelService) {
NBAttr.preRegister(this)
NBParserFactory.preRegister(this)
@@ -142,4 +143,4 @@ class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
NBClassFinder.preRegister(this)
CancelService.preRegister(this, cancelService)
}
}
}
@@ -0,0 +1,46 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.javac.services.fs
import com.sun.tools.javac.file.CacheFSInfo
import java.nio.file.Path
/**
* Singleton class for [CacheFSInfo] to avoid reading attributes of same file multiple times.
*
* @author Akash Yadav
*/
object CacheFSInfoSingleton : CacheFSInfo() {
/**
* Caches information about the given [Path].
*/
@JvmOverloads
fun cache(file: Path, cacheJarClasspath: Boolean = true) {
// Cache canonical path
getCanonicalFile(file)
// Cache attributes
getAttributes(file)
// Cache jar classpath if requested
if (cacheJarClasspath) {
getJarClassPath(file)
}
}
}
@@ -19,6 +19,7 @@ package com.itsaky.androidide.javac.services.fs
import com.itsaky.androidide.zipfs2.ZipFileSystem
import com.itsaky.androidide.zipfs2.ZipFileSystemProvider
import com.sun.tools.javac.file.RelativePath.RelativeDirectory
import java.nio.file.Path
import javax.lang.model.SourceVersion
@@ -30,7 +31,7 @@ import javax.lang.model.SourceVersion
class CachedJarFileSystem(provider: ZipFileSystemProvider?, zfpath: Path?, env: MutableMap<String, *>?) :
ZipFileSystem(provider, zfpath, env) {
internal val packages = mutableMapOf<String, Path>()
internal val packages = mutableMapOf<RelativeDirectory, Path>()
override fun close() {
// Do nothing
@@ -43,7 +44,7 @@ class CachedJarFileSystem(provider: ZipFileSystemProvider?, zfpath: Path?, env:
fun storeJARPackageDir(dir: Path?) : Boolean {
if (isValid(dir?.fileName)) {
packages[rootDir.relativize(dir!!).toString()] = dir
packages[RelativeDirectory(rootDir.relativize(dir!!).toString())] = dir
return true
}
@@ -19,6 +19,7 @@ package com.itsaky.androidide.javac.services.fs
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.zipfs2.JarPackageProvider
import com.sun.tools.javac.file.RelativePath.RelativeDirectory
import java.nio.file.Path
/**
@@ -27,10 +28,7 @@ import java.nio.file.Path
* @author Akash Yadav
*/
object JarPackageProviderImpl : JarPackageProvider {
private val log = ILogger.newInstance(javaClass.simpleName)
override fun getPackages(archivePath: Path): MutableMap<String, Path> {
override fun getPackages(archivePath: Path): MutableMap<RelativeDirectory, Path> {
val fs = CachingJarFileSystemProvider.newFileSystem(archivePath) as CachedJarFileSystem
return fs.packages
}
@@ -19,16 +19,19 @@ package com.itsaky.androidide.projects.api
import android.text.TextUtils
import com.itsaky.androidide.builder.model.IJavaCompilerSettings
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
import com.itsaky.androidide.projects.classpath.JarFsClasspathReader
import com.itsaky.androidide.projects.classpath.ZipFileClasspathReader
import com.itsaky.androidide.tooling.api.model.GradleTask
import com.itsaky.androidide.projects.util.BootClasspathProvider
import com.itsaky.androidide.tooling.api.model.GradleTask
import com.itsaky.androidide.utils.ClassTrie
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.SourceClassTrie
import com.itsaky.androidide.utils.SourceClassTrie.SourceNode
import com.itsaky.androidide.utils.StopWatch
import java.io.File
import java.nio.file.Path
import kotlin.io.path.pathString
/**
* A module project. Base class for [AndroidModule] and [JavaModule].
@@ -56,8 +59,8 @@ abstract class ModuleProject(
const val USAGE_RUNTIME = "java-runtime"
}
@JvmField val compileJavaSourceClasses = com.itsaky.androidide.utils.SourceClassTrie()
@JvmField val compileClasspathClasses = com.itsaky.androidide.utils.ClassTrie()
@JvmField val compileJavaSourceClasses = SourceClassTrie()
@JvmField val compileClasspathClasses = ClassTrie()
/**
* Get the source directories of this module (non-transitive i.e for this module only).
@@ -119,6 +122,13 @@ abstract class ModuleProject(
watch = StopWatch("Indexing classpaths")
val paths = getCompileClasspaths().filter { it.exists() }
for (path in paths) {
// Use 'getCanonicalFile' just to be sure that caches are stored with correct keys
// See JavacFileManager.getContainer(Path) for more details
CacheFSInfoSingleton.cache(CacheFSInfoSingleton.getCanonicalFile(path.toPath()))
}
val topLevelClasses = JarFsClasspathReader().listClasses(paths).filter { it.isTopLevel }
topLevelClasses.forEach { this.compileClasspathClasses.append(it.name) }
@@ -138,14 +148,34 @@ abstract class ModuleProject(
return ""
}
val source = this.compileJavaSourceClasses.findSource(file)
if (source != null) {
return source.packageName
val sourceNode = searchSourceFileRelatively(file)
if (sourceNode != null) {
return sourceNode.packageName
}
return ""
}
private fun searchSourceFileRelatively(file: Path?): SourceNode? {
for (source in getCompileSourceDirectories().map(File::toPath)) {
val relative = source.relativize(file)
if (relative.pathString.contains("..")) {
// This is most probably not the one we're expecting
continue
}
var name = relative.pathString.substringBeforeLast(".java")
name = name.replace('/', '.')
val node = this.compileJavaSourceClasses.findNode(name)
if (node != null && node is SourceNode) {
return node
}
}
return null
}
fun suggestPackageName(file: Path): String {
var dir = file.parent.normalize()
while (dir != null) {
@@ -23,6 +23,7 @@ import androidx.annotation.RestrictTo;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent;
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton;
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider;
import com.itsaky.androidide.lsp.api.ILanguageClient;
import com.itsaky.androidide.lsp.api.ILanguageServer;
@@ -59,7 +60,6 @@ import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.utils.DocumentUtils;
import com.itsaky.androidide.utils.ILogger;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -73,7 +73,6 @@ import java.util.concurrent.CompletableFuture;
public class JavaLanguageServer implements ILanguageServer {
public static final String SERVER_ID = "java";
private static final ILogger LOG = ILogger.newInstance("JavaLanguageServer");
private final CompletionProvider completionProvider;
private final JavaDiagnosticProvider diagnosticProvider;
private ILanguageClient client;
@@ -107,6 +106,7 @@ public class JavaLanguageServer implements ILanguageServer {
public void shutdown() {
JavaCompilerProvider.getInstance().destory();
SourceFileManager.clearCache();
CacheFSInfoSingleton.INSTANCE.clearCache();
CachingJarFileSystemProvider.INSTANCE.clearCache();
EventBus.getDefault().unregister(this);
@@ -20,7 +20,6 @@ package com.itsaky.androidide.lsp.java.compiler;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.CloseUtils;
import com.google.common.collect.Iterables;
import com.itsaky.androidide.javac.services.fs.AndroidFsProviderImpl;
import com.itsaky.androidide.projects.api.AndroidModule;
import com.itsaky.androidide.projects.api.ModuleProject;
@@ -35,9 +34,10 @@ import com.sun.tools.javac.util.Context;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -47,34 +47,54 @@ import java.util.stream.Stream;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
@SuppressWarnings("Since15")
public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
public class SourceFileManager extends JavacFileManager {
public static final String ANDROIDIDE_CACHE_LOCATION = "ANDROIDIDE_CACHE_LOCATION";
public static final SourceFileManager NO_MODULE = new SourceFileManager(null);
private static final ILogger LOG = ILogger.newInstance("SourceFileManager");
private static final Map<ModuleProject, SourceFileManager> cachedFileManagers =
new ConcurrentHashMap<>();
private final ModuleProject module;
private SourceFileManager(final ModuleProject module) {
super(createDelegateFileManager());
this.module = module;
public static SourceFileManager forModule(@NonNull ModuleProject project) {
Objects.requireNonNull(project);
return cachedFileManagers.computeIfAbsent(project, SourceFileManager::createForModule);
}
public static void clearCache() {
for (final SourceFileManager fileManager : cachedFileManagers.values()) {
CloseUtils.closeIO(fileManager);
}
cachedFileManagers.clear();
}
private static SourceFileManager createForModule(@NonNull ModuleProject project) {
LOG.info("Creating source file manager instance for module:", project);
return new SourceFileManager(project);
}
private SourceFileManager(final ModuleProject module) {
super(new Context(), false, Charset.defaultCharset());
this.module = module;
AndroidFsProviderImpl.INSTANCE.init();
listLocations(EnumSet.of(StandardLocation.CLASS_PATH, StandardLocation.PLATFORM_CLASS_PATH));
if (module != null) {
setLocation(StandardLocation.CLASS_PATH, module.getCompileClasspaths());
setLocation(StandardLocation.SOURCE_PATH, module.getCompileSourceDirectories());
setLocationLogError(StandardLocation.CLASS_PATH, module.getCompileClasspaths());
setLocationLogError(StandardLocation.SOURCE_PATH, module.getCompileSourceDirectories());
if (module instanceof AndroidModule) {
final AndroidModule android = ((AndroidModule) module);
setLocation(StandardLocation.PLATFORM_CLASS_PATH, android.getBootClassPaths());
setLocationLogError(StandardLocation.PLATFORM_CLASS_PATH, android.getBootClassPaths());
} else {
setFallbackPlatformClasspath();
}
@@ -85,46 +105,10 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
protected void setFallbackPlatformClasspath() {
// TODO Module system must be enabled for Java modules instead of setting Platform Classpath
setLocation(
setLocationLogError(
StandardLocation.PLATFORM_CLASS_PATH, Collections.singleton(Environment.ANDROID_JAR));
}
private static StandardJavaFileManager createDelegateFileManager() {
JavaCompiler compiler = JavacTool.create();
return compiler.getStandardFileManager(
SourceFileManager::logError, null, Charset.defaultCharset());
}
private static void logError(Diagnostic<?> error) {
LOG.warn("SourceFileManager DiagnosticErr:", error.getMessage(null));
}
void setLocation(Location location, Iterable<? extends File> files) {
try {
fileManager.setLocation(location, files);
} catch (IOException e) {
LOG.error("Failed to update location", location, "with files", Iterables.toString(files), e);
}
}
public static void clearCache() {
for (final SourceFileManager fileManager : cachedFileManagers.values()) {
CloseUtils.closeIO(fileManager);
}
cachedFileManagers.clear();
}
public static SourceFileManager forModule(@NonNull ModuleProject project) {
Objects.requireNonNull(project);
return cachedFileManagers.computeIfAbsent(project, SourceFileManager::createForModule);
}
private static SourceFileManager createForModule(@NonNull ModuleProject project) {
LOG.info("Creating source file manager instance for module:", project);
return new SourceFileManager(project);
}
@Override
public Iterable<JavaFileObject> list(
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
@@ -141,11 +125,6 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
}
}
private JavaFileObject asJavaFileObject(SourceClassTrie.SourceNode node) {
// TODO erase method bodies of files that are not open
return new SourceFileObject(node.getFile());
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (location == StandardLocation.SOURCE_PATH) {
@@ -201,6 +180,38 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
return super.contains(location, file);
}
}
public void setLocationLogError(Location location, Iterable<File> searchPath) {
try {
setLocation(location, searchPath);
} catch (IOException e) {
LOG.error("Unable to set location");
}
}
private void listLocations(final EnumSet<StandardLocation> locations) {
for (StandardLocation location : locations) {
try {
list(
location,
ANDROIDIDE_CACHE_LOCATION,
EnumSet.of(
JavaFileObject.Kind.CLASS,
JavaFileObject.Kind.SOURCE,
JavaFileObject.Kind.HTML,
JavaFileObject.Kind.OTHER),
true);
} catch (IOException e) {
// Ignored
LOG.debug("Failed to list location:", location, e);
}
}
}
private JavaFileObject asJavaFileObject(SourceClassTrie.SourceNode node) {
// TODO erase method bodies of files that are not open
return new SourceFileObject(node.getFile());
}
private String packageNameOrEmpty(Path file) {
return this.module != null ? module.packageNameOrEmpty(file) : "";
@@ -210,15 +221,4 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
int lastDot = fileName.lastIndexOf(".");
return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
}
public void setContext(Context context) {
if (fileManager instanceof JavacFileManager) {
((JavacFileManager) fileManager).setContext(context);
}
}
void setLocationFromPaths(Location location, Collection<? extends Path> searchpath)
throws IOException {
fileManager.setLocationFromPaths(location, searchpath);
}
}
@@ -22,10 +22,6 @@ import static com.itsaky.androidide.javac.services.util.JavacTaskUtil.enterTrees
import static com.itsaky.androidide.javac.services.util.JavacTaskUtil.parse;
import static com.itsaky.androidide.progress.ProgressManager.abortIfCancelled;
import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ReflectUtils;
Binary file not shown.