PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
Loading...
Searching...
No Matches
TimingsHandler.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\timings;
25
26use pmmp\thread\Thread as NativeThread;
32use function array_merge;
33use function array_push;
34use function hrtime;
35use function implode;
36use function spl_object_id;
37
42 private const FORMAT_VERSION = 3; //thread timings collection
43
44 private static bool $enabled = false;
45 private static int $timingStart = 0;
46
48 private static ?ObjectSet $toggleCallbacks = null;
50 private static ?ObjectSet $reloadCallbacks = null;
52 private static ?ObjectSet $collectCallbacks = null;
53
60 private static function lazyGetSet(?ObjectSet &$where) : ObjectSet{
61 //workaround for phpstan bug - allows us to ignore 1 error instead of 6 without suppressing other errors
62 return $where ??= new ObjectSet();
63 }
64
68 public static function getToggleCallbacks() : ObjectSet{ return self::lazyGetSet(self::$toggleCallbacks); }
69
73 public static function getReloadCallbacks() : ObjectSet{ return self::lazyGetSet(self::$reloadCallbacks); }
74
78 public static function getCollectCallbacks() : ObjectSet{ return self::lazyGetSet(self::$collectCallbacks); }
79
84 public static function printCurrentThreadRecords() : array{
85 $threadId = NativeThread::getCurrentThread()?->getThreadId();
86 $groups = [];
87
88 foreach(TimingsRecord::getAll() as $timings){
89 $time = $timings->getTotalTime();
90 $count = $timings->getCount();
91 if($count === 0){
92 //this should never happen - a timings record shouldn't exist if it hasn't been used
93 continue;
94 }
95
96 $avg = $time / $count;
97
98 $group = $timings->getGroup() . ($threadId !== null ? " ThreadId: $threadId" : "");
99 $groups[$group][] = implode(" ", [
100 $timings->getName(),
101 "Time: $time",
102 "Count: $count",
103 "Avg: $avg",
104 "Violations: " . $timings->getViolations(),
105 "RecordId: " . $timings->getId(),
106 "ParentRecordId: " . ($timings->getParentId() ?? "none"),
107 "TimerId: " . $timings->getTimerId(),
108 "Ticks: " . $timings->getTicksActive(),
109 "Peak: " . $timings->getPeakTime(),
110 ]);
111 }
112 $result = [];
113
114 foreach(Utils::stringifyKeys($groups) as $groupName => $lines){
115 $result[] = $groupName;
116 foreach($lines as $line){
117 $result[] = " $line";
118 }
119 }
120
121 return $result;
122 }
123
127 private static function printFooter() : array{
128 $result = [];
129
130 $result[] = "# Version " . Server::getInstance()->getVersion();
131 $result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion();
132
133 $result[] = "# FormatVersion " . self::FORMAT_VERSION;
134
135 $sampleTime = hrtime(true) - self::$timingStart;
136 $result[] = "Sample time $sampleTime (" . ($sampleTime / 1000000000) . "s)";
137
138 return $result;
139 }
140
147 public static function printTimings() : array{
148 $records = self::printCurrentThreadRecords();
149 $footer = self::printFooter();
150
151 return [...$records, ...$footer];
152 }
153
165 public static function requestPrintTimings() : Promise{
166 $thisThreadRecords = self::printCurrentThreadRecords();
167
168 $otherThreadRecordPromises = [];
169 if(self::$collectCallbacks !== null){
170 foreach(self::$collectCallbacks as $callback){
171 $callbackPromises = $callback();
172 array_push($otherThreadRecordPromises, ...$callbackPromises);
173 }
174 }
175
176 $resolver = new PromiseResolver();
177 Promise::all($otherThreadRecordPromises)->onCompletion(
178 function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{
179 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
180 },
181 function() : void{
182 throw new \AssertionError("This promise is not expected to be rejected");
183 }
184 );
185
186 return $resolver->getPromise();
187 }
188
189 public static function isEnabled() : bool{
190 return self::$enabled;
191 }
192
193 public static function setEnabled(bool $enable = true) : void{
194 if($enable === self::$enabled){
195 return;
196 }
197 self::$enabled = $enable;
198 self::internalReload();
199 if(self::$toggleCallbacks !== null){
200 foreach(self::$toggleCallbacks as $callback){
201 $callback($enable);
202 }
203 }
204 }
205
206 public static function getStartTime() : float{
207 return self::$timingStart;
208 }
209
210 private static function internalReload() : void{
211 TimingsRecord::reset();
212 if(self::$enabled){
213 self::$timingStart = hrtime(true);
214 }
215 }
216
217 public static function reload() : void{
218 self::internalReload();
219 if(self::$reloadCallbacks !== null){
220 foreach(self::$reloadCallbacks as $callback){
221 $callback();
222 }
223 }
224 }
225
226 public static function tick(bool $measure = true) : void{
227 if(self::$enabled){
228 TimingsRecord::tick($measure);
229 }
230 }
231
232 private ?TimingsRecord $rootRecord = null;
233 private int $timingDepth = 0;
234
239 private array $recordsByParent = [];
240
241 public function __construct(
242 private string $name,
243 private ?TimingsHandler $parent = null,
244 private string $group = Timings::GROUP_MINECRAFT
245 ){}
246
247 public function getName() : string{ return $this->name; }
248
249 public function getGroup() : string{ return $this->group; }
250
251 public function startTiming() : void{
252 if(self::$enabled){
253 $this->internalStartTiming(hrtime(true));
254 }
255 }
256
257 private function internalStartTiming(int $now) : void{
258 if(++$this->timingDepth === 1){
259 if($this->parent !== null){
260 $this->parent->internalStartTiming($now);
261 }
262
263 $current = TimingsRecord::getCurrentRecord();
264 if($current !== null){
265 $record = $this->recordsByParent[spl_object_id($current)] ?? null;
266 if($record === null){
267 $record = new TimingsRecord($this, $current);
268 $this->recordsByParent[spl_object_id($current)] = $record;
269 }
270 }else{
271 if($this->rootRecord === null){
272 $this->rootRecord = new TimingsRecord($this, null);
273 }
274 $record = $this->rootRecord;
275 }
276 $record->startTiming($now);
277 }
278 }
279
280 public function stopTiming() : void{
281 if(self::$enabled){
282 $this->internalStopTiming(hrtime(true));
283 }
284 }
285
286 private function internalStopTiming(int $now) : void{
287 if($this->timingDepth === 0){
288 //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets
289 //and enable/disable, it would have a performance impact. Therefore, considering the limited
290 //usefulness of bailing here anyway, we don't currently bother.
291 return;
292 }
293 if(--$this->timingDepth !== 0){
294 return;
295 }
296
297 $record = TimingsRecord::getCurrentRecord();
298 $timerId = spl_object_id($this);
299 for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
300 \GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
301 $record->stopTiming($now);
302 }
303 $record?->stopTiming($now);
304 if($this->parent !== null){
305 $this->parent->internalStopTiming($now);
306 }
307 }
308
316 public function time(\Closure $closure){
317 $this->startTiming();
318 try{
319 return $closure();
320 }finally{
321 $this->stopTiming();
322 }
323 }
324
328 public function reset() : void{
329 $this->rootRecord = null;
330 $this->recordsByParent = [];
331 $this->timingDepth = 0;
332 }
333}