mirror of https://github.com/grafana/grafana
Live: performance tests e2e part (#43915)
* #41993: live perf tests e2e part * #41993: added bash upgrade instructions * #41993: remove custom feature toggle * #41993: fix typo in 'integrationFolder'pull/43180/head^2
parent
0c88b39162
commit
e01ac44cfa
@ -0,0 +1,399 @@ |
|||||||
|
{ |
||||||
|
"annotations": { |
||||||
|
"list": [ |
||||||
|
{ |
||||||
|
"builtIn": 1, |
||||||
|
"datasource": "-- Grafana --", |
||||||
|
"enable": true, |
||||||
|
"hide": true, |
||||||
|
"iconColor": "rgba(0, 211, 255, 1)", |
||||||
|
"name": "Annotations & Alerts", |
||||||
|
"target": { |
||||||
|
"limit": 100, |
||||||
|
"matchAny": false, |
||||||
|
"tags": [], |
||||||
|
"type": "dashboard" |
||||||
|
}, |
||||||
|
"type": "dashboard" |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
"editable": true, |
||||||
|
"fiscalYearStartMonth": 0, |
||||||
|
"graphTooltip": 0, |
||||||
|
"id": 82, |
||||||
|
"links": [], |
||||||
|
"liveNow": false, |
||||||
|
"panels": [ |
||||||
|
{ |
||||||
|
"datasource": { |
||||||
|
"type": "grafana", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"fieldConfig": { |
||||||
|
"defaults": { |
||||||
|
"color": { |
||||||
|
"mode": "palette-classic" |
||||||
|
}, |
||||||
|
"custom": { |
||||||
|
"axisLabel": "", |
||||||
|
"axisPlacement": "auto", |
||||||
|
"barAlignment": 0, |
||||||
|
"drawStyle": "line", |
||||||
|
"fillOpacity": 0, |
||||||
|
"gradientMode": "none", |
||||||
|
"hideFrom": { |
||||||
|
"legend": false, |
||||||
|
"tooltip": false, |
||||||
|
"viz": false |
||||||
|
}, |
||||||
|
"lineInterpolation": "linear", |
||||||
|
"lineWidth": 1, |
||||||
|
"pointSize": 5, |
||||||
|
"scaleDistribution": { |
||||||
|
"type": "linear" |
||||||
|
}, |
||||||
|
"showPoints": "auto", |
||||||
|
"spanNulls": false, |
||||||
|
"stacking": { |
||||||
|
"group": "A", |
||||||
|
"mode": "none" |
||||||
|
}, |
||||||
|
"thresholdsStyle": { |
||||||
|
"mode": "off" |
||||||
|
} |
||||||
|
}, |
||||||
|
"mappings": [], |
||||||
|
"thresholds": { |
||||||
|
"mode": "absolute", |
||||||
|
"steps": [ |
||||||
|
{ |
||||||
|
"color": "green", |
||||||
|
"value": null |
||||||
|
}, |
||||||
|
{ |
||||||
|
"color": "red", |
||||||
|
"value": 80 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"overrides": [] |
||||||
|
}, |
||||||
|
"gridPos": { |
||||||
|
"h": 8, |
||||||
|
"w": 12, |
||||||
|
"x": 0, |
||||||
|
"y": 0 |
||||||
|
}, |
||||||
|
"id": 10, |
||||||
|
"maxDataPoints": 1500, |
||||||
|
"options": { |
||||||
|
"legend": { |
||||||
|
"calcs": [], |
||||||
|
"displayMode": "list", |
||||||
|
"placement": "bottom" |
||||||
|
}, |
||||||
|
"tooltip": { |
||||||
|
"mode": "single" |
||||||
|
} |
||||||
|
}, |
||||||
|
"targets": [ |
||||||
|
{ |
||||||
|
"channel": "plugin/testdata/random-20Hz-stream-5", |
||||||
|
"datasource": { |
||||||
|
"type": "datasource", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"queryType": "measurements", |
||||||
|
"refId": "A" |
||||||
|
} |
||||||
|
], |
||||||
|
"title": "5", |
||||||
|
"type": "timeseries" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"datasource": { |
||||||
|
"type": "grafana", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"fieldConfig": { |
||||||
|
"defaults": { |
||||||
|
"color": { |
||||||
|
"mode": "palette-classic" |
||||||
|
}, |
||||||
|
"custom": { |
||||||
|
"axisLabel": "", |
||||||
|
"axisPlacement": "auto", |
||||||
|
"barAlignment": 0, |
||||||
|
"drawStyle": "line", |
||||||
|
"fillOpacity": 0, |
||||||
|
"gradientMode": "none", |
||||||
|
"hideFrom": { |
||||||
|
"legend": false, |
||||||
|
"tooltip": false, |
||||||
|
"viz": false |
||||||
|
}, |
||||||
|
"lineInterpolation": "linear", |
||||||
|
"lineWidth": 1, |
||||||
|
"pointSize": 5, |
||||||
|
"scaleDistribution": { |
||||||
|
"type": "linear" |
||||||
|
}, |
||||||
|
"showPoints": "auto", |
||||||
|
"spanNulls": false, |
||||||
|
"stacking": { |
||||||
|
"group": "A", |
||||||
|
"mode": "none" |
||||||
|
}, |
||||||
|
"thresholdsStyle": { |
||||||
|
"mode": "off" |
||||||
|
} |
||||||
|
}, |
||||||
|
"mappings": [], |
||||||
|
"thresholds": { |
||||||
|
"mode": "absolute", |
||||||
|
"steps": [ |
||||||
|
{ |
||||||
|
"color": "green", |
||||||
|
"value": null |
||||||
|
}, |
||||||
|
{ |
||||||
|
"color": "red", |
||||||
|
"value": 80 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"overrides": [] |
||||||
|
}, |
||||||
|
"gridPos": { |
||||||
|
"h": 8, |
||||||
|
"w": 12, |
||||||
|
"x": 12, |
||||||
|
"y": 0 |
||||||
|
}, |
||||||
|
"id": 6, |
||||||
|
"maxDataPoints": 1500, |
||||||
|
"options": { |
||||||
|
"legend": { |
||||||
|
"calcs": [], |
||||||
|
"displayMode": "list", |
||||||
|
"placement": "bottom" |
||||||
|
}, |
||||||
|
"tooltip": { |
||||||
|
"mode": "single" |
||||||
|
} |
||||||
|
}, |
||||||
|
"targets": [ |
||||||
|
{ |
||||||
|
"channel": "plugin/testdata/random-20Hz-stream-2", |
||||||
|
"datasource": { |
||||||
|
"type": "datasource", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"queryType": "measurements", |
||||||
|
"refId": "A" |
||||||
|
} |
||||||
|
], |
||||||
|
"title": "2", |
||||||
|
"type": "timeseries" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"datasource": { |
||||||
|
"type": "grafana", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"fieldConfig": { |
||||||
|
"defaults": { |
||||||
|
"color": { |
||||||
|
"mode": "palette-classic" |
||||||
|
}, |
||||||
|
"custom": { |
||||||
|
"axisLabel": "", |
||||||
|
"axisPlacement": "auto", |
||||||
|
"barAlignment": 0, |
||||||
|
"drawStyle": "line", |
||||||
|
"fillOpacity": 0, |
||||||
|
"gradientMode": "none", |
||||||
|
"hideFrom": { |
||||||
|
"legend": false, |
||||||
|
"tooltip": false, |
||||||
|
"viz": false |
||||||
|
}, |
||||||
|
"lineInterpolation": "linear", |
||||||
|
"lineWidth": 1, |
||||||
|
"pointSize": 5, |
||||||
|
"scaleDistribution": { |
||||||
|
"type": "linear" |
||||||
|
}, |
||||||
|
"showPoints": "auto", |
||||||
|
"spanNulls": false, |
||||||
|
"stacking": { |
||||||
|
"group": "A", |
||||||
|
"mode": "none" |
||||||
|
}, |
||||||
|
"thresholdsStyle": { |
||||||
|
"mode": "off" |
||||||
|
} |
||||||
|
}, |
||||||
|
"mappings": [], |
||||||
|
"thresholds": { |
||||||
|
"mode": "absolute", |
||||||
|
"steps": [ |
||||||
|
{ |
||||||
|
"color": "green", |
||||||
|
"value": null |
||||||
|
}, |
||||||
|
{ |
||||||
|
"color": "red", |
||||||
|
"value": 80 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"overrides": [] |
||||||
|
}, |
||||||
|
"gridPos": { |
||||||
|
"h": 8, |
||||||
|
"w": 12, |
||||||
|
"x": 0, |
||||||
|
"y": 8 |
||||||
|
}, |
||||||
|
"id": 8, |
||||||
|
"maxDataPoints": 1500, |
||||||
|
"options": { |
||||||
|
"legend": { |
||||||
|
"calcs": [], |
||||||
|
"displayMode": "list", |
||||||
|
"placement": "bottom" |
||||||
|
}, |
||||||
|
"tooltip": { |
||||||
|
"mode": "single" |
||||||
|
} |
||||||
|
}, |
||||||
|
"targets": [ |
||||||
|
{ |
||||||
|
"channel": "plugin/testdata/random-20Hz-stream-4", |
||||||
|
"datasource": { |
||||||
|
"type": "datasource", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"queryType": "measurements", |
||||||
|
"refId": "A" |
||||||
|
} |
||||||
|
], |
||||||
|
"title": "4", |
||||||
|
"type": "timeseries" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"datasource": { |
||||||
|
"type": "grafana", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"fieldConfig": { |
||||||
|
"defaults": { |
||||||
|
"color": { |
||||||
|
"mode": "palette-classic" |
||||||
|
}, |
||||||
|
"custom": { |
||||||
|
"axisLabel": "", |
||||||
|
"axisPlacement": "auto", |
||||||
|
"barAlignment": 0, |
||||||
|
"drawStyle": "line", |
||||||
|
"fillOpacity": 0, |
||||||
|
"gradientMode": "none", |
||||||
|
"hideFrom": { |
||||||
|
"legend": false, |
||||||
|
"tooltip": false, |
||||||
|
"viz": false |
||||||
|
}, |
||||||
|
"lineInterpolation": "linear", |
||||||
|
"lineWidth": 3, |
||||||
|
"pointSize": 5, |
||||||
|
"scaleDistribution": { |
||||||
|
"type": "linear" |
||||||
|
}, |
||||||
|
"showPoints": "auto", |
||||||
|
"spanNulls": false, |
||||||
|
"stacking": { |
||||||
|
"group": "A", |
||||||
|
"mode": "none" |
||||||
|
}, |
||||||
|
"thresholdsStyle": { |
||||||
|
"mode": "off" |
||||||
|
} |
||||||
|
}, |
||||||
|
"mappings": [], |
||||||
|
"thresholds": { |
||||||
|
"mode": "absolute", |
||||||
|
"steps": [ |
||||||
|
{ |
||||||
|
"color": "green", |
||||||
|
"value": null |
||||||
|
}, |
||||||
|
{ |
||||||
|
"color": "red", |
||||||
|
"value": 80 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"overrides": [] |
||||||
|
}, |
||||||
|
"gridPos": { |
||||||
|
"h": 9, |
||||||
|
"w": 12, |
||||||
|
"x": 12, |
||||||
|
"y": 8 |
||||||
|
}, |
||||||
|
"id": 2, |
||||||
|
"maxDataPoints": 1500, |
||||||
|
"options": { |
||||||
|
"legend": { |
||||||
|
"calcs": [], |
||||||
|
"displayMode": "list", |
||||||
|
"placement": "bottom" |
||||||
|
}, |
||||||
|
"tooltip": { |
||||||
|
"mode": "none" |
||||||
|
} |
||||||
|
}, |
||||||
|
"targets": [ |
||||||
|
{ |
||||||
|
"buffer": 60000, |
||||||
|
"channel": "plugin/testdata/random-20Hz-stream", |
||||||
|
"datasource": { |
||||||
|
"type": "datasource", |
||||||
|
"uid": "grafana" |
||||||
|
}, |
||||||
|
"filter": { |
||||||
|
"fields": [] |
||||||
|
}, |
||||||
|
"queryType": "measurements", |
||||||
|
"refId": "A" |
||||||
|
} |
||||||
|
], |
||||||
|
"title": "1", |
||||||
|
"transparent": true, |
||||||
|
"type": "timeseries" |
||||||
|
} |
||||||
|
], |
||||||
|
"refresh": "", |
||||||
|
"schemaVersion": 33, |
||||||
|
"style": "dark", |
||||||
|
"tags": [], |
||||||
|
"templating": { |
||||||
|
"list": [] |
||||||
|
}, |
||||||
|
"time": { |
||||||
|
"from": "now-60s", |
||||||
|
"to": "now" |
||||||
|
}, |
||||||
|
"timepicker": {}, |
||||||
|
"timezone": "", |
||||||
|
"title": "Grafana Live performance benchmarking", |
||||||
|
"uid": "S4M5r9cnk", |
||||||
|
"version": 9, |
||||||
|
"weekStart": "" |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
import { e2e } from '@grafana/e2e'; |
||||||
|
|
||||||
|
type WithGrafanaRuntime<T> = T & { |
||||||
|
grafanaRuntime: { |
||||||
|
livePerformance: { |
||||||
|
start: () => void; |
||||||
|
getStats: () => Record<string, unknown>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const hasGrafanaRuntime = <T>(obj: T): obj is WithGrafanaRuntime<T> => { |
||||||
|
return typeof (obj as any)?.grafanaRuntime === 'object'; |
||||||
|
}; |
||||||
|
|
||||||
|
e2e.benchmark({ |
||||||
|
name: 'Live performance benchmarking - 4x20hz panels', |
||||||
|
dashboard: { |
||||||
|
folder: '/dashboards/live', |
||||||
|
delayAfterOpening: 1000, |
||||||
|
skipPanelValidation: true, |
||||||
|
}, |
||||||
|
repeat: 10, |
||||||
|
duration: 120000, |
||||||
|
appStats: { |
||||||
|
startCollecting: (window) => { |
||||||
|
if (!hasGrafanaRuntime(window)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
return window.grafanaRuntime.livePerformance.start(); |
||||||
|
}, |
||||||
|
collect: (window) => { |
||||||
|
if (!hasGrafanaRuntime(window)) { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
return window.grafanaRuntime.livePerformance.getStats() ?? {}; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.json", |
||||||
|
"include": ["**/*.ts", "../../packages/grafana-e2e/cypress/support/index.d.ts"], |
||||||
|
"resolveJsonModule": true |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
[feature_toggles] |
||||||
|
enable = |
@ -0,0 +1,136 @@ |
|||||||
|
import CDP from 'chrome-remote-interface'; |
||||||
|
import Tracelib, { TraceEvent } from 'tracelib'; |
||||||
|
|
||||||
|
import { countBy, mean } from 'lodash'; |
||||||
|
import ProtocolProxyApi from 'devtools-protocol/types/protocol-proxy-api'; |
||||||
|
import { CollectedData, DataCollector, DataCollectorName } from './DataCollector'; |
||||||
|
|
||||||
|
type CDPDataCollectorDeps = { |
||||||
|
port: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export class CDPDataCollector implements DataCollector { |
||||||
|
private tracingCategories: string[]; |
||||||
|
|
||||||
|
private state: { |
||||||
|
client?: CDP.Client; |
||||||
|
tracingPromise?: Promise<CollectedData>; |
||||||
|
traceEvents: TraceEvent[]; |
||||||
|
}; |
||||||
|
|
||||||
|
constructor(private deps: CDPDataCollectorDeps) { |
||||||
|
this.state = this.getDefaultState(); |
||||||
|
this.tracingCategories = [ |
||||||
|
'disabled-by-default-v8.cpu_profile', |
||||||
|
'disabled-by-default-v8.cpu_profiler', |
||||||
|
'disabled-by-default-v8.cpu_profiler.hires', |
||||||
|
'disabled-by-default-devtools.timeline.frame', |
||||||
|
'disabled-by-default-devtools.timeline', |
||||||
|
'disabled-by-default-devtools.timeline.inputs', |
||||||
|
'disabled-by-default-devtools.timeline.stack', |
||||||
|
'disabled-by-default-devtools.timeline.invalidationTracking', |
||||||
|
'disabled-by-default-layout_shift.debug', |
||||||
|
'disabled-by-default-cc.debug.scheduler.frames', |
||||||
|
'disabled-by-default-blink.debug.display_lock', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
getName = () => DataCollectorName.CDP; |
||||||
|
|
||||||
|
private resetState = async () => { |
||||||
|
if (this.state.client) { |
||||||
|
await this.state.client.close(); |
||||||
|
} |
||||||
|
this.state = this.getDefaultState(); |
||||||
|
}; |
||||||
|
|
||||||
|
private getDefaultState = () => ({ |
||||||
|
traceEvents: [], |
||||||
|
}); |
||||||
|
|
||||||
|
// workaround for type declaration issues in cdp lib
|
||||||
|
private asApis = ( |
||||||
|
client: CDP.Client |
||||||
|
): { |
||||||
|
Profiler: ProtocolProxyApi.ProfilerApi; |
||||||
|
Page: ProtocolProxyApi.PageApi; |
||||||
|
Tracing: ProtocolProxyApi.TracingApi; |
||||||
|
} => client; |
||||||
|
|
||||||
|
private getClientApis = async () => this.asApis(await this.getClient()); |
||||||
|
|
||||||
|
private getClient = async () => { |
||||||
|
if (this.state.client) { |
||||||
|
return this.state.client; |
||||||
|
} |
||||||
|
|
||||||
|
const client = await CDP({ port: this.deps.port }); |
||||||
|
|
||||||
|
const { Profiler, Page } = this.asApis(client); |
||||||
|
await Promise.all([Page.enable(), Profiler.enable(), Profiler.setSamplingInterval({ interval: 100 })]); |
||||||
|
|
||||||
|
this.state.client = client; |
||||||
|
|
||||||
|
return client; |
||||||
|
}; |
||||||
|
|
||||||
|
start: DataCollector['start'] = async ({ id }) => { |
||||||
|
if (this.state.tracingPromise) { |
||||||
|
throw new Error(`collection in progress - can't start another one! ${id}`); |
||||||
|
} |
||||||
|
|
||||||
|
const { Tracing, Profiler } = await this.getClientApis(); |
||||||
|
|
||||||
|
await Promise.all([ |
||||||
|
Tracing.start({ |
||||||
|
bufferUsageReportingInterval: 1000, |
||||||
|
traceConfig: { |
||||||
|
includedCategories: this.tracingCategories, |
||||||
|
}, |
||||||
|
}), |
||||||
|
Profiler.start(), |
||||||
|
]); |
||||||
|
|
||||||
|
Tracing.on('dataCollected', ({ value: events }) => { |
||||||
|
this.state.traceEvents.push(...events); |
||||||
|
}); |
||||||
|
|
||||||
|
let resolveFn: (data: CollectedData) => void; |
||||||
|
this.state.tracingPromise = new Promise<CollectedData>((resolve) => { |
||||||
|
resolveFn = resolve; |
||||||
|
}); |
||||||
|
Tracing.on('tracingComplete', ({ dataLossOccurred }) => { |
||||||
|
const t = new Tracelib(this.state.traceEvents); |
||||||
|
|
||||||
|
const eventCounts = countBy(this.state.traceEvents, (ev) => ev.name); |
||||||
|
|
||||||
|
const fps = t.getFPS(); |
||||||
|
|
||||||
|
resolveFn({ |
||||||
|
eventCounts, |
||||||
|
fps: mean(fps.values), |
||||||
|
tracingDataLoss: dataLossOccurred ? 1 : 0, |
||||||
|
warnings: t.getWarningCounts(), |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
stop: DataCollector['stop'] = async (req) => { |
||||||
|
if (!this.state.tracingPromise) { |
||||||
|
throw new Error(`collection was never started - there is nothing to stop!`); |
||||||
|
} |
||||||
|
|
||||||
|
const { Tracing, Profiler } = await this.getClientApis(); |
||||||
|
|
||||||
|
// TODO: capture profiler data
|
||||||
|
const [, , traceData] = await Promise.all([Profiler.stop(), Tracing.end(), this.state.tracingPromise]); |
||||||
|
|
||||||
|
await this.resetState(); |
||||||
|
|
||||||
|
return traceData; |
||||||
|
}; |
||||||
|
|
||||||
|
close: DataCollector['close'] = async () => { |
||||||
|
await this.resetState(); |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
export type CollectedData = Record<string, unknown>; |
||||||
|
|
||||||
|
export enum DataCollectorName { |
||||||
|
CDP = 'CDP', |
||||||
|
} |
||||||
|
|
||||||
|
type DataCollectorRequest = { id: string }; |
||||||
|
|
||||||
|
export type DataCollector<T extends CollectedData = CollectedData> = { |
||||||
|
start: (input: DataCollectorRequest) => Promise<void>; |
||||||
|
stop: (input: DataCollectorRequest) => Promise<T>; |
||||||
|
getName: () => DataCollectorName; |
||||||
|
close: () => Promise<void>; |
||||||
|
}; |
@ -0,0 +1,138 @@ |
|||||||
|
import { CollectedData, DataCollectorName } from './DataCollector'; |
||||||
|
import { fromPairs } from 'lodash'; |
||||||
|
|
||||||
|
type Stats = { |
||||||
|
sum: number; |
||||||
|
min: number; |
||||||
|
max: number; |
||||||
|
count: number; |
||||||
|
avg: number; |
||||||
|
time: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export enum MeasurementName { |
||||||
|
DataRenderDelay = 'DataRenderDelay', |
||||||
|
} |
||||||
|
|
||||||
|
type LivePerformanceAppStats = Record<MeasurementName, Stats[]>; |
||||||
|
|
||||||
|
const isLivePerformanceAppStats = (data: CollectedData[]): data is LivePerformanceAppStats[] => |
||||||
|
data.some((st) => { |
||||||
|
const stat = st?.[MeasurementName.DataRenderDelay]; |
||||||
|
return Array.isArray(stat) && Boolean(stat?.length); |
||||||
|
}); |
||||||
|
|
||||||
|
type FormattedStats = { |
||||||
|
total: { |
||||||
|
count: number[]; |
||||||
|
avg: number[]; |
||||||
|
}; |
||||||
|
lastInterval: { |
||||||
|
avg: number[]; |
||||||
|
min: number[]; |
||||||
|
max: number[]; |
||||||
|
count: number[]; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const formatAppStats = (allStats: CollectedData[]) => { |
||||||
|
if (!isLivePerformanceAppStats(allStats)) { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
const names = Object.keys(MeasurementName) as MeasurementName[]; |
||||||
|
|
||||||
|
return fromPairs( |
||||||
|
names.map((name) => { |
||||||
|
const statsForMeasurement = allStats.map((s) => s[name]); |
||||||
|
const res: FormattedStats = { |
||||||
|
total: { |
||||||
|
count: [], |
||||||
|
avg: [], |
||||||
|
}, |
||||||
|
lastInterval: { |
||||||
|
avg: [], |
||||||
|
min: [], |
||||||
|
max: [], |
||||||
|
count: [], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
statsForMeasurement.forEach((s) => { |
||||||
|
const total = s.reduce( |
||||||
|
(prev, next) => { |
||||||
|
prev.count += next.count; |
||||||
|
prev.avg += next.avg; |
||||||
|
return prev; |
||||||
|
}, |
||||||
|
{ count: 0, avg: 0 } |
||||||
|
); |
||||||
|
res.total.count.push(Math.round(total.count)); |
||||||
|
res.total.avg.push(Math.round(total.avg / s.length)); |
||||||
|
|
||||||
|
const lastInterval = s[s.length - 1]; |
||||||
|
|
||||||
|
res.lastInterval.avg.push(Math.round(lastInterval?.avg)); |
||||||
|
res.lastInterval.min.push(Math.round(lastInterval?.min)); |
||||||
|
res.lastInterval.max.push(Math.round(lastInterval?.max)); |
||||||
|
res.lastInterval.count.push(Math.round(lastInterval?.count)); |
||||||
|
}); |
||||||
|
|
||||||
|
return [name, res]; |
||||||
|
}) |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
type CDPData = { |
||||||
|
eventCounts: Record<string, unknown>; |
||||||
|
fps: number; |
||||||
|
tracingDataLoss: number; |
||||||
|
warnings: Record<string, unknown>; |
||||||
|
}; |
||||||
|
|
||||||
|
const isCDPData = (data: any[]): data is CDPData[] => data.every((d) => typeof d.eventCounts === 'object'); |
||||||
|
|
||||||
|
type FormattedCDPData = { |
||||||
|
minorGC: number[]; |
||||||
|
majorGC: number[]; |
||||||
|
droppedFrames: number[]; |
||||||
|
fps: number[]; |
||||||
|
tracingDataLossOccurred: boolean; |
||||||
|
longTaskWarnings: number[]; |
||||||
|
}; |
||||||
|
|
||||||
|
const emptyFormattedCDPData = (): FormattedCDPData => ({ |
||||||
|
minorGC: [], |
||||||
|
majorGC: [], |
||||||
|
droppedFrames: [], |
||||||
|
fps: [], |
||||||
|
tracingDataLossOccurred: false, |
||||||
|
longTaskWarnings: [], |
||||||
|
}); |
||||||
|
|
||||||
|
const formatCDPData = (data: any): FormattedCDPData => { |
||||||
|
if (!isCDPData(data)) { |
||||||
|
return emptyFormattedCDPData(); |
||||||
|
} |
||||||
|
|
||||||
|
return data.reduce((acc, next) => { |
||||||
|
acc.majorGC.push((next.eventCounts.MajorGC as number) ?? 0); |
||||||
|
acc.minorGC.push((next.eventCounts.MinorGC as number) ?? 0); |
||||||
|
acc.fps.push(Math.round(next.fps) ?? 0); |
||||||
|
acc.tracingDataLossOccurred = acc.tracingDataLossOccurred || Boolean(next.tracingDataLoss); |
||||||
|
acc.droppedFrames.push((next.eventCounts.DroppedFrame as number) ?? 0); |
||||||
|
acc.longTaskWarnings.push((next.warnings.LongTask as number) ?? 0); |
||||||
|
return acc; |
||||||
|
}, emptyFormattedCDPData()); |
||||||
|
}; |
||||||
|
|
||||||
|
export const formatResults = ( |
||||||
|
results: Array<{ appStats: CollectedData; collectorsData: CollectedData }> |
||||||
|
): CollectedData => { |
||||||
|
return { |
||||||
|
...formatAppStats(results.map(({ appStats }) => appStats)), |
||||||
|
...formatCDPData(results.map(({ collectorsData }) => collectorsData[DataCollectorName.CDP])), |
||||||
|
|
||||||
|
__raw: results, |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,87 @@ |
|||||||
|
import { CollectedData, DataCollector } from './DataCollector'; |
||||||
|
import { CDPDataCollector } from './CDPDataCollector'; |
||||||
|
import { fromPairs } from 'lodash'; |
||||||
|
import fs from 'fs'; |
||||||
|
import { formatResults } from './formatting'; |
||||||
|
const remoteDebuggingPortOptionPrefix = '--remote-debugging-port='; |
||||||
|
|
||||||
|
const getOrAddRemoteDebuggingPort = (args: string[]) => { |
||||||
|
const existing = args.find((arg) => arg.startsWith(remoteDebuggingPortOptionPrefix)); |
||||||
|
|
||||||
|
if (existing) { |
||||||
|
return Number(existing.substring(remoteDebuggingPortOptionPrefix.length)); |
||||||
|
} |
||||||
|
|
||||||
|
const port = 40000 + Math.round(Math.random() * 25000); |
||||||
|
args.push(`${remoteDebuggingPortOptionPrefix}${port}`); |
||||||
|
return port; |
||||||
|
}; |
||||||
|
|
||||||
|
let collectors: DataCollector[] = []; |
||||||
|
let results: Array<{ appStats: CollectedData; collectorsData: CollectedData }> = []; |
||||||
|
|
||||||
|
const startBenchmarking = async ({ testName }: { testName: string }) => { |
||||||
|
await Promise.all(collectors.map((coll) => coll.start({ id: testName }))); |
||||||
|
|
||||||
|
return true; |
||||||
|
}; |
||||||
|
|
||||||
|
const stopBenchmarking = async ({ testName, appStats }: { testName: string; appStats: CollectedData }) => { |
||||||
|
const data = await Promise.all(collectors.map(async (coll) => [coll.getName(), await coll.stop({ id: testName })])); |
||||||
|
|
||||||
|
results.push({ |
||||||
|
collectorsData: fromPairs(data), |
||||||
|
appStats: appStats, |
||||||
|
}); |
||||||
|
|
||||||
|
return true; |
||||||
|
}; |
||||||
|
const afterRun = async () => { |
||||||
|
await Promise.all(collectors.map((coll) => coll.close())); |
||||||
|
collectors = []; |
||||||
|
results = []; |
||||||
|
}; |
||||||
|
|
||||||
|
const afterSpec = (resultsFolder: string) => async (spec: { name: string }) => { |
||||||
|
fs.writeFileSync(`${resultsFolder}/${spec.name}-${Date.now()}.json`, JSON.stringify(formatResults(results), null, 2)); |
||||||
|
|
||||||
|
results = []; |
||||||
|
}; |
||||||
|
|
||||||
|
export const initialize: Cypress.PluginConfig = (on, config) => { |
||||||
|
const resultsFolder = config.env['BENCHMARK_PLUGIN_RESULTS_FOLDER']; |
||||||
|
|
||||||
|
if (!fs.existsSync(resultsFolder)) { |
||||||
|
fs.mkdirSync(resultsFolder, { recursive: true }); |
||||||
|
console.log(`Created folder for benchmark results ${resultsFolder}`); |
||||||
|
} |
||||||
|
|
||||||
|
on('before:browser:launch', async (browser, options) => { |
||||||
|
if (browser.family !== 'chromium' || browser.name === 'electron') { |
||||||
|
throw new Error('benchmarking plugin requires chrome'); |
||||||
|
} |
||||||
|
|
||||||
|
const { args } = options; |
||||||
|
|
||||||
|
const port = getOrAddRemoteDebuggingPort(args); |
||||||
|
collectors.push(new CDPDataCollector({ port })); |
||||||
|
|
||||||
|
args.push('--start-fullscreen'); |
||||||
|
|
||||||
|
console.log( |
||||||
|
`initialized benchmarking plugin with ${collectors.length} collectors: ${collectors |
||||||
|
.map((col) => col.getName()) |
||||||
|
.join(', ')}` |
||||||
|
); |
||||||
|
|
||||||
|
return options; |
||||||
|
}); |
||||||
|
|
||||||
|
on('task', { |
||||||
|
startBenchmarking, |
||||||
|
stopBenchmarking, |
||||||
|
}); |
||||||
|
|
||||||
|
on('after:run', afterRun); |
||||||
|
on('after:spec', afterSpec(resultsFolder)); |
||||||
|
}; |
@ -0,0 +1,15 @@ |
|||||||
|
type TraceEvent = { |
||||||
|
name: string; |
||||||
|
}; |
||||||
|
|
||||||
|
declare class Tracelib { |
||||||
|
constructor(private events: TraceEvent[]) {} |
||||||
|
|
||||||
|
getFPS: () => { times: number[]; values: number[] }; |
||||||
|
getWarningCounts: () => Record<string, number>; |
||||||
|
} |
||||||
|
declare module 'tracelib' { |
||||||
|
export = Tracelib; |
||||||
|
|
||||||
|
export { TraceEvent }; |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import { e2e } from '../'; |
||||||
|
|
||||||
|
export interface BenchmarkArguments { |
||||||
|
name: string; |
||||||
|
dashboard: { |
||||||
|
folder: string; |
||||||
|
delayAfterOpening: number; |
||||||
|
skipPanelValidation: boolean; |
||||||
|
}; |
||||||
|
repeat: number; |
||||||
|
duration: number; |
||||||
|
appStats?: { |
||||||
|
startCollecting?: (window: Window) => void; |
||||||
|
collect: (window: Window) => Record<string, unknown>; |
||||||
|
}; |
||||||
|
skipScenario?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const benchmark = ({ |
||||||
|
name, |
||||||
|
skipScenario = false, |
||||||
|
repeat, |
||||||
|
duration, |
||||||
|
appStats, |
||||||
|
dashboard, |
||||||
|
}: BenchmarkArguments) => { |
||||||
|
if (skipScenario) { |
||||||
|
describe(name, () => { |
||||||
|
it.skip(name, () => {}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
describe(name, () => { |
||||||
|
before(() => { |
||||||
|
e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD')); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
e2e.flows.importDashboards(dashboard.folder, 1000, dashboard.skipPanelValidation); |
||||||
|
Cypress.Cookies.preserveOnce('grafana_session'); |
||||||
|
}); |
||||||
|
|
||||||
|
afterEach(() => e2e.flows.revertAllChanges()); |
||||||
|
after(() => { |
||||||
|
e2e().clearCookies(); |
||||||
|
}); |
||||||
|
|
||||||
|
Array(repeat) |
||||||
|
.fill(0) |
||||||
|
.map((_, i) => { |
||||||
|
const testName = `${name}-${i}`; |
||||||
|
return it(testName, () => { |
||||||
|
e2e.flows.openDashboard(); |
||||||
|
|
||||||
|
e2e().wait(dashboard.delayAfterOpening); |
||||||
|
|
||||||
|
if (appStats) { |
||||||
|
const startCollecting = appStats.startCollecting; |
||||||
|
if (startCollecting) { |
||||||
|
e2e() |
||||||
|
.window() |
||||||
|
.then((win) => startCollecting(win)); |
||||||
|
} |
||||||
|
|
||||||
|
e2e().startBenchmarking(testName); |
||||||
|
e2e().wait(duration); |
||||||
|
|
||||||
|
e2e() |
||||||
|
.window() |
||||||
|
.then((win) => { |
||||||
|
e2e().stopBenchmarking(testName, appStats.collect(win)); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
e2e().startBenchmarking(testName); |
||||||
|
e2e().wait(duration); |
||||||
|
e2e().stopBenchmarking(testName, {}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
Loading…
Reference in new issue