Skip to content

ci(replay): Overhead measurement #6611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 55 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
6fda61a
chore: replay metrics collection - initial script setup
vaind Dec 21, 2022
b333fbe
chore: replay metrics LCP collection
vaind Dec 22, 2022
a65e18c
cls vital collection
vaind Dec 22, 2022
be39bc5
replay: fid metric
vaind Dec 22, 2022
d57e56c
replay metrics - cpu stats
vaind Dec 22, 2022
cda998c
metrics memory profiler
vaind Dec 22, 2022
bcac89c
replay metrics - refactor scenarios
vaind Dec 22, 2022
68300fc
chore: formatting
vaind Dec 28, 2022
628a877
chore: refactoring
vaind Dec 28, 2022
9f37006
define test cases for metrics collection
vaind Dec 28, 2022
2c4aa3c
refactor: split up data collection
vaind Dec 29, 2022
945845e
write collected metrics to file
vaind Dec 29, 2022
b0ec7e6
add metrics processing script
vaind Dec 29, 2022
d3f93d8
metrics: reading json result file
vaind Dec 29, 2022
67a6dd8
replay matrics: jank test app copy
vaind Dec 29, 2022
c019cc6
fix JSON serialization
vaind Dec 29, 2022
5658c6c
yank-test scenario
vaind Dec 29, 2022
64f446f
collect jank test-app metrics with sentry included
vaind Dec 30, 2022
601d67a
feat: analyze collected metrics
vaind Dec 30, 2022
0df3970
metrics: ci configs, git & github utils (wip)
vaind Jan 2, 2023
468f6b2
ci: collect sdk metrics
vaind Jan 2, 2023
f7873b3
metrics collection timeout
vaind Jan 2, 2023
4df8bce
fix metrics collection on linux
vaind Jan 2, 2023
e38bbcc
await artifacts download
vaind Jan 2, 2023
49b4131
fix gh authentication
vaind Jan 2, 2023
940c072
metrics: add PR comment
vaind Jan 3, 2023
c8a71b7
improve pr comment
vaind Jan 3, 2023
78c6a94
metrics: switch from puppeteer to playwright
vaind Jan 3, 2023
cef616c
fix PR commenting
vaind Jan 3, 2023
9ee3943
first self-review
vaind Jan 3, 2023
6133883
fixes
vaind Jan 3, 2023
d4d7f3b
chore - metrics linter issues
vaind Jan 4, 2023
416ac54
more linter fixes
vaind Jan 4, 2023
d8297ec
metrics: improve PR comment contents
vaind Jan 4, 2023
6397225
metrics: improve comment
vaind Jan 4, 2023
2bfaa58
metrics: check stddev when collecting results
vaind Jan 4, 2023
432591e
metrics: filter outliers
vaind Jan 4, 2023
686a313
metrics: fix CPU usage collection
vaind Jan 5, 2023
743221a
tune cpu usage
vaind Jan 5, 2023
cedef00
perf sampler error handling
vaind Jan 5, 2023
7d1b2b4
metrics collection timeout issues
vaind Jan 5, 2023
ec925ab
metrics: fail on error logs
vaind Jan 5, 2023
be830af
move metrics CI job to build.yaml to get the full build cache
vaind Jan 5, 2023
4d0a36c
tune metrics collection
vaind Jan 5, 2023
3afa90b
ci metrics collection issues
vaind Jan 6, 2023
e713fab
tune CI metrics configs
vaind Jan 6, 2023
2078d63
more CI tuning
vaind Jan 6, 2023
0f2d1ac
docs
vaind Jan 6, 2023
d9b8244
show increase as ratio too
vaind Jan 6, 2023
b772b3e
fix: don't post PR comments on forks
vaind Jan 9, 2023
c3fcad9
feat: separate metrics measurements for sentry and sentry+replay
vaind Jan 11, 2023
bedeecf
fix and improve metrics analyzer console output
vaind Jan 12, 2023
77fa886
review changes
vaind Jan 13, 2023
ef05d06
metrics collector: improve close/dispose error handling
vaind Jan 13, 2023
355c4dc
collect metrics only if `ci-overhead-measurements` label is set on a PR
vaind Jan 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,51 @@ jobs:
if: contains(needs.*.result, 'failure')
run: |
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1

replay_metrics:
name: Replay Metrics
needs: [job_get_metadata, job_build]
runs-on: ubuntu-20.04
timeout-minutes: 30
if: contains(github.event.pull_request.labels.*.name, 'ci-overhead-measurements')
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v3
with:
ref: ${{ env.HEAD_COMMIT }}
- name: Set up Node
uses: volta-cli/action@v4
- name: Check dependency cache
uses: actions/cache@v3
with:
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
key: ${{ needs.job_build.outputs.dependency_cache_key }}
- name: Check build cache
uses: actions/cache@v3
with:
path: ${{ env.CACHED_BUILD_PATHS }}
key: ${{ env.BUILD_CACHE_KEY }}

