optimization: do not (re)compute the whole file text on each PSI change - will lead to quadratic nightmare in case of many small changes (part of IJPL-199461 Recursive elements processing causes SOE)

GitOrigin-RevId: a3560c64746733811da5717070cecd0e9e531928
This commit is contained in:
Alexey Kudravtsev
2025-08-01 16:52:28 +00:00
committed by intellij-monorepo-bot
parent 59e42acd46
commit f0c151a9e3
2 changed files with 53 additions and 22 deletions
@@ -27,7 +27,9 @@ import com.intellij.psi.impl.java.stubs.PsiJavaFileStub;
import com.intellij.psi.impl.source.resolve.ClassResolverProcessor;
import com.intellij.psi.impl.source.resolve.SymbolCollectingProcessor;
import com.intellij.psi.impl.source.resolve.SymbolCollectingProcessor.ResultWithContext;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.scope.*;
import com.intellij.psi.scope.processor.MethodsProcessor;
import com.intellij.psi.stubs.StubElement;
@@ -618,31 +620,36 @@ public abstract class PsiJavaFileBaseImpl extends PsiFileImpl implements PsiJava
if (virtualFile == null) virtualFile = getViewProvider().getVirtualFile();
String sourceLevel = null;
try {
CharSequence contents = getViewProvider().getContents();
int lineBound = Strings.indexOf(contents, "\n");
CharSequence line = lineBound > 0 ? contents.subSequence(0, lineBound) : contents;
if (Strings.startsWith(line, 0,"#!")) {
List<String> params = ParametersListUtil.parse(line.toString());
int srcIdx = params.indexOf("--source");
if (srcIdx > 0 && srcIdx + 1 < params.size()) {
sourceLevel = params.get(srcIdx + 1);
LanguageLevel sheBangLevel = LanguageLevel.parse(sourceLevel);
if (sheBangLevel != null) {
return sheBangLevel;
FileElement treeElement = getTreeElement();
ASTNode firstLeaf = treeElement == null ? null : TreeUtil.findFirstLeaf(treeElement, false);
// optimization: do not (re)compute the whole file text on each PSI change - will lead to quadratic nightmare in case of many small changes otherwise
if (firstLeaf == null || Strings.startsWith(firstLeaf.getChars(), 0, "#!")) {
try {
CharSequence contents = getViewProvider().getContents();
int lineBound = Strings.indexOf(contents, "\n");
CharSequence line = lineBound > 0 ? contents.subSequence(0, lineBound) : contents;
if (Strings.startsWith(line, 0, "#!")) {
List<String> params = ParametersListUtil.parse(line.toString());
int srcIdx = params.indexOf("--source");
if (srcIdx > 0 && srcIdx + 1 < params.size()) {
sourceLevel = params.get(srcIdx + 1);
LanguageLevel sheBangLevel = LanguageLevel.parse(sourceLevel);
if (sheBangLevel != null) {
return sheBangLevel;
}
}
}
}
}
catch (Throwable ignored) {
}
finally {
if (!Objects.equals(sourceLevel, virtualFile.getUserData(SHEBANG_SOURCE_LEVEL)) && virtualFile.isInLocalFileSystem()) {
virtualFile.putUserData(SHEBANG_SOURCE_LEVEL, sourceLevel);
VirtualFile file = virtualFile;
ApplicationManager.getApplication().invokeLater(() -> FileContentUtilCore.reparseFiles(file),
ModalityState.nonModal(),
ApplicationManager.getApplication().getDisposed());
catch (Throwable ignored) {
}
finally {
if (!Objects.equals(sourceLevel, virtualFile.getUserData(SHEBANG_SOURCE_LEVEL)) && virtualFile.isInLocalFileSystem()) {
virtualFile.putUserData(SHEBANG_SOURCE_LEVEL, sourceLevel);
VirtualFile file = virtualFile;
ApplicationManager.getApplication().invokeLater(() -> FileContentUtilCore.reparseFiles(file),
ModalityState.nonModal(),
ApplicationManager.getApplication().getDisposed());
}
}
}
@@ -0,0 +1,24 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiField;
import com.intellij.testFramework.LightJavaCodeInsightTestCase;
public class PsiModificationStressTest extends LightJavaCodeInsightTestCase {
public void testManySmallPSIChangesDoNotCauseQuadraticRecomputationsOfWholeFileText() {
int N = 100_000;
String text = " int field;\n".repeat(N);
PsiClass aClass = PsiElementFactory.getInstance(getProject()).createClassFromText(text, null);
assertFalse(aClass.isPhysical());
PsiField firstField = aClass.getFields()[0];
// would pass only if each PSI change does not cause recomputation of the whole file text in com.intellij.psi.impl.source.PsiJavaFileBaseImpl.getLanguageLevelInner
for (int i = 0; i < N; i++) {
firstField.setName("f" + i);
}
assertFalse(aClass.isPhysical());
assertEquals(N, aClass.getFields().length);
}
}