chrome-devtools-frontend
Version:
Chrome DevTools UI
102 lines (90 loc) • 4.1 kB
text/typescript
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Lifted from Lighthouse: https://github.com/GoogleChrome/lighthouse/blob/36cac182a6c637b1671c57326d7c0241633d0076/shared/statistics.js
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// The exact double values for the max and min scores possible in each range.
const MIN_PASSING_SCORE = 0.90000000000000002220446049250313080847263336181640625;
const MAX_AVERAGE_SCORE = 0.899999999999999911182158029987476766109466552734375;
const MIN_AVERAGE_SCORE = 0.5;
const MAX_FAILING_SCORE = 0.499999999999999944488848768742172978818416595458984375;
/**
* Approximates the Gauss error function, the probability that a random variable
* from the standard normal distribution lies within [-x, x]. Moved from
* traceviewer.b.math.erf, based on Abramowitz and Stegun, formula 7.1.26.
*/
function erf(x: number): number {
// erf(-x) = -erf(x);
const sign = Math.sign(x);
x = Math.abs(x);
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const t = 1 / (1 + p * x);
const y = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5))));
return sign * (1 - y * Math.exp(-x * x));
}
/**
* Returns the score (1 - percentile) of `value` in a log-normal distribution
* specified by the `median` value, at which the score will be 0.5, and a 10th
* percentile value, at which the score will be 0.9. The score represents the
* amount of the distribution greater than `value`. All values should be in the
* same units (e.g. milliseconds). See
* https://www.desmos.com/calculator/o98tbeyt1t
* for an interactive view of the relationship between these parameters and the
* typical parameterization (location and shape) of the log-normal distribution.
*/
export function getLogNormalScore({median, p10}: {median: number, p10: number}, value: number): number {
// Required for the log-normal distribution.
if (median <= 0) {
throw new Error('median must be greater than zero');
}
if (p10 <= 0) {
throw new Error('p10 must be greater than zero');
}
// Not strictly required, but if p10 > median, it flips around and becomes the p90 point.
if (p10 >= median) {
throw new Error('p10 must be less than the median');
}
// Non-positive values aren't in the distribution, so always 1.
if (value <= 0) {
return 1;
}
// Closest double to `erfc-1(1/5)`.
const INVERSE_ERFC_ONE_FIFTH = 0.9061938024368232;
// Shape (σ) is `|log(p10/median) / (sqrt(2)*erfc^-1(1/5))|` and
// standardizedX is `1/2 erfc(log(value/median) / (sqrt(2)*σ))`, so simplify a bit.
const xRatio = Math.max(Number.MIN_VALUE, value / median); // value and median are > 0, so is ratio.
const xLogRatio = Math.log(xRatio);
const p10Ratio = Math.max(Number.MIN_VALUE, p10 / median); // p10 and median are > 0, so is ratio.
const p10LogRatio = -Math.log(p10Ratio); // negate to keep σ positive.
const standardizedX = xLogRatio * INVERSE_ERFC_ONE_FIFTH / p10LogRatio;
const complementaryPercentile = (1 - erf(standardizedX)) / 2;
// Clamp to avoid floating-point out-of-bounds issues and keep score in expected range.
let score;
if (value <= p10) {
// Passing. Clamp to [0.9, 1].
score = Math.max(MIN_PASSING_SCORE, Math.min(1, complementaryPercentile));
} else if (value <= median) {
// Average. Clamp to [0.5, 0.9).
score = Math.max(MIN_AVERAGE_SCORE, Math.min(MAX_AVERAGE_SCORE, complementaryPercentile));
} else {
// Failing. Clamp to [0, 0.5).
score = Math.max(0, Math.min(MAX_FAILING_SCORE, complementaryPercentile));
}
return score;
}
/**
* Interpolates the y value at a point x on the line defined by (x0, y0) and (x1, y1)
*/
export function linearInterpolation(x0: number, y0: number, x1: number, y1: number, x: number): number {
const slope = (y1 - y0) / (x1 - x0);
return y0 + (x - x0) * slope;
}