R performance improvements
This commit is contained in:
parent
1eafa237a0
commit
7c1fca5c38
|
|
@ -15,7 +15,6 @@ use Illuminate\Support\Facades\Log;
|
||||||
class Controller
|
class Controller
|
||||||
{
|
{
|
||||||
protected bool $isMockConnection;
|
protected bool $isMockConnection;
|
||||||
protected MockConnectionSocketPair|null $mockConnectionClone = null;
|
|
||||||
|
|
||||||
final public function __construct(
|
final public function __construct(
|
||||||
protected ConnectionInterface $connection,
|
protected ConnectionInterface $connection,
|
||||||
|
|
@ -23,14 +22,7 @@ class Controller
|
||||||
protected string $event,
|
protected string $event,
|
||||||
protected LocalChannelManager|RedisChannelManager $channelManager
|
protected LocalChannelManager|RedisChannelManager $channelManager
|
||||||
) {
|
) {
|
||||||
// Cache class check to avoid repeated get_class() calls (reflection is slow)
|
$this->isMockConnection = $connection instanceof MockConnectionSocketPair;
|
||||||
$connectionClass = get_class($connection);
|
|
||||||
$this->isMockConnection = $connectionClass === MockConnectionSocketPair::class;
|
|
||||||
|
|
||||||
// Pre-clone MockConnection once if needed (reuse across method calls)
|
|
||||||
if ($this->isMockConnection) {
|
|
||||||
$this->mockConnectionClone = clone $connection;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -167,11 +159,7 @@ class Controller
|
||||||
// Pre-encode once (avoid repeated encoding)
|
// Pre-encode once (avoid repeated encoding)
|
||||||
$encoded = json_encode($p);
|
$encoded = json_encode($p);
|
||||||
|
|
||||||
if ($this->isMockConnection) {
|
|
||||||
$this->mockConnectionClone->send($encoded);
|
|
||||||
} else {
|
|
||||||
$this->connection->send($encoded);
|
$this->connection->send($encoded);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -198,11 +186,7 @@ class Controller
|
||||||
// Pre-encode once (avoid repeated encoding)
|
// Pre-encode once (avoid repeated encoding)
|
||||||
$encoded = json_encode($p);
|
$encoded = json_encode($p);
|
||||||
|
|
||||||
if ($this->isMockConnection) {
|
|
||||||
$this->mockConnectionClone->send($encoded);
|
|
||||||
} else {
|
|
||||||
$this->connection->send($encoded);
|
$this->connection->send($encoded);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -240,14 +224,7 @@ class Controller
|
||||||
|
|
||||||
Log::channel('websocket')->error('Send error: ' . @$p['data']['message'], $p);
|
Log::channel('websocket')->error('Send error: ' . @$p['data']['message'], $p);
|
||||||
|
|
||||||
// Pre-encode once (avoid repeated encoding)
|
$this->connection->send(json_encode($p));
|
||||||
$encoded = json_encode($p);
|
|
||||||
|
|
||||||
if ($this->isMockConnection) {
|
|
||||||
$this->mockConnectionClone->send($encoded);
|
|
||||||
} else {
|
|
||||||
$this->connection->send($encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -272,32 +249,29 @@ class Controller
|
||||||
'channel' => $channel,
|
'channel' => $channel,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!$this->isMockConnection) {
|
if ($this->isMockConnection) {
|
||||||
|
$this->connection->broadcast($p, $channel, $including_self);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct broadcast (non-forked context, e.g. testing)
|
||||||
if (! $channel) {
|
if (! $channel) {
|
||||||
$this->error('Channel not found');
|
$this->error('Channel not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-encode ONCE - massive improvement for 100+ connections
|
|
||||||
$encoded = json_encode($p);
|
$encoded = json_encode($p);
|
||||||
|
|
||||||
foreach ($this->channel->getConnections() as $channel_conection) {
|
foreach ($this->channel->getConnections() as $channel_conection) {
|
||||||
if ($channel_conection !== $this->connection) {
|
if ($channel_conection !== $this->connection) {
|
||||||
$channel_conection->send($encoded);
|
$channel_conection->send($encoded);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($including_self) {
|
if ($including_self) {
|
||||||
$this->connection->send($encoded);
|
$this->connection->send($encoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$this->mockConnectionClone->broadcast(
|
|
||||||
$p,
|
|
||||||
$channel,
|
|
||||||
$including_self
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final public function whisper(
|
final public function whisper(
|
||||||
array|string|null $payload = null,
|
array|string|null $payload = null,
|
||||||
|
|
@ -319,19 +293,18 @@ class Controller
|
||||||
'channel' => $channel,
|
'channel' => $channel,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!$this->isMockConnection) {
|
if ($this->isMockConnection) {
|
||||||
// Pre-encode ONCE for all matching sockets
|
$this->connection->whisper($p, $socketIds, $channel);
|
||||||
$encoded = json_encode($p);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use array_flip for O(1) lookup instead of O(n) in_array
|
// Direct whisper (non-forked context, e.g. testing)
|
||||||
|
$encoded = json_encode($p);
|
||||||
$socketIdLookup = array_flip($socketIds);
|
$socketIdLookup = array_flip($socketIds);
|
||||||
$sentTo = [];
|
$sentTo = [];
|
||||||
|
|
||||||
// Search ALL connections across ALL channels to find target socket IDs
|
|
||||||
// This is necessary because whisper targets specific sockets regardless of channel
|
|
||||||
$this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) {
|
$this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) {
|
||||||
foreach ($connections as $connection) {
|
foreach ($connections as $connection) {
|
||||||
// Skip if already sent to this socket (can appear in multiple channels)
|
|
||||||
if (isset($sentTo[$connection->socketId])) {
|
if (isset($sentTo[$connection->socketId])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -342,13 +315,6 @@ class Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
$this->mockConnectionClone->whisper(
|
|
||||||
$p,
|
|
||||||
$socketIds,
|
|
||||||
$channel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function send_error(
|
private static function send_error(
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,12 @@ use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed;
|
use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed;
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\NewConnection;
|
use BlaxSoftware\LaravelWebSockets\Events\NewConnection;
|
||||||
use BlaxSoftware\LaravelWebSockets\Exceptions\WebSocketException;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
|
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
|
||||||
use BlaxSoftware\LaravelWebSockets\Websocket\MockConnectionSocketPair;
|
use BlaxSoftware\LaravelWebSockets\Websocket\MockConnectionSocketPair;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\ConnectionsOverCapacity;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\ConnectionsOverCapacity;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\OriginNotAllowed;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\OriginNotAllowed;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException as ExceptionsWebSocketException;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException as ExceptionsWebSocketException;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Messages\PusherMessageFactory;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\QueryParameters;
|
use BlaxSoftware\LaravelWebSockets\Server\QueryParameters;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -61,6 +59,11 @@ class Handler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
private static ?bool $hotReload = null;
|
private static ?bool $hotReload = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether debug mode is enabled (cached to avoid container resolution per message)
|
||||||
|
*/
|
||||||
|
private static ?bool $debug = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new handler.
|
* Initialize a new handler.
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,7 +179,7 @@ class Handler implements MessageComponentInterface
|
||||||
$this->authenticateConnection($connection, $channel, $messageArray);
|
$this->authenticateConnection($connection, $channel, $messageArray);
|
||||||
|
|
||||||
// Only log in debug mode to reduce I/O
|
// Only log in debug mode to reduce I/O
|
||||||
if (config('app.debug')) {
|
if (self::$debug ??= (bool) config('app.debug')) {
|
||||||
Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload);
|
Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -552,11 +555,10 @@ class Handler implements MessageComponentInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's already a string (JSON), try to parse for broadcast/whisper
|
// Prefix-based routing: B: = broadcast, W: = whisper, else regular response
|
||||||
if (is_string($data)) {
|
// Avoids JSON decode overhead for regular responses (most common path)
|
||||||
$bm = json_decode($data, true);
|
if (str_starts_with($data, 'B:')) {
|
||||||
|
$bm = json_decode(substr($data, 2), true);
|
||||||
if (isset($bm['broadcast']) && $bm['broadcast']) {
|
|
||||||
$this->broadcast(
|
$this->broadcast(
|
||||||
$connection->app->id,
|
$connection->app->id,
|
||||||
$bm['data'] ?? null,
|
$bm['data'] ?? null,
|
||||||
|
|
@ -568,7 +570,8 @@ class Handler implements MessageComponentInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($bm['whisper']) && $bm['whisper']) {
|
if (str_starts_with($data, 'W:')) {
|
||||||
|
$bm = json_decode(substr($data, 2), true);
|
||||||
$this->whisper(
|
$this->whisper(
|
||||||
$connection->app->id,
|
$connection->app->id,
|
||||||
$bm['data'] ?? null,
|
$bm['data'] ?? null,
|
||||||
|
|
@ -579,10 +582,9 @@ class Handler implements MessageComponentInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular response
|
// Regular response - send directly without JSON decode
|
||||||
$connection->send($data);
|
$connection->send($data);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleMessageError(\Throwable $e): void
|
protected function handleMessageError(\Throwable $e): void
|
||||||
{
|
{
|
||||||
|
|
@ -902,11 +904,22 @@ class Handler implements MessageComponentInterface
|
||||||
PrivateChannel|Channel|PresenceChannel|null $channel,
|
PrivateChannel|Channel|PresenceChannel|null $channel,
|
||||||
$message = []
|
$message = []
|
||||||
) {
|
) {
|
||||||
|
// Fast path: auth already resolved for this connection (skips cache read + DB query + cache write)
|
||||||
|
if (isset($connection->authLoaded)) {
|
||||||
|
if ($connection->user) {
|
||||||
|
Auth::login($connection->user);
|
||||||
|
}
|
||||||
|
$this->scheduleLogout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->loadCachedAuth($connection, $channel);
|
$this->loadCachedAuth($connection, $channel);
|
||||||
$this->ensureUserIsSet($connection, $channel);
|
$this->ensureUserIsSet($connection, $channel);
|
||||||
$this->updateAuthState($connection);
|
$this->updateAuthState($connection);
|
||||||
$this->cacheAuthenticatedUser($connection);
|
$this->cacheAuthenticatedUser($connection);
|
||||||
$this->scheduleLogout();
|
$this->scheduleLogout();
|
||||||
|
|
||||||
|
$connection->authLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadCachedAuth(ConnectionInterface $connection, $channel): void
|
protected function loadCachedAuth(ConnectionInterface $connection, $channel): void
|
||||||
|
|
@ -958,7 +971,6 @@ class Handler implements MessageComponentInterface
|
||||||
|
|
||||||
/** @var \App\Models\User */
|
/** @var \App\Models\User */
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$user->refresh();
|
|
||||||
|
|
||||||
$socketId = $connection->socketId;
|
$socketId = $connection->socketId;
|
||||||
|
|
||||||
|
|
@ -1055,23 +1067,23 @@ class Handler implements MessageComponentInterface
|
||||||
bool $including_self = false,
|
bool $including_self = false,
|
||||||
$connection = null
|
$connection = null
|
||||||
): void {
|
): void {
|
||||||
|
|
||||||
$channel = $this->channelManager->findOrCreate($appId, $channel);
|
$channel = $this->channelManager->findOrCreate($appId, $channel);
|
||||||
|
|
||||||
$p = [
|
// Pre-encode once for all connections
|
||||||
'event' => ($event ?? $event),
|
$encoded = json_encode([
|
||||||
|
'event' => $event,
|
||||||
'data' => $payload,
|
'data' => $payload,
|
||||||
'channel' => $channel->getName(),
|
'channel' => $channel->getName(),
|
||||||
];
|
]);
|
||||||
|
|
||||||
foreach ($channel->getConnections() as $channel_conection) {
|
foreach ($channel->getConnections() as $channel_conection) {
|
||||||
if ($channel_conection->socketId !== $connection->socketId) {
|
if ($channel_conection->socketId !== $connection->socketId) {
|
||||||
$channel_conection->send(json_encode($p));
|
$channel_conection->send($encoded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($including_self) {
|
if ($including_self) {
|
||||||
$connection->send(json_encode($p));
|
$connection->send($encoded);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,12 @@ class MockConnectionSocketPair implements ConnectionInterface
|
||||||
bool $including_self = false,
|
bool $including_self = false,
|
||||||
): self {
|
): self {
|
||||||
$data ??= [];
|
$data ??= [];
|
||||||
$data['broadcast'] = true;
|
|
||||||
$data['channel'] ??= $channel;
|
$data['channel'] ??= $channel;
|
||||||
$data['including_self'] = $including_self;
|
$data['including_self'] = $including_self;
|
||||||
|
|
||||||
return $this->send(json_encode($data));
|
// B: prefix for instant routing in parent (avoids JSON decode for regular responses)
|
||||||
|
$this->ipc->sendToParent('B:' . json_encode($data));
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,11 +62,12 @@ class MockConnectionSocketPair implements ConnectionInterface
|
||||||
?string $channel = null,
|
?string $channel = null,
|
||||||
): self {
|
): self {
|
||||||
$data ??= [];
|
$data ??= [];
|
||||||
$data['whisper'] = true;
|
|
||||||
$data['channel'] ??= $channel;
|
$data['channel'] ??= $channel;
|
||||||
$data['socket_ids'] = $socketIds;
|
$data['socket_ids'] = $socketIds;
|
||||||
|
|
||||||
return $this->send(json_encode($data));
|
// W: prefix for instant routing in parent (avoids JSON decode for regular responses)
|
||||||
|
$this->ipc->sendToParent('W:' . json_encode($data));
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function close(): void
|
public function close(): void
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue