Skip to content
trevj edited this page Sep 30, 2016 · 22 revisions

Introduction

We'd like to run uProxy cloud servers on Node.js. This would eliminate the need to spin up the cloud server inside Firefox inside of a headless X server, (hopefully) allowing us run on much cheaper VMs. It's also just easier to understand and reason about and opens up a lot of possibilities for automated testing - basically, less crazy is good.

Background

freedom-for-node supports core.rtcpeerconnection via wrtc - this has enabled uProxy to include two sample apps that run in Node.js:

Run them like this:

git clone https://github.com/uProxy/uproxy.git
cd uproxy
npm install
grunt simpleSocks
node  build/src/lib/samples/simple-socks-node

Zork is what powers cloud servers - cool, we built a Docker image and now you can easily run a Node.js-based uProxy cloud server: (add -d localhost for testing on your workstation)

git clone https://github.com/uProxy/uproxy-docker.git
cd uproxy-docker/testing/run-scripts
./run_cloud.sh -z uproxy/node-stable

TODO: add -z to our installer

Status

We're done, right? Nope - it's slow as hell: ~25% of Firefox's throughput and is affected much more by high latency between each peer than either Chrome or Firefox - >66% slowdown when that latency rises to ~150ms.

That latency issue is doubly puzzling because uProxy made changes to Chrome in 2015 to improve throughput in the presence of high latency between peers - and wrtc is built from the exact same code.

Furthermore, we plan to test any improvements with the help of Docker - but recent versions of wrtc aren't running in Docker.

So, two priorities:

What we know

wrtc relies on pre-built WebRTC libraries: it uses build-webrtc, as do a few other WebRTC-on-Node.js-type NPMs. build-webrtc is a little crazy: it takes >10GB of disk space and frequently has issues (right now, it builds a busted library on Linux - this comment describes a workaround). wrtc uploads build-webrtc builds for several architectures to Amazon and, on npm install first attempts to download from there - only if none is found will it proceed to build build-webrtc.

Here's how to npm install wrtc against your own build of build-webrtc:

##
## build-webrtc
##

cd ~/src
git clone https://github.com/markandrus/build-webrtc.git
cd build-webrtc

# apply this patch to jakelib/environment.js:
#   https://github.com/js-platform/node-webrtc/issues/281#issuecomment-242173952

##
## node-webrtc
##

cd ~/src
git clone https://github.com/js-platform/node-webrtc.git
cd node-webrtc
export SKIP_DOWNLOAD=true
export BUILD_WEBRTC_DEPENDENCY=~/src/build-webrtc/

# apply https://github.com/js-platform/node-webrtc/pull/287:
#   git fetch origin pull/287/head:fake_audio_device
#   git checkout fake_audio_device
#   git merge develop

# on docker you need to run this:
#   echo 'unsafe-perm = true' > /root/.npmrc
npm install

node examples/ping-pong-test.js

(find more advice on custom builds at https://github.com/js-platform/node-webrtc/wiki/Building)

Tools

stress

This pumps 32MB of data through a datachannel. Copy it into examples/ in your node-webrtc clone.

'use strict';

const webrtc = require('..');

const MB = 32;

const pc1 = new webrtc.RTCPeerConnection();
const pc2 = new webrtc.RTCPeerConnection();

pc1.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log('pc1 candidate: ' + JSON.stringify(candidate));
    pc2.addIceCandidate(candidate.candidate);
  }
};

pc2.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log('pc2 candidate: ' + JSON.stringify(candidate));
    pc1.addIceCandidate(candidate.candidate);
  }
};

pc2.ondatachannel = (event) => {
  console.log('pc2: data channel open');
  const dc = event.channel;
  let r = 0;
  dc.onmessage = (event) => {
    r++;
    if (r >= 1024 * MB) {
      console.log('all received!');
      // TODO: signal that we're done
      pc1.close();
      pc2.close();
    }
  };
};

// NOTE: must create the datachannel before the offer
const dc = pc1.createDataChannel('test');
dc.onopen = () => {
  console.log('pc1: data channel open');
  for (let i = 0; i < 1024 * MB; i++) {
    const ab = new ArrayBuffer(1500);
    dc.send(ab);
  }
  console.log('all sent!');
};

pc1.createOffer((offer) => {
  console.log('offer: ' + JSON.stringify(offer));
  pc1.setLocalDescription(offer);
  pc2.setRemoteDescription(offer);
  pc2.createAnswer((answer) => {
    console.log('answer: ' + JSON.stringify(answer));
    pc2.setLocalDescription(answer);
    pc1.setRemoteDescription(answer);
  }, console.error);
}, console.error);

distributed stress

Again, copy these two files into examples/ in your node-webrtc clone. Run it like so:

mkfifo fifo
node stress-send.js < fifo | /usr/bin/time -f %e node stress-receive.js  > fifo

On Docker:

docker run --rm -i --name=stress-send -v ~/wrtc/node-webrtc:/work -w /work node:6 node examples/stress-send.js < fifo | /usr/bin/time -f %e docker run --rm -i --name=stress-receive -v ~/wrtc/node-webrtc:/work -w /work node:6 node examples/stress-receive.js > fifo

TODO: add latency - but tc with Docker is a real pain to do in a one-liner

stress-send.js

'use strict';

const readline = require('readline');
const webrtc = require('.././');

const MB = 32;

const pc = new webrtc.RTCPeerConnection();
const reader = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

pc.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log(JSON.stringify(candidate));
  }
};

// NOTE: must create the datachannel before the offer
const dc = pc.createDataChannel('test');
dc.onopen = () => {
  console.error('sender: data channel open');
  for (let i = 0; i < 1024 * MB; i++) {
    const ab = new ArrayBuffer(1500);
    dc.send(ab);
  }
  console.error('all sent!');
  pc.close();
};

pc.createOffer((offer) => {
  console.log(JSON.stringify(offer));
  pc.setLocalDescription(offer);
}, console.error);

reader.on('line', function(line) {
  try {
    const yo = JSON.parse(line);
    if (yo.type === 'icecandidate') {
      pc.addIceCandidate(yo.candidate);
    } else {
      pc.setRemoteDescription(yo);
    }
  } catch (e) {
    console.error('could not parse line: ' + line);
  }
});

stress-receive.js

'use strict';

const readline = require('readline');
const webrtc = require('.././');

const MB = 32;

const pc = new webrtc.RTCPeerConnection();
const reader = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

pc.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log(JSON.stringify(candidate));
  }
};

pc.ondatachannel = (event) => {
  console.error('receiver: data channel open');
  const dc = event.channel;
  let r = 0;
  dc.onmessage = (event) => {
    r++;
    if (r >= 1024 * MB) {
      console.error('all received!');
      pc.close();
      process.exit();
    }
  };
};

reader.on('line', function(line) {
  try {
    const yo = JSON.parse(line);
    if (yo.type === 'icecandidate') {
      pc.addIceCandidate(yo.candidate);
    } else {
      pc.setRemoteDescription(yo);
      pc.createAnswer((answer) => {
        console.log(JSON.stringify(answer));
        pc.setLocalDescription(answer);
      }, console.error);
    }
  } catch (e) {
    console.error('could not parse line: ' + line);
  }
});
Clone this wiki locally