简体   繁体   中英

Server sent events (SSE) is slow using Three.JS on a simple 3D cube model

I am currently developing a client-server application that takes accelerometer data from the data using SSE and pass them to three.js model and then render the results on the browser.

Technically, the application should visualize accelerometer data in only one direction in real-time on the browser, so latency is important.

Since I am only passing a single data (ie only accelerometerX), the process should be quick and instantaneous; however, it is taking really long to pass the accX value to object.position.x and even the console.log("Accelerometer X-Axis Data: " + (sensor value / 16384)); show data once in a while. Sometimes, the whole browser crash showing "WebGL scene doesn't render because of lost context" error and a very long "precision" error. To this point, I have tried every single method, but I was never able to fix this long lagging issue.

This is what I get from the server, a JSON file that has this particular format:

 "DMObjectsCompleteObject": [
        {
            "DataMapAddress": 1,
            "DataType": 9,
            "DefaultValue": 0,
            "Description": "Accelerometer X Axis Data",
            "MaxValue": 66,
            "MinValue": 18446744073709552000,
            "ReadOnly": false,
            "Value": -18706.4
        },
        {
            "DataMapAddress": 2,
            "DataType": 9,
            "DefaultValue": 0,
            "Description": "Accelerometer Y Axis Data",
            "MaxValue": 66,
            "MinValue": 18446744073709552000,
            "ReadOnly": false,
            "Value": 128
        }
]

and this the code for my client side:

// Importing libraries and data
import * as THREE from "three";
if (!!window.EventSource) {
    var source = new EventSource("/sse");

    source.addEventListener('message', function (event) {
        // Parameters initialization
        const canvas = document.querySelector('#canvas');
        const accelPanel = document.querySelector('#accelPanel');
        const renderer = new THREE.WebGLRenderer({ canvas });
        const fov = 70;
        const aspect = 2;  // the canvas default
        const near = 20;
        const far = 500;

        // Initialize camera perspective
        const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

        // The camera FOV when the model starts to move
        camera.position.z = 25;
        camera.up.set(0, 0, 1);
        camera.lookAt(0, 0, 0);

        // Add background grid and light
        const scene = new THREE.Scene();
        {
            const color = 0x00afaf;
            const intensity = 10;
            const light = new THREE.PointLight(color, intensity);
            scene.add(light);
        }

        // Make the 3D cube model with the XYZ axis
        const boxGeometry = new THREE.BoxGeometry();
        const boxMaterial = new THREE.MeshBasicMaterial({ color: "green", wireframe: false });
        const object = new THREE.Mesh(boxGeometry, boxMaterial);

        var cubeAxis = new THREE.AxesHelper(3);
        object.add(cubeAxis);

        object.scale.set(5, 5, 5)
        scene.add(object);
        scene.background = new THREE.Color(0.22, 0.23, 0.22);

        let currentIndex = 0
        let time = 0
        let velocity = new THREE.Vector3()
        requestAnimationFrame(render);

        // Rendering function responsible of creating the translation motion
        function render(dt) {
            dt *= 0.0001 // in seconds
            time += dt
            document.querySelector("#time").textContent = time.toFixed(2)


            // JSON.parse twice due to over-stringified string from SSE
            var obj = JSON.parse(JSON.parse(event.data));
            
            if (obj !== null) {
                // Sensor data
                if (
                    obj.hasOwnProperty("DataMapChangedObjectsAddressValue") &&
                    obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"] !==
                    undefined
                ) {
                    let sensorAddr =
                        obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"];
                    let sensorValue =
                        obj["DataMapChangedObjectsAddressValue"][0]["Value"];

                    //Accelerometer X Axis
                    //if(sensorAddr === this.despToAddrMap.get("Accelerometer X Axis Data")){
                    if (sensorAddr === 1) {
                        // console.log(obj["DataMapChangedObjectsAddressValue"][2]["Value"])
                        console.log("Accelerometer X Axis Data: " + (sensorValue / 16384));
                    }

                    object.position.x = (sensorValue / 16384) * 500;
                    document.querySelector("#accX").textContent = (sensorValue / 16384) * 500;
                    object.rotation.y = -70.68;
                    var relativeCameraOffset = new THREE.Vector3(5, 10, 1);
                    var cameraOffset = relativeCameraOffset.applyMatrix4(object.matrixWorld);
                    camera.position.x = cameraOffset.x;
                    // camera.position.y = cameraOffset.y;
                    // camera.position.z = cameraOffset.z;
                    camera.lookAt(object.position);
                }
            }

            // // Find datapoint matching current time
            // while (data[currentIndex].time < time) {
            //     currentIndex++
            //     if (currentIndex >= data.length) return
            // }
            // const { rotX, rotY, rotZ, accX, accY, accZ } = data[currentIndex]
            // document.querySelector("#accX").textContent = accX;

            // const acceleration = new THREE.Vector3(accX, accY, accZ)
            // object.position.x = accX * 30;
            // object.rotation.y = -70.68;

            resizeToClient();
            renderer.render(scene, camera);
            requestAnimationFrame(render);
        }

        function resizeToClient() {
            const needResize = resizeRendererToDisplaySize()
            if (needResize) {
                const canvas = renderer.domElement;
                camera.aspect = canvas.clientWidth / canvas.clientHeight;
                camera.updateProjectionMatrix();
            }
        }

        function resizeRendererToDisplaySize() {
            const canvas = renderer.domElement;
            const width = canvas.clientWidth;
            const height = canvas.clientHeight;
            const needResize = canvas.width !== width || canvas.height !== height;
            if (needResize) {
                renderer.setSize(width, height, false);
            }
            return needResize;
        }
    }, false)

    source.addEventListener('open', function (e) {
        // document.getElementById('state').innerHTML = "Connected"
    }, false)

    source.addEventListener('error', function (e) {
        const id_state = document.getElementById('state')
        if (e.eventPhase == EventSource.CLOSED)
            source.close()
        if (e.target.readyState == EventSource.CLOSED) {
            id_state.innerHTML = "Disconnected"
        }
        else if (e.target.readyState == EventSource.CONNECTING) {
            id_state.innerHTML = "Connecting..."
        }
    }, false)
} else {
    console.log("Your browser doesn't support SSE")
}

