Skip to content

Commit 495a31f

Browse files
committed
feat: analyze collected metrics
1 parent bb656b7 commit 495a31f

File tree

7 files changed

+200
-9
lines changed

7 files changed

+200
-9
lines changed
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js';
12
import { Result } from '../../src/results/result.js';
23
import { ResultsSet } from '../../src/results/results-set.js';
34
import { latestResultFile, outDir } from './env.js';
45

56
const resultsSet = new ResultsSet(outDir);
6-
77
const latestResult = Result.readFromFile(latestResultFile);
8-
console.log(latestResult);
98

10-
await resultsSet.add(latestResultFile);
9+
const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet);
10+
11+
const table: { [k: string]: any } = {};
12+
for (const item of analysis) {
13+
const printable: { [k: string]: any } = {};
14+
printable.value = item.value.asString();
15+
if (item.other != undefined) {
16+
printable.baseline = item.other.asString();
17+
}
18+
table[AnalyzerItemMetric[item.metric]] = printable;
19+
}
20+
console.table(table);
21+
22+
await resultsSet.add(latestResultFile, true);

packages/replay/metrics/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
},
1414
"dependencies": {
1515
"@types/node": "^18.11.17",
16+
"filesize": "^10.0.6",
1617
"puppeteer": "^19.4.1",
1718
"simple-git": "^3.15.1",
19+
"simple-statistics": "^7.8.0",
1820
"typescript": "^4.9.4"
1921
},
2022
"devDependencies": {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { GitHash } from '../util/git.js';
2+
import { Result } from './result.js';
3+
import { ResultsSet } from './results-set.js';
4+
import { MetricsStats } from './metrics-stats.js';
5+
import { filesize } from "filesize";
6+
7+
// Compares latest result to previous/baseline results and produces the needed
8+
// info.
9+
export class ResultsAnalyzer {
10+
public static analyze(currentResult: Result, baselineResults: ResultsSet): AnalyzerItem[] {
11+
const items = new ResultsAnalyzer(currentResult).collect();
12+
13+
const baseline = baselineResults.find(
14+
(other) => other.cpuThrottling == currentResult.cpuThrottling &&
15+
other.name == currentResult.name &&
16+
other.networkConditions == currentResult.networkConditions);
17+
18+
if (baseline != undefined) {
19+
const baseItems = new ResultsAnalyzer(baseline[1]).collect();
20+
// update items with baseline results
21+
for (const base of baseItems) {
22+
for (const item of items) {
23+
if (item.metric == base.metric) {
24+
item.other = base.value;
25+
item.otherHash = baseline[0];
26+
}
27+
}
28+
}
29+
}
30+
31+
return items;
32+
}
33+
34+
private constructor(private result: Result) { }
35+
36+
private collect(): AnalyzerItem[] {
37+
const items = new Array<AnalyzerItem>();
38+
39+
const aStats = new MetricsStats(this.result.aResults);
40+
const bStats = new MetricsStats(this.result.bResults);
41+
42+
const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number) {
43+
if (valueA == undefined || valueB == undefined) return;
44+
45+
items.push({
46+
metric: metric,
47+
value: {
48+
unit: unit,
49+
asDiff: () => valueB - valueA,
50+
asRatio: () => valueB / valueA,
51+
asString: () => {
52+
const diff = valueB - valueA;
53+
const prefix = diff >= 0 ? '+' : '';
54+
55+
switch (unit) {
56+
case AnalyzerItemUnit.bytes:
57+
return prefix + filesize(diff);
58+
case AnalyzerItemUnit.ratio:
59+
return prefix + (diff * 100).toFixed(2) + ' %';
60+
default:
61+
return prefix + diff.toFixed(2) + ' ' + AnalyzerItemUnit[unit];
62+
}
63+
}
64+
}
65+
})
66+
}
67+
68+
pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.lcp, bStats.lcp);
69+
pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.cls, bStats.cls);
70+
pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.cpu, bStats.cpu);
71+
pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.memoryAvg, bStats.memoryAvg);
72+
pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.memoryMax, bStats.memoryMax);
73+
74+
return items.filter((item) => item.value != undefined);
75+
}
76+
}
77+
78+
export enum AnalyzerItemUnit {
79+
ms,
80+
ratio, // 1.0 == 100 %
81+
bytes,
82+
}
83+
84+
export interface AnalyzerItemValue {
85+
unit: AnalyzerItemUnit;
86+
asString(): string;
87+
asDiff(): number;
88+
asRatio(): number; // 1.0 == 100 %
89+
}
90+
91+
export enum AnalyzerItemMetric {
92+
lcp,
93+
cls,
94+
cpu,
95+
memoryAvg,
96+
memoryMax,
97+
}
98+
99+
export interface AnalyzerItem {
100+
metric: AnalyzerItemMetric;
101+
value: AnalyzerItemValue;
102+
other?: AnalyzerItemValue;
103+
otherHash?: GitHash;
104+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Metrics } from '../collector';
2+
import * as ss from 'simple-statistics'
3+
4+
export type NumberProvider = (metrics: Metrics) => number;
5+
6+
export class MetricsStats {
7+
constructor(private items: Metrics[]) { }
8+
9+
// See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details
10+
public filterOutliers(dataProvider: NumberProvider): number[] {
11+
let numbers = this.items.map(dataProvider);
12+
// TODO implement, see https://github.com/getsentry/action-app-sdk-overhead-metrics/blob/9ce7d562ff79b317688d22bd5c0bb725cbdfdb81/src/test/kotlin/StartupTimeTest.kt#L27-L37
13+
return numbers;
14+
}
15+
16+
public filteredMean(dataProvider: NumberProvider): number | undefined {
17+
const numbers = this.filterOutliers(dataProvider);
18+
return numbers.length > 0 ? ss.mean(numbers) : undefined;
19+
}
20+
21+
public get lcp(): number | undefined {
22+
return this.filteredMean((metrics) => metrics.vitals.lcp);
23+
}
24+
25+
public get cls(): number | undefined {
26+
return this.filteredMean((metrics) => metrics.vitals.cls);
27+
}
28+
29+
public get cpu(): number | undefined {
30+
return this.filteredMean((metrics) => metrics.cpu.average);
31+
}
32+
33+
public get memoryAvg(): number | undefined {
34+
return this.filteredMean((metrics) => ss.mean(Array.from(metrics.memory.snapshots.values())));
35+
}
36+
37+
public get memoryMax(): number | undefined {
38+
const numbers = this.filterOutliers((metrics) => ss.max(Array.from(metrics.memory.snapshots.values())));
39+
return numbers.length > 0 ? ss.max(numbers) : undefined;
40+
}
41+
}

