Skip to content

Commit 3ce61a8

Browse files
authored
Add analysis view table containing workflow timing statistics (#1254)
analysis: add a view for displaying task run time metrics
1 parent a22213f commit 3ce61a8

File tree

16 files changed

+1072
-13
lines changed

16 files changed

+1072
-13
lines changed

.mailmap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ Renovate Bot[bot] <[email protected]>
2020
github-actions[bot] <[email protected]> <41898282+github-actions[bot]@users.noreply.github.com>
2121
2222
Aaron Cole <[email protected]>
23+
Jamie Allen <[email protected]> JAllen42 <[email protected]>

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ log file subscription.
2323
[#1187](https://github.com/cylc/cylc-ui/pull/1187) - Improved the workflow
2424
filtering menu in the sidebar.
2525

26+
[#1254](https://github.com/cylc/cylc-ui/pull/1254) - Add analysis view:
27+
A new view that displays task timing statistics
28+
2629
### Fixes
2730

2831
[#1249](https://github.com/cylc/cylc-ui/pull/1249) - Fix tasks not loading

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ below.
5757
- Carol Barno
5858
- Giuliano Serrao
5959
- Aaron Cole
60+
- Jamie Allen
6061
<!-- end-shortlog -->
6162

6263
(All contributors are identifiable with email addresses in the git version
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<!--
2+
Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
-->
17+
18+
<template>
19+
<v-row
20+
no-gutters
21+
class="flex-grow-1 position-relative"
22+
>
23+
<v-col
24+
cols="12"
25+
class="mh-100 position-relative"
26+
>
27+
<v-container
28+
fluid
29+
class="pa-0"
30+
>
31+
<v-data-table
32+
:headers="shownHeaders"
33+
:items="tasks"
34+
:sort-by.sync="sortBy"
35+
dense
36+
:footer-props="{
37+
itemsPerPageOptions: [10, 20, 50, 100, 200, -1],
38+
showFirstLastPage: true
39+
}"
40+
:options="{ itemsPerPage: 50 }"
41+
>
42+
<!-- Use custom format for values in columns that have a specified formatter: -->
43+
<!-- Used to make durations human readable -->
44+
<!-- Durations of 0 will be undefined unless allowZeros is true -->
45+
<template
46+
v-for="header in shownHeaders.filter(header => header.hasOwnProperty('formatter'))"
47+
v-slot:[`item.${header.value}`]="{ value }"
48+
>
49+
{{ header.formatter(value, header.allowZeros) }}
50+
</template>
51+
</v-data-table>
52+
</v-container>
53+
</v-col>
54+
</v-row>
55+
</template>
56+
57+
<script>
58+
import { formatDuration } from '@/utils/tasks'
59+
60+
export default {
61+
name: 'AnalysisTableComponent',
62+
63+
props: {
64+
tasks: {
65+
type: Array,
66+
required: true
67+
},
68+
timingOption: {
69+
type: String,
70+
required: true
71+
}
72+
},
73+
74+
data () {
75+
return {
76+
sortBy: 'name',
77+
headers: [
78+
{
79+
text: 'Task',
80+
value: 'name'
81+
},
82+
{
83+
text: 'Platform',
84+
value: 'platform'
85+
},
86+
{
87+
text: 'Count',
88+
value: 'count'
89+
}
90+
]
91+
}
92+
},
93+
94+
computed: {
95+
shownHeaders () {
96+
let times
97+
if (this.timingOption === 'totalTimes') {
98+
times = 'Total'
99+
} else if (this.timingOption === 'runTimes') {
100+
times = 'Run'
101+
} else if (this.timingOption === 'queueTimes') {
102+
times = 'Queue'
103+
} else {
104+
return this.headers
105+
}
106+
const timingHeaders = [
107+
{
108+
text: `Mean T-${times}`,
109+
value: `mean${times}Time`,
110+
formatter: formatDuration,
111+
allowZeros: false
112+
},
113+
{
114+
text: `Std Dev T-${times}`,
115+
value: `stdDev${times}Time`,
116+
formatter: formatDuration,
117+
allowZeros: true
118+
},
119+
{
120+
text: `Min T-${times}`,
121+
value: `min${times}Time`,
122+
formatter: formatDuration,
123+
allowZeros: false
124+
},
125+
{
126+
text: `Q1 T-${times}`,
127+
value: `${times.toLowerCase()}Quartiles[0]`,
128+
formatter: formatDuration,
129+
allowZeros: false
130+
},
131+
{
132+
text: `Median T-${times}`,
133+
value: `${times.toLowerCase()}Quartiles[1]`,
134+
formatter: formatDuration,
135+
allowZeros: false
136+
},
137+
{
138+
text: `Q3 T-${times}`,
139+
value: `${times.toLowerCase()}Quartiles[2]`,
140+
formatter: formatDuration,
141+
allowZeros: false
142+
},
143+
{
144+
text: `Max T-${times}`,
145+
value: `max${times}Time`,
146+
formatter: formatDuration,
147+
allowZeros: false
148+
}
149+
]
150+
return this.headers.concat(timingHeaders)
151+
}
152+
}
153+
}
154+
</script>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
/**
19+
* Function to determine if a task should be displayed given a certain filter
20+
* Checks the name includes a search string and if the platform is equal to
21+
* that chosen
22+
*
23+
* @export
24+
* @param {object} task - The task to evaluate
25+
* @param {object} tasksFilter - The filter to apply to the task
26+
* @return {boolean} Boolean determining if task should be displayed
27+
*/
28+
export function matchTask (task, tasksFilter) {
29+
let ret = true
30+
if (tasksFilter.name?.trim()) {
31+
ret &&= task.name.includes(tasksFilter.name)
32+
}
33+
if (tasksFilter.platformOption?.trim()) {
34+
ret &&= task.platform === tasksFilter.platformOption
35+
}
36+
return ret
37+
}
38+
39+
/**
40+
* Function to find the unique platforms in an array of tasks
41+
*
42+
* @export
43+
* @param {array} tasks - The tasks to search for unique platforms
44+
* @return {array} - An array of unique platform objects
45+
*/
46+
export function platformOptions (tasks) {
47+
const platformOptions = [{ text: 'All', value: null }]
48+
const platforms = []
49+
for (const task of tasks) {
50+
if (!platforms.includes(task.platform)) {
51+
platforms.push(task.platform)
52+
platformOptions.push({ text: task.platform, value: task.platform })
53+
}
54+
}
55+
return platformOptions
56+
}

src/router/paths.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,16 @@ export default [
134134
showSidebar: false
135135
},
136136
props: true
137+
},
138+
{
139+
path: '/analysis/:workflowName(.*)',
140+
view: 'Analysis',
141+
name: 'analysis',
142+
meta: {
143+
layout: 'default',
144+
toolbar: true,
145+
showSidebar: false
146+
},
147+
props: true
137148
}
138149
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"data": {
3+
"tasks": [
4+
{
5+
"name": "succeeded",
6+
"platform": "platform_1",
7+
"count": 42,
8+
"meanTotalTime": 32,
9+
"stdDevTotalTime": 0,
10+
"minTotalTime": 32,
11+
"totalQuartiles": [32, 32, 32],
12+
"maxTotalTime": 32,
13+
"meanRunTime": 21,
14+
"stdDevRunTime": 0,
15+
"minRunTime": 21,
16+
"runQuartiles": [21, 21, 21],
17+
"maxRunTime": 21,
18+
"meanQueueTime": 11,
19+
"stdDevQueueTime": 0,
20+
"minQueueTime": 11,
21+
"queueQuartiles": [11, 11, 11],
22+
"maxQueueTime": 11
23+
},
24+
{
25+
"name": "eventually_succeeded",
26+
"platform": "platform_2",
27+
"count": 42,
28+
"meanTotalTime": 30,
29+
"stdDevTotalTime": 0,
30+
"minTotalTime": 30,
31+
"totalQuartiles": [30, 30, 30],
32+
"maxTotalTime": 30,
33+
"meanRunTime": 20,
34+
"stdDevRunTime": 0,
35+
"minRunTime": 20,
36+
"runQuartiles": [20, 20, 20],
37+
"maxRunTime": 20,
38+
"meanQueueTime": 10,
39+
"stdDevQueueTime": 0,
40+
"minQueueTime": 10,
41+
"queueQuartiles": [10, 10, 10],
42+
"maxQueueTime": 10
43+
},
44+
{
45+
"name": "waiting",
46+
"platform": "platform_1",
47+
"count": 41,
48+
"meanTotalTime": 34,
49+
"stdDevTotalTime": 0,
50+
"minTotalTime": 34,
51+
"totalQuartiles": [34, 34, 34],
52+
"maxTotalTime": 34,
53+
"meanRunTime": 22,
54+
"stdDevRunTime": 0,
55+
"minRunTime": 22,
56+
"runQuartiles": [22, 22, 22],
57+
"maxRunTime": 22,
58+
"meanQueueTime": 12,
59+
"stdDevQueueTime": 0,
60+
"minQueueTime": 12,
61+
"queueQuartiles": [12, 12, 12],
62+
"maxQueueTime": 12
63+
}
64+
]
65+
}
66+
}

src/services/mock/json/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const familyProxy = require('./familyProxy.json')
2222
const App = require('./App')
2323
const LogData = require('./logData.json')
2424
const LogFiles = require('./logFiles.json')
25+
const analysisQuery = require('./analysisQuery.json')
2526

2627
module.exports = {
2728
IntrospectionQuery,
@@ -31,5 +32,6 @@ module.exports = {
3132
LogData,
3233
LogFiles,
3334
App,
34-
Workflow: App
35+
Workflow: App,
36+
analysisQuery
3537
}

src/services/workflow.service.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ class WorkflowService {
139139
)
140140
}
141141

142+
async query2 (query, variables) { // TODO: refactor or come up with better name
143+
const response = await this.apolloClient.query({
144+
query,
145+
variables,
146+
fetchPolicy: 'no-cache'
147+
})
148+
return response
149+
}
150+
142151
/**
143152
* Load mutations, queries and types from GraphQL introspection.
144153
*

src/utils/tasks.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,17 @@ function jobMessageOutputs (jobNode) {
100100
return ret
101101
}
102102

103-
export {
104-
extractGroupState,
105-
latestJob,
106-
jobMessageOutputs
107-
}
108-
109-
export function dtMean (taskNode) {
110-
// Convert to an easily read duration format:
111-
const dur = taskNode.node?.task?.meanElapsedTime
112-
if (dur) {
103+
/**
104+
* Convert duration to an easily read format
105+
* Durations of 0 seconds return undefined unless allowZeros is true
106+
*
107+
* @param {number=} dur Duration in seconds
108+
* @param {boolean} [allowZeros=false] Whether durations of 0 are formatted as
109+
* 00:00:00, rather than undefined
110+
* @return {string=} Formatted duration
111+
*/
112+
function formatDuration (dur, allowZeros = false) {
113+
if (dur || (dur === 0 && allowZeros === true)) {
113114
const seconds = dur % 60
114115
const minutes = ((dur - seconds) / 60) % 60
115116
const hours = ((dur - minutes * 60 - seconds) / 3600) % 24
@@ -129,3 +130,17 @@ export function dtMean (taskNode) {
129130
// return "undefined" rather than a number for these cases
130131
return undefined
131132
}
133+
134+
function dtMean (taskNode) {
135+
// Convert to an easily read duration format:
136+
const dur = taskNode.node?.task?.meanElapsedTime
137+
return formatDuration(dur)
138+
}
139+
140+
export {
141+
extractGroupState,
142+
latestJob,
143+
jobMessageOutputs,
144+
formatDuration,
145+
dtMean
146+
}

0 commit comments

Comments
 (0)