diff --git a/example/bench.php b/example/bench.php new file mode 100644 index 0000000..1fd64ac --- /dev/null +++ b/example/bench.php @@ -0,0 +1,30 @@ +initFromCli()) { + if ($error = $bench->getError()) { + $bench->println('Prepare Error:', $error); + } + + exit(0); +} + +$bench->run(); + +if ($error = $bench->getError()) { + $bench->println('Run Error:', $error); +} diff --git a/src/AutoLoader.php b/src/AutoLoader.php index 78b0bb3..cc3aac2 100644 --- a/src/AutoLoader.php +++ b/src/AutoLoader.php @@ -1,6 +1,6 @@ isCreate()) { + if ($this->isCreated()) { throw new RuntimeException('Memory table have been created, cannot recreated'); } @@ -136,6 +138,9 @@ public function create(): bool $this->table->column($name, $type, $size); } + // Append key column for storage key value. + $this->table->column(self::KEY_FIELD, Table::TYPE_STRING, 255); + // Create memory table $result = $table->create(); @@ -159,10 +164,13 @@ public function create(): bool */ public function set(string $key, array $data): bool { - if (!$this->isCreate()) { + if (!$this->isCreated()) { throw new RuntimeException('Memory table have not been create'); } + // Append key column for storage key value. + $data[self::KEY_FIELD] = $key; + return $this->getTable()->set($key, $data); } @@ -249,14 +257,22 @@ public function count(): int ****************************************************************************/ /** - * flush table + * clear/flush table data */ - // public function flush(): void - // { - // foreach ($this->table as $row) { - // $this->table->del($row['key']); - // } - // } + public function clear(): void + { + $this->flush(); + } + + /** + * clear/flush table data + */ + public function flush(): void + { + foreach ($this->table as $row) { + $this->table->del($row[self::KEY_FIELD]); + } + } /** * Restore data from dbFile diff --git a/src/SwooleBench.php b/src/SwooleBench.php new file mode 100644 index 0000000..aed0818 --- /dev/null +++ b/src/SwooleBench.php @@ -0,0 +1,562 @@ +init(); + } + + /** + * @param string $msg + * @param mixed ...$args + * + * @return bool + */ + protected function addError(string $msg, ...$args): bool + { + if (count($args) > 0) { + $msg = sprintf($msg, ...$args); + } + + $this->error = $msg; + + return false; + } + + /***************************************************************************** + * Quick operate for CLI env + ****************************************************************************/ + + public function initFromCli(): bool + { + if (!$this->parseCliOpts()) { + return false; + } + + $scheme = $this->scheme; + if (!$scheme || !method_exists($this, $scheme)) { + return $this->addError('Not support pressure measurement objects %s', $scheme); + } + + $this->testMethod = $scheme; + + if (!$this->port) { + switch ($scheme) { + case 'tcp': + $this->port = 18309; + break; + case 'ws': + case 'http': + $this->port = 80; + break; + case 'https': + $this->port = 443; + break; + } + } + + return true; + } + + protected function parseCliOpts(): bool + { + $cliArgs = $_SERVER['argv']; + + $this->scriptFile = $cliArgs[0]; + + $shortOpts = 'c:n:l:s:t:d:khv'; + $optValues = getopt($shortOpts); + + if (!$optValues || isset($optValues['h'])) { + $this->showCliHelp(); + return false; + } + + if (isset($optValues['c']) && (int)$optValues['c'] > 0) { + $this->nConcurrency = (int)$optValues['c']; + } + if (isset($optValues['n']) && (int)$optValues['n'] > 0) { + $this->nRequest = (int)$optValues['n']; + } + $this->nShow = $this->nRequest / 10; + + if (isset($optValues['l']) && (int)$optValues['l'] > 0) { + $this->sentLen = (int)$optValues['l']; + } + + // if (!isset($opts['s'])) { + // return $this->addError('Require -s [server_url]. E.g: -s tcp://127.0.0.1:9501'); + // } + + $srvUrl = array_pop($cliArgs); + if (strpos($srvUrl, '//') === false) { + return $this->addError('Require server URL. E.g: tcp://127.0.0.1:9501'); + } + + $urlInfo = parse_url($srvUrl); + if (!$urlInfo) { + return $this->addError('Invalid server URL'); + } + + $this->srvUrl = $srvUrl; + $this->scheme = $urlInfo['scheme']; + if (!filter_var($urlInfo['host'], FILTER_VALIDATE_IP)) { + return $this->addError('Invalid IP address'); + } + + $this->host = $urlInfo['host']; + if (isset($urlInfo['port']) && (int)$urlInfo['port'] > 0) { + $this->port = $urlInfo['port']; + } + + $this->path = $urlInfo['path'] ?? self::PATH; + $this->query = $urlInfo['query'] ?? self::QUERY; + + $this->timeout = (int)($optValues['t'] ?? self::TIMEOUT); + $this->keepAlive = isset($optValues['k']); + $this->verbose = isset($optValues['v']); + + if (isset($optValues['d'])) { + $this->setSentData($optValues['d']); + } + + return true; + } + + public function showCliHelp(): void + { + $help = <<scriptFile} [OPTIONS] URL + +Options: + -c Number of coroutine E.g: -c 100 + -n Number of requests E.g: -n 10000 + -l The length of the data sent per request E.g: -l 1024 + -s Server URL, support: tcp, http, ws E.g: -s tcp://127.0.0.1:9501 + -t Http request timeout detection + Default is 3 seconds, -1 means disable + -k Use HTTP KeepAlive + -d HTTP post data + -v Flag enables verbose progress and debug output + -h Display this help + +Example: + {$this->scriptFile} -c 100 -n 10000 http://127.0.0.1:18306 + {$this->scriptFile} -c 100 -n 10000 tcp://127.0.0.1:18309 +HELP; + echo $help, "\n"; + } + + public function setSentData($data): void + { + $this->sentData = $data; + $this->sentLen = strlen($data); + } + + protected function finish(): void + { + $costTime = $this->format(microtime(true) - $this->startTime); + $nRequest = number_format($this->nRequest); + $requestErrorCount = number_format($this->nRequest - $this->requestCount); + $connectErrorCount = number_format($this->connectErrorCount); + $nSendBytes = number_format($this->nSendBytes); + $nRecvBytes = number_format($this->nRecvBytes); + $requestPerSec = number_format($this->requestCount / $costTime); + $connectTime = $this->format($this->connectTime); + + echo <<srvUrl} +=================================================== +Concurrency number: {$this->nConcurrency} +Complete requests: {$nRequest} +Time taken for tests: {$costTime} seconds +Failed requests: {$requestErrorCount} +Connect failed: {$connectErrorCount} +Total send: {$nSendBytes} bytes +Total receive: {$nRecvBytes} bytes +Requests per second: {$requestPerSec} +Connection time: {$connectTime} seconds\n +EOF; + } + + public function format($time): float + { + return round($time, 4); + } + + /** + * @param mixed ...$args The args allow int, string + */ + public function println(...$args): void + { + echo implode(' ', $args), "\n"; + } + + /** + * @return string + */ + public function getError(): string + { + return $this->error; + } + + /***************************************************************************** + * Bench for TCP server + ****************************************************************************/ + + public function tcp(): self + { + $cli = new TcpClient(SWOOLE_TCP); + + $n = $this->nRequest / $this->nConcurrency; + Coroutine::defer(function () use ($cli) { + $cli->close(); + }); + + if ($this->sentLen === 0) { + $this->sentLen = self::TCP_SENT_LEN; + } + $this->setSentData(str_repeat('A', $this->sentLen)); + + if (!$cli->connect($this->host, $this->port)) { // connection failed + if ($cli->errCode === 111) { // connection refuse + throw new RuntimeException(swoole_strerror($cli->errCode)); + } + if ($cli->errCode === 110) { // connection timeout + $this->connectErrorCount++; + if ($this->verbose) { + echo swoole_strerror($cli->errCode) . PHP_EOL; + } + + return $this; + } + } + + while ($n--) { + // request + if (!$cli->send($this->sentData)) { + if ($this->verbose) { + echo swoole_strerror($cli->errCode) . PHP_EOL; + } + continue; + } + $this->nSendBytes += $this->sentLen; + $this->requestCount++; + if (($this->requestCount % $this->nShow === 0) && $this->verbose) { + echo "Completed {$this->requestCount} requests" . PHP_EOL; + } + + //response + $recvData = $cli->recv(); + if ($recvData === false && $this->verbose) { + echo swoole_strerror($cli->errCode) . PHP_EOL; + } else { + $this->nRecvBytes += strlen($recvData); + } + } + + return $this; + } + + protected function eof(): void + { + $eof = "\r\n\r\n"; + $cli = new TcpClient(SWOOLE_TCP); + $cli->set(['open_eof_check' => true, 'package_eof' => $eof]); + $cli->connect($this->host, $this->port); + $n = $this->nRequest / $this->nConcurrency; + while ($n--) { + // request + $data = $this->sentData . $eof; + $cli->send($data); + $this->nSendBytes += strlen($data); + $this->requestCount++; + // response + $rdata = $cli->recv(); + $this->nRecvBytes += strlen($rdata); + } + $cli->close(); + } + + protected function length(): void + { + $cli = new TcpClient(SWOOLE_TCP); + $cli->set([ + 'open_length_check' => true, + 'package_length_type' => 'N', + 'package_body_offset' => 4, + ]); + $cli->connect($this->host, $this->port); + $n = $this->nRequest / $this->nConcurrency; + while ($n--) { + //request + $data = pack('N', strlen($this->sentData)) . $this->sentData; + $cli->send($data); + $this->nSendBytes += strlen($data); + $this->requestCount++; + //response + $rdata = $cli->recv(); + $this->nRecvBytes += strlen($rdata); + } + $cli->close(); + } + + /***************************************************************************** + * Bench for HTTP server + ****************************************************************************/ + + public function http(): self + { + $httpCli = new HttpClient($this->host, $this->port); + + $n = $this->nRequest / $this->nConcurrency; + Coroutine::defer(function () use ($httpCli) { + $httpCli->close(); + }); + + $headers = [ + 'Host' => "{$this->host}:{$this->port}", + 'Accept' => 'text/html,application/xhtml+xml,application/xml', + 'content-type' => 'application/x-www-form-urlencoded', + ]; + $httpCli->setHeaders($headers); + + $setting = [ + 'timeout' => $this->timeout, + 'keep_alive' => $this->keepAlive, + ]; + $httpCli->set($setting); + if (isset($this->sentData)) { + $httpCli->setData($this->sentData); + } + + $query = empty($this->query) ? '' : "?$this->query"; + + while ($n--) { + $httpCli->execute("{$this->path}{$query}"); + + if (!$this->checkStatusCode($httpCli)) { + continue; + } + + $this->requestCount++; + if ($this->requestCount % $this->nShow === 0 && $this->verbose) { + echo "Completed {$this->requestCount} requests" . PHP_EOL; + } + $recvData = $httpCli->body; + $this->nRecvBytes += strlen($recvData); + } + + return $this; + } + + protected function checkStatusCode(HttpClient $httpCli): bool + { + if ($httpCli->statusCode === -1) { // connection failed + if ($httpCli->errCode === 111) { // connection refused + throw new RuntimeException(swoole_strerror($httpCli->errCode)); + } + if ($httpCli->errCode === 110) { // connection timeout + $this->connectErrorCount++; + if ($this->verbose) { + echo swoole_strerror($httpCli->errCode) . PHP_EOL; + } + return false; + } + } + + if ($httpCli->statusCode === -2) { // request timeout + if ($this->verbose) { + echo swoole_strerror($httpCli->errCode) . PHP_EOL; + } + return false; + } + + if ($httpCli->statusCode === 404) { + $query = empty($this->query) ? '' : "?$this->query"; + $url = "{$this->scheme}://{$this->host}:{$this->port}{$this->path}{$query}"; + throw new RuntimeException("The URL [$url] is non-existent"); + } + + return true; + } + + /***************************************************************************** + * Bench for WebSocket server + ****************************************************************************/ + + protected function ws(): self + { + $wsCli = new HttpClient($this->host, $this->port); + $n = $this->nRequest / $this->nConcurrency; + Coroutine::defer(function () use ($wsCli) { + $wsCli->close(); + }); + + $setting = [ + 'timeout' => $this->timeout, + 'websocket_mask' => true, + ]; + $wsCli->set($setting); + if (!$wsCli->upgrade($this->path)) { + if ($wsCli->errCode === 111) { + throw new RuntimeException(swoole_strerror($wsCli->errCode)); + } + + if ($wsCli->errCode === 110) { + throw new RuntimeException(swoole_strerror($wsCli->errCode)); + } + + throw new RuntimeException('Handshake failed'); + } + + if ($this->sentLen === 0) { + $this->sentLen = self::TCP_SENT_LEN; + } + $this->setSentData(str_repeat('A', $this->sentLen)); + + while ($n--) { + // request + if (!$wsCli->push($this->data)) { + if ($wsCli->errCode === 8502) { + throw new RuntimeException('Error OPCODE'); + } + + if ($wsCli->errCode === 8503) { + throw new RuntimeException('Not connected to the server or the connection has been closed'); + } + + throw new RuntimeException('Handshake failed'); + } + + $this->nSendBytes += $this->sentLen; + $this->requestCount++; + if (($this->requestCount % $this->nShow === 0) && $this->verbose) { + echo "Completed {$this->requestCount} requests" . PHP_EOL; + } + + //response + $frame = $wsCli->recv(); + $this->nRecvBytes += strlen($frame->data); + } + + return $this; + } + + /***************************************************************************** + * Do start bench testing + ****************************************************************************/ + + /** + * @return self + */ + public function run(): self + { + $exitException = false; + $exitExceptingMsg = ''; + $this->startTime = microtime(true); + + $sch = new Coroutine\Scheduler; + $sch->parallel($this->nConcurrency, function () use (&$exitException, &$exitExceptingMsg) { + try { + $this->{$this->testMethod}(); + } catch (RuntimeException $e) { + $exitException = true; + $exitExceptingMsg = $e->getMessage(); + } + }); + $sch->start(); + + $this->beginSendTime = microtime(true); + $this->connectTime = $this->beginSendTime - $this->startTime; + if ($exitException) { + exit($exitExceptingMsg . PHP_EOL); + } + + $this->finish(); + return $this; + } +}