Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.23% covered (success)
92.23%
95 / 103
73.33% covered (warning)
73.33%
11 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
WRStatsReader
92.23% covered (success)
92.23%
95 / 103
73.33% covered (warning)
73.33%
11 / 15
39.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 latest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 timeRange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRate
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getRates
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 fetch
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setCurrentTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resetCurrentTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 now
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 internalGetCount
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
10
 total
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 perSecond
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 perMinute
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 perHour
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 perDay
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\WRStats;
4
5/**
6 * Readers gather a batch of read operations, returning
7 * promises. The batch is executed when the first promise is resolved.
8 *
9 * @since 1.39
10 */
11class WRStatsReader {
12    /** @var StatsStore */
13    private $store;
14    /** @var array<string,MetricSpec> */
15    private $metricSpecs;
16    /** @var string[] */
17    private $prefixComponents;
18    /** @var float|int|null The UNIX timestamp used for the current time */
19    private $now;
20    /** @var bool[] Storage keys ready to be fetched */
21    private $queuedKeys = [];
22    /** @var int[] Unscaled integers returned by the store, indexed by key */
23    private $cachedValues = [];
24
25    /**
26     * @internal Use WRStatsFactory::createReader instead
27     * @param StatsStore $store
28     * @param array<string,array> $specs
29     * @param string|string[] $prefix
30     */
31    public function __construct( StatsStore $store, $specs, $prefix ) {
32        $this->store = $store;
33        $this->metricSpecs = [];
34        foreach ( $specs as $name => $spec ) {
35            $this->metricSpecs[$name] = new MetricSpec( $spec );
36        }
37        $this->prefixComponents = is_array( $prefix ) ? $prefix : [ $prefix ];
38        if ( !count( $this->prefixComponents ) ) {
39            throw new WRStatsError( __METHOD__ .
40                ': there must be at least one prefix component' );
41        }
42    }
43
44    /**
45     * Get a TimeRange for some period ending at the current time. Note that
46     * this will use the same value of the current time for subsequent calls
47     * until resetCurrentTime() is called.
48     *
49     * @param int|float $numSeconds
50     * @return TimeRange
51     */
52    public function latest( $numSeconds ) {
53        $now = $this->now();
54        return new TimeRange( $now - $numSeconds, $now );
55    }
56
57    /**
58     * Get a specified time range
59     *
60     * @param int|float $start The UNIX time of the start of the range
61     * @param int|float $end The UNIX time of the end of the range
62     * @return TimeRange
63     */
64    public function timeRange( $start, $end ) {
65        return new TimeRange( $start, $end );
66    }
67
68    /**
69     * Queue a fetch operation.
70     *
71     * @param string $metricName The metric name, the key into $specs.
72     * @param EntityKey|null $entity Additional storage key components
73     * @param TimeRange $range The time range to fetch
74     * @return RatePromise
75     */
76    public function getRate( $metricName, ?EntityKey $entity, TimeRange $range ) {
77        $metricSpec = $this->metricSpecs[$metricName] ?? null;
78        if ( $metricSpec === null ) {
79            throw new WRStatsError( "Unrecognised metric \"$metricName\"" );
80        }
81        $entity ??= new LocalEntityKey;
82        $now = $this->now();
83        $seqSpec = null;
84        foreach ( $metricSpec->sequences as $seqSpec ) {
85            $seqStart = $now - $seqSpec->softExpiry;
86            if ( $seqStart <= $range->start ) {
87                break;
88            }
89        }
90        $timeStep = $seqSpec->timeStep;
91        $firstBucket = (int)( $range->start / $timeStep );
92        $lastBucket = (int)ceil( $range->end / $timeStep );
93        for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
94            $key = $this->store->makeKey(
95                $this->prefixComponents,
96                [ $metricName, $seqSpec->name, $bucket ],
97                $entity
98            );
99            if ( !isset( $this->cachedValues[$key] ) ) {
100                $this->queuedKeys[$key] = true;
101            }
102        }
103        return new RatePromise( $this, $metricName, $entity, $metricSpec, $seqSpec, $range );
104    }
105
106    /**
107     * Queue a batch of fetch operations for different metrics with the same
108     * time range.
109     *
110     * @param string[] $metricNames
111     * @param EntityKey|null $entity
112     * @param TimeRange $range
113     * @return RatePromise[]
114     */
115    public function getRates( $metricNames, ?EntityKey $entity, TimeRange $range ) {
116        $rates = [];
117        foreach ( $metricNames as $name ) {
118            $rates[$name] = $this->getRate( $name, $entity, $range );
119        }
120        return $rates;
121    }
122
123    /**
124     * Perform any queued fetch operations.
125     */
126    public function fetch() {
127        if ( !$this->queuedKeys ) {
128            return;
129        }
130        $this->cachedValues += $this->store->query( array_keys( $this->queuedKeys ) );
131        $this->queuedKeys = [];
132    }
133
134    /**
135     * Set the current time to be used in latest() etc.
136     *
137     * @param int|float $now
138     */
139    public function setCurrentTime( $now ) {
140        $this->now = $now;
141    }
142
143    /**
144     * Clear the current time so that it will be filled with the real current
145     * time on the next call.
146     */
147    public function resetCurrentTime() {
148        $this->now = null;
149    }
150
151    /**
152     * @return float|int
153     */
154    private function now() {
155        $this->now ??= microtime( true );
156        return $this->now;
157    }
158
159    /**
160     * @internal Utility for resolution in RatePromise
161     * @param string $metricName
162     * @param EntityKey $entity
163     * @param MetricSpec $metricSpec
164     * @param SequenceSpec $seqSpec
165     * @param TimeRange $range
166     * @return float|int
167     */
168    public function internalGetCount(
169        $metricName,
170        EntityKey $entity,
171        MetricSpec $metricSpec,
172        SequenceSpec $seqSpec,
173        TimeRange $range
174    ) {
175        $this->fetch();
176        $timeStep = $seqSpec->timeStep;
177        $firstBucket = (int)( $range->start / $timeStep );
178        $lastBucket = (int)( $range->end / $timeStep );
179        $now = $this->now();
180        $total = 0;
181        for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
182            $key = $this->store->makeKey(
183                $this->prefixComponents,
184                [ $metricName, $seqSpec->name, $bucket ],
185                $entity
186            );
187            $value = $this->cachedValues[$key] ?? 0;
188            if ( !$value ) {
189                continue;
190            } elseif ( $bucket === $firstBucket ) {
191                if ( $bucket === $lastBucket ) {
192                    // It can be assumed that there are zero events in the future
193                    $bucketStartTime = $bucket * $timeStep;
194                    $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
195                    $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
196                    if ( $interpolationDuration > 0 ) {
197                        $total += $value * $range->getDuration() / $interpolationDuration;
198                    }
199                } else {
200                    $overlapDuration = max( ( $bucket + 1 ) * $timeStep - $range->start, 0 );
201                    $total += $value * $overlapDuration / $timeStep;
202                }
203            } elseif ( $bucket === $lastBucket ) {
204                // It can be assumed that there are zero events in the future
205                $bucketStartTime = $bucket * $timeStep;
206                $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
207                $overlapDuration = max( $range->end - $bucketStartTime, 0 );
208                $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
209                if ( $overlapDuration === $interpolationDuration ) {
210                    // Special case for 0/0 -- current time exactly on boundary.
211                    $total += $value;
212                } elseif ( $interpolationDuration > 0 ) {
213                    $total += $value * $overlapDuration / $interpolationDuration;
214                }
215            } else {
216                $total += $value;
217            }
218        }
219        // Round to nearest resolution step for nicer display
220        $rounded = round( $total ) * $metricSpec->resolution;
221        // Convert to integer if integer is expected
222        if ( is_int( $metricSpec->resolution ) ) {
223            $rounded = (int)$rounded;
224        }
225        return $rounded;
226    }
227
228    /**
229     * Resolve a batch of RatePromise objects, returning their counter totals,
230     * indexed as in the input array.
231     *
232     * @param array<mixed,RatePromise> $rates
233     * @return array<mixed,float|int>
234     */
235    public function total( $rates ) {
236        $result = [];
237        foreach ( $rates as $key => $rate ) {
238            $result[$key] = $rate->total();
239        }
240        return $result;
241    }
242
243    /**
244     * Resolve a batch of RatePromise objects, returning their per-second rates.
245     *
246     * @param array<mixed,RatePromise> $rates
247     * @return array<mixed,float>
248     */
249    public function perSecond( $rates ) {
250        $result = [];
251        foreach ( $rates as $key => $rate ) {
252            $result[$key] = $rate->perSecond();
253        }
254        return $result;
255    }
256
257    /**
258     * Resolve a batch of RatePromise objects, returning their per-minute rates.
259     *
260     * @param array<mixed,RatePromise> $rates
261     * @return array<mixed,float>
262     */
263    public function perMinute( $rates ) {
264        $result = [];
265        foreach ( $rates as $key => $rate ) {
266            $result[$key] = $rate->perMinute();
267        }
268        return $result;
269    }
270
271    /**
272     * Resolve a batch of RatePromise objects, returning their per-hour rates.
273     *
274     * @param array<mixed,RatePromise> $rates
275     * @return array<mixed,float>
276     */
277    public function perHour( $rates ) {
278        $result = [];
279        foreach ( $rates as $key => $rate ) {
280            $result[$key] = $rate->perHour();
281        }
282        return $result;
283    }
284
285    /**
286     * Resolve a batch of RatePromise objects, returning their per-day rates.
287     *
288     * @param array<mixed,RatePromise> $rates
289     * @return array<mixed,float>
290     */
291    public function perDay( $rates ) {
292        $result = [];
293        foreach ( $rates as $key => $rate ) {
294            $result[$key] = $rate->perDay();
295        }
296        return $result;
297    }
298}