MediaWiki master
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
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
52 public function latest( $numSeconds ) {
53 $now = $this->now();
54 return new TimeRange( $now - $numSeconds, $now );
55 }
56
64 public function timeRange( $start, $end ) {
65 return new TimeRange( $start, $end );
66 }
67
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
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
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
139 public function setCurrentTime( $now ) {
140 $this->now = $now;
141 }
142
147 public function resetCurrentTime() {
148 $this->now = null;
149 }
150
154 private function now() {
155 $this->now ??= microtime( true );
156 return $this->now;
157 }
158
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
235 public function total( $rates ) {
236 $result = [];
237 foreach ( $rates as $key => $rate ) {
238 $result[$key] = $rate->total();
239 }
240 return $result;
241 }
242
249 public function perSecond( $rates ) {
250 $result = [];
251 foreach ( $rates as $key => $rate ) {
252 $result[$key] = $rate->perSecond();
253 }
254 return $result;
255 }
256
263 public function perMinute( $rates ) {
264 $result = [];
265 foreach ( $rates as $key => $rate ) {
266 $result[$key] = $rate->perMinute();
267 }
268 return $result;
269 }
270
277 public function perHour( $rates ) {
278 $result = [];
279 foreach ( $rates as $key => $rate ) {
280 $result[$key] = $rate->perHour();
281 }
282 return $result;
283 }
284
291 public function perDay( $rates ) {
292 $result = [];
293 foreach ( $rates as $key => $rate ) {
294 $result[$key] = $rate->perDay();
295 }
296 return $result;
297 }
298}
Base class for entity keys.
Definition EntityKey.php:13
Entity key with isGlobal=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.
Narrow interface for WRStatsFactory to a memcached-like key-value store.