@svta/cml-throughput 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/index.d.ts +99 -53
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,5 +30,19 @@ estimator.sample({
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
console.log(estimator.getEstimate());
|
|
33
|
-
//
|
|
33
|
+
// 285
|
|
34
34
|
```
|
|
35
|
+
|
|
36
|
+
## Samples
|
|
37
|
+
|
|
38
|
+
A sample needs an `encodedBodySize` above 0 and a `duration` above 0. The estimators ignore every other sample, and the estimate does not change.
|
|
39
|
+
|
|
40
|
+
A player can pass `PerformanceResourceTiming` entries as samples. For a cross-origin segment, the browser decides whether the entry has a body size:
|
|
41
|
+
|
|
42
|
+
| Browser | Body size of a cross-origin response |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Chrome | Exposed when the response has a `Timing-Allow-Origin` header for the page origin. Chrome plans the Firefox rule for version 158. |
|
|
45
|
+
| Firefox 129 and later | Exposed when the request used CORS. A `no-cors` request reports 0, even with the header. |
|
|
46
|
+
| Safari | 0 for every cross-origin response. |
|
|
47
|
+
|
|
48
|
+
An entry with a body size of 0 is ignored, so the estimate does not change. The byte count of the loader works in every browser. Build the sample from that count when the player has it.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
import { ResourceTiming } from "@svta/cml-utils";
|
|
2
|
-
|
|
3
2
|
//#region src/ThroughputEstimator.d.ts
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
|
-
* Throughput Estimator.
|
|
7
|
-
*
|
|
8
|
-
* @public
|
|
9
|
-
*/
|
|
4
|
+
* Throughput Estimator.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
10
8
|
type ThroughputEstimator = {
|
|
11
9
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
* Adds a sample to the throughput estimator.
|
|
11
|
+
* The estimator ignores a sample with an `encodedBodySize` or a `duration` at or below 0, `NaN`, or `Infinity`.
|
|
12
|
+
*
|
|
13
|
+
* @param sample - The timing sample to add
|
|
14
|
+
*/
|
|
16
15
|
sample(sample: ResourceTiming): void;
|
|
17
16
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
* Returns the current estimate of the throughput (in bits per second)
|
|
18
|
+
*
|
|
19
|
+
* @returns The current estimate of the throughput
|
|
20
|
+
*/
|
|
22
21
|
getEstimate(): number;
|
|
23
22
|
};
|
|
24
23
|
//#endregion
|
|
25
24
|
//#region src/ArithmeticMeanEstimator.d.ts
|
|
26
25
|
/**
|
|
27
|
-
* Arithmetic Mean throughput estimator
|
|
28
|
-
*
|
|
29
|
-
* @public
|
|
30
|
-
*/
|
|
31
|
-
declare class ArithmeticMeanEstimator implements ThroughputEstimator {
|
|
26
|
+
* Arithmetic Mean throughput estimator
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export declare class ArithmeticMeanEstimator implements ThroughputEstimator {
|
|
32
31
|
private samples;
|
|
33
32
|
sample(sample: ResourceTiming): void;
|
|
34
33
|
getEstimate(): number;
|
|
@@ -36,57 +35,104 @@ declare class ArithmeticMeanEstimator implements ThroughputEstimator {
|
|
|
36
35
|
//#endregion
|
|
37
36
|
//#region src/Ewma.d.ts
|
|
38
37
|
/**
|
|
39
|
-
* Exponential Weighted Moving Average (EWMA)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
* Exponential Weighted Moving Average (EWMA)
|
|
39
|
+
*
|
|
40
|
+
* Each sample has a weight, for example the duration of a download in seconds.
|
|
41
|
+
* A half-life of `h` weight units corresponds to an `alpha` of `Math.exp(Math.log(0.5) / h)`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* {@includeCode ../test/Ewma.test.ts#example}
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export declare class Ewma {
|
|
44
49
|
private alpha;
|
|
45
50
|
private estimate;
|
|
46
51
|
private totalDuration;
|
|
52
|
+
/**
|
|
53
|
+
* @param alpha - The smoothing factor, between 0 and 1. A larger `alpha` expires old samples more slowly.
|
|
54
|
+
*/
|
|
47
55
|
constructor(alpha: number);
|
|
48
56
|
/**
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
* Samples the value of the resource and updates the estimate.
|
|
58
|
+
*
|
|
59
|
+
* The new value has the weight `1 - alpha ^ weight`.
|
|
60
|
+
* The method ignores a sample that would make the estimate `NaN`.
|
|
61
|
+
*/
|
|
51
62
|
sample(weight: number, value: number): void;
|
|
52
63
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
* Returns the estimate of the value (in units, as `value` during sampling)
|
|
65
|
+
*
|
|
66
|
+
* For example, if the value is throughput (in bits per seconds), the estimate
|
|
67
|
+
* will also be in bits per seconds.
|
|
68
|
+
*/
|
|
58
69
|
getEstimate(): number;
|
|
59
70
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
* Returns the total duration of the resource (in units, as `weight` during sampling)
|
|
72
|
+
*/
|
|
62
73
|
getTotalDuration(): number;
|
|
63
74
|
}
|
|
64
75
|
//#endregion
|
|
65
76
|
//#region src/EwmaEstimatorOptions.d.ts
|
|
66
77
|
/**
|
|
67
|
-
* Exponential Weighted Moving Average (EWMA) Estimator Options
|
|
68
|
-
*
|
|
69
|
-
* @public
|
|
70
|
-
*/
|
|
78
|
+
* Exponential Weighted Moving Average (EWMA) Estimator Options
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
71
82
|
type EwmaEstimatorOptions = {
|
|
83
|
+
/**
|
|
84
|
+
* The estimate, in bits per second, that `getEstimate` returns before the first sample.
|
|
85
|
+
* `getEstimate` also returns this value while the sampled bytes are below `minTotalBytes`.
|
|
86
|
+
*
|
|
87
|
+
* @defaultValue `NaN`
|
|
88
|
+
*/
|
|
72
89
|
defaultEstimate?: number;
|
|
90
|
+
/**
|
|
91
|
+
* The number of bytes to sample before `getEstimate` returns a measured estimate.
|
|
92
|
+
* Below this total, `getEstimate` returns `defaultEstimate`.
|
|
93
|
+
* shaka-player uses 128000.
|
|
94
|
+
*
|
|
95
|
+
* @defaultValue `0`
|
|
96
|
+
*/
|
|
73
97
|
minTotalBytes?: number;
|
|
98
|
+
/**
|
|
99
|
+
* The smallest `encodedBodySize`, in bytes, that counts as a sample.
|
|
100
|
+
* A sample with an `encodedBodySize` of 0 never counts.
|
|
101
|
+
* The estimator ignores smaller responses, because the time to first byte dominates their duration.
|
|
102
|
+
* shaka-player uses 16000.
|
|
103
|
+
*
|
|
104
|
+
* @defaultValue `0`
|
|
105
|
+
*/
|
|
74
106
|
minBodySize?: number;
|
|
107
|
+
/**
|
|
108
|
+
* The half-life of the fast moving average, in seconds.
|
|
109
|
+
* Samples from the last `fastHalfLife` seconds have half of the weight in the estimate.
|
|
110
|
+
* shaka-player uses 2.
|
|
111
|
+
*/
|
|
75
112
|
fastHalfLife: number;
|
|
113
|
+
/**
|
|
114
|
+
* The half-life of the slow moving average, in seconds.
|
|
115
|
+
* Samples from the last `slowHalfLife` seconds have half of the weight in the estimate.
|
|
116
|
+
* shaka-player uses 5.
|
|
117
|
+
*/
|
|
76
118
|
slowHalfLife: number;
|
|
77
119
|
};
|
|
78
120
|
//#endregion
|
|
79
121
|
//#region src/EwmaEstimator.d.ts
|
|
80
122
|
/**
|
|
81
|
-
* Exponential Weighted Moving Average (EWMA) throughput estimator based on 2 half-lives
|
|
82
|
-
*
|
|
83
|
-
* @
|
|
84
|
-
|
|
85
|
-
|
|
123
|
+
* Exponential Weighted Moving Average (EWMA) throughput estimator based on 2 half-lives
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* {@includeCode ../test/EwmaEstimator.test.ts#example}
|
|
127
|
+
*
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
export declare class EwmaEstimator implements ThroughputEstimator {
|
|
86
131
|
private defaultEstimate;
|
|
87
132
|
private minTotalBytes;
|
|
88
133
|
private minBodySize;
|
|
89
134
|
private totalBytes;
|
|
135
|
+
private hasSamples;
|
|
90
136
|
private fastEwma;
|
|
91
137
|
private slowEwma;
|
|
92
138
|
constructor(options: EwmaEstimatorOptions);
|
|
@@ -96,11 +142,11 @@ declare class EwmaEstimator implements ThroughputEstimator {
|
|
|
96
142
|
//#endregion
|
|
97
143
|
//#region src/HarmonicMeanEstimator.d.ts
|
|
98
144
|
/**
|
|
99
|
-
* Harmonic Mean throughput estimator
|
|
100
|
-
*
|
|
101
|
-
* @public
|
|
102
|
-
*/
|
|
103
|
-
declare class HarmonicMeanEstimator implements ThroughputEstimator {
|
|
145
|
+
* Harmonic Mean throughput estimator
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
export declare class HarmonicMeanEstimator implements ThroughputEstimator {
|
|
104
150
|
private samples;
|
|
105
151
|
sample(sample: ResourceTiming): void;
|
|
106
152
|
getEstimate(): number;
|
|
@@ -108,15 +154,15 @@ declare class HarmonicMeanEstimator implements ThroughputEstimator {
|
|
|
108
154
|
//#endregion
|
|
109
155
|
//#region src/ZlemaEstimator.d.ts
|
|
110
156
|
/**
|
|
111
|
-
* Zero-Lag Exponential Moving Average (ZLEMA) throughput estimator
|
|
112
|
-
*
|
|
113
|
-
* @public
|
|
114
|
-
*/
|
|
115
|
-
declare class ZlemaEstimator implements ThroughputEstimator {
|
|
157
|
+
* Zero-Lag Exponential Moving Average (ZLEMA) throughput estimator
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
*/
|
|
161
|
+
export declare class ZlemaEstimator implements ThroughputEstimator {
|
|
116
162
|
private samples;
|
|
117
163
|
sample(sample: ResourceTiming): void;
|
|
118
164
|
getEstimate(): number;
|
|
119
165
|
}
|
|
120
166
|
//#endregion
|
|
121
|
-
export {
|
|
167
|
+
export type { EwmaEstimatorOptions, ThroughputEstimator };
|
|
122
168
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/ThroughputEstimator.ts","../src/ArithmeticMeanEstimator.ts","../src/Ewma.ts","../src/EwmaEstimatorOptions.ts","../src/EwmaEstimator.ts","../src/HarmonicMeanEstimator.ts","../src/ZlemaEstimator.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/ThroughputEstimator.ts","../src/ArithmeticMeanEstimator.ts","../src/Ewma.ts","../src/EwmaEstimatorOptions.ts","../src/EwmaEstimator.ts","../src/HarmonicMeanEstimator.ts","../src/ZlemaEstimator.ts"],"mappings":";;;;;;;KAOY;;;;;;;EAOX,OAAO,QAAQ;;;;;;EAOf;;;;;;;;;qBCXY,mCAAmC;UACvC;EAER,OAAc,QAAQ;EAQtB;;;;;;;;;;;;;;;qBCVY;UAEJ;UAEA;UAEA;;;;EAKR,YAAY;;;;;;;EAYZ,OAAc,gBAAgB;;;;;;;EAkB9B;;;;EAeA;;;;;;;;;KC9DW;;;;;;;EAOX;;;;;;;;EASA;;;;;;;;;EAUA;;;;;;EAOA;;;;;;EAOA;;;;;;;;;;;;qBC9BY,yBAAyB;UAC7B;UACA;UACA;UACA;UACA;UACA;UACA;EAER,YAAmB,SAAS;EAS5B,OAAc,QAAQ;EAgBtB;;;;;;;;;qBCvCY,iCAAiC;UACrC;EAER,OAAc,QAAQ;EAQtB;;;;;;;;;qBCXY,0BAA0B;UAC9B;EAER,OAAc,QAAQ;EAQtB"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { getBandwidthBps } from "@svta/cml-utils";
|
|
2
|
-
|
|
2
|
+
//#region src/isValidSample.ts
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when the sample has a finite `encodedBodySize` above 0 and a finite `duration` above 0.
|
|
5
|
+
*
|
|
6
|
+
* @param sample - The timing sample
|
|
7
|
+
* @returns Whether the sample counts as a throughput measurement
|
|
8
|
+
*/
|
|
9
|
+
function isValidSample(sample) {
|
|
10
|
+
const { encodedBodySize, duration } = sample;
|
|
11
|
+
return Number.isFinite(encodedBodySize) && encodedBodySize > 0 && Number.isFinite(duration) && duration > 0;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
3
14
|
//#region src/ArithmeticMeanEstimator.ts
|
|
4
15
|
/**
|
|
5
16
|
* Arithmetic Mean throughput estimator
|
|
@@ -11,6 +22,7 @@ var ArithmeticMeanEstimator = class {
|
|
|
11
22
|
this.samples = [];
|
|
12
23
|
}
|
|
13
24
|
sample(sample) {
|
|
25
|
+
if (!isValidSample(sample)) return;
|
|
14
26
|
this.samples.push(sample);
|
|
15
27
|
}
|
|
16
28
|
getEstimate() {
|
|
@@ -19,15 +31,23 @@ var ArithmeticMeanEstimator = class {
|
|
|
19
31
|
return value / this.samples.length;
|
|
20
32
|
}
|
|
21
33
|
};
|
|
22
|
-
|
|
23
34
|
//#endregion
|
|
24
35
|
//#region src/Ewma.ts
|
|
25
36
|
/**
|
|
26
37
|
* Exponential Weighted Moving Average (EWMA)
|
|
27
38
|
*
|
|
39
|
+
* Each sample has a weight, for example the duration of a download in seconds.
|
|
40
|
+
* A half-life of `h` weight units corresponds to an `alpha` of `Math.exp(Math.log(0.5) / h)`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* {@includeCode ../test/Ewma.test.ts#example}
|
|
44
|
+
*
|
|
28
45
|
* @public
|
|
29
46
|
*/
|
|
30
47
|
var Ewma = class {
|
|
48
|
+
/**
|
|
49
|
+
* @param alpha - The smoothing factor, between 0 and 1. A larger `alpha` expires old samples more slowly.
|
|
50
|
+
*/
|
|
31
51
|
constructor(alpha) {
|
|
32
52
|
this.alpha = alpha;
|
|
33
53
|
this.estimate = 0;
|
|
@@ -35,10 +55,15 @@ var Ewma = class {
|
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
37
57
|
* Samples the value of the resource and updates the estimate.
|
|
58
|
+
*
|
|
59
|
+
* The new value has the weight `1 - alpha ^ weight`.
|
|
60
|
+
* The method ignores a sample that would make the estimate `NaN`.
|
|
38
61
|
*/
|
|
39
62
|
sample(weight, value) {
|
|
40
63
|
const adjustedAlpha = Math.pow(this.alpha, weight);
|
|
41
|
-
|
|
64
|
+
const estimate = value * (1 - adjustedAlpha) + adjustedAlpha * this.estimate;
|
|
65
|
+
if (Number.isNaN(estimate)) return;
|
|
66
|
+
this.estimate = estimate;
|
|
42
67
|
this.totalDuration += weight;
|
|
43
68
|
}
|
|
44
69
|
/**
|
|
@@ -48,8 +73,11 @@ var Ewma = class {
|
|
|
48
73
|
* will also be in bits per seconds.
|
|
49
74
|
*/
|
|
50
75
|
getEstimate() {
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
if (this.alpha !== 0) {
|
|
77
|
+
const zeroFactor = 1 - Math.pow(this.alpha, this.totalDuration);
|
|
78
|
+
if (zeroFactor !== 0) return this.estimate / zeroFactor;
|
|
79
|
+
}
|
|
80
|
+
return this.estimate;
|
|
53
81
|
}
|
|
54
82
|
/**
|
|
55
83
|
* Returns the total duration of the resource (in units, as `weight` during sampling)
|
|
@@ -58,43 +86,44 @@ var Ewma = class {
|
|
|
58
86
|
return this.totalDuration;
|
|
59
87
|
}
|
|
60
88
|
};
|
|
61
|
-
|
|
62
89
|
//#endregion
|
|
63
90
|
//#region src/EwmaEstimator.ts
|
|
64
91
|
/**
|
|
65
92
|
* Exponential Weighted Moving Average (EWMA) throughput estimator based on 2 half-lives
|
|
66
93
|
*
|
|
94
|
+
* @example
|
|
95
|
+
* {@includeCode ../test/EwmaEstimator.test.ts#example}
|
|
96
|
+
*
|
|
67
97
|
* @public
|
|
68
98
|
*/
|
|
69
99
|
var EwmaEstimator = class {
|
|
70
100
|
constructor(options) {
|
|
71
101
|
this.totalBytes = 0;
|
|
102
|
+
this.hasSamples = false;
|
|
72
103
|
this.minTotalBytes = options.minTotalBytes || 0;
|
|
73
104
|
this.minBodySize = options.minBodySize || 0;
|
|
74
105
|
this.defaultEstimate = options.defaultEstimate ?? NaN;
|
|
75
|
-
this.slowEwma = new Ewma(options.slowHalfLife);
|
|
76
|
-
this.fastEwma = new Ewma(options.fastHalfLife);
|
|
77
|
-
if (!isNaN(this.defaultEstimate)) {
|
|
78
|
-
this.fastEwma.sample(1, this.defaultEstimate);
|
|
79
|
-
this.slowEwma.sample(1, this.defaultEstimate);
|
|
80
|
-
}
|
|
106
|
+
this.slowEwma = new Ewma(halfLifeToAlpha(options.slowHalfLife));
|
|
107
|
+
this.fastEwma = new Ewma(halfLifeToAlpha(options.fastHalfLife));
|
|
81
108
|
}
|
|
82
109
|
sample(sample) {
|
|
83
|
-
const { encodedBodySize
|
|
84
|
-
if (!
|
|
85
|
-
if (encodedBodySize < this.minBodySize) return;
|
|
110
|
+
const { encodedBodySize } = sample;
|
|
111
|
+
if (!isValidSample(sample) || encodedBodySize < this.minBodySize) return;
|
|
86
112
|
const durationSeconds = sample.duration / 1e3;
|
|
87
113
|
const bandwidthBps = getBandwidthBps(sample);
|
|
88
114
|
this.totalBytes += encodedBodySize;
|
|
115
|
+
this.hasSamples = true;
|
|
89
116
|
this.slowEwma.sample(durationSeconds, bandwidthBps);
|
|
90
117
|
this.fastEwma.sample(durationSeconds, bandwidthBps);
|
|
91
118
|
}
|
|
92
119
|
getEstimate() {
|
|
93
|
-
if (this.totalBytes < this.minTotalBytes) return this.defaultEstimate;
|
|
120
|
+
if (!this.hasSamples || this.totalBytes < this.minTotalBytes) return this.defaultEstimate;
|
|
94
121
|
return Math.floor(Math.min(this.fastEwma.getEstimate(), this.slowEwma.getEstimate()));
|
|
95
122
|
}
|
|
96
123
|
};
|
|
97
|
-
|
|
124
|
+
function halfLifeToAlpha(halfLife) {
|
|
125
|
+
return halfLife ? Math.exp(Math.log(.5) / halfLife) : 0;
|
|
126
|
+
}
|
|
98
127
|
//#endregion
|
|
99
128
|
//#region src/HarmonicMeanEstimator.ts
|
|
100
129
|
/**
|
|
@@ -107,6 +136,7 @@ var HarmonicMeanEstimator = class {
|
|
|
107
136
|
this.samples = [];
|
|
108
137
|
}
|
|
109
138
|
sample(sample) {
|
|
139
|
+
if (!isValidSample(sample)) return;
|
|
110
140
|
this.samples.push(sample);
|
|
111
141
|
}
|
|
112
142
|
getEstimate() {
|
|
@@ -115,7 +145,6 @@ var HarmonicMeanEstimator = class {
|
|
|
115
145
|
return this.samples.length / value;
|
|
116
146
|
}
|
|
117
147
|
};
|
|
118
|
-
|
|
119
148
|
//#endregion
|
|
120
149
|
//#region src/ZlemaEstimator.ts
|
|
121
150
|
/**
|
|
@@ -128,6 +157,7 @@ var ZlemaEstimator = class {
|
|
|
128
157
|
this.samples = [];
|
|
129
158
|
}
|
|
130
159
|
sample(sample) {
|
|
160
|
+
if (!isValidSample(sample)) return;
|
|
131
161
|
this.samples.push(sample);
|
|
132
162
|
}
|
|
133
163
|
getEstimate() {
|
|
@@ -142,7 +172,7 @@ var ZlemaEstimator = class {
|
|
|
142
172
|
return zlema;
|
|
143
173
|
}
|
|
144
174
|
};
|
|
145
|
-
|
|
146
175
|
//#endregion
|
|
147
176
|
export { ArithmeticMeanEstimator, Ewma, EwmaEstimator, HarmonicMeanEstimator, ZlemaEstimator };
|
|
177
|
+
|
|
148
178
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/isValidSample.ts","../src/ArithmeticMeanEstimator.ts","../src/Ewma.ts","../src/EwmaEstimator.ts","../src/HarmonicMeanEstimator.ts","../src/ZlemaEstimator.ts"],"sourcesContent":["import type { ResourceTiming } from '@svta/cml-utils'\n\n/**\n * Returns true when the sample has a finite `encodedBodySize` above 0 and a finite `duration` above 0.\n *\n * @param sample - The timing sample\n * @returns Whether the sample counts as a throughput measurement\n */\nexport function isValidSample(sample: ResourceTiming): boolean {\n\tconst { encodedBodySize, duration } = sample\n\n\treturn Number.isFinite(encodedBodySize) && encodedBodySize > 0 && Number.isFinite(duration) && duration > 0\n}\n","import type { ResourceTiming } from '@svta/cml-utils'\nimport { getBandwidthBps } from '@svta/cml-utils'\nimport { isValidSample } from './isValidSample.ts'\nimport type { ThroughputEstimator } from './ThroughputEstimator.ts'\n\n/**\n * Arithmetic Mean throughput estimator\n *\n * @public\n */\nexport class ArithmeticMeanEstimator implements ThroughputEstimator {\n\tprivate samples: ResourceTiming[] = []\n\n\tpublic sample(sample: ResourceTiming): void {\n\t\tif (!isValidSample(sample)) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.samples.push(sample)\n\t}\n\n\tpublic getEstimate(): number {\n\t\tlet value: number = 0\n\n\t\tfor (const sample of this.samples) {\n\t\t\tvalue += getBandwidthBps(sample)\n\t\t}\n\n\t\treturn value / this.samples.length\n\t}\n}\n","/**\n * Exponential Weighted Moving Average (EWMA)\n *\n * Each sample has a weight, for example the duration of a download in seconds.\n * A half-life of `h` weight units corresponds to an `alpha` of `Math.exp(Math.log(0.5) / h)`.\n *\n * @example\n * {@includeCode ../test/Ewma.test.ts#example}\n *\n * @public\n */\nexport class Ewma {\n\t// The smoothing factor for the EWMA, between 0 and 1\n\tprivate alpha: number\n\t// The current estimate (in units, as `value` during sampling)\n\tprivate estimate: number\n\t// The total weight of the samples (in units, as `weight` during sampling)\n\tprivate totalDuration: number\n\n\t/**\n\t * @param alpha - The smoothing factor, between 0 and 1. A larger `alpha` expires old samples more slowly.\n\t */\n\tconstructor(alpha: number) {\n\t\tthis.alpha = alpha\n\t\tthis.estimate = 0\n\t\tthis.totalDuration = 0\n\t}\n\n\t/**\n\t * Samples the value of the resource and updates the estimate.\n\t *\n\t * The new value has the weight `1 - alpha ^ weight`.\n\t * The method ignores a sample that would make the estimate `NaN`.\n\t */\n\tpublic sample(weight: number, value: number): void {\n\t\tconst adjustedAlpha = Math.pow(this.alpha, weight)\n\t\tconst estimate = value * (1 - adjustedAlpha) + adjustedAlpha * this.estimate\n\n\t\tif (Number.isNaN(estimate)) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.estimate = estimate\n\t\tthis.totalDuration += weight\n\t}\n\n\t/**\n\t * Returns the estimate of the value (in units, as `value` during sampling)\n\t *\n\t * For example, if the value is throughput (in bits per seconds), the estimate\n\t * will also be in bits per seconds.\n\t */\n\tpublic getEstimate(): number {\n\t\tif (this.alpha !== 0) {\n\t\t\tconst zeroFactor = 1 - Math.pow(this.alpha, this.totalDuration)\n\n\t\t\tif (zeroFactor !== 0) {\n\t\t\t\treturn this.estimate / zeroFactor\n\t\t\t}\n\t\t}\n\n\t\treturn this.estimate\n\t}\n\n\t/**\n\t * Returns the total duration of the resource (in units, as `weight` during sampling)\n\t */\n\tpublic getTotalDuration(): number {\n\t\treturn this.totalDuration\n\t}\n}\n","import type { ResourceTiming } from '@svta/cml-utils'\nimport { getBandwidthBps } from '@svta/cml-utils'\nimport { Ewma } from './Ewma.ts'\nimport type { EwmaEstimatorOptions } from './EwmaEstimatorOptions.ts'\nimport { isValidSample } from './isValidSample.ts'\nimport type { ThroughputEstimator } from './ThroughputEstimator.ts'\n\n/**\n * Exponential Weighted Moving Average (EWMA) throughput estimator based on 2 half-lives\n *\n * @example\n * {@includeCode ../test/EwmaEstimator.test.ts#example}\n *\n * @public\n */\nexport class EwmaEstimator implements ThroughputEstimator {\n\tprivate defaultEstimate: number\n\tprivate minTotalBytes: number\n\tprivate minBodySize: number\n\tprivate totalBytes: number = 0\n\tprivate hasSamples: boolean = false\n\tprivate fastEwma: Ewma\n\tprivate slowEwma: Ewma\n\n\tpublic constructor(options: EwmaEstimatorOptions) {\n\t\tthis.minTotalBytes = options.minTotalBytes || 0\n\t\tthis.minBodySize = options.minBodySize || 0\n\t\tthis.defaultEstimate = options.defaultEstimate ?? NaN\n\n\t\tthis.slowEwma = new Ewma(halfLifeToAlpha(options.slowHalfLife))\n\t\tthis.fastEwma = new Ewma(halfLifeToAlpha(options.fastHalfLife))\n\t}\n\n\tpublic sample(sample: ResourceTiming): void {\n\t\tconst { encodedBodySize } = sample\n\n\t\tif (!isValidSample(sample) || encodedBodySize < this.minBodySize) {\n\t\t\treturn\n\t\t}\n\n\t\tconst durationSeconds = sample.duration / 1000\n\t\tconst bandwidthBps = getBandwidthBps(sample)\n\t\tthis.totalBytes += encodedBodySize\n\t\tthis.hasSamples = true\n\n\t\tthis.slowEwma.sample(durationSeconds, bandwidthBps)\n\t\tthis.fastEwma.sample(durationSeconds, bandwidthBps)\n\t}\n\n\tpublic getEstimate(): number {\n\t\tif (!this.hasSamples || this.totalBytes < this.minTotalBytes) {\n\t\t\treturn this.defaultEstimate\n\t\t}\n\n\t\treturn Math.floor(Math.min(this.fastEwma.getEstimate(), this.slowEwma.getEstimate()))\n\t}\n}\n\nfunction halfLifeToAlpha(halfLife: number): number {\n\treturn halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0\n}\n","import type { ResourceTiming } from '@svta/cml-utils'\nimport { getBandwidthBps } from '@svta/cml-utils'\nimport { isValidSample } from './isValidSample.ts'\nimport type { ThroughputEstimator } from './ThroughputEstimator.ts'\n\n/**\n * Harmonic Mean throughput estimator\n *\n * @public\n */\nexport class HarmonicMeanEstimator implements ThroughputEstimator {\n\tprivate samples: ResourceTiming[] = []\n\n\tpublic sample(sample: ResourceTiming): void {\n\t\tif (!isValidSample(sample)) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.samples.push(sample)\n\t}\n\n\tpublic getEstimate(): number {\n\t\tlet value: number = 0\n\n\t\tfor (const sample of this.samples) {\n\t\t\tvalue += 1 / getBandwidthBps(sample)\n\t\t}\n\n\t\treturn this.samples.length / value\n\t}\n}\n","import type { ResourceTiming } from '@svta/cml-utils'\nimport { getBandwidthBps } from '@svta/cml-utils'\nimport { isValidSample } from './isValidSample.ts'\nimport type { ThroughputEstimator } from './ThroughputEstimator.ts'\n\n/**\n * Zero-Lag Exponential Moving Average (ZLEMA) throughput estimator\n *\n * @public\n */\nexport class ZlemaEstimator implements ThroughputEstimator {\n\tprivate samples: ResourceTiming[] = []\n\n\tpublic sample(sample: ResourceTiming): void {\n\t\tif (!isValidSample(sample)) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.samples.push(sample)\n\t}\n\n\tpublic getEstimate(): number {\n\t\tif (this.samples.length === 0) {\n\t\t\treturn NaN\n\t\t}\n\n\t\tconst alpha = 2 / (this.samples.length + 1)\n\n\t\tlet ema: number, zlema: number\n\n\t\tema = zlema = getBandwidthBps(this.samples[this.samples.length - 1])\n\n\t\tfor (const sample of this.samples) {\n\t\t\tema = alpha * getBandwidthBps(sample) + (1 - alpha) * ema\n\t\t\tzlema = alpha * ema + (1 - alpha) * zlema\n\t\t}\n\n\t\treturn zlema\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svta/cml-throughput",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Throughput estimation functionality",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@svta/cml-utils": "1.6.
|
|
54
|
+
"@svta/cml-utils": "^1.6.2"
|
|
55
55
|
}
|
|
56
56
|
}
|