mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-06 01:46:04 +00:00
Support for collecting timings from threads, and implement async task timings (#6333)
The following callbacks can now be registered in timings, to allow threads to be notified of these events: - Turning on/off (`TimingsHandler::getToggleCallbacks()->add(...)`) - Reset (`TimingsHandler::getReloadCallbacks()->add(...)`) - Collect (`TimingsHandler::getCollectCallbacks()->add(...)`) Collect callbacks must return `list<Promise>`. The promises must be `resolve()`d with `list<string>` of printed timings records, as returned by `TimingsHandler::printCurrentThreadRecords()`. It's recommended to use 1 promise per thread. A timings report will be produced once all promises have been resolved. This system is used internally to collect timings for async tasks (closes #6166). For timings viewer developers: Timings format version has been bumped to 3 to accommodate this change. Timings groups should now include a `ThreadId` at the end of timings group names to ensure that their record IDs are segregated correctly, as they could otherwise conflict between threads. The main thread is not required to specify a thread ID. See pmmp/timings@13cefa6279 for implementation examples. New PHPStan error is caused by phpstan/phpstan#10924
This commit is contained in:
@ -26,28 +26,28 @@ namespace pocketmine\command\defaults;
|
||||
use pocketmine\command\Command;
|
||||
use pocketmine\command\CommandSender;
|
||||
use pocketmine\command\utils\InvalidCommandSyntaxException;
|
||||
use pocketmine\errorhandler\ErrorToExceptionHandler;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\permission\DefaultPermissionNames;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\scheduler\BulkCurlTask;
|
||||
use pocketmine\scheduler\BulkCurlTaskOperation;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\InternetException;
|
||||
use pocketmine\utils\InternetRequestResult;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\YmlServerProperties;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function count;
|
||||
use function fclose;
|
||||
use function file_exists;
|
||||
use function fopen;
|
||||
use function fseek;
|
||||
use function fwrite;
|
||||
use function http_build_query;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function json_decode;
|
||||
use function mkdir;
|
||||
use function stream_get_contents;
|
||||
use function strtolower;
|
||||
use const CURLOPT_AUTOREFERER;
|
||||
use const CURLOPT_FOLLOWLOCATION;
|
||||
@ -101,82 +101,91 @@ class TimingsCommand extends VanillaCommand{
|
||||
TimingsHandler::reload();
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_reset());
|
||||
}elseif($mode === "merged" || $mode === "report" || $paste){
|
||||
$timings = "";
|
||||
if($paste){
|
||||
$fileTimings = Utils::assumeNotFalse(fopen("php://temp", "r+b"), "Opening php://temp should never fail");
|
||||
}else{
|
||||
$index = 0;
|
||||
$timingFolder = Path::join($sender->getServer()->getDataPath(), "timings");
|
||||
|
||||
if(!file_exists($timingFolder)){
|
||||
mkdir($timingFolder, 0777);
|
||||
}
|
||||
$timings = Path::join($timingFolder, "timings.txt");
|
||||
while(file_exists($timings)){
|
||||
$timings = Path::join($timingFolder, "timings" . (++$index) . ".txt");
|
||||
}
|
||||
|
||||
$fileTimings = fopen($timings, "a+b");
|
||||
}
|
||||
$lines = TimingsHandler::printTimings();
|
||||
foreach($lines as $line){
|
||||
fwrite($fileTimings, $line . PHP_EOL);
|
||||
}
|
||||
|
||||
if($paste){
|
||||
fseek($fileTimings, 0);
|
||||
$data = [
|
||||
"browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(),
|
||||
"data" => $content = stream_get_contents($fileTimings)
|
||||
];
|
||||
fclose($fileTimings);
|
||||
|
||||
$host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io");
|
||||
|
||||
$sender->getServer()->getAsyncPool()->submitTask(new BulkCurlTask(
|
||||
[new BulkCurlTaskOperation(
|
||||
"https://$host?upload=true",
|
||||
10,
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"User-Agent: $agent",
|
||||
"Content-Type: application/x-www-form-urlencoded"
|
||||
],
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_AUTOREFERER => false,
|
||||
CURLOPT_FOLLOWLOCATION => false
|
||||
]
|
||||
)],
|
||||
function(array $results) use ($sender, $host) : void{
|
||||
/** @phpstan-var array<InternetRequestResult|InternetException> $results */
|
||||
if($sender instanceof Player && !$sender->isOnline()){ // TODO replace with a more generic API method for checking availability of CommandSender
|
||||
return;
|
||||
}
|
||||
$result = $results[0];
|
||||
if($result instanceof InternetException){
|
||||
$sender->getServer()->getLogger()->logException($result);
|
||||
return;
|
||||
}
|
||||
$response = json_decode($result->getBody(), true);
|
||||
if(is_array($response) && isset($response["id"])){
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
|
||||
"https://" . $host . "/?id=" . $response["id"]));
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());
|
||||
}
|
||||
}
|
||||
));
|
||||
}else{
|
||||
fclose($fileTimings);
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsWrite($timings));
|
||||
}
|
||||
$timingsPromise = TimingsHandler::requestPrintTimings();
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_collect());
|
||||
$timingsPromise->onCompletion(
|
||||
fn(array $lines) => $paste ? $this->uploadReport($lines, $sender) : $this->createReportFile($lines, $sender),
|
||||
fn() => throw new AssumptionFailedError("This promise is not expected to be rejected")
|
||||
);
|
||||
}else{
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $lines
|
||||
* @phpstan-param list<string> $lines
|
||||
*/
|
||||
private function createReportFile(array $lines, CommandSender $sender) : void{
|
||||
$index = 0;
|
||||
$timingFolder = Path::join($sender->getServer()->getDataPath(), "timings");
|
||||
|
||||
if(!file_exists($timingFolder)){
|
||||
mkdir($timingFolder, 0777);
|
||||
}
|
||||
$timings = Path::join($timingFolder, "timings.txt");
|
||||
while(file_exists($timings)){
|
||||
$timings = Path::join($timingFolder, "timings" . (++$index) . ".txt");
|
||||
}
|
||||
|
||||
$fileTimings = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($timings, "a+b"));
|
||||
foreach($lines as $line){
|
||||
fwrite($fileTimings, $line . PHP_EOL);
|
||||
}
|
||||
fclose($fileTimings);
|
||||
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsWrite($timings));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $lines
|
||||
* @phpstan-param list<string> $lines
|
||||
*/
|
||||
private function uploadReport(array $lines, CommandSender $sender) : void{
|
||||
$data = [
|
||||
"browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(),
|
||||
"data" => implode("\n", $lines)
|
||||
];
|
||||
|
||||
$host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io");
|
||||
|
||||
$sender->getServer()->getAsyncPool()->submitTask(new BulkCurlTask(
|
||||
[new BulkCurlTaskOperation(
|
||||
"https://$host?upload=true",
|
||||
10,
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"User-Agent: $agent",
|
||||
"Content-Type: application/x-www-form-urlencoded"
|
||||
],
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_AUTOREFERER => false,
|
||||
CURLOPT_FOLLOWLOCATION => false
|
||||
]
|
||||
)],
|
||||
function(array $results) use ($sender, $host) : void{
|
||||
/** @phpstan-var array<InternetRequestResult|InternetException> $results */
|
||||
if($sender instanceof Player && !$sender->isOnline()){ // TODO replace with a more generic API method for checking availability of CommandSender
|
||||
return;
|
||||
}
|
||||
$result = $results[0];
|
||||
if($result instanceof InternetException){
|
||||
$sender->getServer()->getLogger()->logException($result);
|
||||
return;
|
||||
}
|
||||
$response = json_decode($result->getBody(), true);
|
||||
if(is_array($response) && isset($response["id"])){
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
|
||||
"https://" . $host . "/?id=" . $response["id"]));
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user