AT-339 Add metrics for popup shown and first element found to the performance tests for finding usages

GitOrigin-RevId: 07f7795357bb92b3125dd08739a0cdc7f8a2da6b
This commit is contained in:
Maxim.Kolmakov
2023-05-16 17:32:42 +02:00
committed by intellij-monorepo-bot
parent 460522beb6
commit 7530cd5f50
11 changed files with 297 additions and 517 deletions

View File

@@ -1,116 +0,0 @@
package com.intellij.java.performancePlugin
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.find.FindManager
import com.intellij.find.findUsages.FindUsagesHandlerFactory.OperationMode
import com.intellij.find.findUsages.FindUsagesManager
import com.intellij.find.findUsages.JavaClassFindUsagesOptions
import com.intellij.find.findUsages.JavaMethodFindUsagesOptions
import com.intellij.find.findUsages.JavaVariableFindUsagesOptions
import com.intellij.find.impl.FindManagerImpl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.ui.playback.PlaybackContext
import com.intellij.openapi.ui.playback.commands.AbstractCommand
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.usages.Usage
import com.intellij.util.Processors
import com.jetbrains.performancePlugin.PerformanceTestSpan.TRACER
import com.jetbrains.performancePlugin.commands.FindUsagesCommand
import com.jetbrains.performancePlugin.commands.FindUsagesCommand.storeMetricsDumpFoundUsages
import com.jetbrains.performancePlugin.commands.GoToNamedElementCommand
import com.jetbrains.performancePlugin.utils.ActionCallbackProfilerStopper
import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.toPromise
import java.util.*
import java.util.concurrent.CountDownLatch
class FindUsagesJavaCommand(text: String, line: Int) : AbstractCommand(text, line) {
companion object {
const val PREFIX = CMD_PREFIX + "findUsagesJava"
private val LOG = Logger.getInstance(FindUsagesJavaCommand::class.java)
}
override fun _execute(context: PlaybackContext): Promise<Any?> {
val actionCallback = ActionCallbackProfilerStopper()
val arguments = text.split(" ".toRegex()).toTypedArray()
val position = if (arguments.size == 3) arguments[1] else null
val elementName = if (arguments.size == 3) arguments[2] else arguments[1]
if (position != null) {
val result = GoToNamedElementCommand(GoToNamedElementCommand.PREFIX + " $position $elementName", -1).execute(context)
result.onError {
actionCallback.reject("fail to go to element $elementName")
}
}
val findUsagesFinished = CountDownLatch(1)
val allUsages: List<Usage> = ArrayList()
val project = context.project
val span: Ref<Span> = Ref()
DumbService.getInstance(project).smartInvokeLater(Context.current().wrap(fun() {
val editor = FileEditorManager.getInstance(project).selectedTextEditor
if (editor == null) {
actionCallback.reject("The action invoked without editor")
return
}
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
if (psiFile == null) {
actionCallback.reject("Psi File is not found")
return
}
val offset = editor.caretModel.offset
val element = when (GotoDeclarationAction.findElementToShowUsagesOf(editor, offset)) {
null -> GotoDeclarationAction.findTargetElement(project, editor, offset)
else -> GotoDeclarationAction.findElementToShowUsagesOf(editor, offset)
}
if (element == null) {
actionCallback.reject("Can't find an element under $offset offset.")
return
}
val foundElementName = (element as PsiNamedElement).name
if (elementName.isNotEmpty()) {
check(
foundElementName != null && foundElementName == elementName) { "Found element name $foundElementName does not correspond to expected $elementName" }
}
LOG.info("Command find usages is called on element $element")
val findUsagesManager = (FindManager.getInstance(project) as FindManagerImpl).findUsagesManager
val handler = findUsagesManager.getFindUsagesHandler(element, OperationMode.USAGES_WITH_DEFAULT_OPTIONS)
if (handler == null) {
actionCallback.reject("No find usage handler found for the element:" + element.text)
return
}
val findUsagesOptions = when {
element.toString().contains("PsiClass") -> JavaClassFindUsagesOptions(project).apply {
searchScope = GlobalSearchScope.allScope(project)
}
element.toString().contains("PsiMethod") -> JavaMethodFindUsagesOptions(project).apply {
searchScope = GlobalSearchScope.allScope(project)
}
else -> JavaVariableFindUsagesOptions(project).apply {
searchScope = GlobalSearchScope.allScope(project)
}
}
val collectProcessor = Processors.cancelableCollectProcessor(Collections.synchronizedList(allUsages))
span.set(TRACER.spanBuilder(FindUsagesCommand.SPAN_NAME).startSpan())
FindUsagesManager.startProcessUsages(handler, handler.primaryElements, handler.secondaryElements, collectProcessor,
findUsagesOptions) { findUsagesFinished.countDown() }
}))
try {
findUsagesFinished.await()
span.get().setAttribute("number", allUsages.size.toLong())
span.get().end()
}
catch (e: InterruptedException) {
throw RuntimeException(e)
}
storeMetricsDumpFoundUsages(allUsages, project)
actionCallback.setDone()
return actionCallback.toPromise()
}
}

