PocketMine-MP 5.39.3 git-400eb2dddf91a9c112aa09f3b498ffc8c85e98ed
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
158 public static function printTimings() : array{
159 $records = self::printCurrentThreadRecords();
160 $footer = self::printFooter();
161
162 return [...$records, ...$footer];
163 }
164
176 public static function requestPrintTimings() : Promise{
177 $thisThreadRecords = self::printCurrentThreadRecords();
178
179 $otherThreadRecordPromises = [];
180 if(self::$collectCallbacks !== null){
181 foreach(self::$collectCallbacks as $callback){
182 $callbackPromises = $callback();
183 array_push($otherThreadRecordPromises, ...$callbackPromises);
184 }
185 }
186
188 $resolver = new PromiseResolver();
189 Promise::all($otherThreadRecordPromises)->onCompletion(
190 function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{
191 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
192 },
193 function() : void{
194 throw new \AssertionError("This promise is not expected to be rejected");
195 }
196 );
197
198 return $resolver->getPromise();
199 }
200
201 public static function isEnabled() : bool{
202 return self::$enabled;
203 }
204
205 public static function setEnabled(bool $enable = true) : void{
206 if($enable === self::$enabled){
207 return;
208 }
209 self::$enabled = $enable;
210 self::internalReload();
211 if(self::$toggleCallbacks !== null){
212 foreach(self::$toggleCallbacks as $callback){
213 $callback($enable);
214 }
215 }
216 }
217
218 public static function getStartTime() : float{
219 return self::$timingStart;
220 }
221
222 private static function internalReload() : void{
223 TimingsRecord::reset();
224 if(self::$enabled){
225 self::$timingStart = hrtime(true);
226 }
227 }
228
229 public static function reload() : void{
230 self::internalReload();
231 if(self::$reloadCallbacks !== null){
232 foreach(self::$reloadCallbacks as $callback){
233 $callback();
234 }
235 }
236 }
237
238 public static function tick(bool $measure = true) : void{
239 if(self::$enabled){
240 TimingsRecord::tick($measure);
241 }
242 }
243
244 private ?TimingsRecord $rootRecord = null;
245 private int $timingDepth = 0;
246
251 private array $recordsByParent = [];
252
253 public function __construct(
254 private string $name,
255 private ?TimingsHandler $parent = null,
256 private string $group = Timings::GROUP_MINECRAFT
257 ){}
258
259 public function getName() : string{ return $this->name; }
260
261 public function getGroup() : string{ return $this->group; }
262
263 public function startTiming() : void{
264 if(self::$enabled){
265 $this->internalStartTiming(hrtime(true));
266 }
267 }
268
269 private function internalStartTiming(int $now) : void{
270 if(++$this->timingDepth === 1){
271 if($this->parent !== null){
272 $this->parent->internalStartTiming($now);
273 }
274
275 $current = TimingsRecord::getCurrentRecord();
276 if($current !== null){
277 $record = $this->recordsByParent[spl_object_id($current)] ?? null;
278 if($record === null){
279 $record = new TimingsRecord($this, $current);
280 $this->recordsByParent[spl_object_id($current)] = $record;
281 }
282 }else{
283 if($this->rootRecord === null){
284 $this->rootRecord = new TimingsRecord($this, null);
285 }
286 $record = $this->rootRecord;
287 }
288 $record->startTiming($now);
289 }
290 }
291
292 public function stopTiming() : void{
293 if(self::$enabled){
294 $this->internalStopTiming(hrtime(true));
295 }
296 }
297
298 private function internalStopTiming(int $now) : void{
299 if($this->timingDepth === 0){
300 //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets
301 //and enable/disable, it would have a performance impact. Therefore, considering the limited
302 //usefulness of bailing here anyway, we don't currently bother.
303 return;
304 }
305 if(--$this->timingDepth !== 0){
306 return;
307 }
308
309 $record = TimingsRecord::getCurrentRecord();
310 $timerId = spl_object_id($this);
311 for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
312 \GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
313 $record->stopTiming($now);
314 }
315 $record?->stopTiming($now);
316 if($this->parent !== null){
317 $this->parent->internalStopTiming($now);
318 }
319 }
320
328 public function time(\Closure $closure){
329 $this->startTiming();
330 try{
331 return $closure();
332 }finally{
333 $this->stopTiming();
334 }
335 }
336
340 public function reset() : void{
341 $this->rootRecord = null;
342 $this->recordsByParent = [];
343 $this->timingDepth = 0;
344 }
345
355 public static function createReportFile(string $directory, ?string $fileName = null) : Promise{
356 $timingsPromise = self::requestPrintTimings();
357
359 $resolver = new PromiseResolver();
360
361 $timingsPromise->onCompletion(
362 function(array $lines) use ($fileName, $directory, $resolver) : void{
363 if($fileName === null){
364 $date = date('Y-m-d_H.i.s_T');
365 $fileName = "timings_{$date}";
366 }
367 if(!@mkdir($directory, 0777, true) && !is_dir($directory)){
368 $resolver->reject();
369 return;
370 }
371 $timingsFile = Path::join($directory, $fileName . ".txt");
372 try{
373 $handle = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($timingsFile, "x+b"));
374 }catch(\ErrorException){
375 //TODO: it'd be better if we could report this to the promise callback
376 $resolver->reject();
377 return;
378 }
379 foreach($lines as $line){
380 fwrite($handle, $line . PHP_EOL);
381 }
382 fclose($handle);
383
384 $resolver->resolve($timingsFile);
385 },
386 fn() => throw new AssumptionFailedError("This promise is not expected to be rejected")
387 );
388
389 return $resolver->getPromise();
390 }
391}
static createReportFile(string $directory, ?string $fileName=null)