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 $timeStep = $seqSpec->timeStep;
92 $firstBucket = (int)( $range->start / $timeStep );
93 $lastBucket = (int)ceil( $range->end / $timeStep );
94 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
95 $key = $this->store->makeKey(
96 $this->prefixComponents,
97 [ $metricName, $seqSpec->name, $bucket ],
98 $entity
99 );
100 if ( !isset( $this->cachedValues[$key] ) ) {
101 $this->queuedKeys[$key] = true;
102 }
103 }
104 return new RatePromise( $this, $metricName, $entity, $metricSpec, $seqSpec, $range );
105 }
106
116 public function getRates( $metricNames, ?EntityKey $entity, TimeRange $range ) {
117 $rates = [];
118 foreach ( $metricNames as $name ) {
119 $rates[$name] = $this->getRate( $name, $entity, $range );
120 }
121 return $rates;
122 }
123
127 public function fetch() {
128 if ( !$this->queuedKeys ) {
129 return;
130 }
131 $this->cachedValues += $this->store->query( array_keys( $this->queuedKeys ) );
132 $this->queuedKeys = [];
133 }
134
140 public function setCurrentTime( $now ) {
141 $this->now = $now;
142 }
143
148 public function resetCurrentTime() {
149 $this->now = null;
150 }
151
155 private function now() {
156 if ( $this->now === null ) {
157 $this->now = microtime( true );
158 }
159 return $this->now;
160 }
161
172 public function internalGetCount(
173 $metricName,
174 EntityKey $entity,
175 MetricSpec $metricSpec,
176 SequenceSpec $seqSpec,
177 TimeRange $range
178 ) {
179 $this->fetch();
180 $timeStep = $seqSpec->timeStep;
181 $firstBucket = (int)( $range->start / $timeStep );
182 $lastBucket = (int)( $range->end / $timeStep );
183 $now = $this->now();
184 $total = 0;
185 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
186 $key = $this->store->makeKey(
187 $this->prefixComponents,
188 [ $metricName, $seqSpec->name, $bucket ],
189 $entity
190 );
191 $value = $this->cachedValues[$key] ?? 0;
192 if ( !$value ) {
193 continue;
194 } elseif ( $bucket === $firstBucket ) {
195 if ( $bucket === $lastBucket ) {
196 // It can be assumed that there are zero events in the future
197 $bucketStartTime = $bucket * $timeStep;
198 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
199 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
200 if ( $interpolationDuration > 0 ) {
201 $total += $value * $range->getDuration() / $interpolationDuration;
202 }
203 } else {
204 $overlapDuration = max( ( $bucket + 1 ) * $timeStep - $range->start, 0 );
205 $total += $value * $overlapDuration / $timeStep;
206 }
207 } elseif ( $bucket === $lastBucket ) {
208 // It can be assumed that there are zero events in the future
209 $bucketStartTime = $bucket * $timeStep;
210 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
211 $overlapDuration = max( $range->end - $bucketStartTime, 0 );
212 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
213 if ( $overlapDuration === $interpolationDuration ) {
214 // Special case for 0/0 -- current time exactly on boundary.
215 $total += $value;
216 } elseif ( $interpolationDuration > 0 ) {
217 $total += $value * $overlapDuration / $interpolationDuration;
218 }
219 } else {
220 $total += $value;
221 }
222 }
223 // Round to nearest resolution step for nicer display
224 $rounded = round( $total ) * $metricSpec->resolution;
225 // Convert to integer if integer is expected
226 if ( is_int( $metricSpec->resolution ) ) {
227 $rounded = (int)$rounded;
228 }
229 return $rounded;
230 }
231
239 public function total( $rates ) {
240 $result = [];
241 foreach ( $rates as $key => $rate ) {
242 $result[$key] = $rate->total();
243 }
244 return $result;
245 }
246
253 public function perSecond( $rates ) {
254 $result = [];
255 foreach ( $rates as $key => $rate ) {
256 $result[$key] = $rate->perSecond();
257 }
258 return $result;
259 }
260
267 public function perMinute( $rates ) {
268 $result = [];
269 foreach ( $rates as $key => $rate ) {
270 $result[$key] = $rate->perMinute();
271 }
272 return $result;
273 }
274
281 public function perHour( $rates ) {
282 $result = [];
283 foreach ( $rates as $key => $rate ) {
284 $result[$key] = $rate->perHour();
285 }
286 return $result;
287 }
288
295 public function perDay( $rates ) {
296 $result = [];
297 foreach ( $rates as $key => $rate ) {
298 $result[$key] = $rate->perDay();
299 }
300 return $result;
301 }
302}
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.