mirror of
https://github.com/grafana/grafana.git
synced 2025-09-16 07:42:44 +08:00

* initial workflow * add bench reporter * add pr trigger * update workflow * actually add the test * debug ..... * more debug * this it??? * Debugging GHA workflow https newline tree cache node modules try bench again fix path * Add RequestsRecorder and tweak GHA workflow * More debugging GHA workflow cache cache key try renaming path specify env var correctly * Run tests on both envs, wait for requests to finish fix status cat stats cat stats * prefix metric names * cleanup * cleanup gha * codeowners * make gen-jsonnet * quote things * Add host_type label for categorising the various hosts * rename pw_boot_time_seconds to pw_test_run_time_seconds * change prefix * codeowners, gha name * remove node-modules caching * fix codeowners
165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
import { Page, Response, Request } from '@playwright/test';
|
|
import * as prom from 'prom-client';
|
|
|
|
/**
|
|
* Records and tracks network request body sizes.
|
|
*
|
|
* You must call listen() before page.goto() for accurate results - this ensures all responses
|
|
* for a page are tracked.
|
|
*/
|
|
export class RequestsRecorder {
|
|
#page: Page;
|
|
|
|
#documentUrl: string | undefined;
|
|
|
|
#requestsInFlight = 0;
|
|
|
|
#currentRequests: Set<Request> = new Set<Request>();
|
|
|
|
#inflatedSizeBytesCounter: prom.Counter<'type' | 'host_type'>;
|
|
#transferSizeBytesCounter: prom.Counter<'type' | 'host_type'>;
|
|
#requestCountCounter: prom.Counter<'type' | 'host_type'>;
|
|
#resolve?: () => void;
|
|
|
|
constructor(page: Page) {
|
|
this.#page = page;
|
|
|
|
this.#inflatedSizeBytesCounter = new prom.Counter({
|
|
name: 'fe_perf_inflated_size_bytes',
|
|
help: 'The size of the inflated response body in bytes',
|
|
labelNames: ['type', 'host_type'],
|
|
registers: [],
|
|
});
|
|
|
|
this.#transferSizeBytesCounter = new prom.Counter({
|
|
name: 'fe_perf_transfer_size_bytes',
|
|
help: 'The size of the transfered response body in bytes',
|
|
labelNames: ['type', 'host_type'],
|
|
registers: [],
|
|
});
|
|
|
|
this.#requestCountCounter = new prom.Counter({
|
|
name: 'fe_perf_request_count',
|
|
help: 'The number of requests made',
|
|
labelNames: ['type', 'host_type'],
|
|
registers: [],
|
|
});
|
|
}
|
|
|
|
listen() {
|
|
const handler = this.#handleResponse.bind(this);
|
|
const reqHandler = this.#handleRequest.bind(this);
|
|
this.#page.on('request', reqHandler);
|
|
this.#page.on('response', handler);
|
|
|
|
return () => {
|
|
this.#page.off('response', handler);
|
|
this.#page.off('request', reqHandler);
|
|
|
|
if (this.#requestsInFlight === 0) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
console.log('waiting for', this.#requestsInFlight, 'requests to finish');
|
|
|
|
return new Promise<void>((resolve) => {
|
|
this.#resolve = resolve;
|
|
});
|
|
};
|
|
}
|
|
|
|
async #handleRequest(request: Request) {
|
|
const type = request.resourceType();
|
|
|
|
// Once we've recieved a document response, create a list of requests that we'll count the responses of.
|
|
// We also want to count the document request itself.
|
|
if (this.#documentUrl || type === 'document') {
|
|
this.#currentRequests.add(request);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The Playwright page object has have multiple page loads, and responses for one page may come in after the page
|
|
* has been navigated away from. Attempting to get the body of these responses will results in an error, but is also
|
|
* not what we want to track.
|
|
*
|
|
* To solve this, we wait for the 'document' response to come in (a new page has loaded) and then keep track of all
|
|
* future requests. We only record responses for requests that were made after the document response.
|
|
*/
|
|
async #handleResponse(response: Response) {
|
|
const request = response.request();
|
|
|
|
const url = response.url();
|
|
const type = response.request().resourceType();
|
|
|
|
// Record when a document response comes in so we can keep track of future requests
|
|
if (type === 'document') {
|
|
if (this.#documentUrl) {
|
|
console.warn('recieved additional document response', url);
|
|
}
|
|
|
|
this.#documentUrl = url;
|
|
}
|
|
|
|
// Disregard responses that for requests that were initiated before the current page
|
|
if (!this.#currentRequests.has(request)) {
|
|
return;
|
|
}
|
|
|
|
this.#requestsInFlight += 1;
|
|
const hostType = getHostType(response.url(), this.#documentUrl ?? '');
|
|
|
|
// Attempting to get the body of an empty response results in an error, so guess if the response will be empty or not
|
|
const statusCode = response.status();
|
|
const noBodyStatusCode = statusCode <= 199 || statusCode === 204 || statusCode === 205 || statusCode === 304;
|
|
|
|
if (!noBodyStatusCode) {
|
|
const body = await response.body();
|
|
this.#inflatedSizeBytesCounter.inc({ type, host_type: hostType }, body.length);
|
|
}
|
|
|
|
const sizes = await response.request().sizes();
|
|
|
|
this.#transferSizeBytesCounter.inc(
|
|
{ type, host_type: hostType },
|
|
sizes.responseBodySize + sizes.responseHeadersSize
|
|
);
|
|
this.#requestCountCounter.inc({ type, host_type: hostType });
|
|
|
|
this.#requestsInFlight -= 1;
|
|
|
|
if (this.#requestsInFlight === 0 && this.#resolve) {
|
|
this.#resolve();
|
|
}
|
|
}
|
|
|
|
getMetrics() {
|
|
return [this.#inflatedSizeBytesCounter, this.#transferSizeBytesCounter, this.#requestCountCounter];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instead of setting the request host as a label, which may have too high cardinality, we categorise
|
|
* the host into a limited set of types.
|
|
*/
|
|
function getHostType(requestUrl: string, documentUrl: string) {
|
|
const url = new URL(requestUrl);
|
|
const hostname = url.hostname; // FYI `hostname` doesn't include port, `host` does
|
|
|
|
const documentHost = new URL(documentUrl).hostname;
|
|
|
|
if (hostname.match(/^grafana-assets\.grafana(-\w+)?\.net$/)) {
|
|
return 'assets_cdn';
|
|
}
|
|
|
|
if (hostname.match(/^plugins-cdn\.grafana(-\w+)?\.net$/)) {
|
|
return 'plugin_cdn';
|
|
}
|
|
|
|
if (hostname === documentHost) {
|
|
return 'self';
|
|
}
|
|
|
|
return 'other';
|
|
}
|