Skip to content

Commit

Permalink
AT-1333 Triggering Bisect from the IJ-Perf
Browse files Browse the repository at this point in the history
  • Loading branch information
MaXal committed Sep 12, 2024
1 parent d057380 commit 3e0c486
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ServerConfigurator } from "../dataQuery"

interface BisectRequest {
targetValue: string
changes: string
direction: string
test: string
metric: string
branch: string
buildType: string
className: string
}

export class BisectClient {
private readonly serverConfigurator: ServerConfigurator | null

constructor(serverConfigurator: ServerConfigurator | null) {
this.serverConfigurator = serverConfigurator
}

async sendBisectRequest(request: BisectRequest): Promise<string> {
const url = `${this.serverConfigurator?.serverUrl}/api/meta/teamcity/startBisect`
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
})

if (!response.ok) {
const errorMessage = await response.text()
throw new Error(`Failed to send bisect request: ${response.statusText} ${errorMessage}`)
}
return response.text()
}
}
208 changes: 208 additions & 0 deletions dashboard/new-dashboard/src/components/common/sideBar/BisectDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<template>
<Dialog
v-model:visible="showDialog"
modal
header="Run bisect"
:style="{ width: '40vw' }"
>
<div class="flex flex-col space-y-8 mb-4 mt-4">
<FloatLabel>
<InputText
id="targetValue"
v-model="targetValue"
:invalid="!isTargetValueValid()"
/>
<label for="targetValue">Target value</label>
</FloatLabel>
<div class="flex">
<FloatLabel>
<InputText
id="changes"
v-model="firstCommit"
/>
<label for="changes">First commit</label>
</FloatLabel>
<FloatLabel>
<InputText
id="changes"
v-model="lastCommit"
/>
<label for="changes">Last commit</label>
</FloatLabel>
</div>
<FloatLabel>
<Dropdown
id="direction"
v-model="direction"
:options="['IMPROVEMENT', 'DEGRADATION']"
>
<template #value="{ value }">
<div class="group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
{{ value }}
<ChevronDownIcon
class="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</div>
</template>
<template #dropdownicon>
<!-- empty element to avoid ignoring override of slot -->
<span />
</template>
</Dropdown>
<label for="direction">Direction</label>
</FloatLabel>
<Accordion>
<AccordionTab header="Additional parameters">
<div class="flex flex-col space-y-8 mb-4 mt-4">
<FloatLabel>
<InputText
id="test"
v-model="test"
/>
<label for="test">Test name</label>
</FloatLabel>
<FloatLabel>
<InputText
id="metric"
v-model="metric"
/>
<label for="metric">Metric name</label>
</FloatLabel>
<FloatLabel>
<InputText
id="branch"
v-model="branch"
/>
<label for="metric">Branch</label>
</FloatLabel>
<FloatLabel>
<InputText
id="buildType"
v-model="buildType"
/>
<label for="buildType">Build type</label>
</FloatLabel>

<FloatLabel>
<InputText
id="className"
v-model="className"
/>
<label for="className">Class name</label>
</FloatLabel>
</div>
</AccordionTab>
</Accordion>
</div>
<div
v-if="error"
class="text-red-500 mb-4"
>
{{ error }}
</div>
<!-- Footer buttons -->
<template #footer>
<div class="flex justify-end space-x-2">
<Button
label="Cancel"
icon="pi pi-times"
severity="secondary"
@click="showDialog = false"
/>
<Button
label="Start"
icon="pi pi-play"
autofocus
:loading="loading"
:disabled="!isTargetValueValid()"
@click="startBisect"
/>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { InfoData } from "./InfoSidebar"
import { getTeamcityBuildType } from "../../../util/artifacts"
import { injectOrError } from "../../../shared/injectionKeys"
import { serverConfiguratorKey } from "../../../shared/keys"
import { computedAsync } from "@vueuse/core"
import { Ref, ref } from "vue"
import { calculateChanges } from "../../../util/changes"
import { ChevronDownIcon } from "@heroicons/vue/20/solid/index"
import { BisectClient } from "./BisectClient"
const { data } = defineProps<{
data: InfoData
}>()
const serverConfigurator = injectOrError(serverConfiguratorKey)
const showDialog = defineModel<boolean>("showDialog")
const metric = ref(data.series[0].metricName ?? "")
const test = ref(data.projectName)
const branch = ref(data.branch)
const isDegradation = data.deltaPrevious?.includes("-") ?? false
const direction = ref(isDegradation ? "DEGRADATION" : "IMPROVEMENT")
const buildType = computedAsync(async () => await getTeamcityBuildType(serverConfigurator.db, serverConfigurator.table, data.buildId), null)
const firstCommit = ref()
const lastCommit = ref()
const changesMerged = await calculateChanges(serverConfigurator.db, data.installerId ?? data.buildId)
const changesUnmerged = changesMerged?.split("%2C") as string[] | null
if (Array.isArray(changesUnmerged)) {
firstCommit.value = changesUnmerged.at(-1) ?? null
lastCommit.value = changesUnmerged[0] ?? null
}
const methodName = data.description.value?.methodName ?? ""
const className = methodName.slice(0, Math.max(0, methodName.lastIndexOf("#")))
const targetValue: Ref<string | null> = ref(null)
const bisectClient = new BisectClient(serverConfigurator)
const error = ref<string | null>(null)
const loading = ref(false)
function isTargetValueValid() {
const value = Number(targetValue.value)
return targetValue.value !== null && targetValue.value !== "" && Number.isInteger(value)
}
async function startBisect() {
error.value = null
loading.value = true
try {
//todo add validation on all values
const weburl = await bisectClient.sendBisectRequest({
targetValue: targetValue.value as string,
changes: (firstCommit.value as string) + "^.." + (lastCommit.value as string),
direction: direction.value,
test: test.value,
metric: metric.value,
branch: branch.value as string,
buildType: buildType.value as string,
className,
})
showDialog.value = false // Close dialog on success
window.open(weburl, "_blank")
} catch (error_) {
error.value = error_ instanceof Error ? error_.message : "An unknown error occurred"
} finally {
loading.value = false
}
}
</script>

