PocketMine-MP 5.23.3 git-976fc63567edab7a6fb6aeae739f43cf9fe57de4
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
153 public static function requestPrintTimings() : Promise{
154 $thisThreadRecords = self::printCurrentThreadRecords();
155
156 $otherThreadRecordPromises = [];
157 if(self::$collectCallbacks !== null){
158 foreach(self::$collectCallbacks as $callback){
159 $callbackPromises = $callback();
160 array_push($otherThreadRecordPromises, ...$callbackPromises);
161 }
162 }
163
165 $resolver = new PromiseResolver();
166 Promise::all($otherThreadRecordPromises)->onCompletion(
167 function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{
168 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
169 },
170 function() : void{
171 throw new \AssertionError("This promise is not expected to be rejected");
172 }
173 );
174
175 return $resolver->getPromise();
176 }
177
178 public static function isEnabled() : bool{
179 return self::$enabled;
180 }
181
182 public static function setEnabled(bool $enable = true) : void{
183 if($enable === self::$enabled){
184 return;
185 }
186 self::$enabled = $enable;
187 self::internalReload();
188 if(self::$toggleCallbacks !== null){
189 foreach(self::$toggleCallbacks as $callback){
190 $callback($enable);
191 }
192 }
193 }
194
195 public static function getStartTime() : float{
196 return self::$timingStart;
197 }
198
199 private static function internalReload() : void{
200 TimingsRecord::reset();
201 if(self::$enabled){
202 self::$timingStart = hrtime(true);
203 }
204 }
205
206 public static function reload() : void{
207 self::internalReload();
208 if(self::$reloadCallbacks !== null){
209 foreach(self::$reloadCallbacks as $callback){
210 $callback();
211 }
212 }
213 }
214
215 public static function tick(bool $measure = true) : void{
216 if(self::$enabled){
217 TimingsRecord::tick($measure);
218 }
219 }
220
221 private ?TimingsRecord $rootRecord = null;
222 private int $timingDepth = 0;
223
228 private array $recordsByParent = [];
229
230 public function __construct(
231 private string $name,
232 private ?TimingsHandler $parent = null,
233 private string $group = Timings::GROUP_MINECRAFT
234 ){}
235
236 public function getName() : string{ return $this->name; }
237
238 public function getGroup() : string{ return $this->group; }
239
240 public function startTiming() : void{
241 if(self::$enabled){
242 $this->internalStartTiming(hrtime(true));
243 }
244 }
245
246 private function internalStartTiming(int $now) : void{
247 if(++$this->timingDepth === 1){
248 if($this->parent !== null){
249 $this->parent->internalStartTiming($now);
250 }
251
252 $current = TimingsRecord::getCurrentRecord();
253 if($current !== null){
254 $record = $this->recordsByParent[spl_object_id($current)] ?? null;
255 if($record === null){
256 $record = new TimingsRecord($this, $current);
257 $this->recordsByParent[spl_object_id($current)] = $record;
258 }
259 }else{
260 if($this->rootRecord === null){
261 $this->rootRecord = new TimingsRecord($this, null);
262 }
263 $record = $this->rootRecord;
264 }
265 $record->startTiming($now);
266 }
267 }
268
269 public function stopTiming() : void{
270 if(self::$enabled){
271 $this->internalStopTiming(hrtime(true));
272 }
273 }
274
275 private function internalStopTiming(int $now) : void{
276 if($this->timingDepth === 0){
277 //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets
278 //and enable/disable, it would have a performance impact. Therefore, considering the limited
279 //usefulness of bailing here anyway, we don't currently bother.
280 return;
281 }
282 if(--$this->timingDepth !== 0){
283 return;
284 }
285
286 $record = TimingsRecord::getCurrentRecord();
287 $timerId = spl_object_id($this);
288 for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
289 \GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
290 $record->stopTiming($now);
291 }
292 $record?->stopTiming($now);
293 if($this->parent !== null){
294 $this->parent->internalStopTiming($now);
295 }
296 }
297
305 public function time(\Closure $closure){
306 $this->startTiming();
307 try{
308 return $closure();
309 }finally{
310 $this->stopTiming();
311 }
312 }
313
317 public function reset() : void{
318 $this->rootRecord = null;
319 $this->recordsByParent = [];
320 $this->timingDepth = 0;
321 }
322}