packages/replay/metrics/src/results/results-set.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import assert from 'assert';
22
import * as fs from 'fs';
33
import path from 'path';
4-
import { Git } from '../git.js';
4+
import { Git, GitHash } from '../util/git.js';
5+
import { Result } from './result.js';
56

67
const delimiter = '-';
78

@@ -16,7 +17,7 @@ export class ResultSetItem {
1617
return parseInt(this.parts[0]);
1718
}
1819

19-
public get hash(): string {
20+
public get hash(): GitHash {
2021
return this.parts[1];
2122
}
2223

@@ -38,23 +39,42 @@ export class ResultsSet {
3839
return this.items().length;
3940
}
4041

42+
public find(predicate: (value: Result) => boolean): [GitHash, Result] | undefined {
43+
const items = this.items();
44+
for (let i = 0; i < items.length; i++) {
45+
const result = Result.readFromFile(items[i].path);
46+
if (predicate(result)) {
47+
return [items[i].hash, result];
48+
}
49+
}
50+
return undefined;
51+
}
52+
4153
public items(): ResultSetItem[] {
4254
return this.files().map((file) => {
4355
return new ResultSetItem(path.join(this.directory, file.name));
4456
}).filter((item) => !isNaN(item.number));
4557
}
4658

47-
files(): fs.Dirent[] {
59+
private files(): fs.Dirent[] {
4860
return fs.readdirSync(this.directory, { withFileTypes: true }).filter((v) => v.isFile())
4961
}
5062

51-
public async add(newFile: string): Promise<void> {
63+
public async add(newFile: string, onlyIfDifferent: boolean = false): Promise<void> {
5264
console.log(`Preparing to add ${newFile} to ${this.directory}`);
5365
assert(fs.existsSync(newFile));
5466

55-
// Get the list of file sorted by the prefix number in the descending order.
67+
// Get the list of file sorted by the prefix number in the descending order (starting with the oldest files).
5668
const files = this.items().sort((a, b) => b.number - a.number);
5769

70+
if (onlyIfDifferent && files.length > 0) {
71+
const latestFile = files[files.length - 1];
72+
if (fs.readFileSync(latestFile.path, { encoding: 'utf-8' }) == fs.readFileSync(newFile, { encoding: 'utf-8' })) {
73+
console.log(`Skipping - it's already stored as ${latestFile.name}`);
74+
return;
75+
}
76+
}
77+
5878
// Rename all existing files, increasing the prefix
5979
for (const file of files) {
6080
const parts = file.name.split(delimiter);

packages/replay/metrics/src/git.ts renamed to packages/replay/metrics/src/util/git.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { simpleGit } from 'simple-git';
22

3+
export type GitHash = string;
4+
35
// A testing scenario we want to collect metrics for.
46
export const Git = {
5-
get hash(): Promise<string> {
7+
get hash(): Promise<GitHash> {
68
return (async () => {
79
const git = simpleGit();
810
let gitHash = await git.revparse('HEAD');

packages/replay/metrics/yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ fd-slicer@~1.1.0:
278278
dependencies:
279279
pend "~1.2.0"
280280

281+
filesize@^10.0.6:
282+
version "10.0.6"
283+
resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.6.tgz#5f4cd2721664cd925db3a7a5a87bbfd6ab5ebb1a"
284+
integrity sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g==
285+
281286
fs-constants@^1.0.0:
282287
version "1.0.0"
283288
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -521,6 +526,11 @@ simple-git@^3.15.1:
521526
"@kwsites/promise-deferred" "^1.1.1"
522527
debug "^4.3.4"
523528

529+
simple-statistics@^7.8.0:
530+
version "7.8.0"
531+
resolved "https://registry.yarnpkg.com/simple-statistics/-/simple-statistics-7.8.0.tgz#1033d2d613656c7bd34f0e134fd7e69c803e6836"
532+
integrity sha512-lTWbfJc0u6GZhBojLOrlHJMTHu6PdUjSsYLrpiH902dVBiYJyWlN/LdSoG8b5VvfG1D30gIBgarqMNeNmU5nAA==
533+
524534
string_decoder@^1.1.1:
525535
version "1.3.0"
526536
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"

0 commit comments

Comments
 (0)