PocketMine-MP 5.25.3 git-463be36b72d4f519674ec472ca492773c197dbce
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
128 private static function printFooter() : array{
129 $result = [];
130
131 $result[] = "# Version " . Server::getInstance()->getVersion();
132 $result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion();
133
134 $result[] = "# FormatVersion " . self::FORMAT_VERSION;
135
136 $sampleTime = hrtime(true) - self::$timingStart;
137 $result[] = "Sample time $sampleTime (" . ($sampleTime / 1000000000) . "s)";
138
139 return $result;
140 }
141
148 public static function printTimings() : array{
149 $records = self::printCurrentThreadRecords();
150 $footer = self::printFooter();
151
152 return [...$records, ...$footer];
153 }
154
166 public static function requestPrintTimings() : Promise{
167 $thisThreadRecords = self::printCurrentThreadRecords();
168
169 $otherThreadRecordPromises = [];
170 if(self::$collectCallbacks !== null){
171 foreach(self::$collectCallbacks as $callback){
172 $callbackPromises = $callback();
173 array_push($otherThreadRecordPromises, ...$callbackPromises);
174 }
175 }
176
178 $resolver = new PromiseResolver();
179 Promise::all($otherThreadRecordPromises)->onCompletion(
180 function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{
181 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
182 },
183 function() : void{
184 throw new \AssertionError("This promise is not expected to be rejected");
185 }
186 );
187
188 return $resolver->getPromise();
189 }
190
191 public static function isEnabled() : bool{
192 return self::$enabled;
193 }
194
195 public static function setEnabled(bool $enable = true) : void{
196 if($enable === self::$enabled){
197 return;
198 }
199 self::$enabled = $enable;
200 self::internalReload();
201 if(self::$toggleCallbacks !== null){
202 foreach(self::$toggleCallbacks as $callback){
203 $callback($enable);
204 }
205 }
206 }
207
208 public static function getStartTime() : float{
209 return self::$timingStart;
210 }
211
212 private static function internalReload() : void{
213 TimingsRecord::reset();
214 if(self::$enabled){
215 self::$timingStart = hrtime(true);
216 }
217 }
218
219 public static function reload() : void{
220 self::internalReload();
221 if(self::$reloadCallbacks !== null){
222 foreach(self::$reloadCallbacks as $callback){
223 $callback();
224 }
225 }
226 }
227
228 public static function tick(bool $measure = true) : void{
229 if(self::$enabled){
230 TimingsRecord::tick($measure);
231 }
232 }
233
234 private ?TimingsRecord $rootRecord = null;
235 private int $timingDepth = 0;
236
241 private array $recordsByParent = [];
242
243 public function __construct(
244 private string $name,
245 private ?TimingsHandler $parent = null,
246 private string $group = Timings::GROUP_MINECRAFT
247 ){}
248
249 public function getName() : string{ return $this->name; }
250
251 public function getGroup() : string{ return $this->group; }
252
253 public function startTiming() : void{
254 if(self::$enabled){
255 $this->internalStartTiming(hrtime(true));
256 }
257 }
258
259 private function internalStartTiming(int $now) : void{
260 if(++$this->timingDepth === 1){
261 if($this->parent !== null){
262 $this->parent->internalStartTiming($now);
263 }
264
265 $current = TimingsRecord::getCurrentRecord();
266 if($current !== null){
267 $record = $this->recordsByParent[spl_object_id($current)] ?? null;
268 if($record === null){
269 $record = new TimingsRecord($this, $current);
270 $this->recordsByParent[spl_object_id($current)] = $record;
271 }
272 }else{
273 if($this->rootRecord === null){
274 $this->rootRecord = new TimingsRecord($this, null);
275 }
276 $record = $this->rootRecord;
277 }
278 $record->startTiming($now);
279 }
280 }
281
282 public function stopTiming() : void{
283 if(self::$enabled){
284 $this->internalStopTiming(hrtime(true));
285 }
286 }
287
288 private function internalStopTiming(int $now) : void{
289 if($this->timingDepth === 0){
290 //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets
291 //and enable/disable, it would have a performance impact. Therefore, considering the limited
292 //usefulness of bailing here anyway, we don't currently bother.
293 return;
294 }
295 if(--$this->timingDepth !== 0){
296 return;
297 }
298
299 $record = TimingsRecord::getCurrentRecord();
300 $timerId = spl_object_id($this);
301 for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
302 \GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
303 $record->stopTiming($now);
304 }
305 $record?->stopTiming($now);
306 if($this->parent !== null){
307 $this->parent->internalStopTiming($now);
308 }
309 }
310
318 public function time(\Closure $closure){
319 $this->startTiming();
320 try{
321 return $closure();
322 }finally{
323 $this->stopTiming();
324 }
325 }
326
330 public function reset() : void{
331 $this->rootRecord = null;
332 $this->recordsByParent = [];
333 $this->timingDepth = 0;
334 }
335}