Skip to content

Commit bc96578

Browse files
authored
Make /dev non-blocking UI and reduce ~70% time on large repo (#4415)
The file exclusion logic incorrectly traverses ignored directories which significantly increases collection time on projects with large artifacts. Fix by using a visitor that prunes traversal on ignored directories
1 parent b8531b7 commit bc96578

File tree

4 files changed

+86
-19
lines changed

4 files changed

+86
-19
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Amazon Q: Fix an issue where /dev usage would cause the UI to freeze and take an unusually long time to complete. (#4269)"
4+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,38 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq
55

6-
import com.intellij.openapi.application.runReadAction
76
import com.intellij.openapi.project.Project
87
import com.intellij.openapi.project.guessProjectDir
98
import com.intellij.openapi.vfs.VfsUtil
109
import com.intellij.openapi.vfs.VirtualFile
10+
import com.intellij.openapi.vfs.VirtualFileVisitor
11+
import com.intellij.openapi.vfs.isFile
12+
import com.intellij.platform.ide.progress.withBackgroundProgress
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.async
15+
import kotlinx.coroutines.flow.channelFlow
16+
import kotlinx.coroutines.launch
17+
import kotlinx.coroutines.runBlocking
18+
import kotlinx.coroutines.withContext
1119
import org.apache.commons.codec.digest.DigestUtils
12-
import software.aws.toolkits.core.utils.createTemporaryZipFile
20+
import software.aws.toolkits.core.utils.outputStream
1321
import software.aws.toolkits.core.utils.putNextEntry
22+
import software.aws.toolkits.jetbrains.core.coroutines.EDT
23+
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
24+
import software.aws.toolkits.resources.message
1425
import java.io.File
1526
import java.io.FileInputStream
27+
import java.nio.file.Files
28+
import java.nio.file.Path
1629
import java.util.Base64
30+
import java.util.zip.ZipOutputStream
1731
import kotlin.io.path.Path
1832
import kotlin.io.path.relativeTo
1933

2034
class FeatureDevSessionContext(val project: Project) {
2135
// TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.
2236

23-
private val ignorePatterns = listOf(
37+
private val ignorePatterns = setOf(
2438
"\\.aws-sam",
2539
"\\.svn",
2640
"\\.hg/",
@@ -42,47 +56,94 @@ class FeatureDevSessionContext(val project: Project) {
4256
"/license\\.md$",
4357
"/License\\.md$",
4458
"/LICENSE\\.md$",
59+
"node_modules/",
60+
"build/",
61+
"dist/"
4562
).map { Regex(it) }
4663

4764
private var _projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
4865
private var ignorePatternsWithGitIgnore = emptyList<Regex>()
4966
private val gitIgnoreFile = File(projectRoot.path, ".gitignore")
5067

5168
init {
52-
ignorePatternsWithGitIgnore = ignorePatterns + parseGitIgnore().map { Regex(it) }
69+
ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList()
5370
}
5471

5572
fun getProjectZip(): ZipCreationResult {
56-
val zippedProject = runReadAction { zipFiles(projectRoot) }
73+
val zippedProject = runBlocking {
74+
withBackgroundProgress(project, message("amazonqFeatureDev.create_plan.background_progress_title")) {
75+
zipFiles(projectRoot)
76+
}
77+
}
5778
val checkSum256: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(zippedProject)))
5879
return ZipCreationResult(zippedProject, checkSum256, zippedProject.length())
5980
}
6081

61-
fun ignoreFile(file: File): Boolean = try {
62-
ignorePatternsWithGitIgnore.any { p -> p.containsMatchIn(file.path) }
63-
} catch (e: Exception) {
64-
true
82+
private suspend fun ignoreFile(file: File, scope: CoroutineScope): Boolean = with(scope) {
83+
val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
84+
async {
85+
pattern.containsMatchIn(file.path)
86+
}
87+
}
88+
deferredResults.any { it.await() }
6589
}
6690

67-
fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(File(file.path))
91+
suspend fun ignoreFile(file: VirtualFile, scope: CoroutineScope): Boolean = ignoreFile(File(file.path), scope)
92+
93+
suspend fun zipFiles(projectRoot: VirtualFile): File = withContext(getCoroutineBgContext()) {
94+
val files = mutableListOf<VirtualFile>()
95+
VfsUtil.visitChildrenRecursively(
96+
projectRoot,
97+
object : VirtualFileVisitor<Unit>() {
98+
override fun visitFile(file: VirtualFile): Boolean {
99+
if (file.isFile) {
100+
files.add(file)
101+
return true
102+
}
103+
return runBlocking {
104+
!ignoreFile(file, this)
105+
}
106+
}
107+
}
108+
)
68109

69-
private fun zipFiles(projectRoot: VirtualFile): File = createTemporaryZipFile {
70-
VfsUtil.collectChildrenRecursively(projectRoot).map { virtualFile -> File(virtualFile.path) }.forEach { file ->
71-
if (file.isFile() && !ignoreFile(file)) {
110+
// Process files in parallel
111+
val filesToIncludeFlow = channelFlow {
112+
// chunk with some reasonable number because we don't actually need a new job for each file
113+
files.chunked(50).forEach { chunk ->
114+
launch {
115+
for (file in chunk) {
116+
if (file.isFile && !ignoreFile(file, this)) {
117+
send(file)
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
createTemporaryZipFileAsync { zipOutput ->
125+
filesToIncludeFlow.collect { file ->
72126
val relativePath = Path(file.path).relativeTo(projectRoot.toNioPath())
73-
it.putNextEntry(relativePath.toString(), Path(file.path))
127+
zipOutput.putNextEntry(relativePath.toString(), Path(file.path))
74128
}
75129
}
76130
}.toFile()
77131

78-
private fun parseGitIgnore(): List<String> {
132+
private suspend fun createTemporaryZipFileAsync(block: suspend (ZipOutputStream) -> Unit): Path = withContext(EDT) {
133+
val file = Files.createTempFile(null, ".zip")
134+
ZipOutputStream(file.outputStream()).use { zipOutput -> block(zipOutput) }
135+
file
136+
}
137+
138+
private fun parseGitIgnore(): Set<String> {
79139
if (!gitIgnoreFile.exists()) {
80-
return emptyList()
140+
return emptySet()
81141
}
82142
return gitIgnoreFile.readLines()
83143
.filterNot { it.isBlank() || it.startsWith("#") }
84144
.map { it.trim() }
85145
.map { convertGitIgnorePatternToRegex(it) }
146+
.toSet()
86147
}
87148

88149
// gitignore patterns are not regex, method update needed.

plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.intellij.openapi.vfs.VFileProperty
1010
import com.intellij.openapi.vfs.VfsUtil
1111
import com.intellij.openapi.vfs.VirtualFile
1212
import kotlinx.coroutines.TimeoutCancellationException
13+
import kotlinx.coroutines.runBlocking
1314
import kotlinx.coroutines.time.withTimeout
1415
import software.aws.toolkits.core.utils.createTemporaryZipFile
1516
import software.aws.toolkits.core.utils.debug
@@ -140,7 +141,7 @@ class CodeScanSessionConfig(
140141
} else {
141142
VfsUtil.collectChildrenRecursively(projectRoot).filter {
142143
!it.isDirectory && !it.`is`((VFileProperty.SYMLINK)) && (
143-
!featureDevSessionContext.ignoreFile(it)
144+
!featureDevSessionContext.ignoreFile(it, this)
144145
)
145146
}.fold(0L) { acc, next ->
146147
totalSize = acc + next.length
@@ -174,7 +175,7 @@ class CodeScanSessionConfig(
174175
val current = stack.pop()
175176

176177
if (!current.isDirectory) {
177-
if (!featureDevSessionContext.ignoreFile(current)) {
178+
if (runBlocking { !featureDevSessionContext.ignoreFile(current, this) }) {
178179
if (willExceedPayloadLimit(currentTotalFileSize, current.length)) {
179180
break
180181
} else {
@@ -190,7 +191,7 @@ class CodeScanSessionConfig(
190191
}
191192
} else {
192193
// Directory case: only traverse if not ignored
193-
if (!featureDevSessionContext.ignoreFile(current)) {
194+
if (runBlocking { !featureDevSessionContext.ignoreFile(current, this) }) {
194195
for (child in current.children) {
195196
stack.push(child)
196197
}

plugins/toolkit/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ amazonqFeatureDev.code_generation.updated_code=Code has been updated. Would you
5555
amazonqFeatureDev.content_length.error_text=The project you have selected for source code is too large to use as context. Please select a different folder to use for this conversation
5656
amazonqFeatureDev.create_new_plan=What change would you like to discuss?
5757
amazonqFeatureDev.create_plan=Ok, let me create a plan. This may take a few minutes.
58+
amazonqFeatureDev.create_plan.background_progress_title=Creating plans ...
5859
amazonqFeatureDev.error_text=Sorry, we encountered a problem when processing your request.
5960
amazonqFeatureDev.example_text=You can use /dev to:\n- Add a new feature or logic\n- Write tests\n- Fix a bug in your project\n- Generate a README for a file, folder, or project\n\nTo learn more, visit the [Amazon Q User Guide](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/getting-started.html)
6061
amazonqFeatureDev.exception.conversation_not_found=Conversation id must exist before continuing

0 commit comments

Comments
 (0)