MediaWiki REL1_39
WRStatsReader.php
Go to the documentation of this file.
1<?php
2
3namespace Wikimedia\WRStats;
4
13 private $store;
15 private $metricSpecs;
17 private $prefixComponents;
19 private $now;
21 private $queuedKeys = [];
23 private $cachedValues = [];
24
32 public function __construct( StatsStore $store, $specs, $prefix ) {
33 $this->store = $store;
34 $this->metricSpecs = [];
35 foreach ( $specs as $name => $spec ) {
36 $this->metricSpecs[$name] = new MetricSpec( $spec );
37 }
38 $this->prefixComponents = is_array( $prefix ) ? $prefix : [ $prefix ];
39 if ( !count( $this->prefixComponents ) ) {
40 throw new WRStatsError( __METHOD__ .
41 ': there must be at least one prefix component' );
42 }
43 }
44
53 public function latest( $numSeconds ) {
54 $now = $this->now();
55 return new TimeRange( $now - $numSeconds, $now );
56 }
57
65 public function timeRange( $start, $end ) {
66 return new TimeRange( $start, $end );
67 }
68
77 public function getRate( $metricName, ?EntityKey $entity, TimeRange $range ) {
78 $metricSpec = $this->metricSpecs[$metricName] ?? null;
79 if ( $metricSpec === null ) {
80 throw new WRStatsError( __METHOD__ . ": Unrecognised metric \"$metricName\"" );
81 }
82 $entity = $entity ?? new LocalEntityKey;
83 $now = $this->now();
84 $seqSpec = null;
85 foreach ( $metricSpec->sequences as $seqSpec ) {
86 $seqStart = $now - $seqSpec->softExpiry;
87 if ( $seqStart <= $range->start ) {
88 break;
89 }
90 }
91
92 if ( !$seqSpec ) {
93 // This check exists to make Phan happy.
94 // It should never fail since we apply normalization in MetricSpec::__construct()
95 throw new WRStatsError( 'There should have been at least one sequence' );
96 }
97
98 $timeStep = $seqSpec->timeStep;
99 $firstBucket = (int)( $range->start / $timeStep );
100 $lastBucket = (int)ceil( $range->end / $timeStep );
101 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
102 $key = $this->store->makeKey(
103 $this->prefixComponents,
104 [ $metricName, $seqSpec->name, $bucket ],
105 $entity
106 );
107 if ( !isset( $this->cachedValues[$key] ) ) {
108 $this->queuedKeys[$key] = true;
109 }
110 }
111 return new RatePromise( $this, $metricName, $entity, $metricSpec, $seqSpec, $range );
112 }
113
123 public function getRates( $metricNames, ?EntityKey $entity, TimeRange $range ) {
124 $rates = [];
125 foreach ( $metricNames as $name ) {
126 $rates[$name] = $this->getRate( $name, $entity, $range );
127 }
128 return $rates;
129 }
130
134 public function fetch() {
135 if ( !$this->queuedKeys ) {
136 return;
137 }
138 $this->cachedValues += $this->store->query( array_keys( $this->queuedKeys ) );
139 $this->queuedKeys = [];
140 }
141
147 public function setCurrentTime( $now ) {
148 $this->now = $now;
149 }
150
155 public function resetCurrentTime() {
156 $this->now = null;
157 }
158
162 private function now() {
163 if ( $this->now === null ) {
164 $this->now = microtime( true );
165 }
166 return $this->now;
167 }
168
179 public function internalGetCount(
180 $metricName,
181 EntityKey $entity,
182 MetricSpec $metricSpec,
183 SequenceSpec $seqSpec,
184 TimeRange $range
185 ) {
186 $this->fetch();
187 $timeStep = $seqSpec->timeStep;
188 $firstBucket = (int)( $range->start / $timeStep );
189 $lastBucket = (int)( $range->end / $timeStep );
190 $now = $this->now();
191 $total = 0;
192 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
193 $key = $this->store->makeKey(
194 $this->prefixComponents,
195 [ $metricName, $seqSpec->name, $bucket ],
196 $entity
197 );
198 $value = $this->cachedValues[$key] ?? 0;
199 if ( !$value ) {
200 continue;
201 } elseif ( $bucket === $firstBucket ) {
202 if ( $bucket === $lastBucket ) {
203 // It can be assumed that there are zero events in the future
204 $bucketStartTime = $bucket * $timeStep;
205 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
206 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
207 if ( $interpolationDuration > 0 ) {
208 $total += $value * $range->getDuration() / $interpolationDuration;
209 }
210 } else {
211 $overlapDuration = max( ( $bucket + 1 ) * $timeStep - $range->start, 0 );
212 $total += $value * $overlapDuration / $timeStep;
213 }
214 } elseif ( $bucket === $lastBucket ) {
215 // It can be assumed that there are zero events in the future
216 $bucketStartTime = $bucket * $timeStep;
217 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
218 $overlapDuration = max( $range->end - $bucketStartTime, 0 );
219 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
220 if ( $overlapDuration === $interpolationDuration ) {
221 // Special case for 0/0 -- current time exactly on boundary.
222 $total += $value;
223 } elseif ( $interpolationDuration > 0 ) {
224 $total += $value * $overlapDuration / $interpolationDuration;
225 }
226 } else {
227 $total += $value;
228 }
229 }
230 // Round to nearest resolution step for nicer display
231 $rounded = round( $total ) * $metricSpec->resolution;
232 // Convert to integer if integer is expected
233 if ( is_int( $metricSpec->resolution ) ) {
234 $rounded = (int)$rounded;
235 }
236 return $rounded;
237 }
238
246 public function total( $rates ) {
247 $result = [];
248 foreach ( $rates as $key => $rate ) {
249 $result[$key] = $rate->total();
250 }
251 return $result;
252 }
253
260 public function perSecond( $rates ) {
261 $result = [];
262 foreach ( $rates as $key => $rate ) {
263 $result[$key] = $rate->perSecond();
264 }
265 return $result;
266 }
267
274 public function perMinute( $rates ) {
275 $result = [];
276 foreach ( $rates as $key => $rate ) {
277 $result[$key] = $rate->perMinute();
278 }
279 return $result;
280 }
281
288 public function perHour( $rates ) {
289 $result = [];
290 foreach ( $rates as $key => $rate ) {
291 $result[$key] = $rate->perHour();
292 }
293 return $result;
294 }
295
302 public function perDay( $rates ) {
303 $result = [];
304 foreach ( $rates as $key => $rate ) {
305 $result[$key] = $rate->perDay();
306 }
307 return $result;
308 }
309}
Base class for entity keys.
Definition EntityKey.php:13
Entity key with global=false.
Class representation of normalized metric specifications.
A WRStats query result promise.
Class representation of normalized sequence specifications.
getDuration()
Get the duration of the time range in seconds.
Definition TimeRange.php:32
Exception class for errors thrown by the WRStats library.
Readers gather a batch of read operations, returning promises.
fetch()
Perform any queued fetch operations.
latest( $numSeconds)
Get a TimeRange for some period ending at the current time.
internalGetCount( $metricName, EntityKey $entity, MetricSpec $metricSpec, SequenceSpec $seqSpec, TimeRange $range)
perMinute( $rates)
Resolve a batch of RatePromise objects, returning their per-minute rates.
resetCurrentTime()
Clear the current time so that it will be filled with the real current time on the next call.
__construct(StatsStore $store, $specs, $prefix)
perDay( $rates)
Resolve a batch of RatePromise objects, returning their per-day rates.
timeRange( $start, $end)
Get a specified time range.
getRate( $metricName, ?EntityKey $entity, TimeRange $range)
Queue a fetch operation.
perHour( $rates)
Resolve a batch of RatePromise objects, returning their per-hour rates.
getRates( $metricNames, ?EntityKey $entity, TimeRange $range)
Queue a batch of fetch operations for different metrics with the same time range.
perSecond( $rates)
Resolve a batch of RatePromise objects, returning their per-second rates.
total( $rates)
Resolve a batch of RatePromise objects, returning their counter totals, indexed as in the input array...
setCurrentTime( $now)
Set the current time to be used in latest() etc.
The narrow interface WRStats needs into a memcached-like key-value store.