Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terrible performance issues with websocket - every message triggering Angular change detection

I've got a websocket and I'm currently streaming messages at about 45 per second. At this level it completely kills chrome. All functionality in the browser window is locked up.

I've cut back my implementation to try find the root cause of this issue, suspecting it could have been some problem with how I'm processing these messages - or updating/change detection in Angular.

But now I'm left with the simplest websocket implementation I can get:

this.socket = new WebSocket('ws://localhost:5000/ws');

let i = 0;
this.socket.onmessage = (e: MessageEvent) => {
  i++;
  if (i % 100 === 0) {
    console.log('recieved' + i);
  }
};

This is in an Angular Injectable, but it's not interacting with anything.

It writes to the console every 100 messages. This still kills the browser with 100% CPU usage, it doesn't even output to the console most of the time until I stop the message stream, then everything catches up and it squirts a few lines of "recieved x00" messages.

The messages themselves are JSON, and look like this:

{
  "Topic":"2a736d15-a2fe-43b2-8e8b-ee888f15a53a","Type":1,
  "Message": {
     "Value":2,
     "Timestamp":"2017-06-05T14:46:21.615062+01:00"
   }
}

They're typically about 126 characters.

I'd assumed websockets can handle tens of thousands of messages per second, but I can't find any sensible metrics on google, does this sound like unreasonable performance?

I've also checked in the CPU profiler. When everthing's locked out, it's just a big wall of calls, so I turned it down to a single message a second:

enter image description here

Drilled down into one of these spikes (happens for every single message!):enter image description here

I threw a break point in one of these locations, and it appears that Angular is doing something whenever the websocket gets triggered, and it's something to do with Zones (something I have zero experience in!):

The first thing in the stack is a ZoneTask.invoke on the Websocket itself, but then NgZone gets triggered, and ultimately a very expensive change/update chain is called:

enter image description here

How could all this be happening? The 7 lines of code subscribing to the websocket are completely independent from Angular's change detection right? They're not changing any values that are bound to any components. Their only link is they are sitting in a method inside an @Injectable()

like image 609
Joe Avatar asked Jun 05 '17 14:06

Joe


1 Answers

Sods law, as soon as I post a question I work out the answer. Usually I'd delete it but I couldn't find much on the topic so it might be useful for someone.

It was an issue with Zones. Here's a useful blog post. In general, Angular 2 uses zones to drive it's change detection:

The reason that Angular 2 uses the zone.js is to know when our handler finishes and then the NgZone service (NgZone is a class that wrap the zone service) calls to ApplicationRef.tick() method. The tick() method scans the tree components from top to bottom and calculates in each component the expressions that exist in the template. If the result of the expression is not equal to the previous result, (from the previous tick) Angular will update the DOM property that connects to this expression.

An easy solution to simply get the message performance in isolation is to run outside of the zone, and thus angular's change detection with the handy method runOutsideAngular:

this.zone.runOutsideAngular(() => {
  let i = 0;
  this.socket.onmessage = (e: MessageEvent) => {
    i++;
    if (i % 100 === 0) {
      console.log('recieved' + i);
    }
  };
});
like image 54
Joe Avatar answered Nov 09 '22 23:11

Joe