From bc5872dd8f6d165d4eaab43abdbba2f22f2f0b02 Mon Sep 17 00:00:00 2001 From: Simon Ogorodnik Date: Sat, 8 Jul 2017 02:56:26 +0300 Subject: [PATCH] Add completion benchmark to check completion speed --- .../completion/CompletionBenchmarkSink.kt | 89 ++++++ .../idea/completion/CompletionSession.kt | 14 +- .../completion/LookupElementsCollector.kt | 4 + idea/src/META-INF/plugin.xml | 11 + .../internal/BenchmarkCompletionAction.kt | 263 ++++++++++++++++++ .../internal/KotlinInternalActionGroup.kt | 28 ++ .../KotlinInternalModeToggleAction.kt | 4 +- .../libraries/kotlinx_coroutines_core.xml | 11 + ultimate/kotlin-ultimate.iml | 1 + 9 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBenchmarkSink.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/BenchmarkCompletionAction.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalActionGroup.kt create mode 100644 ultimate/.idea/libraries/kotlinx_coroutines_core.xml diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBenchmarkSink.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBenchmarkSink.kt new file mode 100644 index 00000000000..bfc19a47f87 --- /dev/null +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBenchmarkSink.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.completion + +import kotlinx.coroutines.experimental.channels.ConflatedChannel + + +interface CompletionBenchmarkSink { + fun onCompletionStarted(completionSession: CompletionSession) + fun onCompletionEnded(completionSession: CompletionSession) + fun onFirstFlush(completionSession: CompletionSession) + + companion object { + + fun enableAndGet(): Impl = Impl().also { _instance = it } + + fun disable() { + _instance.let { (it as? Impl)?.channel?.close() } + _instance = Empty + } + + val instance get() = _instance + private var _instance: CompletionBenchmarkSink = Empty + } + + private object Empty : CompletionBenchmarkSink { + override fun onCompletionStarted(completionSession: CompletionSession) {} + + override fun onCompletionEnded(completionSession: CompletionSession) {} + + override fun onFirstFlush(completionSession: CompletionSession) {} + } + + class Impl : CompletionBenchmarkSink { + private val pendingSessions = mutableListOf() + private lateinit var results: CompletionBenchmarkResults + val channel = ConflatedChannel() + + override fun onCompletionStarted(completionSession: CompletionSession) = synchronized(this) { + if (pendingSessions.isEmpty()) + results = CompletionBenchmarkResults() + pendingSessions += completionSession + } + + override fun onCompletionEnded(completionSession: CompletionSession) = synchronized(this) { + pendingSessions -= completionSession + if (pendingSessions.isEmpty()) { + results.onEnd() + channel.offer(results) + } + } + + override fun onFirstFlush(completionSession: CompletionSession) = synchronized(this) { + results.onFirstFlush() + } + + fun reset() = synchronized(this) { + pendingSessions.clear() + } + + class CompletionBenchmarkResults { + var start: Long = System.currentTimeMillis() + var firstFlush: Long = 0 + var full: Long = 0 + fun onFirstFlush() { + if (firstFlush == 0L) + firstFlush = System.currentTimeMillis() - start + } + + fun onEnd() { + full = System.currentTimeMillis() - start + } + } + } +} diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt index 683ed7d8b48..6d042b80fd3 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt @@ -78,6 +78,10 @@ abstract class CompletionSession( protected val toFromOriginalFileMapper: ToFromOriginalFileMapper, resultSet: CompletionResultSet ) { + init { + CompletionBenchmarkSink.instance.onCompletionStarted(this) + } + protected val position = parameters.position protected val file = position.containingFile as KtFile protected val resolutionFacade = file.getResolutionFacade() @@ -140,7 +144,7 @@ abstract class CompletionSession( // LookupElementsCollector instantiation is deferred because virtual call to createSorter uses data from derived classes protected val collector: LookupElementsCollector by lazy(LazyThreadSafetyMode.NONE) { - LookupElementsCollector(prefixMatcher, parameters, resultSet, createSorter(), (file as? KtCodeFragment)?.extraCompletionFilter) + LookupElementsCollector(this, prefixMatcher, parameters, resultSet, createSorter(), (file as? KtCodeFragment)?.extraCompletionFilter) } protected val searchScope: GlobalSearchScope = getResolveScope(parameters.originalFile as KtFile) @@ -199,6 +203,14 @@ abstract class CompletionSession( } fun complete(): Boolean { + return try { + _complete() + } finally { + CompletionBenchmarkSink.instance.onCompletionEnded(this) + } + } + + private fun _complete(): Boolean { // we restart completion when prefix becomes "get" or "set" to ensure that properties get lower priority comparing to get/set functions (see KT-12299) val prefixPattern = StandardPatterns.string().with(object : PatternCondition("get or set prefix") { override fun accepts(prefix: String, context: ProcessingContext?) = prefix == "get" || prefix == "set" diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/LookupElementsCollector.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/LookupElementsCollector.kt index b2644feb5d8..667366c6ddf 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/LookupElementsCollector.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/LookupElementsCollector.kt @@ -29,6 +29,7 @@ import org.jetbrains.kotlin.idea.core.completion.DeclarationLookupObject import java.util.* class LookupElementsCollector( + private val session: CompletionSession, private val prefixMatcher: PrefixMatcher, private val completionParameters: CompletionParameters, resultSet: CompletionResultSet, @@ -50,8 +51,11 @@ class LookupElementsCollector( var isResultEmpty: Boolean = true private set + fun flushToResultSet() { if (!elements.isEmpty()) { + CompletionBenchmarkSink.instance.onFirstFlush(session) + resultSet.addAllElements(elements) elements.clear() isResultEmpty = false diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 49679c1b830..154f12fc51c 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -142,10 +142,21 @@ + + + + + + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/BenchmarkCompletionAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/BenchmarkCompletionAction.kt new file mode 100644 index 00000000000..04791725010 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/BenchmarkCompletionAction.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.actions.internal + +import com.intellij.codeInsight.navigation.NavigationUtil +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ScrollType +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.wm.WindowManager +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.search.DelegatingGlobalSearchScope +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.structuralsearch.plugin.util.SmartPsiPointer +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBTextField +import com.intellij.uiDesigner.core.GridConstraints +import com.intellij.uiDesigner.core.GridLayoutManager +import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch +import org.jetbrains.kotlin.idea.caches.resolve.ModuleOrigin +import org.jetbrains.kotlin.idea.caches.resolve.getNullableModuleInfo +import org.jetbrains.kotlin.idea.completion.CompletionBenchmarkSink +import org.jetbrains.kotlin.idea.core.moveCaret +import org.jetbrains.kotlin.idea.core.util.EDT +import org.jetbrains.kotlin.idea.refactoring.getLineCount +import org.jetbrains.kotlin.idea.stubindex.KotlinExactPackagesIndex +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType +import org.jetbrains.kotlin.psi.psiUtil.nextLeafs +import java.awt.Robot +import java.awt.event.KeyEvent +import java.util.* +import javax.swing.JFileChooser +import kotlin.properties.Delegates + +class BenchmarkCompletionAction : AnAction() { + + fun showPopup(project: Project, text: String) { + val statusBar = WindowManager.getInstance().getStatusBar(project) + JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(text, MessageType.ERROR, null) + .setFadeoutTime(5000) + .createBalloon().showInCenterOf(statusBar.component) + } + + override fun actionPerformed(e: AnActionEvent?) { + val project = e?.project!! + + + val scope = object : DelegatingGlobalSearchScope(GlobalSearchScope.allScope(project)) { + override fun isSearchOutsideRootModel(): Boolean { + return false + } + } + val ktFiles = mutableListOf() + val exactPackageIndex = KotlinExactPackagesIndex.getInstance() + exactPackageIndex.processAllKeys(project) { + exactPackageIndex.get(it, project, scope).forEach { + val ptr = SmartPsiPointer(it) + ktFiles += ptr + } + true + } + + + ktFiles.removeAll { + val element = it.element as? KtFile ?: return@removeAll true + val moduleInfo = element.getNullableModuleInfo() ?: return@removeAll true + if (element.isCompiled || !element.isWritable || element.isScript) return@removeAll true + return@removeAll moduleInfo.moduleOrigin != ModuleOrigin.MODULE + } + + data class Settings(val seed: Long, val attempts: Int, val lines: Int) + + fun showSettingsDialog(): Settings? { + var cSeed: JBTextField by Delegates.notNull() + var cAttempts: JBTextField by Delegates.notNull() + var cLines: JBTextField by Delegates.notNull() + val dialogBuilder = DialogBuilder() + + + val jPanel = JBPanel>(GridLayoutManager(3, 3)).apply { + this.add(JBLabel("Random seed: "), GridConstraints().apply { row = 0; column = 0 }) + this.add(JBTextField().apply { + cSeed = this + text = "0" + toolTipText = "Random seed" + }, GridConstraints().apply { row = 0; column = 1; fill = GridConstraints.FILL_HORIZONTAL }) + + this.add(JBLabel("Attempts: "), GridConstraints().apply { row = 1; column = 0 }) + this.add(JBTextField().apply { + cAttempts = this + text = "5" + toolTipText = "Number of files to work with" + }, GridConstraints().apply { row = 1; column = 1; fill = GridConstraints.FILL_HORIZONTAL }) + + this.add(JBLabel("File lines: "), GridConstraints().apply { row = 2; column = 0 }) + this.add(JBTextField().apply { + cLines = this + text = "100" + toolTipText = "File lines" + }, GridConstraints().apply { row = 2; column = 1; fill = GridConstraints.FILL_HORIZONTAL }) + } + dialogBuilder.centerPanel(jPanel) + if (!dialogBuilder.showAndGet()) return null + + return Settings(cSeed.text.toLong(), + cAttempts.text.toInt(), + cLines.text.toInt()) + } + + val settings = showSettingsDialog() ?: return + + ktFiles.removeAll { + val element = it.element as KtFile + element.getLineCount() < settings.lines + } + + if (ktFiles.size < settings.attempts) return showPopup(project, "Number of attempts > then files in project, ${ktFiles.size}") + + + + val random = Random() + random.setSeed(settings.seed) + + fun List.randomElement(): T? = if (this.isNotEmpty()) this[random.nextInt(this.size)] else null + fun Array.randomElement(): T? = if (this.isNotEmpty()) this[random.nextInt(this.size)] else null + + val robot = Robot() + + + fun sendKey(keyCode: Int) { + robot.keyPress(keyCode) + robot.delay(100) + robot.keyRelease(keyCode) + robot.delay(100) + } + + fun type(s: String) { + for (c in s.toCharArray()) { + val keyCode = KeyEvent.getExtendedKeyCodeForChar(c.toInt()) + if (KeyEvent.CHAR_UNDEFINED == keyCode.toChar()) { + throw RuntimeException("Key code not found for character '$c'") + } + if (c.isUpperCase()) { + robot.keyPress(KeyEvent.VK_SHIFT) + robot.delay(10) + } + + sendKey(keyCode) + if (c.isUpperCase()) { + robot.keyRelease(KeyEvent.VK_SHIFT) + robot.delay(10) + } + + } + } + + data class Result(val lines: Int, val filePath: String, val first: Long, val full: Long) + + val allResults = mutableListOf() + + val benchmark = CompletionBenchmarkSink.enableAndGet() + + val typeAtOffsetAndBenchmark: suspend (String, Int, KtFile) -> Unit = { + text: String, offset: Int, file: KtFile -> + NavigationUtil.openFileWithPsiElement(file.navigationElement, false, true) + + val document = PsiDocumentManager.getInstance(project).getDocument(file) + val ourEditor = EditorFactory.getInstance().allEditors.find { it.document == document } + + if (document != null && ourEditor != null) { + + delay(500) + + ourEditor.moveCaret(offset, scrollType = ScrollType.CENTER) + + repeat(2) { sendKey(KeyEvent.VK_ENTER) } + delay(500) + val rangeMarker = document.createRangeMarker(offset, ourEditor.caretModel.offset) + sendKey(KeyEvent.VK_UP) + + val t = text + type(t) + + val results = benchmark.channel.receive() + println("fsize: ${file.getLineCount()}, ${file.virtualFile.path}") + println("first: ${results.firstFlush}, full: ${results.full}") + + allResults += Result(file.getLineCount(), "${file.virtualFile.path}:${document.getLineNumber(offset)}", results.firstFlush, results.full) + + CommandProcessor.getInstance().executeCommand(project, { + runWriteAction { + document.deleteString(rangeMarker.startOffset, rangeMarker.endOffset) + PsiDocumentManager.getInstance(project).commitDocument(document) + } + }, "ss", "ss") + delay(100) + } + } + + launch(EDT) { + for (i in 0 until settings.attempts) { + val file = ktFiles.randomElement()!!.apply { ktFiles.remove(this) }.element as? KtFile ?: continue + + run { + val offset = (file.importList?.nextLeafs?.firstOrNull() as? PsiWhiteSpace)?.endOffset ?: 0 + typeAtOffsetAndBenchmark("fun Str", offset, file) + + } + val ktClassOrObject = file.getChildrenOfType() + .filter { it.getBody() != null } + .randomElement() ?: continue + + run { + val body = ktClassOrObject.getBody() + + val offset = body!!.lBrace!!.endOffset + typeAtOffsetAndBenchmark("fun Str", offset, file) + } + + + } + CompletionBenchmarkSink.disable() + val jfc = JFileChooser() + val result = jfc.showSaveDialog(null) + if (result == JFileChooser.APPROVE_OPTION) { + val file = jfc.selectedFile + file.writeText(buildString { + allResults.joinTo(this, separator = "\n") { (l, f, ff, lf) -> "$f, $l, $ff, $lf" } + }) + } + showPopup(project, "Done") + } + + } + +} diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalActionGroup.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalActionGroup.kt new file mode 100644 index 00000000000..48728d7dbac --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalActionGroup.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.actions.internal + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup + +class KotlinInternalActionGroup : DefaultActionGroup() { + + override fun update(e: AnActionEvent) { + super.update(e) + e.presentation.isEnabledAndVisible = KotlinInternalMode.enabled + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalModeToggleAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalModeToggleAction.kt index 872ae11b018..b73d7d327f5 100644 --- a/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalModeToggleAction.kt +++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/KotlinInternalModeToggleAction.kt @@ -16,9 +16,9 @@ package org.jetbrains.kotlin.idea.actions.internal -import com.intellij.openapi.actionSystem.ToggleAction -import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ToggleAction class KotlinInternalModeToggleAction: ToggleAction("Kotlin Internal Mode", "Show debug highlighting", null) { override fun isSelected(e: AnActionEvent?): Boolean { diff --git a/ultimate/.idea/libraries/kotlinx_coroutines_core.xml b/ultimate/.idea/libraries/kotlinx_coroutines_core.xml new file mode 100644 index 00000000000..46f11e6af44 --- /dev/null +++ b/ultimate/.idea/libraries/kotlinx_coroutines_core.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ultimate/kotlin-ultimate.iml b/ultimate/kotlin-ultimate.iml index df95c2b7b76..a56e25ea93e 100644 --- a/ultimate/kotlin-ultimate.iml +++ b/ultimate/kotlin-ultimate.iml @@ -31,5 +31,6 @@ + \ No newline at end of file