wiki.techinc.nl/tests/phpunit/unit/includes/libs/WRStats/WRStatsReaderTest.php
Tim Starling bcbfc9ccfc Introduce new WRStats library for write-read stats
A library for storage of counter value time series statistics, based
around the observation that memcached getMulti() is apparently fast
enough to do this in a simple manner, with incremented values stored
in time window buckets.

Bug: T310662
Change-Id: I26b1cdba0a06ad16ad8bb71b455e1b6180924d17
2022-07-05 10:35:19 +10:00

310 lines
7.3 KiB
PHP

<?php
namespace Wikimedia\WRStats;
use PHPUnit\Framework\TestCase;
/**
* @covers \Wikimedia\WRStats\WRStatsReader
* @covers \Wikimedia\WRStats\SequenceSpec
* @covers \Wikimedia\WRStats\TimeRange
* @covers \Wikimedia\WRStats\RatePromise
* @covers \Wikimedia\WRStats\ArrayStatsStore
*/
class WRStatsReaderTest extends TestCase {
private $store;
public function testLatest() {
$reader = $this->createReader( [] );
$latest = $reader->latest( 60 );
$this->assertInstanceOf( TimeRange::class, $latest );
$this->assertSame( 1940, $latest->start );
$this->assertSame( 2000, $latest->end );
$this->assertSame( 60, $latest->getDuration() );
}
private function getStore() {
if ( $this->store === null ) {
$this->store = new ArrayStatsStore;
}
return $this->store;
}
private function createReader( $specs ) {
$reader = new WRStatsReader(
$this->getStore(),
$specs,
'prefix'
);
$reader->setCurrentTime( 2000 );
return $reader;
}
private function createWriter( $specs ) {
$writer = new WRStatsWriter(
$this->getStore(),
$specs,
'prefix'
);
$writer->setCurrentTime( 1000 );
return $writer;
}
public function testGetRate() {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 10,
'expiry' => 1000,
] ]
]
];
$writer = $this->createWriter( $specs );
$writer->incr( 'test' );
$writer->flush();
$reader = $this->createReader( $specs );
$reader->setCurrentTime( 1000.001 );
$rate = $reader->getRate( 'test', null, $reader->latest( 10 ) );
$this->assertInstanceOf( RatePromise::class, $rate );
$this->assertSame( 1, $rate->total() );
}
public static function provideInterpolationFlatnessMagic() {
return [
[ 0.5 ],
[ 1 ],
[ 1.1 ],
[ 2 ],
[ 3 ],
];
}
/**
* If the event rate is constant then the range offset w.r.t. the start of
* the window doesn't matter, the interpolated rate will be constant.
* @dataProvider provideInterpolationFlatnessMagic
*/
public function testInterpolationFlatnessMagic( $rangeDuration ) {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 1,
'expiry' => 1000,
] ],
'resolution' => 0.001
]
];
$writer = $this->createWriter( $specs );
for ( $t = 0; $t < 10; $t++ ) {
$writer->setCurrentTime( 1000 + $t );
$writer->incr( 'test' );
}
$writer->flush();
$reader = $this->createReader( $specs );
for ( $timeDeciSeconds = 10000; $timeDeciSeconds <= 10011; $timeDeciSeconds++ ) {
$range = $reader->timeRange(
$timeDeciSeconds / 10,
$timeDeciSeconds / 10 + $rangeDuration
);
$rate = $reader->getRate( 'test', null, $range );
$this->assertEqualsWithDelta(
$rangeDuration,
$rate->total(),
1e-9,
"Flatness at t = {$range->start}"
);
}
}
public static function provideInterpolationLinearity() {
return [
[ 1000.0, 1.0 ],
[ 1000.1, 1.1 ],
[ 1000.9, 1.9 ],
[ 1001.0, 2.0 ]
];
}
/**
* Test interpolation involving two unequal buckets
* @dataProvider provideInterpolationLinearity
*/
public function testInterpolationLinearity( $start, $expected ) {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 1,
'expiry' => 1000,
] ],
'resolution' => 0.001
]
];
$writer = $this->createWriter( $specs );
$writer->setCurrentTime( 1000 );
$writer->incr( 'test' );
$writer->setCurrentTime( 1001 );
$writer->incr( 'test', null, 2 );
$writer->flush();
$reader = $this->createReader( $specs );
$range = $reader->timeRange( $start, $start + 1 );
$rate = $reader->getRate( 'test', null, $range );
$this->assertEqualsWithDelta( $expected, $rate->total(), 1e-9 );
}
public static function provideCurrentTimeAdjustment() {
return [
[ 1000.1, 1000.1, 1 ],
[ 1000.2, 1000.1, 0.5 ],
[ 1000.5, 1000.1, 0.2 ],
[ 1001, 1000.1, 0.1 ],
[ 1002, 1000.1, 0.1 ],
];
}
/**
* Test the interpolation adjustment which occurs when the reader's current
* time falls within the last relevant bucket
*
* @dataProvider provideCurrentTimeAdjustment
*/
public function testCurrentTimeAdjustment( $currentTime, $rangeEnd, $expected ) {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 1,
'expiry' => 1000,
] ],
'resolution' => 0.001
]
];
$writer = $this->createWriter( $specs );
$writer->incr( 'test' );
$writer->flush();
$reader = $this->createReader( $specs );
$reader->setCurrentTime( $currentTime );
$range = $reader->timeRange( 999, $rangeEnd );
$rate = $reader->getRate( 'test', null, $range );
$this->assertEqualsWithDelta( $expected, $rate->total(), 1e-9 );
}
/**
* When the metric has multiple sequences, we select the one with the
* shortest expiry which contains the whole range
*/
public function testMultiSequence() {
$specs = [
'test' => [
'sequences' => [
[
'timeStep' => 1,
'expiry' => 10,
],
[
'timeStep' => 10,
'expiry' => 100,
]
],
'resolution' => 0.001
]
];
$store = $this->getStore();
$store->incr(
[
'local:prefix:test::1999' => 1000,
'local:prefix:test:s1:199' => 555000
],
1000
);
$reader = $this->createReader( $specs );
$range = $reader->timeRange( 1990, 2000 );
$rate = $reader->getRate( 'test', null, $range );
$this->assertSame( 1.0, $rate->total() );
$range = $reader->timeRange( 1900, 2000 );
$rate = $reader->getRate( 'test', null, $range );
$this->assertSame( 555.0, $rate->total() );
}
/**
* Test multiple entity keys
*/
public function testEntityKey() {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 1,
'expiry' => 1000,
] ],
'resolution' => 1
]
];
$writer = $this->createWriter( $specs );
$entity1 = new GlobalEntityKey( [ 'entity', 1 ] );
$entity2 = new GlobalEntityKey( [ 'entity', 3 ] );
$writer->incr( 'test', $entity1, 1 );
$writer->incr( 'test', $entity2, 2 );
$writer->flush();
$reader = $this->createReader( $specs );
$range = $reader->timeRange( 1000, 1001 );
$rate0 = $reader->getRate( 'test', null, $range );
$this->assertSame( 0, $rate0->total() );
$rate1 = $reader->getRate( 'test', $entity1, $range );
$this->assertSame( 1, $rate1->total() );
$rate2 = $reader->getRate( 'test', $entity2, $range );
$this->assertSame( 2, $rate2->total() );
}
/**
* Test the batch rate resolve functions total() and per*()
*/
public function testBatchResolve() {
$specs = [
'test' => [
'sequences' => [ [
'timeStep' => 10,
'expiry' => 1000,
] ],
'resolution' => 0.001
]
];
$writer = $this->createWriter( $specs );
$writer->incr( 'test', null, 10 );
$writer->flush();
$reader = $this->createReader( $specs );
$reader->setCurrentTime( 1010 );
$rates = [
1 => $reader->getRate( 'test', null, $reader->latest( 1 ) ),
2 => $reader->getRate( 'test', null, $reader->latest( 2 ) ),
3 => $reader->getRate( 'test', null, $reader->latest( 3 ) ),
];
$this->assertSame(
[ 1 => 1.0, 2 => 2.0, 3 => 3.0 ],
$reader->total( $rates )
);
$this->assertSame(
[ 1 => 1.0, 2 => 1.0, 3 => 1.0 ],
$reader->perSecond( $rates )
);
$this->assertSame(
[ 1 => 60.0, 2 => 60.0, 3 => 60.0 ],
$reader->perMinute( $rates )
);
$this->assertSame(
[ 1 => 3600.0, 2 => 3600.0, 3 => 3600.0 ],
$reader->perHour( $rates )
);
$this->assertSame(
[ 1 => 86400.0, 2 => 86400.0, 3 => 86400.0 ],
$reader->perDay( $rates )
);
}
}