3

I am currently working on an Angular application that should run 24/7 for at least a month (manufacturing software). It is only accepted by the client that a browser restart occurs only once a month (maintenance interval). The first use case we are implementing only contains a single component that displays some information to the user. There is no user interaction at this point! The information is pushed from the server to the client. Currently I am just polling data updates from the server and display information to the user.

The current interval of 200ms is just for research purposes, in the real scenario it will be 1000ms. The code below cause a memory increase of approximately 40MB over 3 hours in Chrome and also the cpu usage increases up to 50% (consuming one of the two cores).

The target technology for push notifications ist SignalR. Since I have discovered the memory issue using SignalR the polling implementation provided here is used to investigate if the SignalR library is the problem. Unfortunately, I have the same issue here.

Of course executing window.location.reload() every 30 minutes "solves" the problem, but it is not a nice solution. If I execute reload after 3 hours the page just crashes and Chrome displays "oh no ... crashed". I am using Chrome 73 and Edge, with Edge the memory increase is significantly higher than Chrome. Using Angular 7.2

<div *ngIf="info" style="width: 100%; height: 100%; margin: 0 auto;">
  <div class="info status{{ info.Status }}">
    <div class="location"><p class="font2">{{ info.Location }}</p></div>
    <!-- ... further div elements here but no other *ngIf or *ngFor -->

        <div *ngIf="info?.Details" class="no-padding">
          <div class="column1 no-padding" *ngFor="let item of info.Details">
            <div class="inverse"><p class="font1">{{ item.Name }}</p></div>
          </div>
        </div>
        <img class="icon" src="assets/Icon{{info.Icon}}.png"/>
    </div>
  </div>
</div>
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Info } from '../data/info';

@Component({
  selector: 'app-info',
  templateUrl: './info.component.html',
  styleUrls: ['./info.component.scss']
})
export class InfoComponent implements OnInit, OnDestroy {
  subscribetimer: Subscription;
  subscribelistener: Subscription;
  listener: Subject<Info>;
  info: Info;

  constructor(private http: HttpClient) { }

  ngOnInit() {
    this.listener = new Subject<Info>();
    this.subscribelistener = this.listener.subscribe(unit => this.info = unit);

    this.subscribetimer = interval(200)
      .subscribe(data => {
        this.http.get<Info>(`http://localhost:5000/poll`)
            .subscribe(res => this.listener.next(res));
      });
  }

  ngOnDestroy() {
    this.subscribetimer.unsubscribe();
    this.subscribelistener.unsubscribe();
  }
}

I would expect that I can run this small application 24/7 for at least a month without having memory and cpu consumption issues.

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
M4n1
  • 47
  • 1
  • 6
  • is there any special reason for pushing the http response into a subject? – Jota.Toledo Apr 12 '19 at 09:16
  • actually, in this case not, the subject was left over from the ng2-signalr library usage. Do you have any concerns using Subject? – M4n1 Apr 12 '19 at 09:27
  • @M4n1, also check this ["How to detect rxjs related memory leaks in Angular apps"](https://stackoverflow.com/questions/54658614/how-to-detect-rxjs-related-memory-leaks-in-angular-apps) question — it has a couple of useful tools mentioned. – kos Apr 12 '19 at 19:59

2 Answers2

1

It's not clear what is leaking (and if it is leaking), so its hard to advise something particular. Yet here are some tips:

1) Try removing unnecessary Subjects, you can just expose an Observable info$ to the view:

export class AppComponent  {
  info$: Observable<IData>;

  constructor(private http: HttpClient) { }

  ngOnInit() {
    this.info$ = interval(200)
      .pipe(
        exhaustMap(() =>
          this.http.get<Info>(`http://localhost:5000/poll`)
        )
      );
  }
}

and in the view, something like:

<div *ngIf="info$ | async as info">
  <div *ngFor="let item of info.items">
    {{item}}
  </div>
