52 private const FORMAT_VERSION = 3;
54 private static bool $enabled =
false;
55 private static int $timingStart = 0;
58 private static ?
ObjectSet $toggleCallbacks =
null;
60 private static ?
ObjectSet $reloadCallbacks =
null;
62 private static ?
ObjectSet $collectCallbacks =
null;
95 $threadId = NativeThread::getCurrentThread()?->getThreadId();
98 foreach(TimingsRecord::getAll() as $timings){
99 $time = $timings->getTotalTime();
100 $count = $timings->getCount();
106 $avg = $time / $count;
108 $group = $timings->getGroup() . ($threadId !==
null ?
" ThreadId: $threadId" :
"");
109 $groups[$group][] = implode(
" ", [
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(),
124 foreach(Utils::stringifyKeys($groups) as $groupName => $lines){
125 $result[] = $groupName;
126 foreach($lines as $line){
127 $result[] =
" $line";
138 private static function printFooter() : array{
141 $result[] =
"# Version " . Server::getInstance()->getVersion();
142 $result[] =
"# " . Server::getInstance()->getName() .
" " . Server::getInstance()->getPocketMineVersion();
144 $result[] =
"# FormatVersion " . self::FORMAT_VERSION;
146 $sampleTime = hrtime(
true) - self::$timingStart;
147 $result[] =
"Sample time $sampleTime (" . ($sampleTime / 1000000000) .
"s)";
159 $records = self::printCurrentThreadRecords();
160 $footer = self::printFooter();
162 return [...$records, ...$footer];
177 $thisThreadRecords = self::printCurrentThreadRecords();
179 $otherThreadRecordPromises = [];
180 if(self::$collectCallbacks !==
null){
181 foreach(self::$collectCallbacks as $callback){
182 $callbackPromises = $callback();
183 array_push($otherThreadRecordPromises, ...$callbackPromises);
189 Promise::all($otherThreadRecordPromises)->onCompletion(
190 function(array $promisedRecords) use ($resolver, $thisThreadRecords) :
void{
191 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
194 throw new \AssertionError(
"This promise is not expected to be rejected");
198 return $resolver->getPromise();
201 public static function isEnabled() : bool{
202 return self::$enabled;
205 public static function setEnabled(
bool $enable =
true) : void{
206 if($enable === self::$enabled){
209 self::$enabled = $enable;
210 self::internalReload();
211 if(self::$toggleCallbacks !==
null){
212 foreach(self::$toggleCallbacks as $callback){
218 public static function getStartTime() : float{
219 return self::$timingStart;
222 private static function internalReload() : void{
223 TimingsRecord::reset();
225 self::$timingStart = hrtime(
true);
229 public static function reload() : void{
230 self::internalReload();
231 if(self::$reloadCallbacks !==
null){
232 foreach(self::$reloadCallbacks as $callback){
238 public static function tick(
bool $measure =
true) : void{
240 TimingsRecord::tick($measure);
244 private ?TimingsRecord $rootRecord =
null;
245 private int $timingDepth = 0;
251 private array $recordsByParent = [];
253 public function __construct(
254 private string $name,
255 private ?TimingsHandler $parent =
null,
256 private string $group = Timings::GROUP_MINECRAFT
259 public function getName() : string{ return $this->name; }
261 public function getGroup() : string{ return $this->group; }
263 public function startTiming() : void{
265 $this->internalStartTiming(hrtime(
true));
269 private function internalStartTiming(
int $now) : void{
270 if(++$this->timingDepth === 1){
271 if($this->parent !==
null){
272 $this->parent->internalStartTiming($now);
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;
283 if($this->rootRecord ===
null){
284 $this->rootRecord =
new TimingsRecord($this,
null);
286 $record = $this->rootRecord;
288 $record->startTiming($now);
292 public function stopTiming() : void{
294 $this->internalStopTiming(hrtime(
true));
298 private function internalStopTiming(
int $now) : void{
299 if($this->timingDepth === 0){
305 if(--$this->timingDepth !== 0){
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);
315 $record?->stopTiming($now);
316 if($this->parent !==
null){
317 $this->parent->internalStopTiming($now);
328 public function time(\Closure $closure){
329 $this->startTiming();
340 public function reset() : void{
341 $this->rootRecord = null;
342 $this->recordsByParent = [];
343 $this->timingDepth = 0;
356 $timingsPromise = self::requestPrintTimings();
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}";
367 if(!@mkdir($directory, 0777,
true) && !is_dir($directory)){
371 $timingsFile = Path::join($directory, $fileName .
".txt");
373 $handle = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($timingsFile,
"x+b"));
374 }
catch(\ErrorException){
379 foreach($lines as $line){
380 fwrite($handle, $line . PHP_EOL);
384 $resolver->resolve($timingsFile);
386 fn() =>
throw new AssumptionFailedError(
"This promise is not expected to be rejected")
389 return $resolver->getPromise();