mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-05-18 14:47:13 +02:00
Merge pull request #391 from ClusterCockpit/add_job_comparison
Add job comparison
This commit is contained in:
commit
b323ce2eef
@ -158,7 +158,7 @@ type StatsSeries {
|
|||||||
max: [NullableFloat!]!
|
max: [NullableFloat!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobStatsWithScope {
|
type NamedStatsWithScope {
|
||||||
name: String!
|
name: String!
|
||||||
scope: MetricScope!
|
scope: MetricScope!
|
||||||
stats: [ScopedStats!]!
|
stats: [ScopedStats!]!
|
||||||
@ -171,8 +171,21 @@ type ScopedStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JobStats {
|
type JobStats {
|
||||||
name: String!
|
id: Int!
|
||||||
stats: MetricStatistics!
|
jobId: String!
|
||||||
|
startTime: Int!
|
||||||
|
duration: Int!
|
||||||
|
cluster: String!
|
||||||
|
subCluster: String!
|
||||||
|
numNodes: Int!
|
||||||
|
numHWThreads: Int
|
||||||
|
numAccelerators: Int
|
||||||
|
stats: [NamedStats!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamedStats {
|
||||||
|
name: String!
|
||||||
|
data: MetricStatistics!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Unit {
|
type Unit {
|
||||||
@ -259,12 +272,13 @@ type Query {
|
|||||||
|
|
||||||
job(id: ID!): Job
|
job(id: ID!): Job
|
||||||
jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]!
|
jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]!
|
||||||
jobStats(id: ID!, metrics: [String!]): [JobStats!]!
|
jobStats(id: ID!, metrics: [String!]): [NamedStats!]!
|
||||||
scopedJobStats(id: ID!, metrics: [String!], scopes: [MetricScope!]): [JobStatsWithScope!]!
|
scopedJobStats(id: ID!, metrics: [String!], scopes: [MetricScope!]): [NamedStatsWithScope!]!
|
||||||
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
|
|
||||||
|
|
||||||
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!
|
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!
|
||||||
jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: String, numMetricBins: Int): [JobsStatistics!]!
|
jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: String, numMetricBins: Int): [JobsStatistics!]!
|
||||||
|
jobsMetricStats(filter: [JobFilter!], metrics: [String!]): [JobStats!]!
|
||||||
|
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
|
||||||
|
|
||||||
rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]!
|
rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]!
|
||||||
|
|
||||||
@ -287,6 +301,7 @@ type TimeRangeOutput { range: String, from: Time!, to: Time! }
|
|||||||
|
|
||||||
input JobFilter {
|
input JobFilter {
|
||||||
tags: [ID!]
|
tags: [ID!]
|
||||||
|
dbId: [ID!]
|
||||||
jobId: StringInput
|
jobId: StringInput
|
||||||
arrayJobId: Int
|
arrayJobId: Int
|
||||||
user: StringInput
|
user: StringInput
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,7 @@ type IntRangeOutput struct {
|
|||||||
|
|
||||||
type JobFilter struct {
|
type JobFilter struct {
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
DbID []string `json:"dbId,omitempty"`
|
||||||
JobID *StringInput `json:"jobId,omitempty"`
|
JobID *StringInput `json:"jobId,omitempty"`
|
||||||
ArrayJobID *int `json:"arrayJobId,omitempty"`
|
ArrayJobID *int `json:"arrayJobId,omitempty"`
|
||||||
User *StringInput `json:"user,omitempty"`
|
User *StringInput `json:"user,omitempty"`
|
||||||
@ -96,14 +97,16 @@ type JobResultList struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JobStats struct {
|
type JobStats struct {
|
||||||
Name string `json:"name"`
|
ID int `json:"id"`
|
||||||
Stats *schema.MetricStatistics `json:"stats"`
|
JobID string `json:"jobId"`
|
||||||
}
|
StartTime int `json:"startTime"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
type JobStatsWithScope struct {
|
Cluster string `json:"cluster"`
|
||||||
Name string `json:"name"`
|
SubCluster string `json:"subCluster"`
|
||||||
Scope schema.MetricScope `json:"scope"`
|
NumNodes int `json:"numNodes"`
|
||||||
Stats []*ScopedStats `json:"stats"`
|
NumHWThreads *int `json:"numHWThreads,omitempty"`
|
||||||
|
NumAccelerators *int `json:"numAccelerators,omitempty"`
|
||||||
|
Stats []*NamedStats `json:"stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobsStatistics struct {
|
type JobsStatistics struct {
|
||||||
@ -153,6 +156,17 @@ type MetricStatItem struct {
|
|||||||
type Mutation struct {
|
type Mutation struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NamedStats struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data *schema.MetricStatistics `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamedStatsWithScope struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scope schema.MetricScope `json:"scope"`
|
||||||
|
Stats []*ScopedStats `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
type NodeMetrics struct {
|
type NodeMetrics struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
SubCluster string `json:"subCluster"`
|
SubCluster string `json:"subCluster"`
|
||||||
|
@ -400,7 +400,7 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JobStats is the resolver for the jobStats field.
|
// JobStats is the resolver for the jobStats field.
|
||||||
func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []string) ([]*model.JobStats, error) {
|
func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []string) ([]*model.NamedStats, error) {
|
||||||
job, err := r.Query().Job(ctx, id)
|
job, err := r.Query().Job(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Error while querying job %s for metadata", id)
|
log.Warnf("Error while querying job %s for metadata", id)
|
||||||
@ -413,11 +413,11 @@ func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := []*model.JobStats{}
|
res := []*model.NamedStats{}
|
||||||
for name, md := range data {
|
for name, md := range data {
|
||||||
res = append(res, &model.JobStats{
|
res = append(res, &model.NamedStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Stats: &md,
|
Data: &md,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,7 +425,7 @@ func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ScopedJobStats is the resolver for the scopedJobStats field.
|
// ScopedJobStats is the resolver for the scopedJobStats field.
|
||||||
func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.JobStatsWithScope, error) {
|
func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.NamedStatsWithScope, error) {
|
||||||
job, err := r.Query().Job(ctx, id)
|
job, err := r.Query().Job(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Error while querying job %s for metadata", id)
|
log.Warnf("Error while querying job %s for metadata", id)
|
||||||
@ -438,7 +438,7 @@ func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics [
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]*model.JobStatsWithScope, 0)
|
res := make([]*model.NamedStatsWithScope, 0)
|
||||||
for name, scoped := range data {
|
for name, scoped := range data {
|
||||||
for scope, stats := range scoped {
|
for scope, stats := range scoped {
|
||||||
|
|
||||||
@ -451,7 +451,7 @@ func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics [
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, &model.JobStatsWithScope{
|
res = append(res, &model.NamedStatsWithScope{
|
||||||
Name: name,
|
Name: name,
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
Stats: mdlStats,
|
Stats: mdlStats,
|
||||||
@ -462,12 +462,6 @@ func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics [
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobsFootprints is the resolver for the jobsFootprints field.
|
|
||||||
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
|
|
||||||
// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
|
|
||||||
return r.jobsFootprints(ctx, filter, metrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jobs is the resolver for the jobs field.
|
// Jobs is the resolver for the jobs field.
|
||||||
func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) {
|
func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) {
|
||||||
if page == nil {
|
if page == nil {
|
||||||
@ -589,6 +583,62 @@ func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobF
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobsMetricStats is the resolver for the jobsMetricStats field.
|
||||||
|
func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.JobStats, error) {
|
||||||
|
// No Paging, Fixed Order by StartTime ASC
|
||||||
|
order := &model.OrderByInput{
|
||||||
|
Field: "startTime",
|
||||||
|
Type: "col",
|
||||||
|
Order: "ASC",
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err := r.Repo.QueryJobs(ctx, filter, nil, order)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while querying jobs for comparison")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := []*model.JobStats{}
|
||||||
|
for _, job := range jobs {
|
||||||
|
data, err := metricDataDispatcher.LoadJobStats(job, metrics, ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Error while loading comparison jobStats data for job id %d", job.JobID)
|
||||||
|
continue
|
||||||
|
// return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sres := []*model.NamedStats{}
|
||||||
|
for name, md := range data {
|
||||||
|
sres = append(sres, &model.NamedStats{
|
||||||
|
Name: name,
|
||||||
|
Data: &md,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
numThreadsInt := int(job.NumHWThreads)
|
||||||
|
numAccsInt := int(job.NumAcc)
|
||||||
|
res = append(res, &model.JobStats{
|
||||||
|
ID: int(job.ID),
|
||||||
|
JobID: strconv.Itoa(int(job.JobID)),
|
||||||
|
StartTime: int(job.StartTime.Unix()),
|
||||||
|
Duration: int(job.Duration),
|
||||||
|
Cluster: job.Cluster,
|
||||||
|
SubCluster: job.SubCluster,
|
||||||
|
NumNodes: int(job.NumNodes),
|
||||||
|
NumHWThreads: &numThreadsInt,
|
||||||
|
NumAccelerators: &numAccsInt,
|
||||||
|
Stats: sres,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobsFootprints is the resolver for the jobsFootprints field.
|
||||||
|
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
|
||||||
|
// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
|
||||||
|
return r.jobsFootprints(ctx, filter, metrics)
|
||||||
|
}
|
||||||
|
|
||||||
// RooflineHeatmap is the resolver for the rooflineHeatmap field.
|
// RooflineHeatmap is the resolver for the rooflineHeatmap field.
|
||||||
func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) {
|
func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) {
|
||||||
return r.rooflineHeatmap(ctx, filter, rows, cols, minX, minY, maxX, maxY)
|
return r.rooflineHeatmap(ctx, filter, rows, cols, minX, minY, maxX, maxY)
|
||||||
|
@ -146,6 +146,13 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
|
|||||||
// This is an OR-Logic query: Returns all distinct jobs with at least one of the requested tags; TODO: AND-Logic query?
|
// This is an OR-Logic query: Returns all distinct jobs with at least one of the requested tags; TODO: AND-Logic query?
|
||||||
query = query.Join("jobtag ON jobtag.job_id = job.id").Where(sq.Eq{"jobtag.tag_id": filter.Tags}).Distinct()
|
query = query.Join("jobtag ON jobtag.job_id = job.id").Where(sq.Eq{"jobtag.tag_id": filter.Tags}).Distinct()
|
||||||
}
|
}
|
||||||
|
if filter.DbID != nil {
|
||||||
|
dbIDs := make([]string, len(filter.DbID))
|
||||||
|
for i, val := range filter.DbID {
|
||||||
|
dbIDs[i] = val
|
||||||
|
}
|
||||||
|
query = query.Where(sq.Eq{"job.id": dbIDs})
|
||||||
|
}
|
||||||
if filter.JobID != nil {
|
if filter.JobID != nil {
|
||||||
query = buildStringCondition("job.job_id", filter.JobID, query)
|
query = buildStringCondition("job.job_id", filter.JobID, query)
|
||||||
}
|
}
|
||||||
|
@ -297,6 +297,9 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(query["dbId"]) != 0 {
|
||||||
|
filterPresets["dbId"] = query["dbId"]
|
||||||
|
}
|
||||||
if query.Get("jobId") != "" {
|
if query.Get("jobId") != "" {
|
||||||
if len(query["jobId"]) == 1 {
|
if len(query["jobId"]) == 1 {
|
||||||
filterPresets["jobId"] = query.Get("jobId")
|
filterPresets["jobId"] = query.Get("jobId")
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
import { init } from "./generic/utils.js";
|
import { init } from "./generic/utils.js";
|
||||||
import Filters from "./generic/Filters.svelte";
|
import Filters from "./generic/Filters.svelte";
|
||||||
import JobList from "./generic/JobList.svelte";
|
import JobList from "./generic/JobList.svelte";
|
||||||
|
import JobCompare from "./generic/JobCompare.svelte";
|
||||||
import TextFilter from "./generic/helper/TextFilter.svelte";
|
import TextFilter from "./generic/helper/TextFilter.svelte";
|
||||||
import Refresher from "./generic/helper/Refresher.svelte";
|
import Refresher from "./generic/helper/Refresher.svelte";
|
||||||
import Sorting from "./generic/select/SortSelection.svelte";
|
import Sorting from "./generic/select/SortSelection.svelte";
|
||||||
@ -35,8 +36,12 @@
|
|||||||
export let roles;
|
export let roles;
|
||||||
|
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||||
|
let filterBuffer = [];
|
||||||
|
let selectedJobs = [];
|
||||||
let jobList,
|
let jobList,
|
||||||
matchedJobs = null;
|
jobCompare,
|
||||||
|
matchedListJobs,
|
||||||
|
matchedCompareJobs = null;
|
||||||
let sorting = { field: "startTime", type: "col", order: "DESC" },
|
let sorting = { field: "startTime", type: "col", order: "DESC" },
|
||||||
isSortingOpen = false,
|
isSortingOpen = false,
|
||||||
isMetricsSelectionOpen = false;
|
isMetricsSelectionOpen = false;
|
||||||
@ -49,11 +54,16 @@
|
|||||||
: !!ccconfig.plot_list_showFootprint;
|
: !!ccconfig.plot_list_showFootprint;
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||||
let presetProject = filterPresets?.project ? filterPresets.project : ""
|
let presetProject = filterPresets?.project ? filterPresets.project : ""
|
||||||
|
let showCompare = false;
|
||||||
|
|
||||||
// The filterPresets are handled by the Filters component,
|
// The filterPresets are handled by the Filters component,
|
||||||
// so we need to wait for it to be ready before we can start a query.
|
// so we need to wait for it to be ready before we can start a query.
|
||||||
// This is also why JobList component starts out with a paused query.
|
// This is also why JobList component starts out with a paused query.
|
||||||
onMount(() => filterComponent.updateFilters());
|
onMount(() => filterComponent.updateFilters());
|
||||||
|
|
||||||
|
$: if (filterComponent && selectedJobs.length == 0) {
|
||||||
|
filterComponent.updateFilters({dbId: []})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- ROW1: Status-->
|
<!-- ROW1: Status-->
|
||||||
@ -72,10 +82,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ROW2: Tools-->
|
<!-- ROW2: Tools-->
|
||||||
<Row cols={{ xs: 1, md: 2, lg: 4}} class="mb-3">
|
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
|
||||||
<Col lg="2" class="mb-2 mb-lg-0">
|
<Col lg="2" class="mb-2 mb-lg-0">
|
||||||
<ButtonGroup class="w-100">
|
<ButtonGroup class="w-100">
|
||||||
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
|
<Button outline color="primary" on:click={() => (isSortingOpen = true)} disabled={showCompare}>
|
||||||
<Icon name="sort-up" /> Sorting
|
<Icon name="sort-up" /> Sorting
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -87,49 +97,88 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="4" xl="{(presetProject !== '') ? 5 : 6}" class="mb-1 mb-lg-0">
|
<Col lg="4" class="mb-1 mb-lg-0">
|
||||||
<Filters
|
<Filters
|
||||||
|
showFilter={!showCompare}
|
||||||
{filterPresets}
|
{filterPresets}
|
||||||
{matchedJobs}
|
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
on:update-filters={({ detail }) => {
|
on:update-filters={({ detail }) => {
|
||||||
selectedCluster = detail.filters[0]?.cluster
|
selectedCluster = detail.filters[0]?.cluster
|
||||||
? detail.filters[0].cluster.eq
|
? detail.filters[0].cluster.eq
|
||||||
: null;
|
: null;
|
||||||
jobList.queryJobs(detail.filters);
|
filterBuffer = [...detail.filters]
|
||||||
|
if (showCompare) {
|
||||||
|
jobCompare.queryJobs(detail.filters);
|
||||||
|
} else {
|
||||||
|
jobList.queryJobs(detail.filters);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="3" xl="{(presetProject !== '') ? 3 : 2}" class="mb-2 mb-lg-0">
|
<Col lg="2" class="mb-2 mb-lg-0">
|
||||||
<TextFilter
|
{#if !showCompare}
|
||||||
{presetProject}
|
<TextFilter
|
||||||
bind:authlevel
|
{presetProject}
|
||||||
bind:roles
|
bind:authlevel
|
||||||
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
bind:roles
|
||||||
/>
|
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="3" xl="2" class="mb-1 mb-lg-0">
|
<Col lg="2" class="mb-1 mb-lg-0">
|
||||||
<Refresher on:refresh={() => {
|
{#if !showCompare}
|
||||||
jobList.refreshJobs()
|
<Refresher on:refresh={() => {
|
||||||
jobList.refreshAllMetrics()
|
jobList.refreshJobs()
|
||||||
}} />
|
jobList.refreshAllMetrics()
|
||||||
|
}} />
|
||||||
|
{/if}
|
||||||
|
</Col>
|
||||||
|
<Col lg="2" class="mb-2 mb-lg-0">
|
||||||
|
<ButtonGroup class="w-100">
|
||||||
|
<Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} on:click={() => {
|
||||||
|
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
|
||||||
|
showCompare = !showCompare
|
||||||
|
}} >
|
||||||
|
{showCompare ? 'Return to List' :
|
||||||
|
'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)}
|
||||||
|
</Button>
|
||||||
|
{#if !showCompare && selectedJobs.length != 0}
|
||||||
|
<Button color="warning" on:click={() => {
|
||||||
|
selectedJobs = [] // Only empty array, filters handled by reactive reset
|
||||||
|
}}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ButtonGroup>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- ROW3: Job List-->
|
<!-- ROW3: Job List / Job Compare-->
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<JobList
|
{#if !showCompare}
|
||||||
bind:this={jobList}
|
<JobList
|
||||||
bind:metrics
|
bind:this={jobList}
|
||||||
bind:sorting
|
bind:metrics
|
||||||
bind:matchedJobs
|
bind:sorting
|
||||||
bind:showFootprint
|
bind:matchedListJobs
|
||||||
/>
|
bind:showFootprint
|
||||||
|
bind:selectedJobs
|
||||||
|
{filterBuffer}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<JobCompare
|
||||||
|
bind:this={jobCompare}
|
||||||
|
bind:metrics
|
||||||
|
bind:matchedCompareJobs
|
||||||
|
{filterBuffer}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
<Sorting bind:sorting bind:isOpen={isSortingOpen}/>
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:cluster={selectedCluster}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
export let disableClusterSelection = false;
|
export let disableClusterSelection = false;
|
||||||
export let startTimeQuickSelect = false;
|
export let startTimeQuickSelect = false;
|
||||||
export let matchedJobs = -2;
|
export let matchedJobs = -2;
|
||||||
|
export let showFilter = true;
|
||||||
|
|
||||||
const startTimeSelectOptions = [
|
const startTimeSelectOptions = [
|
||||||
{ range: "", rangeLabel: "No Selection"},
|
{ range: "", rangeLabel: "No Selection"},
|
||||||
@ -58,6 +59,39 @@
|
|||||||
contains: " Contains",
|
contains: " Contains",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterReset = {
|
||||||
|
projectMatch: "contains",
|
||||||
|
userMatch: "contains",
|
||||||
|
jobIdMatch: "eq",
|
||||||
|
nodeMatch: "eq",
|
||||||
|
|
||||||
|
cluster: null,
|
||||||
|
partition: null,
|
||||||
|
states: allJobStates,
|
||||||
|
startTime: { from: null, to: null, range: ""},
|
||||||
|
tags: [],
|
||||||
|
duration: {
|
||||||
|
lessThan: null,
|
||||||
|
moreThan: null,
|
||||||
|
from: null,
|
||||||
|
to: null,
|
||||||
|
},
|
||||||
|
dbId: [],
|
||||||
|
jobId: "",
|
||||||
|
arrayJobId: null,
|
||||||
|
user: "",
|
||||||
|
project: "",
|
||||||
|
jobName: "",
|
||||||
|
|
||||||
|
node: null,
|
||||||
|
energy: { from: null, to: null },
|
||||||
|
numNodes: { from: null, to: null },
|
||||||
|
numHWThreads: { from: null, to: null },
|
||||||
|
numAccelerators: { from: null, to: null },
|
||||||
|
|
||||||
|
stats: [],
|
||||||
|
};
|
||||||
|
|
||||||
let filters = {
|
let filters = {
|
||||||
projectMatch: filterPresets.projectMatch || "contains",
|
projectMatch: filterPresets.projectMatch || "contains",
|
||||||
userMatch: filterPresets.userMatch || "contains",
|
userMatch: filterPresets.userMatch || "contains",
|
||||||
@ -78,6 +112,7 @@
|
|||||||
from: null,
|
from: null,
|
||||||
to: null,
|
to: null,
|
||||||
},
|
},
|
||||||
|
dbId: filterPresets.dbId || [],
|
||||||
jobId: filterPresets.jobId || "",
|
jobId: filterPresets.jobId || "",
|
||||||
arrayJobId: filterPresets.arrayJobId || null,
|
arrayJobId: filterPresets.arrayJobId || null,
|
||||||
user: filterPresets.user || "",
|
user: filterPresets.user || "",
|
||||||
@ -106,10 +141,17 @@
|
|||||||
isAccsModified = false;
|
isAccsModified = false;
|
||||||
|
|
||||||
// Can be called from the outside to trigger a 'update' event from this component.
|
// Can be called from the outside to trigger a 'update' event from this component.
|
||||||
export function updateFilters(additionalFilters = null) {
|
// 'force' option empties existing filters and then applies only 'additionalFilters'
|
||||||
if (additionalFilters != null)
|
export function updateFilters(additionalFilters = null, force = false) {
|
||||||
|
// Empty Current Filter For Force
|
||||||
|
if (additionalFilters != null && force) {
|
||||||
|
filters = {...filterReset}
|
||||||
|
}
|
||||||
|
// Add Additional Filters
|
||||||
|
if (additionalFilters != null) {
|
||||||
for (let key in additionalFilters) filters[key] = additionalFilters[key];
|
for (let key in additionalFilters) filters[key] = additionalFilters[key];
|
||||||
|
}
|
||||||
|
// Construct New Filter
|
||||||
let items = [];
|
let items = [];
|
||||||
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
||||||
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
|
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
|
||||||
@ -137,6 +179,8 @@
|
|||||||
items.push({
|
items.push({
|
||||||
energy: { from: filters.energy.from, to: filters.energy.to },
|
energy: { from: filters.energy.from, to: filters.energy.to },
|
||||||
});
|
});
|
||||||
|
if (filters.dbId.length != 0)
|
||||||
|
items.push({ dbId: filters.dbId });
|
||||||
if (filters.jobId)
|
if (filters.jobId)
|
||||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
||||||
if (filters.arrayJobId != null)
|
if (filters.arrayJobId != null)
|
||||||
@ -180,8 +224,8 @@
|
|||||||
|
|
||||||
function changeURL() {
|
function changeURL() {
|
||||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||||
|
|
||||||
let opts = [];
|
let opts = [];
|
||||||
|
|
||||||
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
||||||
if (filters.node) opts.push(`node=${filters.node}`);
|
if (filters.node) opts.push(`node=${filters.node}`);
|
||||||
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
|
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
|
||||||
@ -196,6 +240,11 @@
|
|||||||
if (filters.startTime.range) {
|
if (filters.startTime.range) {
|
||||||
opts.push(`startTime=${filters.startTime.range}`)
|
opts.push(`startTime=${filters.startTime.range}`)
|
||||||
}
|
}
|
||||||
|
if (filters.dbId.length != 0) {
|
||||||
|
for (let dbi of filters.dbId) {
|
||||||
|
opts.push(`dbId=${dbi}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (filters.jobId.length != 0)
|
if (filters.jobId.length != 0)
|
||||||
if (filters.jobIdMatch != "in") {
|
if (filters.jobIdMatch != "in") {
|
||||||
opts.push(`jobId=${filters.jobId}`);
|
opts.push(`jobId=${filters.jobId}`);
|
||||||
@ -237,8 +286,8 @@
|
|||||||
for (let stat of filters.stats) {
|
for (let stat of filters.stats) {
|
||||||
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
|
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
|
||||||
}
|
}
|
||||||
if (opts.length == 0 && window.location.search.length <= 1) return;
|
|
||||||
|
|
||||||
|
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||||
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
||||||
window.history.replaceState(null, "", newurl);
|
window.history.replaceState(null, "", newurl);
|
||||||
}
|
}
|
||||||
@ -246,60 +295,63 @@
|
|||||||
|
|
||||||
<!-- Dropdown-Button -->
|
<!-- Dropdown-Button -->
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
|
{#if showFilter}
|
||||||
<DropdownToggle outline caret color="success">
|
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
|
||||||
<Icon name="sliders" />
|
<DropdownToggle outline caret color="success">
|
||||||
Filters
|
<Icon name="sliders" />
|
||||||
</DropdownToggle>
|
Filters
|
||||||
<DropdownMenu>
|
</DropdownToggle>
|
||||||
<DropdownItem header>Manage Filters</DropdownItem>
|
<DropdownMenu>
|
||||||
{#if menuText}
|
<DropdownItem header>Manage Filters</DropdownItem>
|
||||||
<DropdownItem disabled>{menuText}</DropdownItem>
|
{#if menuText}
|
||||||
<DropdownItem divider />
|
<DropdownItem disabled>{menuText}</DropdownItem>
|
||||||
{/if}
|
<DropdownItem divider />
|
||||||
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
{/if}
|
||||||
<Icon name="cpu" /> Cluster/Partition
|
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="cpu" /> Cluster/Partition
|
||||||
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="gear-fill" /> Job States
|
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="gear-fill" /> Job States
|
||||||
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="calendar-range" /> Start Time
|
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="calendar-range" /> Start Time
|
||||||
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="stopwatch" /> Duration
|
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="stopwatch" /> Duration
|
||||||
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="tags" /> Tags
|
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="tags" /> Tags
|
||||||
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="hdd-stack" /> Resources
|
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="hdd-stack" /> Resources
|
||||||
<DropdownItem on:click={() => (isEnergyOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="lightning-charge-fill" /> Energy
|
<DropdownItem on:click={() => (isEnergyOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="lightning-charge-fill" /> Energy
|
||||||
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
</DropdownItem>
|
||||||
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
|
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
||||||
</DropdownItem>
|
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
|
||||||
{#if startTimeQuickSelect}
|
</DropdownItem>
|
||||||
<DropdownItem divider />
|
{#if startTimeQuickSelect}
|
||||||
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
|
<DropdownItem divider />
|
||||||
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
|
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
|
||||||
<DropdownItem
|
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
|
||||||
on:click={() => {
|
<DropdownItem
|
||||||
filters.startTime.from = null
|
on:click={() => {
|
||||||
filters.startTime.to = null
|
filters.startTime.from = null
|
||||||
filters.startTime.range = range;
|
filters.startTime.to = null
|
||||||
updateFilters();
|
filters.startTime.range = range;
|
||||||
}}
|
updateFilters();
|
||||||
>
|
}}
|
||||||
<Icon name="calendar-range" />
|
>
|
||||||
{rangeLabel}
|
<Icon name="calendar-range" />
|
||||||
</DropdownItem>
|
{rangeLabel}
|
||||||
{/each}
|
</DropdownItem>
|
||||||
{/if}
|
{/each}
|
||||||
</DropdownMenu>
|
{/if}
|
||||||
</ButtonDropdown>
|
</DropdownMenu>
|
||||||
|
</ButtonDropdown>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if matchedJobs >= -1}
|
{#if matchedJobs >= -1}
|
||||||
<Button class="mb-1" style="margin-right: 0.5rem;" disabled outline>
|
<Button class="mb-1" style="margin-right: 0.5rem;" disabled outline>
|
||||||
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
|
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
|
||||||
@ -307,109 +359,111 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<!-- SELECTED FILTER PILLS -->
|
{#if showFilter}
|
||||||
{#if filters.cluster}
|
<!-- SELECTED FILTER PILLS -->
|
||||||
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
|
{#if filters.cluster}
|
||||||
{filters.cluster}
|
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
|
||||||
{#if filters.partition}
|
{filters.cluster}
|
||||||
({filters.partition})
|
{#if filters.partition}
|
||||||
{/if}
|
({filters.partition})
|
||||||
</Info>
|
{/if}
|
||||||
{/if}
|
</Info>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if filters.states.length != allJobStates.length}
|
{#if filters.states.length != allJobStates.length}
|
||||||
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
||||||
{filters.states.join(", ")}
|
{filters.states.join(", ")}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.startTime.from || filters.startTime.to}
|
{#if filters.startTime.from || filters.startTime.to}
|
||||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
||||||
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
||||||
filters.startTime.to,
|
filters.startTime.to,
|
||||||
).toLocaleString()}
|
).toLocaleString()}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.startTime.range}
|
{#if filters.startTime.range}
|
||||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
||||||
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
|
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.from || filters.duration.to}
|
{#if filters.duration.from || filters.duration.to}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
||||||
(filters.duration.from % 3600) / 60,
|
(filters.duration.from % 3600) / 60,
|
||||||
)}m -
|
)}m -
|
||||||
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
|
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
|
||||||
(filters.duration.to % 3600) / 60,
|
(filters.duration.to % 3600) / 60,
|
||||||
)}m
|
)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.lessThan}
|
{#if filters.duration.lessThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
Duration less than {Math.floor(
|
Duration less than {Math.floor(
|
||||||
filters.duration.lessThan / 3600,
|
filters.duration.lessThan / 3600,
|
||||||
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.moreThan}
|
{#if filters.duration.moreThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
Duration more than {Math.floor(
|
Duration more than {Math.floor(
|
||||||
filters.duration.moreThan / 3600,
|
filters.duration.moreThan / 3600,
|
||||||
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.tags.length != 0}
|
{#if filters.tags.length != 0}
|
||||||
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
|
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
|
||||||
{#each filters.tags as tagId}
|
{#each filters.tags as tagId}
|
||||||
{#key tagId}
|
{#key tagId}
|
||||||
<Tag id={tagId} clickable={false} />
|
<Tag id={tagId} clickable={false} />
|
||||||
{/key}
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||||
{#if isNodesModified}
|
{#if isNodesModified}
|
||||||
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
||||||
{/if}
|
{/if}
|
||||||
{#if isNodesModified && isHwthreadsModified},
|
{#if isNodesModified && isHwthreadsModified},
|
||||||
{/if}
|
{/if}
|
||||||
{#if isHwthreadsModified}
|
{#if isHwthreadsModified}
|
||||||
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
||||||
{/if}
|
{/if}
|
||||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
||||||
{/if}
|
{/if}
|
||||||
{#if isAccsModified}
|
{#if isAccsModified}
|
||||||
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
|
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
|
||||||
{/if}
|
{/if}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.node != null}
|
{#if filters.node != null}
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||||
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
|
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.energy.from || filters.energy.to}
|
{#if filters.energy.from || filters.energy.to}
|
||||||
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
|
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
|
||||||
Total Energy: {filters.energy.from} - {filters.energy.to}
|
Total Energy: {filters.energy.from} - {filters.energy.to}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.stats.length > 0}
|
{#if filters.stats.length > 0}
|
||||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
||||||
{filters.stats
|
{filters.stats
|
||||||
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
|
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</Info>
|
</Info>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Cluster
|
<Cluster
|
||||||
|
394
web/frontend/src/generic/JobCompare.svelte
Normal file
394
web/frontend/src/generic/JobCompare.svelte
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
<!--
|
||||||
|
@component jobCompare component; compares jobs according to set filters or job selection
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `matchedJobs Number?`: Number of matched jobs for selected filters [Default: 0]
|
||||||
|
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
|
||||||
|
- `showFootprint Bool`: If to display the jobFootprint component
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import uPlot from "uplot";
|
||||||
|
import {
|
||||||
|
queryStore,
|
||||||
|
gql,
|
||||||
|
getContextClient,
|
||||||
|
// mutationStore,
|
||||||
|
} from "@urql/svelte";
|
||||||
|
import { Row, Col, Card, Spinner, Table, Input, InputGroup, InputGroupText, Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
import { formatTime, roundTwoDigits } from "./units.js";
|
||||||
|
import Comparogram from "./plots/Comparogram.svelte";
|
||||||
|
|
||||||
|
const ccconfig = getContext("cc-config"),
|
||||||
|
// initialized = getContext("initialized"),
|
||||||
|
globalMetrics = getContext("globalMetrics");
|
||||||
|
|
||||||
|
export let matchedCompareJobs = 0;
|
||||||
|
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||||
|
export let filterBuffer = [];
|
||||||
|
|
||||||
|
let filter = [...filterBuffer] || [];
|
||||||
|
let comparePlotData = {};
|
||||||
|
let compareTableData = [];
|
||||||
|
let compareTableSorting = {};
|
||||||
|
let jobIds = [];
|
||||||
|
let jobClusters = [];
|
||||||
|
let tableJobIDFilter = "";
|
||||||
|
|
||||||
|
/*uPlot*/
|
||||||
|
let plotSync = uPlot.sync("compareJobsView");
|
||||||
|
|
||||||
|
/* GQL */
|
||||||
|
const client = getContextClient();
|
||||||
|
// Pull All Series For Metrics Statistics Only On Node Scope
|
||||||
|
const compareQuery = gql`
|
||||||
|
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||||
|
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||||
|
id
|
||||||
|
jobId
|
||||||
|
startTime
|
||||||
|
duration
|
||||||
|
cluster
|
||||||
|
subCluster
|
||||||
|
numNodes
|
||||||
|
numHWThreads
|
||||||
|
numAccelerators
|
||||||
|
stats {
|
||||||
|
name
|
||||||
|
data {
|
||||||
|
min
|
||||||
|
avg
|
||||||
|
max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/* REACTIVES */
|
||||||
|
|
||||||
|
$: compareData = queryStore({
|
||||||
|
client: client,
|
||||||
|
query: compareQuery,
|
||||||
|
variables:{ filter, metrics },
|
||||||
|
});
|
||||||
|
|
||||||
|
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
||||||
|
|
||||||
|
$: if ($compareData.data != null) {
|
||||||
|
jobIds = [];
|
||||||
|
jobClusters = [];
|
||||||
|
comparePlotData = {};
|
||||||
|
compareTableData = [...$compareData.data.jobsMetricStats];
|
||||||
|
jobs2uplot($compareData.data.jobsMetricStats, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ((!$compareData.fetching && !$compareData.error) && metrics) {
|
||||||
|
// Meta
|
||||||
|
compareTableSorting['meta'] = {
|
||||||
|
startTime: { dir: "down", active: true },
|
||||||
|
duration: { dir: "up", active: false },
|
||||||
|
cluster: { dir: "up", active: false },
|
||||||
|
};
|
||||||
|
// Resources
|
||||||
|
compareTableSorting['resources'] = {
|
||||||
|
Nodes: { dir: "up", active: false },
|
||||||
|
Threads: { dir: "up", active: false },
|
||||||
|
Accs: { dir: "up", active: false },
|
||||||
|
};
|
||||||
|
// Metrics
|
||||||
|
for (let metric of metrics) {
|
||||||
|
compareTableSorting[metric] = {
|
||||||
|
min: { dir: "up", active: false },
|
||||||
|
avg: { dir: "up", active: false },
|
||||||
|
max: { dir: "up", active: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FUNCTIONS */
|
||||||
|
// (Re-)query and optionally set new filters; Query will be started reactively.
|
||||||
|
export function queryJobs(filters) {
|
||||||
|
if (filters != null) {
|
||||||
|
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
|
||||||
|
if (minRunningFor && minRunningFor > 0) {
|
||||||
|
filters.push({ minRunningFor });
|
||||||
|
}
|
||||||
|
filter = filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBy(key, field) {
|
||||||
|
let s = compareTableSorting[key][field];
|
||||||
|
if (s.active) {
|
||||||
|
s.dir = s.dir == "up" ? "down" : "up";
|
||||||
|
} else {
|
||||||
|
for (let key in compareTableSorting)
|
||||||
|
for (let field in compareTableSorting[key]) compareTableSorting[key][field].active = false;
|
||||||
|
s.active = true;
|
||||||
|
}
|
||||||
|
compareTableSorting = { ...compareTableSorting };
|
||||||
|
|
||||||
|
if (key == 'resources') {
|
||||||
|
let longField = "";
|
||||||
|
switch (field) {
|
||||||
|
case "Nodes":
|
||||||
|
longField = "numNodes"
|
||||||
|
break
|
||||||
|
case "Threads":
|
||||||
|
longField = "numHWThreads"
|
||||||
|
break
|
||||||
|
case "Accs":
|
||||||
|
longField = "numAccelerators"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log("Unknown Res Field", field)
|
||||||
|
}
|
||||||
|
compareTableData = compareTableData.sort((j1, j2) => {
|
||||||
|
if (j1[longField] == null || j2[longField] == null) return -1;
|
||||||
|
return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField];
|
||||||
|
});
|
||||||
|
} else if (key == 'meta') {
|
||||||
|
compareTableData = compareTableData.sort((j1, j2) => {
|
||||||
|
if (j1[field] == null || j2[field] == null) return -1;
|
||||||
|
if (field == 'cluster') {
|
||||||
|
let c1 = `${j1.cluster} (${j1.subCluster})`
|
||||||
|
let c2 = `${j2.cluster} (${j2.subCluster})`
|
||||||
|
return s.dir != "up" ? c1.localeCompare(c2) : c2.localeCompare(c1)
|
||||||
|
} else {
|
||||||
|
return s.dir != "up" ? j1[field] - j2[field] : j2[field] - j1[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
compareTableData = compareTableData.sort((j1, j2) => {
|
||||||
|
let s1 = j1.stats.find((m) => m.name == key)?.data;
|
||||||
|
let s2 = j2.stats.find((m) => m.name == key)?.data;
|
||||||
|
if (s1 == null || s2 == null) return -1;
|
||||||
|
return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobs2uplot(jobs, metrics) {
|
||||||
|
// Resources Init
|
||||||
|
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
|
||||||
|
// Metric Init
|
||||||
|
for (let m of metrics) {
|
||||||
|
// Get Unit
|
||||||
|
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
||||||
|
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
||||||
|
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate jobs if exists
|
||||||
|
if (jobs) {
|
||||||
|
let plotIndex = 0
|
||||||
|
jobs.forEach((j) => {
|
||||||
|
// Collect JobIDs & Clusters for X-Ticks and Legend
|
||||||
|
jobIds.push(j.jobId)
|
||||||
|
jobClusters.push(`${j.cluster} ${j.subCluster}`)
|
||||||
|
// Resources
|
||||||
|
comparePlotData['resources'].data[0].push(plotIndex)
|
||||||
|
comparePlotData['resources'].data[1].push(j.startTime)
|
||||||
|
comparePlotData['resources'].data[2].push(j.duration)
|
||||||
|
comparePlotData['resources'].data[3].push(j.numNodes)
|
||||||
|
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
||||||
|
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
||||||
|
// Metrics
|
||||||
|
for (let s of j.stats) {
|
||||||
|
comparePlotData[s.name].data[0].push(plotIndex)
|
||||||
|
comparePlotData[s.name].data[1].push(j.startTime)
|
||||||
|
comparePlotData[s.name].data[2].push(j.duration)
|
||||||
|
comparePlotData[s.name].data[3].push(s.data.min)
|
||||||
|
comparePlotData[s.name].data[4].push(s.data.avg)
|
||||||
|
comparePlotData[s.name].data[5].push(s.data.max)
|
||||||
|
}
|
||||||
|
plotIndex++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapt for Persisting Job Selections in DB later down the line
|
||||||
|
// const updateConfigurationMutation = ({ name, value }) => {
|
||||||
|
// return mutationStore({
|
||||||
|
// client: client,
|
||||||
|
// query: gql`
|
||||||
|
// mutation ($name: String!, $value: String!) {
|
||||||
|
// updateConfiguration(name: $name, value: $value)
|
||||||
|
// }
|
||||||
|
// `,
|
||||||
|
// variables: { name, value },
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// function updateConfiguration(value, page) {
|
||||||
|
// updateConfigurationMutation({
|
||||||
|
// name: "plot_list_jobsPerPage",
|
||||||
|
// value: value,
|
||||||
|
// }).subscribe((res) => {
|
||||||
|
// if (res.fetching === false && !res.error) {
|
||||||
|
// jobs = [] // Empty List
|
||||||
|
// paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
||||||
|
// } else if (res.fetching === false && res.error) {
|
||||||
|
// throw res.error;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $compareData.fetching}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Spinner secondary />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else if $compareData.error}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Card body color="danger" class="mb-3"
|
||||||
|
><h2>{$compareData.error.message}</h2></Card
|
||||||
|
>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
{#key comparePlotData}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Comparogram
|
||||||
|
title={'Compare Resources'}
|
||||||
|
xlabel="JobIDs"
|
||||||
|
xticks={jobIds}
|
||||||
|
xinfo={jobClusters}
|
||||||
|
ylabel={'Resource Counts'}
|
||||||
|
data={comparePlotData['resources'].data}
|
||||||
|
{plotSync}
|
||||||
|
forResources
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{#each metrics as m}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Comparogram
|
||||||
|
title={`Compare Metric '${m}'`}
|
||||||
|
xlabel="JobIDs"
|
||||||
|
xticks={jobIds}
|
||||||
|
xinfo={jobClusters}
|
||||||
|
ylabel={m}
|
||||||
|
metric={m}
|
||||||
|
yunit={comparePlotData[m].unit}
|
||||||
|
data={comparePlotData[m].data}
|
||||||
|
{plotSync}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
|
<hr/>
|
||||||
|
<Card>
|
||||||
|
<Table hover>
|
||||||
|
<thead>
|
||||||
|
<!-- Header Row 1 -->
|
||||||
|
<tr>
|
||||||
|
<th style="width:8%; max-width:10%;">JobID</th>
|
||||||
|
<th>StartTime</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Cluster</th>
|
||||||
|
<th colspan="3">Resources</th>
|
||||||
|
{#each metrics as metric}
|
||||||
|
<th colspan="3">{metric} {comparePlotData[metric]?.unit? `(${comparePlotData[metric]?.unit})` : ''}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
<!-- Header Row 2: Fields -->
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="text" bind:value={tableJobIDFilter}/>
|
||||||
|
<InputGroupText>
|
||||||
|
<Icon name="search"></Icon>
|
||||||
|
</InputGroupText>
|
||||||
|
</InputGroup>
|
||||||
|
</th>
|
||||||
|
<th on:click={() => sortBy('meta', 'startTime')}>
|
||||||
|
Sort
|
||||||
|
<Icon
|
||||||
|
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th on:click={() => sortBy('meta', 'duration')}>
|
||||||
|
Sort
|
||||||
|
<Icon
|
||||||
|
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th on:click={() => sortBy('meta', 'cluster')}>
|
||||||
|
Sort
|
||||||
|
<Icon
|
||||||
|
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{#each ["Nodes", "Threads", "Accs"] as res}
|
||||||
|
<th on:click={() => sortBy('resources', res)}>
|
||||||
|
{res}
|
||||||
|
<Icon
|
||||||
|
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
{#each metrics as metric}
|
||||||
|
{#each ["min", "avg", "max"] as stat}
|
||||||
|
<th on:click={() => sortBy(metric, stat)}>
|
||||||
|
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
||||||
|
<Icon
|
||||||
|
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each compareTableData.filter((j) => j.jobId.includes(tableJobIDFilter)) as job (job.id)}
|
||||||
|
<tr>
|
||||||
|
<td><b><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a></b></td>
|
||||||
|
<td>{new Date(job.startTime * 1000).toLocaleString()}</td>
|
||||||
|
<td>{formatTime(job.duration)}</td>
|
||||||
|
<td>{job.cluster} ({job.subCluster})</td>
|
||||||
|
<td>{job.numNodes}</td>
|
||||||
|
<td>{job.numHWThreads}</td>
|
||||||
|
<td>{job.numAccelerators}</td>
|
||||||
|
{#each metrics as metric}
|
||||||
|
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.min)}</td>
|
||||||
|
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.avg)}</td>
|
||||||
|
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.max)}</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan={7 + (metrics.length * 3)}><b>No jobs found.</b></td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
@ -35,15 +35,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export let sorting = { field: "startTime", type: "col", order: "DESC" };
|
export let sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||||
export let matchedJobs = 0;
|
export let matchedListJobs = 0;
|
||||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||||
export let showFootprint;
|
export let showFootprint;
|
||||||
|
export let filterBuffer = [];
|
||||||
|
export let selectedJobs = [];
|
||||||
|
|
||||||
let usePaging = ccconfig.job_list_usePaging
|
let usePaging = ccconfig.job_list_usePaging
|
||||||
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
|
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let paging = { itemsPerPage, page };
|
let paging = { itemsPerPage, page };
|
||||||
let filter = [];
|
let filter = [...filterBuffer];
|
||||||
let lastFilter = [];
|
let lastFilter = [];
|
||||||
let lastSorting = null;
|
let lastSorting = null;
|
||||||
let triggerMetricRefresh = false;
|
let triggerMetricRefresh = false;
|
||||||
@ -141,7 +143,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
||||||
|
|
||||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||||
export function refreshJobs() {
|
export function refreshJobs() {
|
||||||
@ -284,7 +286,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
{#each jobs as job (job)}
|
{#each jobs as job (job)}
|
||||||
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
|
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
||||||
|
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
|
||||||
|
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={metrics.length + 1}> No jobs found </td>
|
<td colspan={metrics.length + 1}> No jobs found </td>
|
||||||
@ -310,7 +315,7 @@
|
|||||||
bind:page
|
bind:page
|
||||||
{itemsPerPage}
|
{itemsPerPage}
|
||||||
itemText="Jobs"
|
itemText="Jobs"
|
||||||
totalItems={matchedJobs}
|
totalItems={matchedListJobs}
|
||||||
on:update-paging={({ detail }) => {
|
on:update-paging={({ detail }) => {
|
||||||
if (detail.itemsPerPage != itemsPerPage) {
|
if (detail.itemsPerPage != itemsPerPage) {
|
||||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
export let username = null;
|
export let username = null;
|
||||||
export let authlevel= null;
|
export let authlevel= null;
|
||||||
export let roles = null;
|
export let roles = null;
|
||||||
|
export let isSelected = null;
|
||||||
|
export let showSelect = false;
|
||||||
|
|
||||||
function formatDuration(duration) {
|
function formatDuration(duration) {
|
||||||
const hours = Math.floor(duration / 3600);
|
const hours = Math.floor(duration / 3600);
|
||||||
@ -76,18 +78,39 @@
|
|||||||
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||||
({job.cluster})
|
({job.cluster})
|
||||||
</span>
|
</span>
|
||||||
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} >
|
<span>
|
||||||
{#if displayCheck}
|
{#if showSelect}
|
||||||
<Icon name="clipboard2-check-fill"/>
|
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
|
||||||
{:else}
|
on:click={() => {
|
||||||
<Icon name="clipboard2"/>
|
isSelected = !isSelected
|
||||||
|
}}>
|
||||||
|
{#if isSelected}
|
||||||
|
<Icon name="check-square"/>
|
||||||
|
{:else if isSelected == false}
|
||||||
|
<Icon name="square"/>
|
||||||
|
{:else}
|
||||||
|
<Icon name="plus-square-dotted" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Tooltip
|
||||||
|
target={`${job.cluster}-${job.jobId}-select`}
|
||||||
|
placement="left">
|
||||||
|
{ 'Add or Remove Job to/from Comparison Selection' }
|
||||||
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} >
|
||||||
<Tooltip
|
{#if displayCheck}
|
||||||
target={`${job.cluster}-${job.jobId}-clipboard`}
|
<Icon name="clipboard2-check-fill"/>
|
||||||
placement="right">
|
{:else}
|
||||||
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
|
<Icon name="clipboard2"/>
|
||||||
</Tooltip>
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Tooltip
|
||||||
|
target={`${job.cluster}-${job.jobId}-clipboard`}
|
||||||
|
placement="right">
|
||||||
|
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{#if job.metaData?.jobName}
|
{#if job.metaData?.jobName}
|
||||||
{#if job.metaData?.jobName.length <= 25}
|
{#if job.metaData?.jobName.length <= 25}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext, createEventDispatcher } from "svelte";
|
||||||
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import { maxScope, checkMetricDisabled } from "../utils.js";
|
import { maxScope, checkMetricDisabled } from "../utils.js";
|
||||||
import JobInfo from "./JobInfo.svelte";
|
import JobInfo from "./JobInfo.svelte";
|
||||||
@ -25,7 +25,9 @@
|
|||||||
export let plotHeight = 275;
|
export let plotHeight = 275;
|
||||||
export let showFootprint;
|
export let showFootprint;
|
||||||
export let triggerMetricRefresh = false;
|
export let triggerMetricRefresh = false;
|
||||||
|
export let previousSelect = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
const resampleConfig = getContext("resampling") || null;
|
const resampleConfig = getContext("resampling") || null;
|
||||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||||
|
|
||||||
@ -39,6 +41,8 @@
|
|||||||
let zoomStates = {};
|
let zoomStates = {};
|
||||||
let thresholdStates = {};
|
let thresholdStates = {};
|
||||||
|
|
||||||
|
$: isSelected = previousSelect || null;
|
||||||
|
|
||||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const query = gql`
|
const query = gql`
|
||||||
@ -112,6 +116,12 @@
|
|||||||
refreshMetrics();
|
refreshMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (isSelected == true && previousSelect == false) {
|
||||||
|
dispatch("select-job", job.id)
|
||||||
|
} else if (isSelected == false && previousSelect == true) {
|
||||||
|
dispatch("unselect-job", job.id)
|
||||||
|
}
|
||||||
|
|
||||||
// Helper
|
// Helper
|
||||||
const selectScope = (jobMetrics) =>
|
const selectScope = (jobMetrics) =>
|
||||||
jobMetrics.reduce(
|
jobMetrics.reduce(
|
||||||
@ -152,7 +162,7 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<JobInfo {job} />
|
<JobInfo {job} bind:isSelected showSelect/>
|
||||||
</td>
|
</td>
|
||||||
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
||||||
<td colspan={metrics.length}>
|
<td colspan={metrics.length}>
|
||||||
|
314
web/frontend/src/generic/plots/Comparogram.svelte
Normal file
314
web/frontend/src/generic/plots/Comparogram.svelte
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<!--
|
||||||
|
@component Main plot component, based on uPlot; metricdata values by time
|
||||||
|
|
||||||
|
Only width/height should change reactively.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `metric String`: The metric name
|
||||||
|
- `width Number?`: The plot width [Default: 0]
|
||||||
|
- `height Number?`: The plot height [Default: 300]
|
||||||
|
- `data [Array]`: The metric data object
|
||||||
|
- `cluster String`: Cluster name of the parent job / data
|
||||||
|
- `subCluster String`: Name of the subCluster of the parent job
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import uPlot from "uplot";
|
||||||
|
import { roundTwoDigits, formatTime, formatNumber } from "../units.js";
|
||||||
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
export let metric = "";
|
||||||
|
export let width = 0;
|
||||||
|
export let height = 300;
|
||||||
|
export let data = null;
|
||||||
|
export let xlabel = "";
|
||||||
|
export let xticks = [];
|
||||||
|
export let xinfo = [];
|
||||||
|
export let ylabel = "";
|
||||||
|
export let yunit = "";
|
||||||
|
export let title = "";
|
||||||
|
export let forResources = false;
|
||||||
|
export let plotSync;
|
||||||
|
|
||||||
|
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
||||||
|
|
||||||
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||||
|
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||||
|
|
||||||
|
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
||||||
|
function legendAsTooltipPlugin({
|
||||||
|
className,
|
||||||
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
|
} = {}) {
|
||||||
|
let legendEl;
|
||||||
|
|
||||||
|
function init(u, opts) {
|
||||||
|
legendEl = u.root.querySelector(".u-legend");
|
||||||
|
|
||||||
|
legendEl.classList.remove("u-inline");
|
||||||
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
|
uPlot.assign(legendEl.style, {
|
||||||
|
minWidth: "100px",
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "none",
|
||||||
|
display: "none",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
|
...style,
|
||||||
|
});
|
||||||
|
|
||||||
|
// hide series color markers:
|
||||||
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
|
for (let i = 0; i < idents.length; i++)
|
||||||
|
idents[i].style.display = "none";
|
||||||
|
|
||||||
|
const overEl = u.over;
|
||||||
|
overEl.style.overflow = "visible";
|
||||||
|
|
||||||
|
// move legend into plot bounds
|
||||||
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
|
// show/hide tooltip on enter/exit
|
||||||
|
overEl.addEventListener("mouseenter", () => {
|
||||||
|
legendEl.style.display = null;
|
||||||
|
});
|
||||||
|
overEl.addEventListener("mouseleave", () => {
|
||||||
|
legendEl.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(u) {
|
||||||
|
const { left, top } = u.cursor;
|
||||||
|
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
|
||||||
|
legendEl.style.transform =
|
||||||
|
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
init: init,
|
||||||
|
setCursor: update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const plotSeries = [
|
||||||
|
{
|
||||||
|
label: "JobID",
|
||||||
|
scale: "x",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return `${xticks[didx]} | ${xinfo[didx]}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Starttime",
|
||||||
|
scale: "xst",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return new Date(ts * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Duration",
|
||||||
|
scale: "xrt",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return formatTime(ts);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (forResources) {
|
||||||
|
const resSeries = [
|
||||||
|
{
|
||||||
|
label: "Nodes",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "black",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Threads",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "rgb(0,0,255)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Accelerators",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: cbmode ? "rgb(0,255,0)" : "red",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
plotSeries.push(...resSeries)
|
||||||
|
} else {
|
||||||
|
const statsSeries = [
|
||||||
|
{
|
||||||
|
label: "Min",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: cbmode ? "rgb(0,255,0)" : "red",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return `${roundTwoDigits(ts)} ${yunit}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Avg",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "black",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return `${roundTwoDigits(ts)} ${yunit}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Max",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: cbmode ? "rgb(0,0,255)" : "green",
|
||||||
|
value: (u, ts, sidx, didx) => {
|
||||||
|
return `${roundTwoDigits(ts)} ${yunit}`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
plotSeries.push(...statsSeries)
|
||||||
|
};
|
||||||
|
|
||||||
|
const plotBands = [
|
||||||
|
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
||||||
|
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
|
plugins: [legendAsTooltipPlugin()],
|
||||||
|
series: plotSeries,
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
scale: "x",
|
||||||
|
space: 25, // Tick Spacing
|
||||||
|
rotate: 30,
|
||||||
|
show: true,
|
||||||
|
label: xlabel,
|
||||||
|
values(self, splits) {
|
||||||
|
return splits.map(s => xticks[s]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: "xst",
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: "xrt",
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: "y",
|
||||||
|
grid: { show: true },
|
||||||
|
labelFont: "sans-serif",
|
||||||
|
label: ylabel + (yunit ? ` (${yunit})` : ''),
|
||||||
|
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bands: forResources ? [] : plotBands,
|
||||||
|
padding: [5, 10, 0, 0],
|
||||||
|
hooks: {
|
||||||
|
draw: [
|
||||||
|
(u) => {
|
||||||
|
// Draw plot type label:
|
||||||
|
let textl = forResources ? "Job Resources by Type" : "Metric Min/Avg/Max for Job Duration";
|
||||||
|
let textr = "Earlier <- StartTime -> Later";
|
||||||
|
u.ctx.save();
|
||||||
|
u.ctx.textAlign = "start";
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
|
||||||
|
u.ctx.textAlign = "end";
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(
|
||||||
|
textr,
|
||||||
|
u.bbox.left + u.bbox.width - 10,
|
||||||
|
u.bbox.top + 10,
|
||||||
|
);
|
||||||
|
u.ctx.restore();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { time: false },
|
||||||
|
xst: { time: false },
|
||||||
|
xrt: { time: false },
|
||||||
|
y: {auto: true, distr: forResources ? 3 : 1},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
// Display legend
|
||||||
|
show: true,
|
||||||
|
live: true,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
drag: { x: true, y: true },
|
||||||
|
sync: {
|
||||||
|
key: plotSync.key,
|
||||||
|
scales: ["x", null],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// RENDER HANDLING
|
||||||
|
let plotWrapper = null;
|
||||||
|
let uplot = null;
|
||||||
|
let timeoutId = null;
|
||||||
|
|
||||||
|
function render(ren_width, ren_height) {
|
||||||
|
if (!uplot) {
|
||||||
|
opts.width = ren_width;
|
||||||
|
opts.height = ren_height;
|
||||||
|
uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Ymin][Yavg][Ymax]]
|
||||||
|
plotSync.sub(uplot)
|
||||||
|
} else {
|
||||||
|
uplot.setSize({ width: ren_width, height: ren_height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSizeChange(chg_width, chg_height) {
|
||||||
|
if (!uplot) return;
|
||||||
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
timeoutId = null;
|
||||||
|
render(chg_width, chg_height);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (plotWrapper) {
|
||||||
|
render(width, height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
if (uplot) uplot.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// This updates plot on all size changes if wrapper (== data) exists
|
||||||
|
$: if (plotWrapper) {
|
||||||
|
onSizeChange(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Define $width Wrapper and NoData Card -->
|
||||||
|
{#if data && data[0].length > 0}
|
||||||
|
<div bind:this={plotWrapper} bind:clientWidth={width}
|
||||||
|
style="background-color: rgba(255, 255, 255, 1.0);" class="rounded"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Card body color="warning" class="mx-4 my-2"
|
||||||
|
>Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card
|
||||||
|
>
|
||||||
|
{/if}
|
@ -16,7 +16,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { formatNumber } from "../units.js";
|
import { formatNumber, formatTime } from "../units.js";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
@ -36,21 +36,6 @@
|
|||||||
points: 2,
|
points: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTime(t) {
|
|
||||||
if (t !== null) {
|
|
||||||
if (isNaN(t)) {
|
|
||||||
return t;
|
|
||||||
} else {
|
|
||||||
const tAbs = Math.abs(t);
|
|
||||||
const h = Math.floor(tAbs / 3600);
|
|
||||||
const m = Math.floor((tAbs % 3600) / 60);
|
|
||||||
if (h == 0) return `${m}m`;
|
|
||||||
else if (m == 0) return `${h}h`;
|
|
||||||
else return `${h}:${m}h`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||||
let s = u.series[seriesIdx];
|
let s = u.series[seriesIdx];
|
||||||
let style = s.drawStyle;
|
let style = s.drawStyle;
|
||||||
|
@ -21,22 +21,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script context="module">
|
<script context="module">
|
||||||
function formatTime(t, forNode = false) {
|
|
||||||
if (t !== null) {
|
|
||||||
if (isNaN(t)) {
|
|
||||||
return t;
|
|
||||||
} else {
|
|
||||||
const tAbs = Math.abs(t);
|
|
||||||
const h = Math.floor(tAbs / 3600);
|
|
||||||
const m = Math.floor((tAbs % 3600) / 60);
|
|
||||||
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
|
||||||
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
|
|
||||||
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
|
|
||||||
else return `${forNode ? "-" : ""}${h}:${m}h`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeIncrs(timestep, maxX, forNode) {
|
function timeIncrs(timestep, maxX, forNode) {
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||||
@ -118,7 +102,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { formatNumber } from "../units.js";
|
import { formatNumber, formatTime } from "../units.js";
|
||||||
import { getContext, onMount, onDestroy, createEventDispatcher } from "svelte";
|
import { getContext, onMount, onDestroy, createEventDispatcher } from "svelte";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
const getValues = (type) => labels.map(name => {
|
const getValues = (type) => labels.map(name => {
|
||||||
// Peak is adapted and scaled for job shared state
|
// Peak is adapted and scaled for job shared state
|
||||||
const peak = polarMetrics.find(m => m?.name == name)?.peak
|
const peak = polarMetrics.find(m => m?.name == name)?.peak
|
||||||
const metric = polarData.find(m => m?.name == name)?.stats
|
const metric = polarData.find(m => m?.name == name)?.data
|
||||||
const value = (peak && metric) ? (metric[type] / peak) : 0
|
const value = (peak && metric) ? (metric[type] / peak) : 0
|
||||||
return value <= 1. ? value : 1.
|
return value <= 1. ? value : 1.
|
||||||
})
|
})
|
||||||
|
@ -17,6 +17,10 @@ export function formatNumber(x) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function roundTwoDigits(x) {
|
||||||
|
return Math.round(x * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
export function scaleNumbers(x, y , p = '') {
|
export function scaleNumbers(x, y , p = '') {
|
||||||
const oldPower = power[prefix.indexOf(p)]
|
const oldPower = power[prefix.indexOf(p)]
|
||||||
const rawXValue = x * oldPower
|
const rawXValue = x * oldPower
|
||||||
@ -31,4 +35,20 @@ export function scaleNumbers(x, y , p = '') {
|
|||||||
return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}`
|
return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTime(t, forNode = false) {
|
||||||
|
if (t !== null) {
|
||||||
|
if (isNaN(t)) {
|
||||||
|
return t;
|
||||||
|
} else {
|
||||||
|
const tAbs = Math.abs(t);
|
||||||
|
const h = Math.floor(tAbs / 3600);
|
||||||
|
const m = Math.floor((tAbs % 3600) / 60);
|
||||||
|
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
||||||
|
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
|
||||||
|
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
|
||||||
|
else return `${forNode ? "-" : ""}${h}:${m}h`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// export const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
// export const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||||
|
@ -461,11 +461,11 @@ export function convert2uplot(canvasData, secondsToMinutes = false, secondsToHou
|
|||||||
} else { // Default -> Fill Histodata with zero values on unused value placing -> maybe allows zoom trigger as known
|
} else { // Default -> Fill Histodata with zero values on unused value placing -> maybe allows zoom trigger as known
|
||||||
if (secondsToHours) {
|
if (secondsToHours) {
|
||||||
let hours = cd.value / 3600
|
let hours = cd.value / 3600
|
||||||
console.log("x seconds to y hours", cd.value, hours)
|
// console.log("x seconds to y hours", cd.value, hours)
|
||||||
uplotData[0].push(hours)
|
uplotData[0].push(hours)
|
||||||
} else if (secondsToMinutes) {
|
} else if (secondsToMinutes) {
|
||||||
let minutes = cd.value / 60
|
let minutes = cd.value / 60
|
||||||
console.log("x seconds to y minutes", cd.value, minutes)
|
// console.log("x seconds to y minutes", cd.value, minutes)
|
||||||
uplotData[0].push(minutes)
|
uplotData[0].push(minutes)
|
||||||
} else {
|
} else {
|
||||||
uplotData[0].push(cd.value)
|
uplotData[0].push(cd.value)
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
||||||
jobStats(id: $dbid, metrics: $selectedMetrics) {
|
jobStats(id: $dbid, metrics: $selectedMetrics) {
|
||||||
name
|
name
|
||||||
stats {
|
data {
|
||||||
min
|
min
|
||||||
avg
|
avg
|
||||||
max
|
max
|
||||||
|
Loading…
x
Reference in New Issue
Block a user