</div>

2) You might have huge timeouts in the http-get, e.g. your timer ticks every 200ms, and http-get might take 500ms. exhaustMap will handle the back pressure, yet you should add a timeout to limit request time and definitely add some error handling, cz there will be errors on http-get. A very basic example:

this.http.get<Info>(`http://localhost:5000/poll`).pipe(
  // killing requests taking too long
  timeout(400),
  // some error handling logic should go here
  catchError(error => {
    return EMPTY;
  })
)

A more sophisticated approach might have a timeout with a retry .

Apart from that, http response itself might be erroneous or be non-json, which will lead to errors. So error handling here is a must.

Heres a more detailed overview of error handling in rxjs.

Stackblitz example for above said.

SIDE NOTE: You don't know what might happen while running it 24/7 for a month. So you'll definitely want to add some logging to your system as well. Just to be able to learn, if it fails.

kos
  • 5,044
  • 1
  • 17
  • 35
  • 1
    I now had some time to investigate and I have also tried your suggestions. Unfortunately, it does not solve the problem. I have also reduced the number of notifications per second. I think using `*ngIf` and `{{value}}` is not the best choice here. I found this [link]https://stackoverflow.com/questions/43034758/what-is-the-difference-between-ngif-and-hidden question. I tried to change `*ngIf` to `[hidden]` and `{{value}}` to data binding. In the first place it got better, but after some hours, same issue. According to Chrome heap profiler it seems that only Zone object instances are left over. – M4n1 Apr 18 '19 at 05:07
  • @M4n1, thank you for returning and giving feedback! So it may be not Rx, but rather Zone wrapper? Just to be sure: are you running in [prod mode](https://stackoverflow.com/questions/35721206/how-to-enable-production-mode-in-angular-2)? Alas, I'm not familiar with tools to analyze zone.js. FYI, there are some opened issues on angular for ["memory leak"](https://github.com/angular/angular/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+memory+leak) — it'd be hard digging, but you may find insights there. – kos Apr 18 '19 at 09:59
0

According to rxjs docs:

interval returns an Observable that emits an infinite sequence of ascending integers, with a constant interval of time of your choosing between those emissions. The first emission is not sent immediately, but only after the first period has passed. By default, this operator uses the async SchedulerLike to provide a notion of time, but you may pass any SchedulerLike to it.

I think the problem lies here:

this.subscribetimer = interval(200)
  .subscribe(data => {
    this.http.get<Info>(`http://localhost:5000/poll`)
        .subscribe(res => this.listener.next(res));
  });

Basically, every 200ms you are creating a new subscribe function. Most likely these objects are never garbage disposed, so they add up to memory consumption.

I would suggest to review the code to properly unsubscribe once the response has been collected.

Alternatively, if you have control over the API server, I would definitely use a web socket instead. Have a look at socket.io, it makes it extremely straightforward to start a simple socket server.

don
  • 4,113
  • 13
  • 45
  • 70
  • Take a look at the following question, http observables are self-terminating: https://stackoverflow.com/questions/35042929/is-it-necessary-to-unsubscribe-from-observables-created-by-http-methods – jarodsmk Apr 12 '19 at 10:54
  • The second most upvoted answer (32 votes) to that question indicates that it is better to unsubscribe. Moreover, in the code from the OP there are __two__ subscribe functions, and the first one (`interval(200).subscribe`) it's not an http observable, so I would still strongly suggest to unsubscribe explicitly and check if memory is still being filled up. – don Apr 12 '19 at 11:15
  • You are right, this is not the best idea. I have now changed it to use `.pipe(exhaustMap(...))` as siggested by @Kos, but it does not solve the problem. The Chrome heap profiler indicates that only Zone objects are left in the memory. Any Idea why they are never cleaned up? – M4n1 Apr 18 '19 at 05:11
  • Can you update the code in the answer with the modified code? – don Apr 18 '19 at 09:14