- name: Setup
run: yarn install
working-directory: packages/replay/metrics

- name: Collect
run: yarn ci:collect
working-directory: packages/replay/metrics

- name: Process
id: process
run: yarn ci:process
working-directory: packages/replay/metrics
# Don't run on forks - the PR comment cannot be added.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Upload results
uses: actions/upload-artifact@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
with:
name: ${{ steps.process.outputs.artifactName }}
path: ${{ steps.process.outputs.artifactPath }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ tmp.js

# eslint
.eslintcache
eslintcache/*
**/eslintcache/*
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"name": "Debug replay metrics collection script",
"request": "launch",
"cwd": "${workspaceFolder}/packages/replay/metrics/",
"program": "${workspaceFolder}/packages/replay/metrics/configs/dev/collect.ts",
"preLaunchTask": "Build Replay metrics script",
},
{
"type": "node",
"name": "Debug replay metrics processing script",
"request": "launch",
"cwd": "${workspaceFolder}/packages/replay/metrics/",
"program": "${workspaceFolder}/packages/replay/metrics/configs/dev/process.ts",
"preLaunchTask": "Build Replay metrics script",
},
// Run rollup using the config file which is in the currently active tab.
{
"name": "Debug rollup (config from open file)",
Expand Down
8 changes: 7 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
"type": "npm",
"script": "predebug",
"path": "packages/nextjs/test/integration/",
"detail": "Link the SDK (if not already linked) and build test app"
"detail": "Link the SDK (if not already linked) and build test app",
},
{
"label": "Build Replay metrics script",
"type": "npm",
"script": "build",
"path": "packages/replay/metrics",
}
]
}
1 change: 1 addition & 0 deletions packages/replay/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ build/
demo/build/
# TODO: Check if we can re-introduce linting in demo
demo
metrics
14 changes: 14 additions & 0 deletions packages/replay/metrics/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
extends: ['../.eslintrc.js'],
ignorePatterns: ['test-apps'],
overrides: [
{
files: ['*.ts'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'import/no-unresolved': 'off',
},
},
],
};
1 change: 1 addition & 0 deletions packages/replay/metrics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
out
11 changes: 11 additions & 0 deletions packages/replay/metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Replay performance metrics

Evaluates Replay impact on website performance by running a web app in Chromium via Playwright and collecting various metrics.

The general idea is to run a web app without Sentry Replay and then run the same app again with Sentry and another one with Sentry+Replay included.
For the three scenarios, we collect some metrics (CPU, memory, vitals) and later compare them and post as a comment in a PR.
Changes in the metrics, compared to previous runs from the main branch, should be evaluated on case-by-case basis when preparing and reviewing the PR.

## Resources

* https://github.com/addyosmani/puppeteer-webperf
4 changes: 4 additions & 0 deletions packages/replay/metrics/configs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Replay metrics configuration & entrypoints (scripts)

* [dev](dev) contains scripts launched during local development
* [ci](ci) contains scripts launched in CI
54 changes: 54 additions & 0 deletions packages/replay/metrics/configs/ci/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Metrics, MetricsCollector } from '../../src/collector.js';
import { MetricsStats, NumberProvider } from '../../src/results/metrics-stats.js';
import { JankTestScenario } from '../../src/scenarios.js';
import { printStats } from '../../src/util/console.js';
import { latestResultFile } from './env.js';

function checkStdDev(results: Metrics[], name: string, provider: NumberProvider, max: number): boolean {
const value = MetricsStats.stddev(results, provider);
if (value == undefined) {
console.warn(`✗ | Discarding results because StandardDeviation(${name}) is undefined`);
return false;
} else if (value > max) {
console.warn(`✗ | Discarding results because StandardDeviation(${name}) is larger than ${max}. Actual value: ${value}`);
return false;
} else {
console.log(`✓ | StandardDeviation(${name}) is ${value} (<= ${max})`)
}
return true;
}

const collector = new MetricsCollector({ headless: true, cpuThrottling: 2 });
const result = await collector.execute({
name: 'jank',
scenarios: [
new JankTestScenario('index.html'),
new JankTestScenario('with-sentry.html'),
new JankTestScenario('with-replay.html'),
],
runs: 10,
tries: 10,
async shouldAccept(results: Metrics[]): Promise<boolean> {
await printStats(results);

if (!checkStdDev(results, 'lcp', MetricsStats.lcp, 50)
|| !checkStdDev(results, 'cls', MetricsStats.cls, 0.1)
|| !checkStdDev(results, 'cpu', MetricsStats.cpu, 1)
|| !checkStdDev(results, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024)
|| !checkStdDev(results, 'memory-max', MetricsStats.memoryMax, 1000 * 1024)) {
return false;
}

const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!;
if (cpuUsage > 0.85) {
// Note: complexity on the "JankTest" is defined by the `minimum = ...,` setting in app.js - specifying the number of animated elements.
console.warn(`✗ | Discarding results because CPU usage is too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`,
'Consider simplifying the scenario or changing the CPU throttling factor.');
return false;
}

return true;
},
});

result.writeToFile(latestResultFile);
4 changes: 4 additions & 0 deletions packages/replay/metrics/configs/ci/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const previousResultsDir = 'out/previous-results';
export const baselineResultsDir = 'out/baseline-results';
export const latestResultFile = 'out/latest-result.json';
export const artifactName = 'replay-sdk-metrics'
44 changes: 44 additions & 0 deletions packages/replay/metrics/configs/ci/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import path from 'path';

import { ResultsAnalyzer } from '../../src/results/analyzer.js';
import { PrCommentBuilder } from '../../src/results/pr-comment.js';
import { Result } from '../../src/results/result.js';
import { ResultsSet } from '../../src/results/results-set.js';
import { Git } from '../../src/util/git.js';
import { GitHub } from '../../src/util/github.js';
import { artifactName, baselineResultsDir, latestResultFile, previousResultsDir } from './env.js';

const latestResult = Result.readFromFile(latestResultFile);
const branch = await Git.branch;
const baseBranch = await Git.baseBranch;

await GitHub.downloadPreviousArtifact(baseBranch, baselineResultsDir, artifactName);
await GitHub.downloadPreviousArtifact(branch, previousResultsDir, artifactName);

GitHub.writeOutput('artifactName', artifactName)
GitHub.writeOutput('artifactPath', path.resolve(previousResultsDir));

const previousResults = new ResultsSet(previousResultsDir);

const prComment = new PrCommentBuilder();
if (baseBranch != branch) {
const baseResults = new ResultsSet(baselineResultsDir);
await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), 'Baseline');
await prComment.addAdditionalResultsSet(
`Baseline results on branch: <code>${baseBranch}</code>`,
// We skip the first one here because it's already included as `Baseline` column above in addCurrentResult().
baseResults.items().slice(1, 10)
);
} else {
await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), 'Previous');
}

await prComment.addAdditionalResultsSet(
`Previous results on branch: <code>${branch}</code>`,
previousResults.items().slice(0, 10)
);

await GitHub.addOrUpdateComment(prComment);

// Copy the latest test run results to the archived result dir.
await previousResults.add(latestResultFile, true);
30 changes: 30 additions & 0 deletions packages/replay/metrics/configs/dev/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Metrics, MetricsCollector } from '../../src/collector.js';
import { MetricsStats } from '../../src/results/metrics-stats.js';
import { JankTestScenario } from '../../src/scenarios.js';
import { printStats } from '../../src/util/console.js';
import { latestResultFile } from './env.js';

const collector = new MetricsCollector();
const result = await collector.execute({
name: 'dummy',
scenarios: [
new JankTestScenario('index.html'),
new JankTestScenario('with-sentry.html'),
new JankTestScenario('with-replay.html'),
],
runs: 1,
tries: 1,
async shouldAccept(results: Metrics[]): Promise<boolean> {
printStats(results);

const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!;
if (cpuUsage > 0.9) {
console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`,
'Consider simplifying the scenario or changing the CPU throttling factor.');
return false;
}
return true;
},
});

result.writeToFile(latestResultFile);
2 changes: 2 additions & 0 deletions packages/replay/metrics/configs/dev/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const outDir = 'out/results-dev';
export const latestResultFile = 'out/latest-result.json';
13 changes: 13 additions & 0 deletions packages/replay/metrics/configs/dev/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ResultsAnalyzer } from '../../src/results/analyzer.js';
import { Result } from '../../src/results/result.js';
import { ResultsSet } from '../../src/results/results-set.js';
import { printAnalysis } from '../../src/util/console.js';
import { latestResultFile, outDir } from './env.js';

const resultsSet = new ResultsSet(outDir);
const latestResult = Result.readFromFile(latestResultFile);

const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet);
printAnalysis(analysis);

await resultsSet.add(latestResultFile, true);
32 changes: 32 additions & 0 deletions packages/replay/metrics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"private": true,
"name": "metrics",
"main": "index.js",
"author": "Sentry",
"license": "MIT",
"type": "module",
"scripts": {
"build": "tsc",
"deps": "yarn --cwd ../ build:bundle && yarn --cwd ../../tracing/ build:bundle",
"dev:collect": "ts-node-esm ./configs/dev/collect.ts",
"dev:process": "ts-node-esm ./configs/dev/process.ts",
"ci:collect": "ts-node-esm ./configs/ci/collect.ts",
"ci:process": "ts-node-esm ./configs/ci/process.ts"
},
"dependencies": {
"@octokit/rest": "^19.0.5",
"@types/node": "^18.11.17",
"axios": "^1.2.2",
"extract-zip": "^2.0.1",
"filesize": "^10.0.6",
"p-timeout": "^6.0.0",
"playwright": "^1.29.1",
"playwright-core": "^1.29.1",
"simple-git": "^3.15.1",
"simple-statistics": "^7.8.0",
"typescript": "^4.9.4"
},
"devDependencies": {
"ts-node": "^10.9.1"
}
}
Loading