Skip to content

Commit

Permalink
Merge pull request #19048 from mvdbeek/invocation_metrics_api
Browse files Browse the repository at this point in the history
Add job metrics per invocation
  • Loading branch information
bgruening authored Nov 6, 2024
2 parents 4251715 + 946d2ff commit 206b729
Show file tree
Hide file tree
Showing 12 changed files with 951 additions and 18 deletions.
5 changes: 5 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"dom-to-image": "^2.6.0",
"dompurify": "^3.0.6",
"dumpmeta-webpack-plugin": "^0.2.0",
"echarts": "^5.5.1",
"elkjs": "^0.8.2",
"file-saver": "^2.0.5",
"flush-promises": "^1.0.2",
Expand Down Expand Up @@ -100,7 +101,11 @@
"tus-js-client": "^3.1.1",
"underscore": "^1.13.6",
"util": "^0.12.5",
"vega": "^5.30.0",
"vega-embed": "^6.26.0",
"vega-lite": "^5.21.0",
"vue": "^2.7.14",
"vue-echarts": "^7.0.3",
"vue-infinite-scroll": "^2.0.2",
"vue-multiselect": "^2.1.7",
"vue-observe-visibility": "^1.0.0",
Expand Down
104 changes: 104 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2594,6 +2594,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/invocations/{invocation_id}/metrics": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Invocation Metrics */
get: operations["get_invocation_metrics_api_invocations__invocation_id__metrics_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/invocations/{invocation_id}/prepare_store_download": {
parameters: {
query?: never;
Expand Down Expand Up @@ -18327,6 +18344,49 @@ export interface components {
[key: string]: number;
};
};
/**
* WorkflowJobMetric
* @example {
* "name": "start_epoch",
* "plugin": "core",
* "raw_value": "1614261340.0000000",
* "title": "Job Start Time",
* "value": "2021-02-25 14:55:40"
* }
*/
WorkflowJobMetric: {
/**
* Name
* @description The name of the metric variable.
*/
name: string;
/**
* Plugin
* @description The instrumenter plugin that generated this metric.
*/
plugin: string;
/**
* Raw Value
* @description The raw value of the metric as a string.
*/
raw_value: string;
/** Step Index */
step_index: number;
/** Step Label */
step_label: string | null;
/**
* Title
* @description A descriptive title for this metric.
*/
title: string;
/** Tool Id */
tool_id: string;
/**
* Value
* @description The textual representation of the metric value.
*/
value: string;
};
/** WorkflowLandingRequest */
WorkflowLandingRequest: {
/** Request State */
Expand Down Expand Up @@ -27031,6 +27091,50 @@ export interface operations {
};
};
};
get_invocation_metrics_api_invocations__invocation_id__metrics_get: {
parameters: {
query?: never;
header?: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
"run-as"?: string | null;
};
path: {
/** @description The encoded database identifier of the Invocation. */
invocation_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkflowJobMetric"][];
};
};
/** @description Request Error */
"4XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
/** @description Server Error */
"5XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
};
};
prepare_store_download_api_invocations__invocation_id__prepare_store_download_post: {
parameters: {
query?: never;
Expand Down
49 changes: 49 additions & 0 deletions client/src/components/WorkflowInvocationState/VegaWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<div ref="chartContainer" class="chart"></div>
</template>

<script setup lang="ts">
import { useResizeObserver } from "@vueuse/core";
import embed, { type VisualizationSpec } from "vega-embed";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
export interface VisSpec {
spec: VisualizationSpec;
}
const props = defineProps<VisSpec>();
const chartContainer = ref<HTMLDivElement | null>(null);
let vegaView: any;
async function embedChart() {
if (vegaView) {
vegaView.finalize();
}
if (chartContainer.value !== null) {
const result = await embed(chartContainer.value, props.spec, { renderer: "svg" });
vegaView = result.view;
}
}
onMounted(embedChart);
watch(props, embedChart, { immediate: true, deep: true });
useResizeObserver(chartContainer, () => {
embedChart();
});
// Cleanup the chart when the component is unmounted
onBeforeUnmount(() => {
if (vegaView) {
vegaView.finalize();
}
});
</script>

<style scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<script setup lang="ts">
import type { VisualizationSpec } from "vega-embed";
import { computed, ref, watch } from "vue";
import { type ComputedRef } from "vue";
import { type components, GalaxyApi } from "@/api";
import { errorMessageAsString } from "@/utils/simple-error";
const VegaWrapper = () => import("./VegaWrapper.vue");
const props = defineProps({
invocationId: {
type: String,
required: true,
},
});
const groupBy = ref<"tool_id" | "step_id">("tool_id");
const jobMetrics = ref<components["schemas"]["WorkflowJobMetric"][]>();
const fetchError = ref<string>();
const attributeToLabel = {
tool_id: "Tool ID",
step_id: "Step",
};
async function fetchMetrics() {
const { data, error } = await GalaxyApi().GET("/api/invocations/{invocation_id}/metrics", {
params: {
path: {
invocation_id: props.invocationId,
},
},
});
if (error) {
fetchError.value = errorMessageAsString(error);
} else {
jobMetrics.value = data;
}
}
watch(props, () => fetchMetrics(), { immediate: true });
function itemToX(item: components["schemas"]["WorkflowJobMetric"]) {
if (groupBy.value === "tool_id") {
return item.tool_id;
} else if (groupBy.value === "step_id") {
return `${item.step_index + 1}: ${item.step_label || item.tool_id}`;
} else {
throw Error("Cannot happen");
}
}
interface boxplotData {
x_title: string;
y_title: string;
values?: { x: string; y: Number }[];
}
function metricToSpecData(
jobMetrics: components["schemas"]["WorkflowJobMetric"][] | undefined,
metricName: string,
yTitle: string,
transform?: (param: number) => number
) {
const wallclock = jobMetrics?.filter((jobMetric) => jobMetric.name == metricName);
const values = wallclock?.map((item) => {
let y = parseFloat(item.raw_value);
if (transform !== undefined) {
y = transform(y);
}
return {
y,
x: itemToX(item),
};
});
return {
x_title: attributeToLabel[groupBy.value],
y_title: yTitle,
values,
};
}
const wallclock: ComputedRef<boxplotData> = computed(() => {
return metricToSpecData(jobMetrics.value, "runtime_seconds", "Runtime (in Seconds)");
});
const coresAllocated: ComputedRef<boxplotData> = computed(() => {
return metricToSpecData(jobMetrics.value, "galaxy_slots", "Cores Allocated");
});
const memoryAllocated: ComputedRef<boxplotData> = computed(() => {
return metricToSpecData(jobMetrics.value, "galaxy_memory_mb", "Memory Allocated (in MB)");
});
const peakMemory: ComputedRef<boxplotData> = computed(() => {
return metricToSpecData(jobMetrics.value, "memory.peak", "Max memory usage recorded (in MB)", (v) => v / 1024 ** 2);
});
function itemToSpec(item: boxplotData) {
const spec: VisualizationSpec = {
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
description: "A boxplot with jittered points.",
data: {
values: item.values!,
},
transform: [
{
calculate: "random() - 0.5",
as: "random_jitter",
},
],
layer: [
{
mark: { type: "boxplot", opacity: 0.5 },
encoding: {
x: { field: "x", type: "nominal" },
y: { field: "y", type: "quantitative" },
},
width: "container",
},
{
mark: {
type: "point",
opacity: 0.7,
},
encoding: {
x: {
field: "x",
type: "nominal",
title: item.x_title,
axis: {
labelAngle: -45,
labelAlign: "right",
},
},
xOffset: { field: "random_jitter", type: "quantitative", scale: { domain: [-2, 2] } },
y: {
field: "y",
type: "quantitative",
scale: { zero: false },
title: item.y_title,
},
},
width: "container",
},
],
};
return spec;
}
const specs = computed(() => {
const items = [wallclock.value, coresAllocated.value, memoryAllocated.value, peakMemory.value].filter(
(item) => item.values?.length
);
const specs = Object.fromEntries(items.map((item) => [item.y_title, itemToSpec(item)]));
return specs;
});
</script>

<template>
<div>
<b-tabs lazy>
<b-tab title="Summary by Tool" @click="groupBy = 'tool_id'">
<div v-for="(spec, key) in specs" :key="key">
<h2 class="h-l truncate text-center">{{ key }}</h2>
<VegaWrapper :spec="spec" />
</div>
</b-tab>
<b-tab title="Summary by Workflow Step" @click="groupBy = 'step_id'">
<div v-for="(spec, key) in specs" :key="key">
<h2 class="h-l truncate text-center">{{ key }}</h2>
<VegaWrapper :spec="spec" />
</div>
</b-tab>
</b-tabs>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import InvocationReport from "../Workflow/InvocationReport.vue";
import WorkflowInvocationExportOptions from "./WorkflowInvocationExportOptions.vue";
import WorkflowInvocationHeader from "./WorkflowInvocationHeader.vue";
import WorkflowInvocationInputOutputTabs from "./WorkflowInvocationInputOutputTabs.vue";
import WorkflowInvocationMetrics from "./WorkflowInvocationMetrics.vue";
import WorkflowInvocationOverview from "./WorkflowInvocationOverview.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
Expand Down Expand Up @@ -185,6 +186,9 @@ function cancelWorkflowSchedulingLocal() {
<LoadingSpan message="Waiting to complete invocation" />
</BAlert>
</BTab>
<BTab title="Metrics" :lazy="true">
<WorkflowInvocationMetrics :invocation-id="invocation.id"></WorkflowInvocationMetrics>
</BTab>
</BTabs>
</div>
<BAlert v-else-if="errorMessage" variant="danger" show>
Expand Down
Loading

0 comments on commit 206b729

Please sign in to comment.