Routing and Connection Acceptability
Routers in Electric UI are created on a per-device basis in the CreateRouterCallback
passed to the Device Manager.
The simplest router is one that broadcasts all packets across all connections.
import { MessageRouterBroadcast } from '@electricui/core'
function createRouter(device: Device) { const router = new MessageRouterBroadcast(device) return router}deviceManager.setCreateRouterCallback(createRouter)
This isn't ideal, as it virtually guarantees race conditions across connections with varying latencies, and uses much more bandwidth than necessary.
Log Ratio Metadata Router
Ratios
The default router instead tries to maintain a concept of the 'best' connection, and route packets only through that connection.
It takes a series of ConnectionMetadataRatio
s, and ConnectionMetadataRule
s in order to build this idea of best
, regardless of what kind of connections are available.
Any metadata reported by the connection can be fed into its general purpose algorithm.
Any connection that supports Electric UI heartbeats will have latency
, jitter
, packetLoss
, and consecutiveHeartbeats
metadata available if running the heartbeat metadata reporter.
Other transports may notions of signal strength, or other signals of connection health that can be fed into this system.
The ConnectionMetadataRatio
s tell the router how to weight different pieces of metadata against each other. By default reducing packetLoss
is rated as more important than latency
, which is in turn rated as more important than jitter
.
These values can be tuned for your specific use case.
The API is as follows:
new ConnectionMetadataRatio( 'latency', // the metadata key false, // whether more is better, in this instance, we want to optimise for lowest latency 1, // latency has a relative importance of '1', more is more important. (sum: number, latency: number) => sum + latency,)new ConnectionMetadataRatio( 'packetLoss', false, 2, // packet loss is more important than latency (factor: number, packetLoss: number) => factor * packetLoss,)
The last argument of each is how this metadata stacks when applied to multiple connections in a chain.
Latency adds up the more connections run through, whereas packet loss is multiplitive.
Rules
Some connections will be considered unacceptable no matter the overall score if certain metadata conditions aren't met. These conditions are distinguished with ConnectionMetadataRule
s. A connection must be deemed acceptable by all of these rules to be deemed acceptable overall.
new ConnectionMetadataRule(['latency'], ({ latency }) => latency < 400)
A connection is only considered acceptable if its latency is below 400ms. Above that, no matter how little packet loss or jitter, it shouldn't be used.
Given it's an average over time, packet loss is a slow moving metadata. Derived from the last n
heartbeat packets, when a connection recovers, it will move slowly from a low value up to a high value.
By combining it with the consecutiveHeartbeats
metadata, this slowness can be avoided upon recovery, while upon problematic periods it can remain optimistic.
new ConnectionMetadataRule( ['packetLoss', 'consecutiveHeartbeats'], ({ packetLoss, consecutiveHeartbeats }) => { // If there are more than three consecutive heartbeats, the connection // is considered acceptable despite potential previous packet loss. if (consecutiveHeartbeats > 3) { return true } // Otherwise we require less than 20% packet loss return packetLoss <= 0.2 },)
Comparing disparate connections
Not all connections share the same metadata keys, and the absolute numbers of latency
or packetLoss
are somewhat meaningless when compared to each other.
Instead of comparing absolute values to each other, connections are compared on 'like' metadata values and their score is derived by how many doublings or halvings better or worse that connection is than it's contendor. Each disparate metadata key can then be compared with each other with the ConnectionMetadataRatio
relative importance factor.
To pick the best connection, we loop over every connection comparing it to the current 'best' one, attempting to beat it. (If there's no current best connection, we pick one randomly, the best one will still come out on top.)
For every ConnectionMetadataRatio
, the values are compared between the two connections. If the contendor's latency
for example is twice as good, it gets a score of 1
. If it's latency is 4x as good, it gets a score of 2
.
If the connections don't share say an rssi
metadata key, they are not compared on that basis.
If for example the connection had double the latency but also double the packet loss, the following calculation would occur:
latency -> 2x as good -> score of 1 (weighted one) = 1packet loss -> 1/2 as good -> score of -1 (weighted double) = -2
Packet loss is weighted double in our example, so the resulting score would be 1 - 2 = -1
, therefore it is worse, the connection with the lower latency but better packet loss metrics wins.
Configuration example
import { MessageRouterLogRatioMetadata } from '@electricui/core'
function createRouter(device: Device) { const router = new MessageRouterLogRatioMetadata({ device, ratios: [ new ConnectionMetadataRatio('latency', false, 1, (sum: number, latency: number) => sum + latency), // prettier-ignore new ConnectionMetadataRatio('jitter', false, 0.1, (sum: number, jitter: number) => sum + jitter), // prettier-ignore new ConnectionMetadataRatio('packetLoss', false, 2, (factor: number, packetLoss: number) => factor * packetLoss), // prettier-ignore new ConnectionMetadataRatio('consecutiveHeartbeats', true, 0.1, (minimum: number, consecutiveHeartbeats: number) => Math.min(minimum, consecutiveHeartbeats)), // prettier-ignore ], rules: [ new ConnectionMetadataRule(['latency'], ({ latency }) => latency < 400), new ConnectionMetadataRule( ['packetLoss', 'consecutiveHeartbeats'], ({ packetLoss, consecutiveHeartbeats }) => { // If there are more than three consecutive heartbeats, the connection // is considered acceptable despite potential previous packet loss. if (consecutiveHeartbeats > 3) { return true } // Otherwise we require less than 20% packet loss return packetLoss <= 0.2 }, ), ], }) return router}