View File

@@ -11,7 +11,6 @@ final class JavaCommandProvider implements CommandProvider {
public @NotNull
Map<String, CreateCommand> getCommands() {
return Map.of(
FindUsagesJavaCommand.PREFIX, FindUsagesJavaCommand::new,
BuildCommand.PREFIX, BuildCommand::new,
SyncJpsLibrariesCommand.PREFIX, SyncJpsLibrariesCommand::new,
CreateJavaFileCommand.PREFIX, CreateJavaFileCommand::new

View File

@@ -56,6 +56,8 @@ import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.platform.diagnostic.telemetry.IJTracer;
import com.intellij.platform.diagnostic.telemetry.TelemetryTracer;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
@@ -77,6 +79,8 @@ import com.intellij.util.concurrency.EdtScheduledExecutorService;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.*;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import org.jetbrains.annotations.*;
import javax.swing.*;
@@ -91,6 +95,7 @@ import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -103,6 +108,7 @@ import java.util.stream.Collectors;
import static com.intellij.find.actions.ResolverKt.findShowUsages;
import static com.intellij.find.actions.ShowUsagesActionHandler.getSecondInvocationHint;
import static com.intellij.find.findUsages.FindUsagesHandlerFactory.OperationMode.USAGES_WITH_DEFAULT_OPTIONS;
import static com.intellij.util.FindUsagesScopeKt.FindUsagesScope;
import static com.intellij.util.ObjectUtils.doIfNotNull;
import static org.jetbrains.annotations.Nls.Capitalization.Sentence;
@@ -112,6 +118,8 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
private static final String SPLITTER_SERVICE_KEY = "ShowUsagesActions.splitterServiceKey";
private static final String PREVIEW_PROPERTY_KEY = "ShowUsagesActions.previewPropertyKey";
private final static IJTracer myFindUsagesTracer = TelemetryTracer.getInstance().getTracer(FindUsagesScope);
private static int ourPopupDelayTimeout = 300;
public ShowUsagesAction() {
@@ -241,14 +249,23 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
HintManager.getInstance().hideHints(HintManager.HIDE_BY_ANY_KEY, false, false);
}
public static void startFindUsages(@NotNull PsiElement element, @NotNull RelativePoint popupPosition, @Nullable Editor editor) {
@ApiStatus.Internal
public static Future<Collection<Usage>> startFindUsagesWithResult(@NotNull PsiElement element,
@NotNull RelativePoint popupPosition,
@Nullable Editor editor,
@Nullable SearchScope scope) {
Project project = element.getProject();
FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(project)).getFindUsagesManager();
FindUsagesHandlerBase handler = findUsagesManager.getFindUsagesHandler(element, USAGES_WITH_DEFAULT_OPTIONS);
if (handler == null) return;
if (handler == null) return null;
//noinspection deprecation
FindUsagesOptions options = handler.getFindUsagesOptions(DataManager.getInstance().getDataContext());
showElementUsages(ShowUsagesParameters.initial(project, editor, popupPosition), createActionHandler(handler, options));
if (scope != null) options.searchScope = scope;
return showElementUsagesWithResult(ShowUsagesParameters.initial(project, editor, popupPosition), createActionHandler(handler, options));
}
public static void startFindUsages(@NotNull PsiElement element, @NotNull RelativePoint popupPosition, @Nullable Editor editor) {
startFindUsagesWithResult(element, popupPosition, editor, null);
}
private static void rulesChanged(@NotNull UsageViewImpl usageView, @NotNull PingEDT pingEDT, JBPopup popup) {
@@ -407,8 +424,18 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
}
static void showElementUsages(@NotNull ShowUsagesParameters parameters, @NotNull ShowUsagesActionHandler actionHandler) {
showElementUsagesWithResult(parameters, actionHandler);
}
static Future<Collection<Usage>> showElementUsagesWithResult(@NotNull ShowUsagesParameters parameters,
@NotNull ShowUsagesActionHandler actionHandler) {
ApplicationManager.getApplication().assertIsDispatchThread();
Span findUsageSpan = myFindUsagesTracer.spanBuilder("findUsages").startSpan();
Scope opentelemetryScope = findUsageSpan.makeCurrent();
Span popupSpan = myFindUsagesTracer.spanBuilder("findUsage_popup").startSpan();
Span firstUsageSpan = myFindUsagesTracer.spanBuilder("findUsages_firstUsage").startSpan();
Project project = parameters.project;
UsageViewImpl usageView = createUsageView(project, actionHandler.getTargetLanguage());
ReadAction.nonBlocking(() -> actionHandler.getEventData()).submit(AppExecutorUtil.getAppExecutorService()).onSuccess(
@@ -478,6 +505,7 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
EdtScheduledExecutorService.getInstance().schedule(() -> {
if (!usageView.isDisposed()) {
showPopupIfNeedTo(popup, parameters.popupPosition, popupShownTimeRef);
popupSpan.end();
}
}, ourPopupDelayTimeout, TimeUnit.MILLISECONDS);
}
@@ -555,6 +583,7 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
return true;
}
synchronized (usages) {
firstUsageSpan.end();
if (visibleUsages.size() >= parameters.maxUsages) {
tooManyResults.set(true);
return false;
@@ -583,11 +612,13 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
UsageSearcher usageSearcher = actionHandler.createUsageSearcher();
long searchStarted = System.nanoTime();
var result = new CompletableFuture<Collection<Usage>>();
FindUsagesManager.startProcessUsages(indicator, project, usageSearcher, collect, () -> ApplicationManager.getApplication().invokeLater(
() -> {
showUsagesPopupData.header.disposeProcessIcon();
pingEDT.ping(); // repaint status
synchronized (usages) {
findUsageSpan.setAttribute("number", usages.size());
if (visibleUsages.isEmpty()) {
if (usages.isEmpty()) {
String hint = UsageViewBundle.message("no.usages.found.in", searchScope.getDisplayName());
@@ -621,8 +652,9 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
}
}
}
result.complete(usages);
}
findUsageSpan.end();
long current = System.nanoTime();
UsageViewStatisticsCollector.logSearchFinished(project, usageView,
actionHandler.getTargetClass(), searchScope, actionHandler.getTargetLanguage(),
@@ -635,6 +667,8 @@ public class ShowUsagesAction extends AnAction implements PopupAction, HintManag
},
project.getDisposed()
));
opentelemetryScope.close();
return result;
}
private static void toggleFilters(@NotNull List<? extends ToggleAction> unselectedActions) {

View File

@@ -0,0 +1,8 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util
import com.intellij.platform.diagnostic.telemetry.PlatformMetrics
import com.intellij.platform.diagnostic.telemetry.Scope
@JvmField
val FindUsagesScope = Scope("findUsages", PlatformMetrics)

View File

@@ -4,7 +4,6 @@ package com.intellij.performance.performancePlugin
import com.intellij.performance.performancePlugin.commands.AssertKotlinFileInSpecificRootCommand
import com.intellij.performance.performancePlugin.commands.ClearLibraryCaches
import com.intellij.performance.performancePlugin.commands.ClearSourceCaches
import com.intellij.performance.performancePlugin.commands.FindUsagesKotlinCommand
import com.intellij.performance.performancePlugin.commands.GCCommand
import com.intellij.performance.performancePlugin.commands.KotlinEditorOptionsChangeCommand
import com.intellij.performance.performancePlugin.commands.CreateKotlinFileCommand
@@ -20,7 +19,6 @@ class KotlinPluginCommandProvider : CommandProvider {
GCCommand.PREFIX to CreateCommand(::GCCommand),
AssertKotlinFileInSpecificRootCommand.PREFIX to CreateCommand(::AssertKotlinFileInSpecificRootCommand),
KotlinEditorOptionsChangeCommand.PREFIX to CreateCommand(::KotlinEditorOptionsChangeCommand),
FindUsagesKotlinCommand.PREFIX to CreateCommand(::FindUsagesKotlinCommand),
CreateKotlinFileCommand.PREFIX to CreateCommand(::CreateKotlinFileCommand)
)
}

View File

@@ -1,135 +0,0 @@
package com.intellij.performance.performancePlugin.commands
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.find.FindManager
import com.intellij.find.findUsages.FindUsagesHandlerFactory
import com.intellij.find.findUsages.FindUsagesManager
import com.intellij.find.impl.FindManagerImpl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.ui.playback.PlaybackContext
import com.intellij.openapi.util.Ref
import com.intellij.usages.Usage
import com.intellij.util.Processors
import com.jetbrains.performancePlugin.commands.FindUsagesCommand
import com.jetbrains.performancePlugin.commands.GoToNamedElementCommand
import com.jetbrains.performancePlugin.commands.PerformanceCommand
import com.jetbrains.performancePlugin.utils.ActionCallbackProfilerStopper
import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.toPromise
import org.jetbrains.kotlin.idea.base.searching.usages.KotlinClassFindUsagesOptions
import org.jetbrains.kotlin.idea.base.searching.usages.KotlinFunctionFindUsagesOptions
import org.jetbrains.kotlin.idea.base.searching.usages.KotlinPropertyFindUsagesOptions
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class FindUsagesKotlinCommand(text: String, line: Int) : PerformanceCommand(text, line) {
companion object {
const val NAME = "findUsagesKotlin"
const val PREFIX = CMD_PREFIX + NAME
private val LOG = Logger.getInstance(FindUsagesKotlinCommand::class.java)
}
override fun getName(): String {
return NAME
}
override fun _execute(context: PlaybackContext): Promise<Any?> {
val actionCallback = ActionCallbackProfilerStopper()
val arguments = text.split(Regex.fromLiteral(" ")).toTypedArray()
val position = arguments[1]
val elementName = arguments[2]
val result = GoToNamedElementCommand(GoToNamedElementCommand.PREFIX + " $position $elementName", -1).execute(context)
result.onError {
actionCallback.reject("fail to go to element $elementName")
}
result.blockingGet(30, TimeUnit.SECONDS)
val project = context.project
val findUsagesFinished = CountDownLatch(1)
val allUsages: List<Usage> = ArrayList()
val span: Ref<Span> = Ref()
DumbService.getInstance(project).smartInvokeLater(Context.current().wrap(fun() {
val editor = FileEditorManager.getInstance(context.project).selectedTextEditor
val offset = editor!!.caretModel.offset
val element = when (GotoDeclarationAction.findElementToShowUsagesOf(editor, offset)) {
null -> GotoDeclarationAction.findTargetElement(project, editor, offset)
else -> GotoDeclarationAction.findElementToShowUsagesOf(editor, offset)
}
if (element == null) {
actionCallback.reject("Can't find an element under current $offset offset.")
return
}
val findUsagesManager = (FindManager.getInstance(project) as FindManagerImpl).findUsagesManager
val handler = findUsagesManager.getFindUsagesHandler(element, FindUsagesHandlerFactory.OperationMode.USAGES_WITH_DEFAULT_OPTIONS)
if (handler == null) {
actionCallback.reject("No find usage handler found for the element:" + element.text)
return
}
val findUsagesOptions = when (element.toString()) {
"FUN" -> KotlinFunctionFindUsagesOptions(project).apply {
isOverridingMethods = false
isImplementingMethods = false
isCheckDeepInheritance = true
isIncludeInherited = false
isIncludeOverloadUsages = false
isImplicitToString = true
isSearchForBaseMethod = true
isSkipImportStatements = false
isSearchForTextOccurrences = false
isUsages = true
searchExpected = true
}
"CLASS", "OBJECT_DECLARATION" -> KotlinClassFindUsagesOptions(project).apply {
searchExpected = true
searchConstructorUsages = true
isMethodsUsages = false
isFieldsUsages = false
isDerivedClasses = false
isImplementingClasses = false
isDerivedInterfaces = false
isCheckDeepInheritance = true
isIncludeInherited = false
isSkipImportStatements = false
isSearchForTextOccurrences = true
isUsages = true
}
else -> KotlinPropertyFindUsagesOptions(project).apply {
searchExpected = true
isReadWriteAccess = true
searchOverrides = false
isReadAccess = true
isWriteAccess = true
isSearchForAccessors = false
isSearchInOverridingMethods = false
isSearchForBaseAccessors = false
isSkipImportStatements = false
isSearchForTextOccurrences = false
isUsages = true
}
}
val collectProcessor = Processors.cancelableCollectProcessor(Collections.synchronizedList(allUsages))
span.set(startSpan(FindUsagesCommand.SPAN_NAME))
FindUsagesManager.startProcessUsages(handler, handler.primaryElements, handler.secondaryElements, collectProcessor,
findUsagesOptions) { findUsagesFinished.countDown() }
}))
try {
findUsagesFinished.await()
span.get().setAttribute("number", allUsages.size.toLong())
span.get().end()
}
catch (e: InterruptedException) {
throw RuntimeException(e)
}
FindUsagesCommand.storeMetricsDumpFoundUsages(allUsages, project)
actionCallback.setDone()
return actionCallback.toPromise()
}
}

View File

@@ -12,7 +12,7 @@ class AssertFindUsagesCommand(text: String, line: Int) : PlaybackCommandCoroutin
}
override suspend fun doExecute(context: PlaybackContext) {
val lastUsageReport = FindUsagesCommand.getFoundUsagesJsonPath()
val lastUsageReport = FindUsagesDumper.getFoundUsagesJsonPath()
if (lastUsageReport == null) {
throw IllegalStateException("Find usages report is null")
}
@@ -24,7 +24,7 @@ class AssertFindUsagesCommand(text: String, line: Int) : PlaybackCommandCoroutin
throw IllegalStateException("Provide expected count of usages")
}
val data: FindUsagesCommand.FoundUsagesReport = DataDumper.read(lastUsageReport, FindUsagesCommand.FoundUsagesReport::class.java)
val data: FindUsagesDumper.FoundUsagesReport = DataDumper.read(lastUsageReport, FindUsagesDumper.FoundUsagesReport::class.java)
val expected = args[0].toInt()
if (data.totalNumberOfUsages != expected) {
throw IllegalStateException("Expected ${expected} find usages, but got ${data.totalNumberOfUsages}")

View File

@@ -0,0 +1,14 @@
package com.jetbrains.performancePlugin.commands;
import com.sampullara.cli.Argument;
public class FindUsagesArguments {
@Argument
String position;
@Argument
String scope = "All Places";
@Argument
String expectedName;
}

View File

@@ -1,257 +0,0 @@
package com.jetbrains.performancePlugin.commands;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
import com.intellij.platform.diagnostic.telemetry.impl.TraceUtil;
import com.intellij.find.FindManager;
import com.intellij.find.findUsages.FindUsagesHandler;
import com.intellij.find.findUsages.FindUsagesHandlerFactory;
import com.intellij.find.findUsages.FindUsagesManager;
import com.intellij.find.findUsages.FindUsagesOptions;
import com.intellij.find.impl.FindManagerImpl;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.playback.PlaybackContext;
import com.intellij.openapi.ui.playback.commands.AbstractCommand;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.usages.Usage;
import com.intellij.usages.UsageInfo2UsageAdapter;
import com.intellij.util.Processor;
import com.intellij.util.Processors;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.diagnostic.dump.paths.PortableFilePath;
import com.intellij.util.indexing.diagnostic.dump.paths.PortableFilePaths;
import com.jetbrains.performancePlugin.PerformanceTestSpan;
import com.jetbrains.performancePlugin.utils.ActionCallbackProfilerStopper;
import com.jetbrains.performancePlugin.utils.DataDumper;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CountDownLatch;
/**
* Command looks for and count psi usages of element under caret.
* Example: %findUsages
*/
public class FindUsagesCommand extends AbstractCommand {
private static final String DUMP_FOUND_USAGES_DESTINATION_FILE = "find.usages.command.found.usages.list.file";
public static final String PREFIX = CMD_PREFIX + "findUsages";
private static final Logger LOG = Logger.getInstance(FindUsagesCommand.class);
public static final String SPAN_NAME = "findUsages";
public FindUsagesCommand(String text, int line) {
super(text, line);
}
@SuppressWarnings("TestOnlyProblems")
@Override
protected @NotNull Promise<Object> _execute(@NotNull PlaybackContext context) {
final ActionCallback actionCallback = new ActionCallbackProfilerStopper();
CountDownLatch findUsagesFinished = new CountDownLatch(1);
List<Usage> allUsages = new ArrayList<>();
@NotNull Project project = context.getProject();
TraceUtil.runWithSpanThrows(PerformanceTestSpan.TRACER, SPAN_NAME, span -> {
DumbService.getInstance(project).smartInvokeLater(Context.current().wrap(() -> {
Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
if (editor == null) {
span.setStatus(StatusCode.ERROR, "The action invoked without editor");
actionCallback.reject("The action invoked without editor");
return;
}
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (psiFile == null) {
span.setStatus(StatusCode.ERROR, "Psi File is not found");
actionCallback.reject("Psi File is not found");
return;
}
int offset = editor.getCaretModel().getOffset();
PsiElement element;
span.addEvent("Finding declaration");
if (GotoDeclarationAction.findElementToShowUsagesOf(editor, offset) == null) {
element = GotoDeclarationAction.findTargetElement(project, editor, offset);
}
else {
element = GotoDeclarationAction.findElementToShowUsagesOf(editor, offset);
}
if (element == null) {
span.setStatus(StatusCode.ERROR, "Can't find an element under " + offset + " offset.");
actionCallback.reject("Can't find an element under " + offset + " offset.");
return;
}
span.addEvent("Command find usages is called on element " + element);
LOG.info("Command find usages is called on element " + element);
FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(project)).getFindUsagesManager();
FindUsagesHandler handler = findUsagesManager.getFindUsagesHandler(
element, FindUsagesHandlerFactory.OperationMode.HIGHLIGHT_USAGES
);
if (handler == null) {
span.setStatus(StatusCode.ERROR, "No find usage handler found for the element:" + element.getText());
actionCallback.reject("No find usage handler found for the element:" + element.getText());
return;
}
FindUsagesOptions findUsagesOptions = new FindUsagesOptions(project);
findUsagesOptions.isUsages = true;
findUsagesOptions.isSearchForTextOccurrences = false;
findUsagesOptions.searchScope = GlobalSearchScope.allScope(project);
Processor<Usage> collectProcessor = Processors.cancelableCollectProcessor(Collections.synchronizedList(allUsages));
FindUsagesManager.startProcessUsages(handler,
handler.getPrimaryElements(),
handler.getSecondaryElements(),
collectProcessor,
findUsagesOptions,
() -> {
findUsagesFinished.countDown();
});
}));
try {
findUsagesFinished.await();
span.setAttribute("number", allUsages.size());
}
catch (InterruptedException e) {
span.recordException(e);
throw new RuntimeException(e);
}
});
storeMetricsDumpFoundUsages(allUsages, project);
actionCallback.setDone();
return Promises.toPromise(actionCallback);
}
public static void storeMetricsDumpFoundUsages(List<Usage> allUsages, @NotNull Project project) {
List<FoundUsage> foundUsages = ContainerUtil.map(allUsages, usage -> convertToFoundUsage(project, usage));
Path jsonPath = getFoundUsagesJsonPath();
if (jsonPath != null) {
dumpFoundUsagesToFile(foundUsages, jsonPath);
}
}
public static void dumpFoundUsagesToFile(@NotNull List<FoundUsage> foundUsages,
@NotNull Path jsonPath) {
LOG.info("Found usages will be dumped to " + jsonPath);
Collections.sort(foundUsages);
FoundUsagesReport foundUsagesReport = new FoundUsagesReport(foundUsages.size(), foundUsages);
DataDumper.dump(foundUsagesReport, jsonPath);
}
@NotNull
public static FoundUsage convertToFoundUsage(@NotNull Project project, @NotNull Usage usage) {
PortableFilePath portableFilePath = null;
Integer line = null;
if (usage instanceof UsageInfo2UsageAdapter adapter) {
VirtualFile file = ReadAction.compute(() -> adapter.getFile());
if (file != null) {
portableFilePath = PortableFilePaths.INSTANCE.getPortableFilePath(file, project);
}
line = adapter.getLine() + 1;
}
String text = ReadAction.compute(() -> usage.getPresentation().getPlainText());
return new FoundUsage(text, portableFilePath, line);
}
@Nullable
public static Path getFoundUsagesJsonPath() {
String property = System.getProperty(DUMP_FOUND_USAGES_DESTINATION_FILE);
if (property != null) {
return Paths.get(property);
}
return null;
}
@NotNull
public static FoundUsagesReport parseFoundUsagesReportFromFile(@NotNull Path reportPath) throws IOException {
return DataDumper.objectMapper.readValue(reportPath.toFile(), FoundUsagesReport.class);
}
public static final class FoundUsagesReport {
public final int totalNumberOfUsages;
public final List<FoundUsage> usages;
@JsonCreator
public FoundUsagesReport(
@JsonProperty("totalNumberOfUsages") int totalNumberOfUsages,
@JsonProperty("usages") @NotNull List<FoundUsage> foundUsages
) {
this.totalNumberOfUsages = totalNumberOfUsages;
this.usages = foundUsages;
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class FoundUsage implements Comparable<FoundUsage> {
public final @NotNull String text;
public final @Nullable PortableFilePath portableFilePath;
public final @Nullable Integer line;
@JsonCreator
private FoundUsage(
@JsonProperty("text") @NotNull String text,
@JsonProperty("portableFilePath") @Nullable PortableFilePath portableFilePath,
@JsonProperty("line") @Nullable Integer line
) {
this.portableFilePath = portableFilePath;
this.text = text;
this.line = line;
}
private static final Comparator<FoundUsage> COMPARATOR =
Comparator.<FoundUsage, String>comparing(usage -> usage.portableFilePath != null ? usage.portableFilePath.getPresentablePath() : "")
.thenComparingInt(usage -> usage.line != null ? usage.line : -1)
.thenComparing(usage -> usage.text);
@Override
public int compareTo(@NotNull FindUsagesCommand.FoundUsage other) {
return COMPARATOR.compare(this, other);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FoundUsage usage)) return false;
return text.equals(usage.text) &&
Objects.equals(portableFilePath, usage.portableFilePath) &&
Objects.equals(line, usage.line);
}
@Override
public int hashCode() {
return Objects.hash(text, portableFilePath, line);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (portableFilePath != null) {
builder.append("In file '").append(portableFilePath.getPresentablePath()).append("' ");
}
if (line != null) {
builder.append("(at line #").append(line).append(") ");
}
if (builder.length() > 0) {
builder.append("\n");
}
builder.append(text);
return builder.toString();
}
}
}

View File

@@ -0,0 +1,87 @@
package com.jetbrains.performancePlugin.commands
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.find.FindSettings
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.find.findUsages.FindUsagesOptions
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.options.advanced.AdvancedSettings
import com.intellij.openapi.ui.playback.PlaybackContext
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.PsiNamedElement
import com.intellij.usages.Usage
import com.sampullara.cli.Args
import io.opentelemetry.context.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.NonNls
import java.util.concurrent.Future
/**
* Command to execute find usages with popup
* Example: %findUsages [-position <elementName>] [-expectedName <elementName>] [-scope <All Places>] <WARMUP>
*/
class FindUsagesCommand(text: String, line: Int) : PerformanceCommandCoroutineAdapter(text, line) {
companion object {
const val PREFIX: @NonNls String = CMD_PREFIX + "findUsages"
const val SPAN_NAME: @NonNls String = "findUsages"
private val LOG = Logger.getInstance(FindUsagesCommand::class.java)
}
@Suppress("TestOnlyProblems")
override suspend fun doExecute(context: PlaybackContext) {
val options = FindUsagesArguments()
Args.parse(options, extractCommandArgument(PREFIX).split(" ").toTypedArray())
val position = options.position
val elementName = options.expectedName
if (position != null) {
val result = GoToNamedElementCommand(GoToNamedElementCommand.PREFIX + " $position $elementName", -1).execute(context)
result.onError {
throw Exception("fail to go to element $elementName")
}
}
val currentOTContext = Context.current()
var findUsagesFuture: Future<Collection<Usage>>
withContext(Dispatchers.EDT) {
currentOTContext.makeCurrent().use {
val editor = FileEditorManager.getInstance(context.project).selectedTextEditor
if (editor == null) {
throw Exception("No editor is opened")
}
val offset = editor.caretModel.offset
val element = if (GotoDeclarationAction.findElementToShowUsagesOf(editor, offset) == null) {
GotoDeclarationAction.findTargetElement(context.project, editor, offset)
}
else {
GotoDeclarationAction.findElementToShowUsagesOf(editor, offset)
}
if (element == null) {
throw Exception("Can't find an element under $offset offset.")
}
val foundElementName = (element as PsiNamedElement).name
if (!elementName.isNullOrEmpty()) {
check(
foundElementName != null && foundElementName == elementName) { "Found element name $foundElementName does not correspond to expected $elementName" }
}
LOG.info("Command find usages is called on element $element")
val popupPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor)
//configuration for find usages
AdvancedSettings.setInt("ide.usages.page.size", Int.MAX_VALUE) //by default, it's 100, we need to find all usages to compare
val scope = FindUsagesOptions.findScopeByName(context.project, null, options.scope)
findUsagesFuture = ShowUsagesAction.startFindUsagesWithResult(element, popupPosition, editor, scope)
}
}
val results = findUsagesFuture.get()
FindUsagesDumper.storeMetricsDumpFoundUsages(results.toMutableList(), context.project)
}
override fun getName(): String = PREFIX
}

View File

@@ -0,0 +1,148 @@
package com.jetbrains.performancePlugin.commands;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.usages.Usage;
import com.intellij.usages.UsageInfo2UsageAdapter;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.diagnostic.dump.paths.PortableFilePath;
import com.intellij.util.indexing.diagnostic.dump.paths.PortableFilePaths;
import com.jetbrains.performancePlugin.utils.DataDumper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
public class FindUsagesDumper {
private static final String DUMP_FOUND_USAGES_DESTINATION_FILE = "find.usages.command.found.usages.list.file";
private static final Logger LOG = Logger.getInstance(FindUsagesDumper.class);
public static void storeMetricsDumpFoundUsages(List<Usage> allUsages, @NotNull Project project) {
List<FoundUsage> foundUsages = ContainerUtil.map(allUsages, usage -> convertToFoundUsage(project, usage));
Path jsonPath = getFoundUsagesJsonPath();
if (jsonPath != null) {
dumpFoundUsagesToFile(foundUsages, jsonPath);
}
}
public static void dumpFoundUsagesToFile(@NotNull List<FoundUsage> foundUsages,
@NotNull Path jsonPath) {
LOG.info("Found usages will be dumped to " + jsonPath);
Collections.sort(foundUsages);
FoundUsagesReport foundUsagesReport = new FoundUsagesReport(foundUsages.size(), foundUsages);
DataDumper.dump(foundUsagesReport, jsonPath);
}
@NotNull
public static FoundUsage convertToFoundUsage(@NotNull Project project, @NotNull Usage usage) {
PortableFilePath portableFilePath = null;
Integer line = null;
if (usage instanceof UsageInfo2UsageAdapter adapter) {
VirtualFile file = ReadAction.compute(() -> adapter.getFile());
if (file != null) {
portableFilePath = PortableFilePaths.INSTANCE.getPortableFilePath(file, project);
}
line = adapter.getLine() + 1;
}
String text = ReadAction.compute(() -> usage.getPresentation().getPlainText());
return new FoundUsage(text, portableFilePath, line);
}
@Nullable
public static Path getFoundUsagesJsonPath() {
String property = System.getProperty(DUMP_FOUND_USAGES_DESTINATION_FILE);
if (property != null) {
return Paths.get(property);
}
return null;
}
@NotNull
public static FoundUsagesReport parseFoundUsagesReportFromFile(@NotNull Path reportPath) throws IOException {
return DataDumper.objectMapper.readValue(reportPath.toFile(), FoundUsagesReport.class);
}
public static final class FoundUsagesReport {
public final int totalNumberOfUsages;
public final List<FoundUsage> usages;
@JsonCreator
public FoundUsagesReport(
@JsonProperty("totalNumberOfUsages") int totalNumberOfUsages,
@JsonProperty("usages") @NotNull List<FoundUsage> foundUsages
) {
this.totalNumberOfUsages = totalNumberOfUsages;
this.usages = foundUsages;
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class FoundUsage implements Comparable<FoundUsage> {
public final @NotNull String text;
public final @Nullable PortableFilePath portableFilePath;
public final @Nullable Integer line;
@JsonCreator
private FoundUsage(
@JsonProperty("text") @NotNull String text,
@JsonProperty("portableFilePath") @Nullable PortableFilePath portableFilePath,
@JsonProperty("line") @Nullable Integer line
) {
this.portableFilePath = portableFilePath;
this.text = text;
this.line = line;
}
private static final Comparator<FoundUsage> COMPARATOR =
Comparator.<FoundUsage, String>comparing(usage -> usage.portableFilePath != null ? usage.portableFilePath.getPresentablePath() : "")
.thenComparingInt(usage -> usage.line != null ? usage.line : -1)
.thenComparing(usage -> usage.text);
@Override
public int compareTo(@NotNull FindUsagesDumper.FoundUsage other) {
return COMPARATOR.compare(this, other);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FoundUsage usage)) return false;
return text.equals(usage.text) &&
Objects.equals(portableFilePath, usage.portableFilePath) &&
Objects.equals(line, usage.line);
}
@Override
public int hashCode() {
return Objects.hash(text, portableFilePath, line);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (portableFilePath != null) {
builder.append("In file '").append(portableFilePath.getPresentablePath()).append("' ");
}
if (line != null) {
builder.append("(at line #").append(line).append(") ");
}
if (!builder.isEmpty()) {
builder.append("\n");
}
builder.append(text);
return builder.toString();
}
}
}