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 ConnectionMetadataRatios, and ConnectionMetadataRules 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 ConnectionMetadataRatios 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 ConnectionMetadataRules. 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) = 1
packet 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
}