diff --git a/api/schema.graphqls b/api/schema.graphqls index 60b4446..3165177 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -236,7 +236,7 @@ type Query { jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList! - jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: Int, numMetricBins: Int): [JobsStatistics!]! + jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: String, numMetricBins: Int): [JobsStatistics!]! rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]! diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index a9e94ce..dbac551 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -257,7 +257,7 @@ type ComplexityRoot struct { JobMetrics func(childComplexity int, id string, metrics []string, scopes []schema.MetricScope, resolution *int) int Jobs func(childComplexity int, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) int JobsFootprints func(childComplexity int, filter []*model.JobFilter, metrics []string) int - JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *int, numMetricBins *int) int + JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) int NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int RooflineHeatmap func(childComplexity int, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) int Tags func(childComplexity int) int @@ -382,7 +382,7 @@ type QueryResolver interface { JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) - JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *int, numMetricBins *int) ([]*model.JobsStatistics, error) + JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) } @@ -1372,7 +1372,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.JobsStatistics(childComplexity, args["filter"].([]*model.JobFilter), args["metrics"].([]string), args["page"].(*model.PageRequest), args["sortBy"].(*model.SortByAggregate), args["groupBy"].(*model.Aggregate), args["numDurationBins"].(*int), args["numMetricBins"].(*int)), true + return e.complexity.Query.JobsStatistics(childComplexity, args["filter"].([]*model.JobFilter), args["metrics"].([]string), args["page"].(*model.PageRequest), args["sortBy"].(*model.SortByAggregate), args["groupBy"].(*model.Aggregate), args["numDurationBins"].(*string), args["numMetricBins"].(*int)), true case "Query.nodeMetrics": if e.complexity.Query.NodeMetrics == nil { @@ -2132,7 +2132,7 @@ type Query { jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList! - jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: Int, numMetricBins: Int): [JobsStatistics!]! + jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate, numDurationBins: String, numMetricBins: Int): [JobsStatistics!]! rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]! @@ -2985,22 +2985,22 @@ func (ec *executionContext) field_Query_jobsStatistics_argsGroupBy( func (ec *executionContext) field_Query_jobsStatistics_argsNumDurationBins( ctx context.Context, rawArgs map[string]interface{}, -) (*int, error) { +) (*string, error) { // We won't call the directive if the argument is null. // Set call_argument_directives_with_null to true to call directives // even if the argument is null. _, ok := rawArgs["numDurationBins"] if !ok { - var zeroVal *int + var zeroVal *string return zeroVal, nil } ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("numDurationBins")) if tmp, ok := rawArgs["numDurationBins"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) + return ec.unmarshalOString2ᚖstring(ctx, tmp) } - var zeroVal *int + var zeroVal *string return zeroVal, nil } @@ -9829,7 +9829,7 @@ func (ec *executionContext) _Query_jobsStatistics(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().JobsStatistics(rctx, fc.Args["filter"].([]*model.JobFilter), fc.Args["metrics"].([]string), fc.Args["page"].(*model.PageRequest), fc.Args["sortBy"].(*model.SortByAggregate), fc.Args["groupBy"].(*model.Aggregate), fc.Args["numDurationBins"].(*int), fc.Args["numMetricBins"].(*int)) + return ec.resolvers.Query().JobsStatistics(rctx, fc.Args["filter"].([]*model.JobFilter), fc.Args["metrics"].([]string), fc.Args["page"].(*model.PageRequest), fc.Args["sortBy"].(*model.SortByAggregate), fc.Args["groupBy"].(*model.Aggregate), fc.Args["numDurationBins"].(*string), fc.Args["numMetricBins"].(*int)) }) if err != nil { ec.Error(ctx, err) diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 3bab7d1..053bb86 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -357,12 +357,12 @@ func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, pag } // JobsStatistics is the resolver for the jobsStatistics field. -func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *int, numMetricBins *int) ([]*model.JobsStatistics, error) { +func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) { var err error var stats []*model.JobsStatistics // Top Level Defaults - var defaultDurationBins int = 25 + var defaultDurationBins string = "1h" var defaultMetricBins int = 10 if requireField(ctx, "totalJobs") || requireField(ctx, "totalWalltime") || requireField(ctx, "totalNodes") || requireField(ctx, "totalCores") || diff --git a/internal/repository/stats.go b/internal/repository/stats.go index 3015058..ad518bd 100644 --- a/internal/repository/stats.go +++ b/internal/repository/stats.go @@ -446,28 +446,38 @@ func (r *JobRepository) AddHistograms( ctx context.Context, filter []*model.JobFilter, stat *model.JobsStatistics, - targetBinCount *int, + durationBins *string, ) (*model.JobsStatistics, error) { start := time.Now() - // targetBinCount : Frontendargument - // -> Min Bins: 25 -> Min Resolution: By Hour - // -> In Between Bins: 50 -> Resolution by Half Hour - // 100 -> Resolution by Quarter Hour - // 150 -> Resolution by 10 Minutes - // 300 -> Resolution by 5 Minutes - // 750 -> Resolution by 2 Minutes - // -> Max Bins: 1500 -> Max Resolution: By Minute - - binSizeSeconds := (90000 / *targetBinCount) - - // Important Note: Fixed to 25h max display range -> Too site specific! Configurable or Extend? -> Start view with "classic" by hour histogram, zoom mostly required for "small" runtimes + var targetBinCount int + var targetBinSize int + switch { + case *durationBins == "1m": // 1 Minute Bins + Max 60 Bins -> Max 60 Minutes + targetBinCount = 60 + targetBinSize = 60 + case *durationBins == "10m": // 10 Minute Bins + Max 72 Bins -> Max 12 Hours + targetBinCount = 72 + targetBinSize = 600 + case *durationBins == "1h": // 1 Hour Bins + Max 48 Bins -> Max 48 Hours + targetBinCount = 48 + targetBinSize = 3600 + case *durationBins == "6h": // 6 Hour Bins + Max 12 Bins -> Max 3 Days + targetBinCount = 12 + targetBinSize = 21600 + case *durationBins == "12h": // 12 hour Bins + Max 14 Bins -> Max 7 Days + targetBinCount = 14 + targetBinSize = 43200 + default: // 24h + targetBinCount = 24 + targetBinSize = 3600 + } castType := r.getCastType() var err error // Return X-Values always as seconds, will be formatted into minutes and hours in frontend - value := fmt.Sprintf(`CAST(ROUND(((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) / %d) + 1) as %s) as value`, time.Now().Unix(), binSizeSeconds, castType) - stat.HistDuration, err = r.jobsDurationStatisticsHistogram(ctx, value, filter, binSizeSeconds, targetBinCount) + value := fmt.Sprintf(`CAST(ROUND(((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) / %d) + 1) as %s) as value`, time.Now().Unix(), targetBinSize, castType) + stat.HistDuration, err = r.jobsDurationStatisticsHistogram(ctx, value, filter, targetBinSize, &targetBinCount) if err != nil { log.Warn("Error while loading job statistics histogram: job duration") return nil, err diff --git a/web/frontend/src/User.root.svelte b/web/frontend/src/User.root.svelte index 853a89b..57720b8 100644 --- a/web/frontend/src/User.root.svelte +++ b/web/frontend/src/User.root.svelte @@ -17,6 +17,9 @@ Icon, Card, Spinner, + Input, + InputGroup, + InputGroupText } from "@sveltestrap/sveltestrap"; import { queryStore, @@ -59,9 +62,11 @@ let showFootprint = filterPresets.cluster ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`] : !!ccconfig.plot_list_showFootprint; - let numDurationBins; - let numMetricBins; - + + let numDurationBins = "1h"; + let numMetricBins = 10; + let durationBinOptions = ["1m","10m","1h","6h","12h"]; + let metricBinOptions = [10, 20, 50, 100]; $: metricsInHistograms = selectedCluster ? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || [] @@ -71,7 +76,7 @@ $: stats = queryStore({ client: client, query: gql` - query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!], $numDurationBins: Int, $numMetricBins: Int) { + query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) { jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) { totalJobs shortJobs @@ -102,38 +107,6 @@ variables: { jobFilters, metricsInHistograms, numDurationBins, numMetricBins }, }); - let durationZoomState = null; - let metricZoomState = null; - let pendingDurationBinCount = null; - let pendingMetricBinCount = null; - let pendingZoomState = null; - function handleZoom(detail) { - if ( // States have to differ, causes deathloop if just set - (pendingZoomState?.x?.min !== detail?.lastZoomState?.x?.min) && - (pendingZoomState?.y?.max !== detail?.lastZoomState?.y?.max) - ) { - pendingZoomState = {...detail.lastZoomState}; - } - - if (detail?.durationBinCount) { // Triggers GQL - pendingDurationBinCount = detail.durationBinCount; - } - - if (detail?.metricBinCount) { // Triggers GQL - pendingMetricBinCount = detail.metricBinCount; - } - }; - - $: if (pendingDurationBinCount !== numDurationBins) { - durationZoomState = {...pendingZoomState}; - numDurationBins = pendingDurationBinCount; - } - - $: if (pendingMetricBinCount !== numMetricBins) { - metricZoomState = {...pendingZoomState}; - numMetricBins = pendingMetricBinCount; - } - onMount(() => filterComponent.updateFilters()); @@ -153,8 +126,8 @@ {/if} - - + + - + - + + + + + + + Duration Bin Size + + + {#each durationBinOptions as dbin} + + {/each} + + + + filterComponent.updateFilters(detail)} /> - + { jobList.refreshJobs() jobList.refreshAllMetrics() @@ -248,16 +236,13 @@ {#key $stats.data.jobsStatistics[0].histDuration} { handleZoom(detail) }} data={convert2uplot($stats.data.jobsStatistics[0].histDuration)} title="Duration Distribution" xlabel="Job Runtimes" xunit="Runtime" ylabel="Number of Jobs" yunit="Jobs" - lastBinCount={pendingDurationBinCount} - {durationZoomState} - zoomableHistogram + usesBins xtime /> {/key} @@ -278,16 +263,32 @@ - - + + + + + + + + + Metric Bins + + + {#each metricBinOptions as mbin} + + {/each} + + + {#if metricsInHistograms?.length > 0} {#if $stats.error} @@ -312,17 +313,13 @@ itemsPerRow={3} > { handleZoom(detail) }} data={convert2uplot(item.data)} - usesBins={true} title="Distribution of '{item.metric} ({item.stat})' footprints" xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`} xunit={item.unit} ylabel="Number of Jobs" yunit="Jobs" - lastBinCount={pendingMetricBinCount} - {metricZoomState} - zoomableHistogram + usesBins /> {/key} diff --git a/web/frontend/src/generic/plots/Histogram.svelte b/web/frontend/src/generic/plots/Histogram.svelte index 8331a9b..fbc5a33 100644 --- a/web/frontend/src/generic/plots/Histogram.svelte +++ b/web/frontend/src/generic/plots/Histogram.svelte @@ -15,7 +15,7 @@