PocketMine-MP 5.37.1 git-da6732df2656426fbd1b7898ed06c8286969d2f1
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;
34use Symfony\Component\Filesystem\Path;
35use function array_merge;
36use function array_push;
37use function date;
38use function fclose;
39use function fopen;
40use function fwrite;
41use function hrtime;
42use function implode;
43use function is_dir;
44use function mkdir;
45use function spl_object_id;
46use const PHP_EOL;
47
52 private const FORMAT_VERSION = 3; //thread timings collection
53
54 private static bool $enabled = false;
55 private static int $timingStart = 0;
56
58 private static ?ObjectSet $toggleCallbacks = null;
60 private static ?ObjectSet $reloadCallbacks = null;
62 private static ?ObjectSet $collectCallbacks = null;
63
70 private static function lazyGetSet(?ObjectSet &$where) : ObjectSet{
71 //workaround for phpstan bug - allows us to ignore 1 error instead of 6 without suppressing other errors
72 return $where ??= new ObjectSet();
73 }
74
78 public static function getToggleCallbacks() : ObjectSet{ return self::lazyGetSet(self::$toggleCallbacks); }
79
83 public static function getReloadCallbacks() : ObjectSet{ return self::lazyGetSet(self::$reloadCallbacks); }
84
88 public static function getCollectCallbacks() : ObjectSet{ return self::lazyGetSet(self::$collectCallbacks); }
89
94 public static function printCurrentThreadRecords() : array{
95 $threadId = NativeThread::getCurrentThread()?->getThreadId();
96 $groups = [];
97
98 foreach(TimingsRecord::getAll() as $timings){
99 $time = $timings->getTotalTime();
100 $count = $timings->getCount();
101 if($count === 0){
102 //this should never happen - a timings record shouldn't exist if it hasn't been used
103 continue;
104 }
105
106 $avg = $time / $count;
107
108 $group = $timings->getGroup() . ($threadId !== null ? " ThreadId: $threadId" : "");
109 $groups[$group][] = implode(" ", [
110 $timings->getName(),
111 "Time: $time",
112 "Count: $count",
113 "Avg: $avg",
114 "Violations: " . $timings->getViolations(),
115 "RecordId: " . $timings->getId(),
116 "ParentRecordId: " . ($timings->getParentId() ?? "none"),
117 "TimerId: " . $timings->getTimerId(),
118 "Ticks: " . $timings->getTicksActive(),
119 "Peak: " . $timings->getPeakTime(),
120 ]);
121 }
122 $result = [];
123
124 foreach(Utils::stringifyKeys($groups) as $groupName => $lines){
125 $result[] = $groupName;
126 foreach($lines as $line){
127 $result[] = " $line";
128 }
129 }
130
131 return $result;
132 }
133
138 private static function printFooter() : array{
139 $result = [];
140
141 $result[] = "# Version " . Server::getInstance()->getVersion();
142 $result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion();
143
144 $result[] = "# FormatVersion " . self::FORMAT_VERSION;
145
146 $sampleTime = hrtime(true) - self::$timingStart;
147 $result[] = "Sample time $sampleTime (" . ($sampleTime / 1000000000) . "s)";
148
149 return $result;
150 }
151
163 public static function requestPrintTimings() : Promise{
164 $thisThreadRecords = self::printCurrentThreadRecords();
165
166 $otherThreadRecordPromises = [];
167 if(self::$collectCallbacks !== null){
168 foreach(self::$collectCallbacks as $callback){
169 $callbackPromises = $callback();
170 array_push($otherThreadRecordPromises, ...$callbackPromises);
171 }
172 }
173
175 $resolver = new PromiseResolver();
176 Promise::all($otherThreadRecordPromises)->onCompletion(
177 function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{
178 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
179 },
180 function() : void{
181 throw new \AssertionError("This promise is not expected to be rejected");
182 }
183 );
184
185 return $resolver->getPromise();
186 }
187
188 public static function isEnabled() : bool{
189 return self::$enabled;
190 }
191
192 public static function setEnabled(bool $enable = true) : void{
193 if($enable === self::$enabled){
194 return;
195 }
196 self::$enabled = $enable;
197 self::internalReload();
198 if(self::$toggleCallbacks !== null){
199 foreach(self::$toggleCallbacks as $callback){
200 $callback($enable);
201 }
202 }
203 }
204
205 public static function getStartTime() : float{
206 return self::$timingStart;
207 }
208
209 private static function internalReload() : void{
210 TimingsRecord::reset();
211 if(self::$enabled){
212 self::$timingStart = hrtime(true);
213 }
214 }
215
216 public static function reload() : void{
217 self::internalReload();
218 if(self::$reloadCallbacks !== null){
219 foreach(self::$reloadCallbacks as $callback){
220 $callback();
221 }
222 }
223 }
224
225 public static function tick(bool $measure = true) : void{
226 if(self::$enabled){
227 TimingsRecord::tick($measure);
228 }
229 }
230
231 private ?TimingsRecord $rootRecord = null;
232 private int $timingDepth = 0;
233
238 private array $recordsByParent = [];
239
240 public function __construct(
241 private string $name,
242 private ?TimingsHandler $parent = null,
243 private string $group = Timings::GROUP_MINECRAFT
244 ){}
245
246 public function getName() : string{ return $this->name; }
247
248 public function getGroup() : string{ return $this->group; }
249
250 public function startTiming() : void{
251 if(self::$enabled){
252 $this->internalStartTiming(hrtime(true));
253 }
254 }
255
256 private function internalStartTiming(int $now) : void{
257 if(++$this->timingDepth === 1){
258 if($this->parent !== null){
259 $this->parent->internalStartTiming($now);
260 }
261
262 $current = TimingsRecord::getCurrentRecord();
263 if($current !== null){
264 $record = $this->recordsByParent[spl_object_id($current)] ?? null;
265 if($record === null){
266 $record = new TimingsRecord($this, $current);
267 $this->recordsByParent[spl_object_id($current)] = $record;
268 }
269 }else{
270 if($this->rootRecord === null){
271 $this->rootRecord = new TimingsRecord($this, null);
272 }
273 $record = $this->rootRecord;
274 }
275 $record->startTiming($now);
276 }
277 }
278
279 public function stopTiming() : void{
280 if(self::$enabled){
281 $this->internalStopTiming(hrtime(true));
282 }
283 }
284
285 private function internalStopTiming(int $now) : void{
286 if($this->timingDepth === 0){
287 //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets
288 //and enable/disable, it would have a performance impact. Therefore, considering the limited
289 //usefulness of bailing here anyway, we don't currently bother.
290 return;
291 }
292 if(--$this->timingDepth !== 0){
293 return;
294 }
295
296 $record = TimingsRecord::getCurrentRecord();
297 $timerId = spl_object_id($this);
298 for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
299 \GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
300 $record->stopTiming($now);
301 }
302 $record?->stopTiming($now);
303 if($this->parent !== null){
304 $this->parent->internalStopTiming($now);
305 }
306 }
307
315 public function time(\Closure $closure){
316 $this->startTiming();
317 try{
318 return $closure();
319 }finally{
320 $this->stopTiming();
321 }
322 }
323
327 public function reset() : void{
328 $this->rootRecord = null;
329 $this->recordsByParent = [];
330 $this->timingDepth = 0;
331 }
332
342 public static function createReportFile(string $directory, ?string $fileName = null) : Promise{
343 $timingsPromise = self::requestPrintTimings();
344
346 $resolver = new PromiseResolver();
347
348 $timingsPromise->onCompletion(
349 function(array $lines) use ($fileName, $directory, $resolver) : void{
350 if($fileName === null){
351 $date = date('Y-m-d_H.i.s_T');
352 $fileName = "timings_{$date}";
353 }
354 if(!@mkdir($directory, 0777, true) && !is_dir($directory)){
355 $resolver->reject();
356 return;
357 }
358 $timingsFile = Path::join($directory, $fileName . ".txt");
359 try{
360 $handle = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($timingsFile, "x+b"));
361 }catch(\ErrorException){
362 //TODO: it'd be better if we could report this to the promise callback
363 $resolver->reject();
364 return;
365 }
366 foreach($lines as $line){
367 fwrite($handle, $line . PHP_EOL);
368 }
369 fclose($handle);
370
371 $resolver->resolve($timingsFile);
372 },
373 fn() => throw new AssumptionFailedError("This promise is not expected to be rejected")
374 );
375
376 return $resolver->getPromise();
377 }
378}
static createReportFile(string $directory, ?string $fileName=null)