Reputation: 3384
I've created a simple Angular app with one Form and a Table (PrimeNg DataTable).
Just to get a substantial difference in memory utilization with initial and final readings, I performed multiple Form Post calls (300 times) and navigated Table till page 20 with 1000 rows per page also sorting table 5 times with all columns and took the Heap snapshots with initial and final states (with all add-ons disabled in private/incognito tabs and with angular-cli app running in dev env).
In chrome, the heap memory size increased from 55 MB to 146 MB (91 MB gain)
In Chrome, the heap memory size increased from 23.16 MB to 137.1 MB (113.95 MB gain)
I am unsubscribing from all the subscriptions when my component destroys (this will have no effect as this is a single component) also I've set changeDetectionStrategy to onPush.
app.component.html:
<div [ngBusy]="{busy: busy, message: 'Loading data, this may take few minutes!'}"></div>
<div style="width:50%">
<form [formGroup]="taskForm" (ngSubmit)="addTask(taskForm.value)">
<div class="width:100%;float:left; clear:both;">
<div style="width:20%;float:left">
<input type="text" [formControl]="index" class="form-control" placeholder="index" />
</div>
<div style="width:20%;float:left">
<input type="text" [formControl]="name" class="form-control" placeholder="name" />
</div>
<div style="width:20%;float:left">
<input type="text" [formControl]="userId" class="form-control" placeholder="userId" />
</div>
<div style="width:20%;float:left">
<input type="text" [formControl]="mobile" class="form-control" placeholder="mobile" />
</div>
<div style="width:20%;float:left">
<input type="date" [formControl]="taskDate" class="form-control" placeholder="taskDate" />
</div>
</div>
<div class="width:100%;float:left; clear:both;">
<button type="submit" class="btn btn-success">Add Task</button>{{taskPostCount}}
</div>
</form>
<code *ngIf="addTaskResponse">{{addTaskResponse | json}}</code>
</div>
<div style="text-align:center">
<p-dataTable [dataKey]="'_id'" *ngIf="isTableVisible()" [value]="table" expandableRows="true" (onFilter)="onColumnFilterChanged($event)"
(onSort)="onTableColumnSortChanged($event)" [lazy]="true">
<p-column field="index" header="Index" [filter]="true" [sortable]="true"></p-column>
<p-column field="name" header="Task Name" [filter]="true" [sortable]="true"></p-column>
<p-column field="mobile" header="Mobile" [filter]="true" [sortable]="true"></p-column>
<p-column field="taskDate" header="Task Date"></p-column>
<p-column field="createdAt" header="Date Created"></p-column>
<p-column field="updatedAt" header="Date Updated"></p-column>
</p-dataTable>
<p-paginator [rowsPerPageOptions]="[10,20,30,50,100,200,300,400,500,1000]" [first]="tableSearchConfig.firstRowIndex" [totalRecords]="tableSearchConfig.totalRecords"
*ngIf="isTableVisible()" [rows]="tableSearchConfig.rowsPerPage" (onPageChange)="onPageChanged($event)"></p-paginator>
</div>
app.component.ts:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [AppService, TimeFromNow],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
// Properties declarations
private unsubscribe: Subject<boolean> = new Subject<boolean>();
constructor(private appService: AppService, private timeFromNow: TimeFromNow, private ref: ChangeDetectorRef, fb: FormBuilder) {
this.taskForm = fb.group({
'index': ['', Validators.compose([])],
'name': ['', Validators.compose([])],
'userId': ['', Validators.compose([])],
'mobile': ['', Validators.compose([])],
'taskDate': ['', Validators.compose([])],
});
this.index = this.taskForm.controls['index'];
this.name = this.taskForm.controls['name'];
this.userId = this.taskForm.controls['userId'];
this.mobile = this.taskForm.controls['mobile'];
this.taskDate = this.taskForm.controls['taskDate'];
this.setTableSearchConfig();
}
ngOnInit() {
this.addBulkTasksOnLoad();
}
ngOnDestroy() {
this.unsubscribe.next(true);
this.unsubscribe.complete();
this.unsubscribe.unsubscribe();
}
addBulkTasksOnLoad() {
this.busy = this.appService.addTaskOnLoad().subscribe((res: any) => {
this.loadTable();
}, (err: any) => {
});
}
addTask(taskForm: any) {
this.taskPostCount++;
this.appService.addTask(taskForm).takeUntil(this.unsubscribe).subscribe((res: any) => {
this.addTaskResponse = res;
},
err => {
this.addTaskResponse = err;
});
}
loadTable(paginateEvent?: PaginateEvent, sortEvent?: SortEvent, filterEvent?: FilterEvent) {
this.appService.getTable(this.tableSearchConfig).takeUntil(this.unsubscribe).subscribe((res: any) => {
for (const history of res.data) {
history.updatedAt = this.timeFromNow.transform(history.updatedAt);
}
this.table = res.data;
this.setTableSearchConfig(paginateEvent, sortEvent, filterEvent, this.tableSearchConfig.pageNumberToRequest, res.totalRecords);
this.ref.detectChanges();
});
}
.
.
.
.
}
Is it a case of memory leak and if yes what exactly I am doing wrong or it is normal behavior to have this increase in memory after heavy usage of the app? The app's frame rate also dropped at the end quite significantly.
Upvotes: 4
Views: 11620
Reputation: 1
It is definitely a memory leak but the app needs detailed profiling to zero down on the problem causing this. For your case, we can see the heap size grow but in order to know what exactly is causing the leaks or where the leaks are... you need to expand the identifiers and see which objects are still retained in memory(having a reference to the parent node that is your app) and which have lost a reference to the parent node. This will help you track down the pieces of your code(dom orJS) that are causing these memory leaks.
There a bunch of good practices that can prevent your app from leaking that much... you can find some in this article(it is for angularjs but same concepts apply).
I also find it useful to isolate state in a separate state wrapper like ngrx/store so that you're sure that its not the size of the data you're cueing as with state you can clearly see the size of the state object as well as clearly and definitively mutate(not literally) or add things to the state object.
Furthermore, the change strategy does not very much affect(if it even does affect it at all) the size of your memory heap but just helps with saving the program extra checks in the memory heap to see if the app model has changed to cause angular to effect changes in the DOM. But it leaves the memory heap untouched. SO it has nothing to do with the clear leaks
Expand each identifier to look for which DOM nodes are detached and are still retained in the heap! And then you can devise how to design for the leaks... like use ngIf or any other angular directives that call onDestroy (which garbage collects the DOM node or objects) on the components they're are attached.
Upvotes: 0