R performance improvements

This commit is contained in:
Fabian @ Blax Software 2026-02-09 13:19:09 +01:00
parent 1eafa237a0
commit 7c1fca5c38
3 changed files with 103 additions and 123 deletions

View File

@ -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->connection->send($encoded);
$this->mockConnectionClone->send($encoded);
} else {
$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->connection->send($encoded);
$this->mockConnectionClone->send($encoded);
} else {
$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,30 +249,27 @@ class Controller
'channel' => $channel, 'channel' => $channel,
]; ];
if (!$this->isMockConnection) { if ($this->isMockConnection) {
if (! $channel) { $this->connection->broadcast($p, $channel, $including_self);
$this->error('Channel not found'); return;
return; }
// Direct broadcast (non-forked context, e.g. testing)
if (! $channel) {
$this->error('Channel not found');
return;
}
$encoded = json_encode($p);
foreach ($this->channel->getConnections() as $channel_conection) {
if ($channel_conection !== $this->connection) {
$channel_conection->send($encoded);
} }
}
// Pre-encode ONCE - massive improvement for 100+ connections if ($including_self) {
$encoded = json_encode($p); $this->connection->send($encoded);
foreach ($this->channel->getConnections() as $channel_conection) {
if ($channel_conection !== $this->connection) {
$channel_conection->send($encoded);
}
if ($including_self) {
$this->connection->send($encoded);
}
}
} else {
$this->mockConnectionClone->broadcast(
$p,
$channel,
$including_self
);
} }
} }
@ -319,36 +293,28 @@ 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
$socketIdLookup = array_flip($socketIds);
$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) {
foreach ($connections as $connection) {
// Skip if already sent to this socket (can appear in multiple channels)
if (isset($sentTo[$connection->socketId])) {
continue;
}
if (isset($socketIdLookup[$connection->socketId])) {
$connection->send($encoded);
$sentTo[$connection->socketId] = true;
}
}
});
} else {
$this->mockConnectionClone->whisper(
$p,
$socketIds,
$channel
);
} }
// Direct whisper (non-forked context, e.g. testing)
$encoded = json_encode($p);
$socketIdLookup = array_flip($socketIds);
$sentTo = [];
$this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) {
foreach ($connections as $connection) {
if (isset($sentTo[$connection->socketId])) {
continue;
}
if (isset($socketIdLookup[$connection->socketId])) {
$connection->send($encoded);
$sentTo[$connection->socketId] = true;
}
}
});
} }
private static function send_error( private static function send_error(

View File

@ -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,36 +555,35 @@ 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, $bm['event'] ?? null,
$bm['event'] ?? null, $bm['channel'] ?? null,
$bm['channel'] ?? null, $bm['including_self'] ?? false,
$bm['including_self'] ?? false, $connection
$connection );
); return;
return;
}
if (isset($bm['whisper']) && $bm['whisper']) {
$this->whisper(
$connection->app->id,
$bm['data'] ?? null,
$bm['event'] ?? null,
$bm['socket_ids'] ?? [],
$bm['channel'] ?? null,
);
return;
}
// Regular response
$connection->send($data);
} }
if (str_starts_with($data, 'W:')) {
$bm = json_decode(substr($data, 2), true);
$this->whisper(
$connection->app->id,
$bm['data'] ?? null,
$bm['event'] ?? null,
$bm['socket_ids'] ?? [],
$bm['channel'] ?? null,
);
return;
}
// Regular response - send directly without JSON decode
$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);
}
} }
} }

View File

@ -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