diff --git a/api/schema.graphqls b/api/schema.graphqls index b911d07..ca8ab95 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -173,6 +173,7 @@ type ScopedStats { type JobStats { jobId: Int! startTime: Int! + duration: Int! stats: [NamedStats!]! } diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index 0671d48..1eaf841 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -171,6 +171,7 @@ type ComplexityRoot struct { } JobStats struct { + Duration func(childComplexity int) int JobID func(childComplexity int) int StartTime func(childComplexity int) int Stats func(childComplexity int) int @@ -941,6 +942,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.JobResultList.Offset(childComplexity), true + case "JobStats.duration": + if e.complexity.JobStats.Duration == nil { + break + } + + return e.complexity.JobStats.Duration(childComplexity), true + case "JobStats.jobId": if e.complexity.JobStats.JobID == nil { break @@ -2290,6 +2298,7 @@ type ScopedStats { type JobStats { jobId: Int! startTime: Int! + duration: Int! stats: [NamedStats!]! } @@ -7453,6 +7462,50 @@ func (ec *executionContext) fieldContext_JobStats_startTime(_ context.Context, f return fc, nil } +func (ec *executionContext) _JobStats_duration(ctx context.Context, field graphql.CollectedField, obj *model.JobStats) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_JobStats_duration(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Duration, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_JobStats_duration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "JobStats", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _JobStats_stats(ctx context.Context, field graphql.CollectedField, obj *model.JobStats) (ret graphql.Marshaler) { fc, err := ec.fieldContext_JobStats_stats(ctx, field) if err != nil { @@ -11252,6 +11305,8 @@ func (ec *executionContext) fieldContext_Query_jobsMetricStats(ctx context.Conte return ec.fieldContext_JobStats_jobId(ctx, field) case "startTime": return ec.fieldContext_JobStats_startTime(ctx, field) + case "duration": + return ec.fieldContext_JobStats_duration(ctx, field) case "stats": return ec.fieldContext_JobStats_stats(ctx, field) } @@ -17587,6 +17642,11 @@ func (ec *executionContext) _JobStats(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { out.Invalids++ } + case "duration": + out.Values[i] = ec._JobStats_duration(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "stats": out.Values[i] = ec._JobStats_stats(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 4cadf22..d4486fc 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -99,6 +99,7 @@ type JobResultList struct { type JobStats struct { JobID int `json:"jobId"` StartTime int `json:"startTime"` + Duration int `json:"duration"` Stats []*NamedStats `json:"stats"` } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index a93a67e..771565b 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -618,6 +618,7 @@ func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.Job res = append(res, &model.JobStats{ JobID: int(job.JobID), StartTime: int(job.StartTime.Unix()), + Duration: int(job.Duration), Stats: sres, }) } diff --git a/web/frontend/src/generic/JobCompare.svelte b/web/frontend/src/generic/JobCompare.svelte index 9c1ff94..cd37a07 100644 --- a/web/frontend/src/generic/JobCompare.svelte +++ b/web/frontend/src/generic/JobCompare.svelte @@ -39,7 +39,6 @@ let filter = [...filterBuffer]; let comparePlotData = {}; let jobIds = []; - let jobStarts = []; const sorting = { field: "startTime", type: "col", order: "DESC" }; /* GQL */ @@ -51,6 +50,7 @@ jobsMetricStats(filter: $filter, metrics: $metrics) { jobId startTime + duration stats { name data { @@ -74,7 +74,6 @@ $: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1; $: if ($compareData.data != null) { jobIds = []; - jobStarts = []; comparePlotData = {} jobs2uplot($compareData.data.jobsMetricStats, metrics) } @@ -116,7 +115,7 @@ const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "") // Init - comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[]]} // data: [X, Y1, Y2, Y3] + comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX] } // Iterate jobs if exists @@ -124,13 +123,13 @@ let plotIndex = 0 jobs.forEach((j) => { jobIds.push(j.jobId) - jobStarts.push(j.startTime) for (let s of j.stats) { - // comparePlotData[s.name].data[0].push(j.startTime) comparePlotData[s.name].data[0].push(plotIndex) - comparePlotData[s.name].data[1].push(s.data.min) - comparePlotData[s.name].data[2].push(s.data.avg) - comparePlotData[s.name].data[3].push(s.data.max) + 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++ }) @@ -186,7 +185,6 @@ title={'Compare '+ m} xlabel="JobIds" xticks={jobIds} - xtimes={jobStarts} ylabel={m} metric={m} yunit={comparePlotData[m].unit} diff --git a/web/frontend/src/generic/plots/Comparogram.svelte b/web/frontend/src/generic/plots/Comparogram.svelte index 386434d..da31b5c 100644 --- a/web/frontend/src/generic/plots/Comparogram.svelte +++ b/web/frontend/src/generic/plots/Comparogram.svelte @@ -24,15 +24,15 @@ export let data; export let xlabel; export let xticks; - export let xtimes; export let ylabel; export let yunit; export let title; // export let cluster = ""; // export let subCluster = ""; - // $: console.log('LABEL:', metric, yunit) - // $: console.log('DATA:', data) + $: console.log('LABEL:', metric, yunit) + $: console.log('DATA:', data) + $: console.log('XTICKS:', xticks) const metricConfig = null // DEBUG FILLER // const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric); // Args woher @@ -40,6 +40,22 @@ const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio; const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false; + // Format Seconds to hh:mm + 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`; + } + } + } + // UPLOT PLUGIN // converts the legend into a simple tooltip function legendAsTooltipPlugin({ className, @@ -120,34 +136,48 @@ const plotSeries = [ { label: "JobID", + scale: "x", value: (u, ts, sidx, didx) => { - return xticks[didx] + ' (' + new Date(xtimes[didx] * 1000).toLocaleString() + ')'; + return xticks[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); + }, + }, + { + label: "Min", + scale: "y", + width: lineWidth, + stroke: cbmode ? "rgb(0,255,0)" : "red", + }, + { + label: "Avg", + scale: "y", + width: lineWidth, + stroke: "black", + }, + { + label: "Max", + scale: "y", + width: lineWidth, + stroke: cbmode ? "rgb(0,0,255)" : "green", } ]; - plotSeries.push({ - label: "min", - scale: "y", - width: lineWidth, - stroke: cbmode ? "rgb(0,255,0)" : "red", - }); - plotSeries.push({ - label: "avg", - scale: "y", - width: lineWidth, - stroke: "black", - }); - plotSeries.push({ - label: "max", - scale: "y", - width: lineWidth, - stroke: cbmode ? "rgb(0,0,255)" : "green", - }); - const plotBands = [ - { series: [3, 2], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" }, - { series: [2, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" }, + { 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 = { @@ -167,6 +197,14 @@ return splits.map(s => xticks[s]); } }, + { + scale: "xst", + show: false, + }, + { + scale: "xrt", + show: false, + }, { scale: "y", grid: { show: true }, @@ -180,8 +218,8 @@ draw: [ (u) => { // Draw plot type label: - let textl = "Jobs min/avg/max"; - let textr = ""; + let textl = "Metric Min/Avg/Max in Duration"; + let textr = "Earlier <- StartTime -> Later"; u.ctx.save(); u.ctx.textAlign = "start"; // 'end' u.ctx.fillStyle = "black"; @@ -216,6 +254,8 @@ }, scales: { x: { time: false }, + xst: { time: false }, + xrt: { time: false }, y: maxY ? { min: 0, max: (maxY * 1.1) } : {auto: true}, // Add some space to upper render limit }, legend: {