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)";
164 $thisThreadRecords = self::printCurrentThreadRecords();
166 $otherThreadRecordPromises = [];
167 if(self::$collectCallbacks !==
null){
168 foreach(self::$collectCallbacks as $callback){
169 $callbackPromises = $callback();
170 array_push($otherThreadRecordPromises, ...$callbackPromises);
176 Promise::all($otherThreadRecordPromises)->onCompletion(
177 function(array $promisedRecords) use ($resolver, $thisThreadRecords) :
void{
178 $resolver->resolve([...$thisThreadRecords, ...array_merge(...$promisedRecords), ...self::printFooter()]);
181 throw new \AssertionError(
"This promise is not expected to be rejected");
185 return $resolver->getPromise();
188 public static function isEnabled() : bool{
189 return self::$enabled;
192 public static function setEnabled(
bool $enable =
true) : void{
193 if($enable === self::$enabled){
196 self::$enabled = $enable;
197 self::internalReload();
198 if(self::$toggleCallbacks !==
null){
199 foreach(self::$toggleCallbacks as $callback){
205 public static function getStartTime() : float{
206 return self::$timingStart;
209 private static function internalReload() : void{
210 TimingsRecord::reset();
212 self::$timingStart = hrtime(
true);
216 public static function reload() : void{
217 self::internalReload();
218 if(self::$reloadCallbacks !==
null){
219 foreach(self::$reloadCallbacks as $callback){
225 public static function tick(
bool $measure =
true) : void{
227 TimingsRecord::tick($measure);
231 private ?TimingsRecord $rootRecord =
null;
232 private int $timingDepth = 0;
238 private array $recordsByParent = [];
240 public function __construct(
241 private string $name,
242 private ?TimingsHandler $parent =
null,
243 private string $group = Timings::GROUP_MINECRAFT
246 public function getName() : string{ return $this->name; }
248 public function getGroup() : string{ return $this->group; }
250 public function startTiming() : void{
252 $this->internalStartTiming(hrtime(
true));
256 private function internalStartTiming(
int $now) : void{
257 if(++$this->timingDepth === 1){
258 if($this->parent !==
null){
259 $this->parent->internalStartTiming($now);
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;
270 if($this->rootRecord ===
null){
271 $this->rootRecord =
new TimingsRecord($this,
null);
273 $record = $this->rootRecord;
275 $record->startTiming($now);
279 public function stopTiming() : void{
281 $this->internalStopTiming(hrtime(
true));
285 private function internalStopTiming(
int $now) : void{
286 if($this->timingDepth === 0){
292 if(--$this->timingDepth !== 0){
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);
302 $record?->stopTiming($now);
303 if($this->parent !==
null){
304 $this->parent->internalStopTiming($now);
315 public function time(\Closure $closure){
316 $this->startTiming();
327 public function reset() : void{
328 $this->rootRecord = null;
329 $this->recordsByParent = [];
330 $this->timingDepth = 0;
343 $timingsPromise = self::requestPrintTimings();
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}";
354 if(!@mkdir($directory, 0777,
true) && !is_dir($directory)){
358 $timingsFile = Path::join($directory, $fileName .
".txt");
360 $handle = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($timingsFile,
"x+b"));
361 }
catch(\ErrorException){
366 foreach($lines as $line){
367 fwrite($handle, $line . PHP_EOL);
371 $resolver->resolve($timingsFile);
373 fn() =>
throw new AssumptionFailedError(
"This promise is not expected to be rejected")
376 return $resolver->getPromise();