Skip to content

Commit 2d2403d

Browse files
committed
perf: reduce the number of REST API calls
- instead of resolving the template for each workspace and then extract agent information, we can rely on workspace.latest_build.resources to retrieve the agents for the running instances - this greatly reduces the number of calls to the REST API, to basically only one call. - icons are retrieved asynchronously
1 parent 08887b3 commit 2d2403d

File tree

3 files changed

+122
-113
lines changed

3 files changed

+122
-113
lines changed

src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ data class WorkspaceAgentModel(
1212
val name: String,
1313
val templateID: UUID,
1414
val templateName: String,
15-
val templateIcon: Icon,
15+
val templateIconPath: String,
16+
var templateIcon: Icon?,
1617
val status: WorkspaceVersionStatus,
1718
val agentStatus: WorkspaceAgentStatus,
1819
val lastBuildTransition: WorkspaceTransition,

src/main/kotlin/com/coder/gateway/sdk/TemplateIconDownloader.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import javax.swing.Icon
1313
@Service(Service.Level.APP)
1414
class TemplateIconDownloader {
1515
private val coderClient: CoderRestClientService = service()
16+
private val cache = mutableMapOf<Pair<String, String>, Icon>()
1617

1718
fun load(path: String, templateName: String): Icon {
1819
var url: URL? = null
@@ -23,9 +24,15 @@ class TemplateIconDownloader {
2324
}
2425

2526
if (url != null) {
27+
val cachedIcon = cache[Pair(templateName, path)]
28+
if (cachedIcon != null) {
29+
return cachedIcon
30+
}
2631
var img = ImageLoader.loadFromUrl(url)
2732
if (img != null) {
28-
return IconUtil.toRetinaAwareIcon(Scalr.resize(ImageUtil.toBufferedImage(img), Scalr.Method.ULTRA_QUALITY, 32))
33+
val icon = IconUtil.toRetinaAwareIcon(Scalr.resize(ImageUtil.toBufferedImage(img), Scalr.Method.ULTRA_QUALITY, 32))
34+
cache[Pair(templateName, path)] = icon
35+
return icon
2936
}
3037
}
3138

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 112 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.intellij.openapi.diagnostic.Logger
3636
import com.intellij.openapi.progress.ProgressIndicator
3737
import com.intellij.openapi.progress.ProgressManager
3838
import com.intellij.openapi.progress.Task
39+
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
3940
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
4041
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
4142
import com.intellij.ui.AnActionButton
@@ -57,14 +58,14 @@ import com.intellij.util.ui.JBFont
5758
import com.intellij.util.ui.JBUI
5859
import com.intellij.util.ui.ListTableModel
5960
import com.intellij.util.ui.table.IconTableCellRenderer
61+
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
6062
import kotlinx.coroutines.CoroutineScope
6163
import kotlinx.coroutines.Dispatchers
6264
import kotlinx.coroutines.Job
6365
import kotlinx.coroutines.cancel
6466
import kotlinx.coroutines.delay
6567
import kotlinx.coroutines.isActive
6668
import kotlinx.coroutines.launch
67-
import kotlinx.coroutines.runBlocking
6869
import kotlinx.coroutines.withContext
6970
import org.zeroturnaround.exec.ProcessExecutor
7071
import java.awt.Component
@@ -123,7 +124,7 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
123124
enableNextButtonCallback(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS.LINUX)
124125
if (selectedObject?.agentOS != OS.LINUX) {
125126
notificationBanner.apply {
126-
isVisible = true
127+
component.isVisible = true
127128
showInfo(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.os.info"))
128129
}
129130
} else {
@@ -391,62 +392,56 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
391392
appPropertiesService.setValue(SESSION_TOKEN, token)
392393
val cliManager = CoderCLIManager(localWizardModel.coderURL.toURL(), coderClient.buildVersion)
393394

394-
395395
localWizardModel.apply {
396396
this.token = token
397397
buildVersion = coderClient.buildVersion
398398
localCliPath = cliManager.localCli.toAbsolutePath().toString()
399399
}
400400

401-
val authTask = object : Task.Modal(null, CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"), false) {
402-
override fun run(pi: ProgressIndicator) {
403-
pi.apply {
404-
isIndeterminate = false
405-
text = "Retrieving Workspaces..."
406-
fraction = 0.1
407-
}
408-
runBlocking {
409-
loadWorkspaces()
410-
}
401+
LifetimeDefinition().launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"), canBeCancelled = false, isIndeterminate = true) {
402+
this.indicator.apply {
403+
isIndeterminate = false
404+
text = "Retrieving Workspaces..."
405+
fraction = 0.1
406+
}
411407

412-
pi.apply {
413-
isIndeterminate = false
414-
text = "Downloading Coder CLI..."
415-
fraction = 0.3
416-
}
408+
withContext(Dispatchers.IO) {
409+
loadWorkspaces()
410+
}
417411

418-
cliManager.downloadCLI()
419-
if (getOS() != OS.WINDOWS) {
420-
pi.fraction = 0.4
421-
val chmodOutput = ProcessExecutor().command("chmod", "+x", localWizardModel.localCliPath).readOutput(true).execute().outputUTF8()
422-
logger.info("chmod +x ${cliManager.localCli.toAbsolutePath()} $chmodOutput")
423-
}
424-
pi.apply {
425-
text = "Configuring Coder CLI..."
426-
fraction = 0.5
427-
}
412+
this.indicator.apply {
413+
isIndeterminate = false
414+
text = "Downloading Coder CLI..."
415+
fraction = 0.3
416+
}
428417

429-
val loginOutput = ProcessExecutor().command(localWizardModel.localCliPath, "login", localWizardModel.coderURL, "--token", localWizardModel.token).readOutput(true).execute().outputUTF8()
430-
logger.info("coder-cli login output: $loginOutput")
431-
pi.fraction = 0.8
432-
val sshConfigOutput = ProcessExecutor().command(localWizardModel.localCliPath, "config-ssh", "--yes", "--use-previous-options").readOutput(true).execute().outputUTF8()
433-
logger.info("Result of `${localWizardModel.localCliPath} config-ssh --yes --use-previous-options`: $sshConfigOutput")
418+
cliManager.downloadCLI()
419+
if (getOS() != OS.WINDOWS) {
420+
this.indicator.fraction = 0.4
421+
val chmodOutput = ProcessExecutor().command("chmod", "+x", localWizardModel.localCliPath).readOutput(true).execute().outputUTF8()
422+
logger.info("chmod +x ${cliManager.localCli.toAbsolutePath()} $chmodOutput")
423+
}
424+
this.indicator.apply {
425+
text = "Configuring Coder CLI..."
426+
fraction = 0.5
427+
}
434428

435-
pi.apply {
436-
text = "Remove old Coder CLI versions..."
437-
fraction = 0.9
438-
}
439-
cliManager.removeOldCli()
429+
val loginOutput = ProcessExecutor().command(localWizardModel.localCliPath, "login", localWizardModel.coderURL, "--token", localWizardModel.token).readOutput(true).execute().outputUTF8()
430+
logger.info("coder-cli login output: $loginOutput")
431+
this.indicator.fraction = 0.8
432+
val sshConfigOutput = ProcessExecutor().command(localWizardModel.localCliPath, "config-ssh", "--yes", "--use-previous-options").readOutput(true).execute().outputUTF8()
433+
logger.info("Result of `${localWizardModel.localCliPath} config-ssh --yes --use-previous-options`: $sshConfigOutput")
440434

441-
pi.fraction = 1.0
435+
this.indicator.apply {
436+
text = "Remove old Coder CLI versions..."
437+
fraction = 0.9
442438
}
443-
}
439+
cliManager.removeOldCli()
444440

445-
cs.launch {
446-
ProgressManager.getInstance().run(authTask)
441+
this.indicator.fraction = 1.0
442+
updateWorkspaceActions()
443+
triggerWorkspacePolling()
447444
}
448-
updateWorkspaceActions()
449-
triggerWorkspacePolling()
450445
}
451446

452447
private fun askToken(): String? {
@@ -483,102 +478,108 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
483478
}
484479

485480
private suspend fun loadWorkspaces() {
486-
withContext(Dispatchers.IO) {
481+
val ws = withContext(Dispatchers.IO) {
487482
val timeBeforeRequestingWorkspaces = System.currentTimeMillis()
488483
try {
489484
val ws = coderClient.workspaces()
485+
val ams = ws.flatMap { it.toAgentModels() }.toSet()
490486
val timeAfterRequestingWorkspaces = System.currentTimeMillis()
491487
logger.info("Retrieving the workspaces took: ${timeAfterRequestingWorkspaces - timeBeforeRequestingWorkspaces} millis")
492-
ws.resolveAndDisplayAgents()
488+
return@withContext ams
493489
} catch (e: Exception) {
494490
logger.error("Could not retrieve workspaces for ${coderClient.me.username} on ${coderClient.coderURL}. Reason: $e")
491+
emptySet()
492+
}
493+
}
494+
withContext(Dispatchers.Main) {
495+
val selectedWorkspace = tableOfWorkspaces.selectedObject?.name
496+
listTableModelOfWorkspaces.items = ws.toList()
497+
if (selectedWorkspace != null) {
498+
tableOfWorkspaces.selectItem(selectedWorkspace)
495499
}
496500
}
497501
}
498502

499-
private fun List<Workspace>.resolveAndDisplayAgents() {
500-
this.forEach { workspace ->
501-
cs.launch(Dispatchers.IO) {
502-
val timeBeforeRequestingAgents = System.currentTimeMillis()
503-
workspace.agentModels().forEach { am ->
503+
private fun Workspace.toAgentModels(): Set<WorkspaceAgentModel> {
504+
return when (this.latestBuild.resources.size) {
505+
0 -> {
506+
val wm = WorkspaceAgentModel(
507+
this.id,
508+
this.name,
509+
this.name,
510+
this.templateID,
511+
this.templateName,
512+
this.templateIcon,
513+
null,
514+
WorkspaceVersionStatus.from(this),
515+
WorkspaceAgentStatus.from(this),
516+
this.latestBuild.transition,
517+
null,
518+
null,
519+
null
520+
)
521+
cs.launch(Dispatchers.IO) {
522+
wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.templateName)
504523
withContext(Dispatchers.Main) {
505-
val selectedWorkspace = tableOfWorkspaces.selectedObject?.name
506-
if (listTableModelOfWorkspaces.indexOf(am) >= 0) {
507-
val index = listTableModelOfWorkspaces.indexOf(am)
508-
listTableModelOfWorkspaces.setItem(index, am)
509-
} else {
510-
listTableModelOfWorkspaces.addRow(am)
511-
}
512-
if (selectedWorkspace != null) {
513-
tableOfWorkspaces.selectItem(selectedWorkspace)
514-
}
524+
tableOfWorkspaces.updateUI()
515525
}
516526
}
517-
val timeAfterRequestingAgents = System.currentTimeMillis()
518-
logger.info("Retrieving the agents for ${workspace.name} took: ${timeAfterRequestingAgents - timeBeforeRequestingAgents} millis")
527+
setOf(wm)
519528
}
520-
}
521-
}
522-
523-
private fun Workspace.agentModels(): List<WorkspaceAgentModel> {
524-
return try {
525-
val agents = coderClient.workspaceAgentsByTemplate(this)
526-
when (agents.size) {
527-
0 -> {
528-
listOf(
529-
WorkspaceAgentModel(
530-
this.id,
531-
this.name,
532-
this.name,
533-
this.templateID,
534-
this.templateName,
535-
iconDownloader.load(this@agentModels.templateIcon, this.name),
536-
WorkspaceVersionStatus.from(this),
537-
WorkspaceAgentStatus.from(this),
538-
this.latestBuild.transition,
539-
null,
540-
null,
541-
null
542-
)
543-
)
544-
}
545529

546-
else -> agents.map { agent ->
530+
else -> {
531+
val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent ->
547532
val workspaceWithAgentName = "${this.name}.${agent.name}"
548-
WorkspaceAgentModel(
533+
val wm = WorkspaceAgentModel(
549534
this.id,
550535
this.name,
551536
workspaceWithAgentName,
552537
this.templateID,
553538
this.templateName,
554-
iconDownloader.load(this@agentModels.templateIcon, workspaceWithAgentName),
539+
this.templateIcon,
540+
null,
555541
WorkspaceVersionStatus.from(this),
556542
WorkspaceAgentStatus.from(this),
557543
this.latestBuild.transition,
558544
OS.from(agent.operatingSystem),
559545
Arch.from(agent.architecture),
560546
agent.directory
561547
)
562-
}.toList()
548+
cs.launch(Dispatchers.IO) {
549+
wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.templateName)
550+
withContext(Dispatchers.Main) {
551+
tableOfWorkspaces.updateUI()
552+
}
553+
}
554+
wm
555+
}.toSet()
556+
557+
if (wam.isNullOrEmpty()) {
558+
val wm = WorkspaceAgentModel(
559+
this.id,
560+
this.name,
561+
this.name,
562+
this.templateID,
563+
this.templateName,
564+
this.templateIcon,
565+
null,
566+
WorkspaceVersionStatus.from(this),
567+
WorkspaceAgentStatus.from(this),
568+
this.latestBuild.transition,
569+
null,
570+
null,
571+
null
572+
)
573+
cs.launch(Dispatchers.IO) {
574+
wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.templateName)
575+
withContext(Dispatchers.Main) {
576+
tableOfWorkspaces.updateUI()
577+
}
578+
}
579+
return setOf(wm)
580+
}
581+
return wam
563582
}
564-
} catch (e: Exception) {
565-
logger.warn("Agent(s) for ${this.name} could not be retrieved. Reason: $e")
566-
listOf(
567-
WorkspaceAgentModel(
568-
this.id,
569-
this.name,
570-
this.name,
571-
this.templateID,
572-
this.templateName,
573-
iconDownloader.load(this@agentModels.templateIcon, this.name),
574-
WorkspaceVersionStatus.from(this),
575-
WorkspaceAgentStatus.from(this),
576-
this.latestBuild.transition,
577-
null,
578-
null,
579-
null
580-
)
581-
)
582583
}
583584
}
584585

@@ -627,7 +628,7 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
627628

628629
private class WorkspaceIconColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentModel, String>(columnName) {
629630
override fun valueOf(workspace: WorkspaceAgentModel?): String? {
630-
return workspace?.agentOS?.name
631+
return workspace?.templateName
631632
}
632633

633634
override fun getRenderer(item: WorkspaceAgentModel?): TableCellRenderer {

0 commit comments

Comments
 (0)