Page MenuHomeWickedGov Phorge

StatsUtils.php
No OneTemporary

Size
6 KB
Referenced Files
None
Subscribers
None

StatsUtils.php

<?php
/**
* @license GPL-2.0-or-later
* @file
*/
declare( strict_types=1 );
namespace Wikimedia\Stats;
use InvalidArgumentException;
use Wikimedia\Stats\Exceptions\InvalidConfigurationException;
/**
* Functionality common to all metric types.
*
* @author Cole White
* @since 1.38
*/
class StatsUtils {
public const RE_VALID_NAME_AND_LABEL_NAME = "/^[a-zA-Z_][a-zA-Z0-9_]*$/";
public const DEFAULT_SAMPLE_RATE = 1.0;
/**
* Validates the new sample rate. Throws InvalidArgumentException if provided an invalid rate.
*
* @param float $newSampleRate
* @throws InvalidArgumentException
*/
public static function validateNewSampleRate( float $newSampleRate ): void {
if ( $newSampleRate < 0.0 || $newSampleRate > 1.0 ) {
throw new InvalidArgumentException( "Sample rate can only be between 0.0 and 1.0. Got: " . $newSampleRate );
}
}
/**
* Returns a subset of samples based on configured sample rate.
*
* @param float $sampleRate
* @param array $samples
* @return array
*/
public static function getFilteredSamples( float $sampleRate, array $samples ): array {
if ( $sampleRate === 1.0 ) {
return $samples;
}
$output = [];
$randMax = mt_getrandmax();
foreach ( $samples as $sample ) {
if ( mt_rand() / $randMax < $sampleRate ) {
$output[] = $sample;
}
}
return $output;
}
/**
* Determines if provided string is a valid name.
*
* @param string $name
* @return void
* @throws InvalidArgumentException
* @throws InvalidConfigurationException
*/
public static function validateMetricName( string $name ) {
if ( $name === "" ) {
throw new InvalidArgumentException( "Stats: Metric name cannot be empty." );
}
if ( !preg_match( self::RE_VALID_NAME_AND_LABEL_NAME, $name ) ) {
throw new InvalidConfigurationException( "Invalid metric name: '" . $name . "'" );
}
}
/**
* Determines if provided string is a valid label key.
*
* @param string $key
* @return void
* @throws InvalidArgumentException
* @throws InvalidConfigurationException
*/
public static function validateLabelKey( string $key ) {
if ( $key === "" ) {
throw new InvalidArgumentException( "Stats: Label key cannot be empty." );
}
if ( !preg_match( self::RE_VALID_NAME_AND_LABEL_NAME, $key ) ) {
throw new InvalidConfigurationException( "Invalid label key: '" . $key . "'" );
}
}
public static function validateLabelValue( string $value ) {
if ( $value === "" ) {
throw new InvalidArgumentException( "Stats: Label value cannot be empty." );
}
}
/**
* Normalize an array of strings.
*
* @param string[] $entities
* @return string[]
*/
public static function normalizeArray( array $entities ): array {
$normalizedEntities = [];
foreach ( $entities as $entity ) {
$normalizedEntities[] = self::normalizeString( $entity );
}
return $normalizedEntities;
}
/**
* Normalize strings to a metrics-compatible format.
*
* Replace all other non-alphanumeric characters with an underscore.
* Trim leading or trailing underscores.
*
* Note: We are not using /i (case-insensitive flag)
* or \d (digit character class escape) here because
* their behavior changes with respect to locale settings.
*
* @param string $entity
* @return string
*/
public static function normalizeString( string $entity ): string {
$entity = preg_replace( '/[^a-zA-Z0-9]+/', '_', $entity );
return trim( $entity, '_' );
}
/**
* The E12 series
* @see https://en.wikipedia.org/wiki/E_series_of_preferred_numbers
*/
private const E12 = [
1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2, 10.0
];
/**
* Make a set of HistogramMetric buckets from a mean and skip value.
*
* Beware: this is for storing non-time data in histograms, like byte
* sizes, or time data outside of the range [5ms, 60s].
*
* Avoid changing the buckets once a metric has been deployed,
* as it may generate excessive churn.
*
* That said, this method quantizes the mean so modest shifts should
* maintain most buckets, and multiplying or dividing the "skip"
* by a small factor should also maintain commonality.
*
* The range of buckets for typical skips is roughly:
*
* $skip = 1: [0.5*mean, 2*mean]
* $skip = 2: [0.2*mean, 5*mean]
* $skip = 3: [0.1*mean, 10*mean]
* $skip = 4: [0.05*mean, 20*mean]
* $skip = 5: [0.02*mean, 50*mean]
* $skip = 6: [0.01*mean, 100*mean]
* ...
* $skip = 12: [0.001*mean, 10000*mean]
*
* @param float $mean The mean value expected.
* @param int $skip The range of values expected. With $skip = 1,
* each bucket will be greater than the last by a factor of 10^(1/12),
* which means 12 buckets per decade of range. This is the E12 series.
* With $skip = 2 we take every other bucket (6 buckets per decade),
* $skip = 3 means every third bucket (4 buckets per decade),
* $skip = 4 means every fourth bucket (3 buckets per decade), etc.
* The ::getTiming() metric effectively uses $skip = 4, which
* corresponds roughly to the usual `[0.1, 0.2, 0.5, 1]`
* progression, and that is the default value. As mentioned
* above, take great care when changing $skip on metrics already
* in production.
* @return float[] An array of 9 buckets, centered around the mean
*/
public static function makeBucketsFromMean( float $mean, int $skip ): array {
// assert $mean > 0 and $skip > 0
if ( $mean <= 0 ) {
throw new InvalidArgumentException( 'mean must be positive' );
}
if ( $skip < 1 ) {
throw new InvalidArgumentException( 'skip must be at least 1' );
}
// Find the appropriate starting location in the E12 series.
$pos = (int)round( log10( $mean ) * 12 );
// Further quantize $pos according to $skip, so changes in $mean
// don't shift all the buckets
$pos -= ( $pos % $skip );
// Compute buckets around the quantized starting position
// By using the E12 series and powers of ten our cutoffs will
// be compact (not too many digits) and consistent.
return array_map( static function ( $x ) use ( $pos, $skip ) {
$y = $pos + ( $x * $skip );
$rem = $y % 12;
if ( $rem < 0 ) {
$rem += 12;
}
$decade = intdiv( $y - $rem, 12 ); // floor($y/12)
// Use an explicit round() here to ensure float math doesn't create
// extra tiny variances.
return round( ( 10 ** $decade ) * self::E12[$rem], 1 - $decade );
}, [
// 9 buckets, centered around the (quantized) mean
-4, -3, -2, -1, 0, 1, 2, 3, 4
] );
}
}

File Metadata

Mime Type
text/x-php
Expires
Wed, Aug 19, 14:41 (3 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
44/ef/df73d7e4aad8d5a77c341a11e9db
Default Alt Text
StatsUtils.php (6 KB)

Event Timeline