<style scoped>
.p-inputtext {
@apply w-full;
}
.p-float-label {
@apply w-full;
}
label {
@apply text-sm;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@
size="small"
@click="createAccident()"
/>
<Button
v-if="bisectSupported && data != null"
class="text-sm"
label="Bisect"
text
size="small"
@click="showBisectDialog = true"
/>
</div>
</div>
<ReportMetricDialog
Expand All @@ -232,6 +240,13 @@
v-model="showStacktrace"
:accident="accidentToEdit!!"
/>
<Suspense>
<BisectDialog
v-if="showBisectDialog"
v-model:show-dialog="showBisectDialog"
:data="data!!"
/>
</Suspense>
</template>
<script setup lang="ts">
import { computed, provide, Ref, ref } from "vue"
Expand All @@ -244,14 +259,16 @@ import { replaceToLink } from "../../../util/linkReplacer"
import BranchIcon from "../BranchIcon.vue"
import SpaceIcon from "../SpaceIcon.vue"
import { useScrollListeners, useScrollStore } from "../scrollStore"
import { getArtifactsUrl, getSpaceUrl, InfoData, tcUrl } from "./InfoSidebar"
import { DBType, getArtifactsUrl, getSpaceUrl, InfoData, tcUrl } from "./InfoSidebar"
import RelatedAccidents from "./RelatedAccidents.vue"
import ReportMetricDialog from "./ReportMetricDialog.vue"
import TestActions from "./TestActions.vue"
import YoutrackDialog from "../youtrack/YoutrackDialog.vue"
import StacktraceModal from "./StacktraceModal.vue"
import { YoutrackClient } from "../youtrack/YoutrackClient"
import { TimeRangeConfigurator } from "../../../configurators/TimeRangeConfigurator"
import BisectDialog from "./BisectDialog.vue"
import { dbTypeStore } from "../../../shared/dbTypes"

const { timerangeConfigurator } = defineProps<{
timerangeConfigurator: TimeRangeConfigurator
Expand All @@ -261,6 +278,8 @@ const vm = injectOrError(sidebarVmKey)
const showDialog = ref(false)
const showYoutrackDialog = ref(false)
const showStacktrace = ref(false)
const showBisectDialog = ref(false)
const bisectSupported = dbTypeStore().dbType == DBType.INTELLIJ_DEV
const accidentToEdit: Ref<Accident | null> = ref(null)

const serverConfigurator = injectOrNull(serverConfiguratorKey)
Expand Down
58 changes: 58 additions & 0 deletions pkg/server/meta/teamcity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package meta

import (
"encoding/json"
"net/http"
)

type BisectRequest struct {
TargetValue string `json:"targetValue"`
Changes string `json:"changes"`
Direction string `json:"direction"`
Test string `json:"test"`
Metric string `json:"metric"`
Branch string `json:"branch"`
BuildType string `json:"buildType"`
ClassName string `json:"className"`
}

func CreatePostStartBisect() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
var bisectReq BisectRequest
decoder := json.NewDecoder(request.Body)
defer request.Body.Close()
err := decoder.Decode(&bisectReq)
if err != nil {
http.Error(writer, "Invalid request body: "+err.Error(), http.StatusBadRequest)
return
}

weburlPtr, err := teamCityClient.startBuild(request.Context(), "ijplatform_master_BisectChangeset", map[string]string{
"target.bisect.direction": bisectReq.Direction,
"target.bisected.metric": bisectReq.Metric,
"target.bisected.simple.class": bisectReq.ClassName,
"target.bisected.test": bisectReq.Test,
"target.branch": bisectReq.Branch,
"target.configuration.id": bisectReq.BuildType,
"target.git.commits": bisectReq.Changes,
"target.value.before.changed.point": bisectReq.TargetValue,
})

if err != nil {
http.Error(writer, "Failed to start bisect: "+err.Error(), http.StatusInternalServerError)
return
}

if weburlPtr != nil {
byteSlice := []byte(*weburlPtr)
_, err = writer.Write(byteSlice)
if err != nil {
http.Error(writer, "Failed to write response: "+err.Error(), http.StatusInternalServerError)
return
}
} else {
http.Error(writer, "TC response doesn't have weburl", http.StatusInternalServerError)
}

}
}
Loading

0 comments on commit 3e0c486

Please sign in to comment.