From a76a840fec6fa3c15b312741fb27c250cb1ebb49 Mon Sep 17 00:00:00 2001 From: Yuriy Artamonov Date: Sat, 2 Sep 2023 21:25:04 +0200 Subject: [PATCH] [indexing] IDEA-310006 Split too big jsonToHtmlConverter.kt GitOrigin-RevId: f05f4aa6d67970255214895879834a40e69b1b64 --- .../diagnostic/presentation/aggregateHtml.kt | 294 +++++++++++++++ .../diagnostic/presentation/htmlUtils.kt | 34 ++ ...jsonToHtmlConverter.kt => indexingHtml.kt} | 354 ++---------------- 3 files changed, 349 insertions(+), 333 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/aggregateHtml.kt create mode 100644 platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/htmlUtils.kt rename platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/{jsonToHtmlConverter.kt => indexingHtml.kt} (73%) diff --git a/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/aggregateHtml.kt b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/aggregateHtml.kt new file mode 100644 index 000000000000..39007ea3e23d --- /dev/null +++ b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/aggregateHtml.kt @@ -0,0 +1,294 @@ +// 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.indexing.diagnostic.presentation + +import com.intellij.util.asSafely +import com.intellij.util.indexing.diagnostic.ChangedFilesPushedEvent +import com.intellij.util.indexing.diagnostic.IndexDiagnosticDumper +import com.intellij.util.indexing.diagnostic.JsonSharedIndexDiagnosticEvent +import com.intellij.util.indexing.diagnostic.dto.* +import kotlinx.html.* +import kotlinx.html.stream.appendHTML +import java.nio.charset.StandardCharsets +import java.util.* + +internal fun createAggregateActivityHtml( + target: Appendable, + projectName: String, + diagnostics: List, + sharedIndexEvents: List, + changedFilesPushEvents: List +) { + target.appendHTML().html { + head { + title("Indexing diagnostics of '$projectName'") + style { + unsafe { + +INDEX_DIAGNOSTIC_CSS_STYLES + } + } + script { + unsafe { + +LINKABLE_TABLE_ROW_SCRIPT + } + } + } + body { + div(classes = "aggregate-activity-report-content") { + h1(classes = "aggregate-header") { text(projectName) } + + div { + text("All durations are wall time; total and content loading time include pauses") + table(classes = "centered-text table-with-margin activity-table") { + unsafe { + +"Click for details" + } + thead { + tr { + th("History of scannings and indexings") { colSpan = "14" } + } + tr { + th("Time") { + colSpan = "7" + } + th("Files") { + colSpan = "5" + } + th("Scanning ID(s)") { + rowSpan = "2" + } + th("Scanning Type") { + rowSpan = "2" + } + } + tr { + th("Started") + th("Started dumb mode") + th("Finished") + th { text("Total"); unsafe { raw(" ") }; text("time") } + th("Time spent on pause") + th("Full dumb mode time") + th("Dumb mode time w/o pauses") + th("Scanned") + th("Shared indexes (w/o content loading)") + th("Scheduled for indexing") + th("Shared indexes (content loaded)") + th("Total indexed (shared indexes included)") + } + } + tbody { + for (diagnostic in diagnostics.sortedByDescending { it.indexingTimes.updatingStart.instant }) { + val times = diagnostic.indexingTimes + + val classes = if (times is JsonProjectScanningHistoryTimes) { + "linkable-table-row scanning-table-row" + } + else { + "linkable-table-row" + } + tr(classes = classes) { + attributes["href"] = diagnostic.htmlFile.fileName.toString() + printIndexingActivityRow(times, diagnostic.fileCount) + } + } + } + } + } + + if (sharedIndexEvents.isNotEmpty()) { + val indexIdToEvents = sharedIndexEvents.groupBy { it.chunkUniqueId } + div { + table(classes = "table-with-margin activity-table") { + thead { + tr { + th("Shared indexes") { + colSpan = "9" + } + } + tr { + th("Time") + th("Kind") + th("Name") + th("Size") + th("Download time") + th("Download speed") + th("Status") + th("ID") + th("Generation time") + } + } + tbody { + for (event in sharedIndexEvents.filterIsInstance().sortedByDescending { it.time.instant }) { + val events = indexIdToEvents.getOrDefault(event.chunkUniqueId, emptyList()) + val lastAttach = events.filterIsInstance().maxByOrNull { it.time.instant } + ?: continue + tr { + td(event.time.presentableLocalDateTime()) + td(lastAttach.kind) + td((lastAttach as? JsonSharedIndexDiagnosticEvent.Attached.Success)?.indexName ?: NOT_APPLICABLE) + td(event.packedSize.presentableSize()) + td(event.downloadTime.presentableDuration()) + td(event.downloadSpeed.presentableSpeed()) + td(event.finishType + ((lastAttach as? JsonSharedIndexDiagnosticEvent.Attached.Success)?.let { + " FB: ${it.fbMatch.presentablePercentages()}, Stub: ${it.stubMatch.presentablePercentages()}" + } ?: " Incompatible")) + td(event.chunkUniqueId) + td(event.generationTime?.presentableLocalDateTime() ?: "unknown") + } + } + } + } + } + } + + if (changedFilesPushEvents.isNotEmpty()) { + div { + table(classes = "table-with-margin activity-table") { + thead { + tr { + th("Scanning to push properties of changed files") { + colSpan = "5" + } + } + tr { + th("Time") + th("Reason") + th("Full duration") + th("Is cancelled") + th("Number") + } + } + tbody { + val eventsToUnify = mutableListOf() + for (event in changedFilesPushEvents.sortedByDescending { it.startTime.instant }) { + if (canUnify(event, eventsToUnify)) { + eventsToUnify.add(event) + } + else { + printUnified(eventsToUnify) + eventsToUnify.clear() + printEvent(event) + } + } + printUnified(eventsToUnify) + } + } + } + } + } + } + } +} + +private fun TR.printIndexingActivityRow(times: JsonProjectIndexingActivityHistoryTimes, + fileCount: JsonProjectIndexingActivityFileCount) { + // Time section + td(times.updatingStart.presentableLocalDateTime()) + + td(times.dumbModeStart?.presentableLocalDateTime() ?: DID_NOT_START) + + td { + if (times.wasInterrupted) { + strong("Cancelled") + br() + } + text(times.updatingEnd.presentableLocalDateTime()) + } + + td(times.totalWallTimeWithPauses.presentableDuration()) + td(times.wallTimeOnPause.presentableDuration()) + + td(times.dumbWallTimeWithPauses.presentableDuration()) + td(times.dumbWallTimeWithoutPauses.presentableDuration()) + + // Files section. + when (fileCount) { + is JsonProjectScanningFileCount -> { + td(fileCount.numberOfScannedFiles.toString()) + td(fileCount.numberOfFilesIndexedByInfrastructureExtensionsDuringScan.toString()) + td(fileCount.numberOfFilesScheduledForIndexingAfterScan.toString()) + td(NOT_APPLICABLE) + td(NOT_APPLICABLE) + } + is JsonProjectDumbIndexingFileCount -> { + if (IndexDiagnosticDumper.shouldPrintInformationAboutChangedDuringIndexingActionFilesInAggregateHtml) { + td(fileCount.numberOfChangedDuringIndexingFiles.toString()) + td(NOT_APPLICABLE) + td(fileCount.numberOfChangedDuringIndexingFiles.toString()) + } + else { + td(NOT_APPLICABLE) + td(NOT_APPLICABLE) + td(NOT_APPLICABLE) + } + td(fileCount.numberOfFilesIndexedByInfrastructureExtensionsDuringIndexingStage.toString()) + if (fileCount.numberOfChangedDuringIndexingFiles > 0) { + td { + text(fileCount.numberOfFilesIndexedWithLoadingContent.toString()) + br() + text("(incl. ${fileCount.numberOfChangedDuringIndexingFiles} changed in VFS)") + } + } + else { + td(fileCount.numberOfFilesIndexedWithLoadingContent.toString()) + } + } + } + + //Scanning ID(s) section + td(times.asSafely()?.scanningId?.toString() + ?: times.asSafely()?.scanningIds?.let { + if (it.isEmpty()) "None" + else it.toString().trimStart('[').trimEnd(']') + } + ?: "Unexpected times $times") + + //Scanning type section + td(times.asSafely()?.scanningType?.name?.lowercase(Locale.ENGLISH)?.replace('_', ' ') + ?: NOT_APPLICABLE) +} + +private fun TBODY.printEvent(event: ChangedFilesPushedEvent) { + tr { + td(event.startTime.presentableLocalDateTime()) + td(event.reason) + td(event.duration.presentableDuration()) + td(if (event.isCancelled) "cancelled" else "fully finished") + td("1") + } +} + +private fun TBODY.printUnified(eventsToUnify: List) { + if (eventsToUnify.isEmpty()) return + val event = eventsToUnify[0] + if (eventsToUnify.size == 1) { + printEvent(event) + return + } + tr { + td(event.startTime.presentableLocalDateTime()) + td(event.reason) + td(JsonDuration(eventsToUnify.sumOf { it.duration.nano }).presentableDuration()) + td(if (event.isCancelled) "cancelled" else "fully finished") + td(eventsToUnify.size.toString()) + } +} + +private fun canUnify(event: ChangedFilesPushedEvent, baseList: List): Boolean { + if (event.isCancelled || event.duration.nano > 1_000_000) { + return false + } + if (baseList.isEmpty()) { + return true + } + val first = baseList[0] + return first.reason == event.reason +} + +private val LINKABLE_TABLE_ROW_SCRIPT : String + get() { + val inputStream = IndexDiagnosticDumper::class.java.getResourceAsStream( + "/com/intellij/util/indexing/diagnostic/presentation/res/table-row.js") + return inputStream!!.use { + it.readAllBytes().toString(StandardCharsets.UTF_8) + } + } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/htmlUtils.kt b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/htmlUtils.kt new file mode 100644 index 000000000000..636d1bdaaa29 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/htmlUtils.kt @@ -0,0 +1,34 @@ +// 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.indexing.diagnostic.presentation + +import com.intellij.util.indexing.diagnostic.IndexDiagnosticDumper +import kotlinx.html.* +import java.nio.charset.StandardCharsets + +internal const val NOT_APPLICABLE = "N/A" +internal const val DID_NOT_START = "Didn't start" + +internal val INDEX_DIAGNOSTIC_CSS_STYLES: String + get() { + val inputStream = IndexDiagnosticDumper::class.java.getResourceAsStream( + "/com/intellij/util/indexing/diagnostic/presentation/res/styles.css") + return inputStream!!.use { + it.readAllBytes().toString(StandardCharsets.UTF_8) + } + } + +internal inline fun TR.th(value: String, crossinline block : TH.() -> Unit = {}) { + th { + block() + +value + } +} +internal inline fun TR.td(value: String, crossinline block : TD.() -> Unit = {}) { + td { + if (value == NOT_APPLICABLE || value == DID_NOT_START) { + classes += "not-applicable-data" + } + block() + +value + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/jsonToHtmlConverter.kt b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/indexingHtml.kt similarity index 73% rename from platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/jsonToHtmlConverter.kt rename to platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/indexingHtml.kt index 68180e6f09f7..9d4f1827e979 100644 --- a/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/jsonToHtmlConverter.kt +++ b/platform/lang-impl/src/com/intellij/util/indexing/diagnostic/presentation/indexingHtml.kt @@ -4,293 +4,14 @@ package com.intellij.util.indexing.diagnostic.presentation import com.intellij.openapi.util.text.StringUtil -import com.intellij.util.asSafely -import com.intellij.util.indexing.diagnostic.ChangedFilesPushedEvent import com.intellij.util.indexing.diagnostic.IndexDiagnosticDumper import com.intellij.util.indexing.diagnostic.IndexingFileSetStatistics -import com.intellij.util.indexing.diagnostic.JsonSharedIndexDiagnosticEvent import com.intellij.util.indexing.diagnostic.dto.* import kotlinx.html.* import kotlinx.html.stream.appendHTML import java.nio.charset.StandardCharsets import java.util.* -internal fun createAggregateActivityHtml( - target: Appendable, - projectName: String, - diagnostics: List, - sharedIndexEvents: List, - changedFilesPushEvents: List -) { - target.appendHTML().html { - head { - title("Indexing diagnostics of '$projectName'") - style { - unsafe { - +CSS_STYLE - } - } - script { - unsafe { - +LINKABLE_TABLE_ROW_SCRIPT - } - } - } - body { - div(classes = "aggregate-activity-report-content") { - h1(classes = "aggregate-header") { text(projectName) } - - div { - text("All durations are wall time; total and content loading time include pauses") - table(classes = "centered-text table-with-margin activity-table") { - unsafe { - +"Click for details" - } - thead { - tr { - th("History of scannings and indexings") { colSpan = "14" } - } - tr { - th("Time") { - colSpan = "7" - } - th("Files") { - colSpan = "5" - } - th("Scanning ID(s)") { - rowSpan = "2" - } - th("Scanning Type") { - rowSpan = "2" - } - } - tr { - th("Started") - th("Started dumb mode") - th("Finished") - th { text("Total"); unsafe { raw(" ") }; text("time") } - th("Time spent on pause") - th("Full dumb mode time") - th("Dumb mode time w/o pauses") - th("Scanned") - th("Shared indexes (w/o content loading)") - th("Scheduled for indexing") - th("Shared indexes (content loaded)") - th("Total indexed (shared indexes included)") - } - } - tbody { - for (diagnostic in diagnostics.sortedByDescending { it.indexingTimes.updatingStart.instant }) { - val times = diagnostic.indexingTimes - - val classes = if (times is JsonProjectScanningHistoryTimes) { - "linkable-table-row scanning-table-row" - } - else { - "linkable-table-row" - } - tr(classes = classes) { - attributes["href"] = diagnostic.htmlFile.fileName.toString() - printIndexingActivityRow(times, diagnostic.fileCount) - } - } - } - } - } - - if (sharedIndexEvents.isNotEmpty()) { - val indexIdToEvents = sharedIndexEvents.groupBy { it.chunkUniqueId } - div { - table(classes = "table-with-margin activity-table") { - thead { - tr { - th("Shared indexes") { - colSpan = "9" - } - } - tr { - th("Time") - th("Kind") - th("Name") - th("Size") - th("Download time") - th("Download speed") - th("Status") - th("ID") - th("Generation time") - } - } - tbody { - for (event in sharedIndexEvents.filterIsInstance().sortedByDescending { it.time.instant }) { - val events = indexIdToEvents.getOrDefault(event.chunkUniqueId, emptyList()) - val lastAttach = events.filterIsInstance().maxByOrNull { it.time.instant } - ?: continue - tr { - td(event.time.presentableLocalDateTime()) - td(lastAttach.kind) - td((lastAttach as? JsonSharedIndexDiagnosticEvent.Attached.Success)?.indexName ?: NOT_APPLICABLE) - td(event.packedSize.presentableSize()) - td(event.downloadTime.presentableDuration()) - td(event.downloadSpeed.presentableSpeed()) - td(event.finishType + ((lastAttach as? JsonSharedIndexDiagnosticEvent.Attached.Success)?.let { - " FB: ${it.fbMatch.presentablePercentages()}, Stub: ${it.stubMatch.presentablePercentages()}" - } ?: " Incompatible")) - td(event.chunkUniqueId) - td(event.generationTime?.presentableLocalDateTime() ?: "unknown") - } - } - } - } - } - } - - if (changedFilesPushEvents.isNotEmpty()) { - div { - table(classes = "table-with-margin activity-table") { - thead { - tr { - th("Scanning to push properties of changed files") { - colSpan = "5" - } - } - tr { - th("Time") - th("Reason") - th("Full duration") - th("Is cancelled") - th("Number") - } - } - tbody { - val eventsToUnify = mutableListOf() - for (event in changedFilesPushEvents.sortedByDescending { it.startTime.instant }) { - if (canUnify(event, eventsToUnify)) { - eventsToUnify.add(event) - } - else { - printUnified(eventsToUnify) - eventsToUnify.clear() - printEvent(event) - } - } - printUnified(eventsToUnify) - } - } - } - } - } - } - } -} - -private fun TR.printIndexingActivityRow(times: JsonProjectIndexingActivityHistoryTimes, - fileCount: JsonProjectIndexingActivityFileCount) { - // Time section - td(times.updatingStart.presentableLocalDateTime()) - - td(times.dumbModeStart?.presentableLocalDateTime() ?: DID_NOT_START) - - td { - if (times.wasInterrupted) { - strong("Cancelled") - br() - } - text(times.updatingEnd.presentableLocalDateTime()) - } - - td(times.totalWallTimeWithPauses.presentableDuration()) - td(times.wallTimeOnPause.presentableDuration()) - - td(times.dumbWallTimeWithPauses.presentableDuration()) - td(times.dumbWallTimeWithoutPauses.presentableDuration()) - - // Files section. - when (fileCount) { - is JsonProjectScanningFileCount -> { - td(fileCount.numberOfScannedFiles.toString()) - td(fileCount.numberOfFilesIndexedByInfrastructureExtensionsDuringScan.toString()) - td(fileCount.numberOfFilesScheduledForIndexingAfterScan.toString()) - td(NOT_APPLICABLE) - td(NOT_APPLICABLE) - } - is JsonProjectDumbIndexingFileCount -> { - if (IndexDiagnosticDumper.shouldPrintInformationAboutChangedDuringIndexingActionFilesInAggregateHtml) { - td(fileCount.numberOfChangedDuringIndexingFiles.toString()) - td(NOT_APPLICABLE) - td(fileCount.numberOfChangedDuringIndexingFiles.toString()) - } - else { - td(NOT_APPLICABLE) - td(NOT_APPLICABLE) - td(NOT_APPLICABLE) - } - td(fileCount.numberOfFilesIndexedByInfrastructureExtensionsDuringIndexingStage.toString()) - if (fileCount.numberOfChangedDuringIndexingFiles > 0) { - td { - text(fileCount.numberOfFilesIndexedWithLoadingContent.toString()) - br() - text("(incl. ${fileCount.numberOfChangedDuringIndexingFiles} changed in VFS)") - } - } - else { - td(fileCount.numberOfFilesIndexedWithLoadingContent.toString()) - } - } - } - - //Scanning ID(s) section - td(times.asSafely()?.scanningId?.toString() - ?: times.asSafely()?.scanningIds?.let { - if (it.isEmpty()) "None" - else it.toString().trimStart('[').trimEnd(']') - } - ?: "Unexpected times $times") - - //Scanning type section - td(times.asSafely()?.scanningType?.name?.lowercase(Locale.ENGLISH)?.replace('_', ' ') - ?: NOT_APPLICABLE) -} - -private fun TBODY.printEvent(event: ChangedFilesPushedEvent) { - tr { - td(event.startTime.presentableLocalDateTime()) - td(event.reason) - td(event.duration.presentableDuration()) - td(if (event.isCancelled) "cancelled" else "fully finished") - td("1") - } -} - -private fun TBODY.printUnified(eventsToUnify: List) { - if (eventsToUnify.isEmpty()) return - val event = eventsToUnify[0] - if (eventsToUnify.size == 1) { - printEvent(event) - return - } - tr { - td(event.startTime.presentableLocalDateTime()) - td(event.reason) - td(JsonDuration(eventsToUnify.sumOf { it.duration.nano }).presentableDuration()) - td(if (event.isCancelled) "cancelled" else "fully finished") - td(eventsToUnify.size.toString()) - } -} - -private fun canUnify(event: ChangedFilesPushedEvent, baseList: List): Boolean { - if (event.isCancelled || event.duration.nano > 1_000_000) { - return false - } - if (baseList.isEmpty()) { - return true - } - val first = baseList[0] - return first.reason == event.reason -} - -private const val NOT_APPLICABLE = "N/A" -private const val DID_NOT_START = "Didn't start" - private const val SECTION_PROJECT_NAME_ID = "id-project-name" private const val SECTION_PROJECT_NAME_TITLE = "Project name" @@ -393,12 +114,12 @@ private fun JsonProjectScanningHistory.generateScanningHtml(target: Appendable, title("Indexing diagnostics of '${projectName}'") style { unsafe { - +CSS_STYLE + +INDEX_DIAGNOSTIC_CSS_STYLES } } script { unsafe { - +JS_SCRIPT + +INDEX_DIAGNOSTIC_HIDE_ELEMENTS_SCRIPT } } } @@ -641,12 +362,12 @@ private fun JsonProjectDumbIndexingHistory.generateDumbIndexingHtml(target: Appe title("Indexing diagnostics of '${projectName}'") style { unsafe { - +CSS_STYLE + +INDEX_DIAGNOSTIC_CSS_STYLES } } script { unsafe { - +JS_SCRIPT + +INDEX_DIAGNOSTIC_HIDE_ELEMENTS_SCRIPT } } } @@ -969,25 +690,25 @@ private fun DIV.printProjectNameForActivity(projectName: String) { } } -private val CSS_STYLE: String - get() { - val inputStream = IndexDiagnosticDumper::class.java.getResourceAsStream( - "/com/intellij/util/indexing/diagnostic/presentation/res/styles.css") - return inputStream!!.use { - it.readAllBytes().toString(StandardCharsets.UTF_8) +private inline fun FlowContent.div(id: String, crossinline block : DIV.() -> Unit = {}) { + div { + this.id = id + block() + } +} + +private fun FlowOrInteractiveOrPhrasingContent.textArea(text: String) { + textArea(rows = "10", cols = "75") { + attributes["readonly"] = "true" + attributes["placeholder"] = "empty" + attributes["style"] = "white-space: pre; border: none" + unsafe { + +text } } +} -private val LINKABLE_TABLE_ROW_SCRIPT : String - get() { - val inputStream = IndexDiagnosticDumper::class.java.getResourceAsStream( - "/com/intellij/util/indexing/diagnostic/presentation/res/table-row.js") - return inputStream!!.use { - it.readAllBytes().toString(StandardCharsets.UTF_8) - } - } - -private val JS_SCRIPT: String +private val INDEX_DIAGNOSTIC_HIDE_ELEMENTS_SCRIPT: String get() { val inputStream = IndexDiagnosticDumper::class.java.getResourceAsStream( "/com/intellij/util/indexing/diagnostic/presentation/res/hide-elements.js") @@ -1003,37 +724,4 @@ private val JETBRAINS_GRAYSCALE_LOGO_SVG: String return inputStream!!.use { it.readAllBytes().toString(StandardCharsets.UTF_8) } - } - -private inline fun FlowContent.div(id: String, crossinline block : DIV.() -> Unit = {}) { - div { - this.id = id - block() - } -} -private inline fun TR.th(value: String, crossinline block : TH.() -> Unit = {}) { - th { - block() - +value - } -} -private inline fun TR.td(value: String, crossinline block : TD.() -> Unit = {}) { - td { - if (value == NOT_APPLICABLE || value == DID_NOT_START) { - classes += "not-applicable-data" - } - block() - +value - } -} - -private fun FlowOrInteractiveOrPhrasingContent.textArea( text: String) { - textArea(rows = "10", cols = "75") { - attributes["readonly"] = "true" - attributes["placeholder"] = "empty" - attributes["style"] = "white-space: pre; border: none" - unsafe { - +text - } - } -} + } \ No newline at end of file