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
91 if ( !$seqSpec ) {
92 // This check exists to make Phan happy.
93 // It should never fail since we apply normalization in MetricSpec::__construct()
94 throw new WRStatsError( 'There should have been at least one sequence' );
95 }
96
97 $timeStep = $seqSpec->timeStep;
98 $firstBucket = (int)( $range->start / $timeStep );
99 $lastBucket = (int)ceil( $range->end / $timeStep );
100 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
101 $key = $this->store->makeKey(
102 $this->prefixComponents,
103 [ $metricName, $seqSpec->name, $bucket ],
104 $entity
105 );
106 if ( !isset( $this->cachedValues[$key] ) ) {
107 $this->queuedKeys[$key] = true;
108 }
109 }
110 return new RatePromise( $this, $metricName, $entity, $metricSpec, $seqSpec, $range );
111 }
112
122 public function getRates( $metricNames, ?EntityKey $entity, TimeRange $range ) {
123 $rates = [];
124 foreach ( $metricNames as $name ) {
125 $rates[$name] = $this->getRate( $name, $entity, $range );
126 }
127 return $rates;
128 }
129
133 public function fetch() {
134 if ( !$this->queuedKeys ) {
135 return;
136 }
137 $this->cachedValues += $this->store->query( array_keys( $this->queuedKeys ) );
138 $this->queuedKeys = [];
139 }
140
146 public function setCurrentTime( $now ) {
147 $this->now = $now;
148 }
149
154 public function resetCurrentTime() {
155 $this->now = null;
156 }
157
161 private function now() {
162 $this->now ??= microtime( true );
163 return $this->now;
164 }
165
175 public function internalGetCount(
176 $metricName,
177 EntityKey $entity,
178 MetricSpec $metricSpec,
179 SequenceSpec $seqSpec,
180 TimeRange $range
181 ) {
182 $this->fetch();
183 $timeStep = $seqSpec->timeStep;
184 $firstBucket = (int)( $range->start / $timeStep );
185 $lastBucket = (int)( $range->end / $timeStep );
186 $now = $this->now();
187 $total = 0;
188 for ( $bucket = $firstBucket; $bucket <= $lastBucket; $bucket++ ) {
189 $key = $this->store->makeKey(
190 $this->prefixComponents,
191 [ $metricName, $seqSpec->name, $bucket ],
192 $entity
193 );
194 $value = $this->cachedValues[$key] ?? 0;
195 if ( !$value ) {
196 continue;
197 } elseif ( $bucket === $firstBucket ) {
198 if ( $bucket === $lastBucket ) {
199 // It can be assumed that there are zero events in the future
200 $bucketStartTime = $bucket * $timeStep;
201 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
202 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
203 if ( $interpolationDuration > 0 ) {
204 $total += $value * $range->getDuration() / $interpolationDuration;
205 }
206 } else {
207 $overlapDuration = max( ( $bucket + 1 ) * $timeStep - $range->start, 0 );
208 $total += $value * $overlapDuration / $timeStep;
209 }
210 } elseif ( $bucket === $lastBucket ) {
211 // It can be assumed that there are zero events in the future
212 $bucketStartTime = $bucket * $timeStep;
213 $rateInterpolationEndTime = min( $bucketStartTime + $timeStep, $now );
214 $overlapDuration = max( $range->end - $bucketStartTime, 0 );
215 $interpolationDuration = $rateInterpolationEndTime - $bucketStartTime;
216 if ( $overlapDuration === $interpolationDuration ) {
217 // Special case for 0/0 -- current time exactly on boundary.
218 $total += $value;
219 } elseif ( $interpolationDuration > 0 ) {
220 $total += $value * $overlapDuration / $interpolationDuration;
221 }
222 } else {
223 $total += $value;
224 }
225 }
226 // Round to nearest resolution step for nicer display
227 $rounded = round( $total ) * $metricSpec->resolution;
228 // Convert to integer if integer is expected
229 if ( is_int( $metricSpec->resolution ) ) {
230 $rounded = (int)$rounded;
231 }
232 return $rounded;
233 }
234
242 public function total( $rates ) {
243 $result = [];
244 foreach ( $rates as $key => $rate ) {
245 $result[$key] = $rate->total();
246 }
247 return $result;
248 }
249
256 public function perSecond( $rates ) {
257 $result = [];
258 foreach ( $rates as $key => $rate ) {
259 $result[$key] = $rate->perSecond();
260 }
261 return $result;
262 }
263
270 public function perMinute( $rates ) {
271 $result = [];
272 foreach ( $rates as $key => $rate ) {
273 $result[$key] = $rate->perMinute();
274 }
275 return $result;
276 }
277
284 public function perHour( $rates ) {
285 $result = [];
286 foreach ( $rates as $key => $rate ) {
287 $result[$key] = $rate->perHour();
288 }
289 return $result;
290 }
291
298 public function perDay( $rates ) {
299 $result = [];
300 foreach ( $rates as $key => $rate ) {
301 $result[$key] = $rate->perDay();
302 }
303 return $result;
304 }
305}
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.