This is how the client renders the 3D cube: 在此处输入图像描述

But it barely moves, and it is too laggy. Can someone please suggest a solution to this problem? It would be much appreciated.

Its also important how your SSE server is implemented. If you use expressjs style one, like this one (sorry for not very clean code, its from live project)

'use strict';

const util = require('util');
// const config = require('../lib/config');
const requireAuthorization = require('../middlewares/requireAuthorization');
const router = require('express').Router();
// const thinky = require('../lib/thinky');
// const r = thinky.r;
// const Errors = thinky.Errors;
const logger = require('../lib/logger');
const helpers = require('../lib/helpers');

// redis feed for events

const subscriber = require('../lib/redis').createClient();
const EventEmitter = require('events');
const spine = new EventEmitter();
subscriber.on('message', function (channel, message) {
  spine.emit(channel, message);
});

// setInterval(function () {
//   spine.emit('scubamailer_feed', JSON.stringify({time: Date.now()}));
// }, 500);

subscriber.subscribe('scubamailer_feed');


router.use(requireAuthorization);

router.get('/subscribe', function (req, res, next) {

  // Good read -
  // https://learn.javascript.ru/server-sent-events#tipy-sobytiy
  // https://www.terlici.com/2015/12/04/realtime-node-expressjs-with-sse.html
  // http://stackoverflow.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy
  // http://stackoverflow.com/a/33414096/1885921
  // https://github.com/expressjs/compression/issues/17

  // how to remove listeners
  // https://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx

  logger.info('User %s subscribed to event feed from IP %s.', req.user.email, helpers.extractIPfromReq(req), {
    user: req.user.email,
    type: 'user/unsubFromEvents'
  });

  req.isSSE = true; // PLEASE, DO NOT TOUCH IT, OK???
  req.socket.setTimeout(24 * 60 * 60 * 1000);
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');

  // redis feed events
  const listen = function (message) {
    res.write(util.format('event: notification\ndata: %s\n\n', message));
    // res.flush(); // https://github.com/expressjs/compression#server-sent-events
  };
  const tickerInterval = setInterval(function () {
    res.write(util.format('event: time\ndata: %s\n\n', Date.now()));
  }, 500);


  const stopListening = function () {
    logger.info('User %s [%s] unsubscribed from event feed...', req.user.email, helpers.extractIPfromReq(req), {
      user: req.user.email,
      type: 'user/unsubFromEvents'
    });
    clearInterval(tickerInterval);
    spine.removeListener('scubamailer_feed', listen);
  };
  res.once('close', stopListening);
  res.once('finish', stopListening);
  spine.on('scubamailer_feed', listen);


  const entitiesToMonitor = [
    'Campaign',
    'EmailImport',
    'EmailExport',
    'User',
    'Server'
  ];

  return Promise.all(entitiesToMonitor.map(function (entity) {
    return req.model[entity].changes()
      .then(function (feed) {
        feed.each(function (error, doc) {
          if (error) {
            throw error;
          }
          if (doc.isSaved()) { // send updates only if document is persisted in database
            if (entity === 'User') {
              res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc.formatToJSON())));
            } else {
              res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc)));
            }

            // res.flush(); // https://github.com/expressjs/compression#server-sent-events
          }
        });
        return Promise.resolve();
      });
  }))
    .catch(next);
});

module.exports = exports = router;

as you can see, yo need to provide all headers required

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');

X-Accel-Buffering is required to nginx to process SSE feeds properly.

Also, if you use nodejs, it can be importnt to ensure compression middleware does not compress endpoint of SSE stream, since it makes event delivery laggy

There are too many moving parts here to know what the problem is.

Replace your message handler with something like:

source.addEventListener('message', function (event) {
console.log(event)
}

Now confirm your accelerometer data is arriving in the console log in real time. (This is also a good time to confirm the data structure is exactly as expected.)

If it does work quickly and reliably, then SSE is working fine and your question is just about ThreeJS. (I'd move your static functions outside the message handler for a start - if only to make the code easier to read.)

But if your data stream is very quiet, then a big chunk of data arrives, then it is quiet again before another big chunk arrives, that probably means your server is buffering. You haven't described the server-side system, but look into options to flush data, or switch off output buffering.

If the data arrives irregularly, sometimes working, sometimes not, then I'd look into network issues. The developer tools in the browser can help diagnose these kind of issues. Or removing the browser from the equation, and using curl (or similar) to debug the SSE stream.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM