app/vmui: show query execution duration in the header of query input field

This should simplify the process of query optimization
This commit is contained in:
Aliaksandr Valialkin 2023-11-01 16:42:51 +01:00
parent 4fafdda13e
commit 6a98f9df54
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
13 changed files with 103 additions and 73 deletions

View file

@ -30,7 +30,8 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries
] ]
}, },
"stats":{ "stats":{
"seriesFetched": "{%d qs.SeriesFetched %}" "seriesFetched": {%dl qs.SeriesFetched %},
"executionTimeMsec": {%dl qs.ExecutionTimeMsec %}
} }
{% code {% code
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount) qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)

View file

@ -72,85 +72,89 @@ func StreamQueryRangeResponse(qw422016 *qt422016.Writer, isPartial bool, rs []ne
//line app/vmselect/prometheus/query_range_response.qtpl:29 //line app/vmselect/prometheus/query_range_response.qtpl:29
} }
//line app/vmselect/prometheus/query_range_response.qtpl:29 //line app/vmselect/prometheus/query_range_response.qtpl:29
qw422016.N().S(`]},"stats":{"seriesFetched": "`) qw422016.N().S(`]},"stats":{"seriesFetched":`)
//line app/vmselect/prometheus/query_range_response.qtpl:33 //line app/vmselect/prometheus/query_range_response.qtpl:33
qw422016.N().D(qs.SeriesFetched) qw422016.N().DL(qs.SeriesFetched)
//line app/vmselect/prometheus/query_range_response.qtpl:33 //line app/vmselect/prometheus/query_range_response.qtpl:33
qw422016.N().S(`"}`) qw422016.N().S(`,"executionTimeMsec":`)
//line app/vmselect/prometheus/query_range_response.qtpl:36 //line app/vmselect/prometheus/query_range_response.qtpl:34
qw422016.N().DL(qs.ExecutionTimeMsec)
//line app/vmselect/prometheus/query_range_response.qtpl:34
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:37
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount) qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)
qtDone() qtDone()
//line app/vmselect/prometheus/query_range_response.qtpl:39 //line app/vmselect/prometheus/query_range_response.qtpl:40
streamdumpQueryTrace(qw422016, qt) streamdumpQueryTrace(qw422016, qt)
//line app/vmselect/prometheus/query_range_response.qtpl:39 //line app/vmselect/prometheus/query_range_response.qtpl:40
qw422016.N().S(`}`) qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
} }
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
func WriteQueryRangeResponse(qq422016 qtio422016.Writer, isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) { func WriteQueryRangeResponse(qq422016 qtio422016.Writer, isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
StreamQueryRangeResponse(qw422016, isPartial, rs, qt, qtDone, qs) StreamQueryRangeResponse(qw422016, isPartial, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
} }
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
func QueryRangeResponse(isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string { func QueryRangeResponse(isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
WriteQueryRangeResponse(qb422016, isPartial, rs, qt, qtDone, qs) WriteQueryRangeResponse(qb422016, isPartial, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
return qs422016 return qs422016
//line app/vmselect/prometheus/query_range_response.qtpl:41 //line app/vmselect/prometheus/query_range_response.qtpl:42
} }
//line app/vmselect/prometheus/query_range_response.qtpl:43 //line app/vmselect/prometheus/query_range_response.qtpl:44
func streamqueryRangeLine(qw422016 *qt422016.Writer, r *netstorage.Result) { func streamqueryRangeLine(qw422016 *qt422016.Writer, r *netstorage.Result) {
//line app/vmselect/prometheus/query_range_response.qtpl:43 //line app/vmselect/prometheus/query_range_response.qtpl:44
qw422016.N().S(`{"metric":`) qw422016.N().S(`{"metric":`)
//line app/vmselect/prometheus/query_range_response.qtpl:45 //line app/vmselect/prometheus/query_range_response.qtpl:46
streammetricNameObject(qw422016, &r.MetricName) streammetricNameObject(qw422016, &r.MetricName)
//line app/vmselect/prometheus/query_range_response.qtpl:45 //line app/vmselect/prometheus/query_range_response.qtpl:46
qw422016.N().S(`,"values":`) qw422016.N().S(`,"values":`)
//line app/vmselect/prometheus/query_range_response.qtpl:46 //line app/vmselect/prometheus/query_range_response.qtpl:47
streamvaluesWithTimestamps(qw422016, r.Values, r.Timestamps) streamvaluesWithTimestamps(qw422016, r.Values, r.Timestamps)
//line app/vmselect/prometheus/query_range_response.qtpl:46 //line app/vmselect/prometheus/query_range_response.qtpl:47
qw422016.N().S(`}`) qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
} }
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
func writequeryRangeLine(qq422016 qtio422016.Writer, r *netstorage.Result) { func writequeryRangeLine(qq422016 qtio422016.Writer, r *netstorage.Result) {
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
streamqueryRangeLine(qw422016, r) streamqueryRangeLine(qw422016, r)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
} }
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
func queryRangeLine(r *netstorage.Result) string { func queryRangeLine(r *netstorage.Result) string {
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
writequeryRangeLine(qb422016, r) writequeryRangeLine(qb422016, r)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
return qs422016 return qs422016
//line app/vmselect/prometheus/query_range_response.qtpl:48 //line app/vmselect/prometheus/query_range_response.qtpl:49
} }

View file

@ -32,7 +32,8 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries
] ]
}, },
"stats":{ "stats":{
"seriesFetched": "{%d qs.SeriesFetched %}" "seriesFetched": {%dl qs.SeriesFetched %},
"executionTimeMsec": {%dl qs.ExecutionTimeMsec %}
} }
{% code {% code
qt.Printf("generate /api/v1/query response for series=%d", seriesCount) qt.Printf("generate /api/v1/query response for series=%d", seriesCount)

View file

@ -82,44 +82,48 @@ func StreamQueryResponse(qw422016 *qt422016.Writer, isPartial bool, rs []netstor
//line app/vmselect/prometheus/query_response.qtpl:31 //line app/vmselect/prometheus/query_response.qtpl:31
} }
//line app/vmselect/prometheus/query_response.qtpl:31 //line app/vmselect/prometheus/query_response.qtpl:31
qw422016.N().S(`]},"stats":{"seriesFetched": "`) qw422016.N().S(`]},"stats":{"seriesFetched":`)
//line app/vmselect/prometheus/query_response.qtpl:35 //line app/vmselect/prometheus/query_response.qtpl:35
qw422016.N().D(qs.SeriesFetched) qw422016.N().DL(qs.SeriesFetched)
//line app/vmselect/prometheus/query_response.qtpl:35 //line app/vmselect/prometheus/query_response.qtpl:35
qw422016.N().S(`"}`) qw422016.N().S(`,"executionTimeMsec":`)
//line app/vmselect/prometheus/query_response.qtpl:38 //line app/vmselect/prometheus/query_response.qtpl:36
qw422016.N().DL(qs.ExecutionTimeMsec)
//line app/vmselect/prometheus/query_response.qtpl:36
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_response.qtpl:39
qt.Printf("generate /api/v1/query response for series=%d", seriesCount) qt.Printf("generate /api/v1/query response for series=%d", seriesCount)
qtDone() qtDone()
//line app/vmselect/prometheus/query_response.qtpl:41 //line app/vmselect/prometheus/query_response.qtpl:42
streamdumpQueryTrace(qw422016, qt) streamdumpQueryTrace(qw422016, qt)
//line app/vmselect/prometheus/query_response.qtpl:41 //line app/vmselect/prometheus/query_response.qtpl:42
qw422016.N().S(`}`) qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
} }
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
func WriteQueryResponse(qq422016 qtio422016.Writer, isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) { func WriteQueryResponse(qq422016 qtio422016.Writer, isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
StreamQueryResponse(qw422016, isPartial, rs, qt, qtDone, qs) StreamQueryResponse(qw422016, isPartial, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
} }
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
func QueryResponse(isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string { func QueryResponse(isPartial bool, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
WriteQueryResponse(qb422016, isPartial, rs, qt, qtDone, qs) WriteQueryResponse(qb422016, isPartial, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
return qs422016 return qs422016
//line app/vmselect/prometheus/query_response.qtpl:43 //line app/vmselect/prometheus/query_response.qtpl:44
} }

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"unsafe" "unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
@ -144,8 +145,7 @@ type EvalConfig struct {
// QueryStats contains various stats for the currently executed query. // QueryStats contains various stats for the currently executed query.
// //
// The caller must initialize the QueryStats if it needs the stats. // The caller must initialize QueryStats, otherwise it isn't collected.
// Otherwise the stats isn't collected.
QueryStats *QueryStats QueryStats *QueryStats
timestamps []int64 timestamps []int64
@ -178,13 +178,24 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
// QueryStats contains various stats for the query. // QueryStats contains various stats for the query.
type QueryStats struct { type QueryStats struct {
// SeriesFetched contains the number of series fetched from storage during the query evaluation. // SeriesFetched contains the number of series fetched from storage during the query evaluation.
SeriesFetched int SeriesFetched int64
// ExecutionTimeMsec contains the number of milliseconds the query took to execute.
ExecutionTimeMsec int64
} }
func (qs *QueryStats) addSeriesFetched(n int) { func (qs *QueryStats) addSeriesFetched(n int) {
if qs != nil { if qs == nil {
qs.SeriesFetched += n return
} }
atomic.AddInt64(&qs.SeriesFetched, int64(n))
}
func (qs *QueryStats) addExecutionTimeMsec(startTime time.Time) {
if qs == nil {
return
}
d := time.Since(startTime).Milliseconds()
atomic.AddInt64(&qs.ExecutionTimeMsec, d)
} }
func (ec *EvalConfig) updateIsPartialResponse(isPartialResponse bool) { func (ec *EvalConfig) updateIsPartialResponse(isPartialResponse bool) {

View file

@ -49,7 +49,10 @@ func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly boo
if querystats.Enabled() { if querystats.Enabled() {
startTime := time.Now() startTime := time.Now()
ac := ec.AuthToken ac := ec.AuthToken
defer querystats.RegisterQuery(ac.AccountID, ac.ProjectID, q, ec.End-ec.Start, startTime) defer func() {
querystats.RegisterQuery(ac.AccountID, ac.ProjectID, q, ec.End-ec.Start, startTime)
ec.QueryStats.addExecutionTimeMsec(startTime)
}()
} }
ec.validate() ec.validate()

View file

@ -1,13 +1,13 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.b863450b.css", "main.css": "./static/css/main.b863450b.css",
"main.js": "./static/js/main.19e7f129.js", "main.js": "./static/js/main.0c55974f.js",
"static/js/522.da77e7b3.chunk.js": "./static/js/522.da77e7b3.chunk.js", "static/js/522.da77e7b3.chunk.js": "./static/js/522.da77e7b3.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.8644fd7c964802dd34a9.md", "static/media/MetricsQL.md": "./static/media/MetricsQL.8644fd7c964802dd34a9.md",
"index.html": "./index.html" "index.html": "./index.html"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.b863450b.css", "static/css/main.b863450b.css",
"static/js/main.19e7f129.js" "static/js/main.0c55974f.js"
] ]
} }

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.19e7f129.js"></script><link href="./static/css/main.b863450b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.0c55974f.js"></script><link href="./static/css/main.b863450b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View file

@ -22,7 +22,8 @@ export interface TracingData {
} }
export interface QueryStats { export interface QueryStats {
seriesFetched?: string; seriesFetched?: number;
executionTimeMsec?: number;
resultLength?: number; resultLength?: number;
isPartial?: boolean; isPartial?: boolean;
} }

View file

@ -41,7 +41,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
const warning = [ const warning = [
{ {
show: stats?.seriesFetched === "0" && !stats.resultLength, show: stats?.seriesFetched == 0 && !stats.resultLength,
text: seriesFetchedWarning text: seriesFetchedWarning
}, },
{ {
@ -50,6 +50,10 @@ const QueryEditor: FC<QueryEditorProps> = ({
} }
].filter((w) => w.show).map(w => w.text).join(""); ].filter((w) => w.show).map(w => w.text).join("");
if (stats) {
label = `${label} (${stats.executionTimeMsec || 0}ms)`;
}
const handleSelect = (val: string) => { const handleSelect = (val: string) => {
onChange(val); onChange(val);
}; };

View file

@ -62,8 +62,9 @@ The sandbox cluster installation is running under the constant load generated by
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add support for functions, labels, values in autocomplete. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3006). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add support for functions, labels, values in autocomplete. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3006).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): retain specified time interval when executing a query from `Top Queries`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5097). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): retain specified time interval when executing a query from `Top Queries`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5097).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): improve repeated VMUI page load times by enabling caching of static js and css at web browser side according to [these recommendations](https://developer.chrome.com/docs/lighthouse/performance/uses-long-cache-ttl/). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): improve repeated VMUI page load times by enabling caching of static js and css at web browser side according to [these recommendations](https://developer.chrome.com/docs/lighthouse/performance/uses-long-cache-ttl/).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): show information about lines with bigger values at the top of the legend under the graph in order to simplify graph analysis. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): sort legend under the graph in descending order of median values. This should simplify graph analysis, since usually the most important lines have bigger values.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce vertical space usage, so more information is visible on the screen without scrolling. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce vertical space usage, so more information is visible on the screen without scrolling.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): show query execution duration in the header of query input field. This should help optimizing query performance.
* FEATURE: support `Strict-Transport-Security`, `Content-Security-Policy` and `X-Frame-Options` HTTP response headers in the all VictoriaMetrics components. The values for headers can be specified via the following command-line flags: `-http.header.hsts`, `-http.header.csp` and `-http.header.frameOptions`. * FEATURE: support `Strict-Transport-Security`, `Content-Security-Policy` and `X-Frame-Options` HTTP response headers in the all VictoriaMetrics components. The values for headers can be specified via the following command-line flags: `-http.header.hsts`, `-http.header.csp` and `-http.header.frameOptions`.
* FEATURE: [vmalert-tool](https://docs.victoriametrics.com/#vmalert-tool): add `unittest` command to run unittest for alerting and recording rules. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4789) for details. * FEATURE: [vmalert-tool](https://docs.victoriametrics.com/#vmalert-tool): add `unittest` command to run unittest for alerting and recording rules. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4789) for details.
* FEATURE: dashboards/vmalert: add new panel `Missed evaluations` for indicating alerting groups that miss their evaluations. * FEATURE: dashboards/vmalert: add new panel `Missed evaluations` for indicating alerting groups that miss their evaluations.