Crashing browsers with Angular2 DebugElement

Marta Tatiana
7 min readNov 27, 2016

--

Short story:

Edited: DebugElement returned by Angular2 testing API can have many keys. Passing it to function that traverses it recursively may result in OutOfMemory error. That happens with console.log or expect(object).toBe (as pointed out in comments it’s not the comparison itself, which works on references, but jasmine’s pretty printing at fault)

Long story:

Last week, I was happily writing some component tests for Angular2 based frontend project. Since Angular2 has been recklessly changing test APIs from release candidate to release candidate, we hadn’t been writing too many tests so far; now, after we hopefully got to stable release we decided to invest in tests and cover a few more cases. Technology stack is Angular2 + Typescript + webpack, with karma test runner and mocha reporter and WebStorm as an IDE.

I still had some problems with my development environment; for example, I was not able to get my tests to work in debug. Therefore, my main debugging tool was console.log that came to rescue whenever I was not sure what would be returned from API call. So I was adding test cases, printing some things on console whenever I was not sure what’s going on and suddenly…. crash.

One test I wrote made a browser (both PhantomJS and Chrome) crash and I had no idea why. There were two clues: Phantom, when crashing, would return error code 0xc0000005 and Chrome would display it’s out of memory page.

Quick look at task manager would show Phantom crash at ~1GB of memory and Chrome crash at ~1.5GB. The big question was what may be the possible reason for high memory consumption given quite small size of the application and even smaller size of test suite...? We’d had problems with launching tests on Chrome of Phantom but they were never occurring for small test suites; they would happen when project reached a couple of hundred tests and only on Windows machines. What was even more surprising, that this particular test would crash the browser even if it was the only test run, which made it unlikely to be some memory leak connected with how we write tests, lack of cleanup or any other usual suspect from Stack Overflow.

First day of fight to debug issue was: checking on various OS-es (Linux, Mac and Windows), rising debug levels in karma and webpack, attempt to record memory profile (unsuccessful), googling through issues in Angular2 and karma-runner and Stack Overflow questions — basically everything but some decent thinking... After whole day of struggling, I had nothing promising.

I needed a few hours of break to realize that I had been approaching the problem all wrong. I had been trying to debug application build on a technology stack that is too complex for me to deeply understand. I was even not able to limit the problem to only one layer. Was it Angular2 API problem? Maybe something wrong with the bundle built by webpack? Something wrong in my code? Some strange issue happening when transpiling to javascript? Chrome? Phantom? Karma launchers?

I took another approach. I decided to reduce the project to the minimum size and configuration that would still enable me to reproduce the problem. As the application was already too complex to painlessly cut dependencies from it, I started with angular2-webpack-starter. I added missing template parser to it, added Phantom to test configuration, removed coverage and source-map config from webpack and added two super-simple test components that I felt were mimicking the structure of original components under test. Added one test… and bum! PhantomJS crash!

See by yourself in repo — just run npm install and then npm run test in the main directory.

Fun fact — I cloned angular2-webpack-starter a few times to make some experiments, and depending on if I had run npm install on Friday or on Saturday, browser’s crash would be reported differently (or not at all). That’s probably (not sure) due to karma server update in the meantime and liberal dependencies in package.json of angular2-webpack starter. So don’t be surprised if instead of PhantomJS crash (as on screenshot above) you will see only mysterious message “Disconnected, because no message in 10000 ms.”).

So, since we have small and generic project to reproduce the issue, let’s take a look into code. Important parts are in app/src/broken folder, with test code in parent.component.spec.ts:

import {By} from '@angular/platform-browser';
import {async, TestBed} from '@angular/core/testing';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ParentComponent} from './parent.component';

let fixture;
let component: ParentComponent;
let element: HTMLElement;

describe('ParentComponent', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [],
declarations: [
ParentComponent
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()
.then(() => {
fixture = TestBed.createComponent(ParentComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
}));

it('should display correct model data', (() => {
component.someModel = { value : '01234'};
fixture.detectChanges();
let modelValue = fixture.debugElement.query(By.css('.for-tests-markup'));
// let modelValue = fixture.nativeElement.querySelector('.for-tests-markup');
expect
(modelValue).toBe('01234');
}));
});

Test in question tests a parent component, which has child component in it. When it comes to inits or constructors — nothing happens there. We have no providers, no imports and no mocks in the test setup. What we try to test is actually if Angular2 data binding will work. We assign value to the model in component, call change detection on component and expect to find correct value in DOM. The test in current shape obviously ought to fail, as object of type “DebugElement” (returned by query(By.css(…)) call) is expected to equal string. Instead, the test causes PhantomJS to crash.

Some further observations: if, instead of using Angular2’s query API, we use just the standard querySelector on HTMLElement, the test will fail (as it should) and PhantomJS will not crash.

And here comes the break (finally, it’s weekend!): what happens with this small(er) test case when we use other browsers? Let’s check Chrome on this version! In this case Chrome will fail the test and the output will look like:

And here comes “a-ha” moment. Usually, when test fails on expect(a).toBe(b), the test reporter (is it the reporter?) will print something like “Expected ‘12345’ to be ‘54321’”. So it looks like those long lines are the printout of contents of the first object passed to expectation. How big is the object if it takes so many lines to print it? I run the test again, this time, redirecting the output to text file. And resulting file size is 157 MB.

It’s a pity there is no sizeof operator in javascript. So I copy some size-counting code from the Stack Overflow and…

I finally realized, that only problem with my test was attempt to access DebugElement object returned by Angular API. It seems to be reasonable assumption that both console.log I used to debug and expect(object).toBe(anotherObject) may traverse object recursively to print objects contents. I am however not sure if the crash was due to too much output on browser’s console or too much recursion.

What I’ve been doing the whole time, was contaminating the crime scene, by not necessary logs and code that was not essential for the test itself and would be removed in the future.

The quickest fix is to not attempt to print or compare the whole object, just use the property we need (for example, assertions on DebugElement.nativeElement contents would be enough for checking if we have expected values in DOM).

So, who is to blame for two days lost on debugging? It’s me, for doing reckless things like trying to print object to console or to pass it to jasmine’s expect().toBe without thinking about what it may actually be. It’s also me, for adding the complexity to the thing I tried to debug (more console logs, more…) instead of reducing size of the problem and isolating it in the first place.

But I am also a bit angry with Angular2 developers — seriously, why is DebugElement for a simple test and simple application so deep/complex, that it causes browsers to crash with OutOfMemory errors? What’s the point of exposing it via API if it cannot be used safely? From what I can see in 157 MB of output from the test in Chrome, it looks like the DebugElement contains DebugView which, in turn, holds the history of all view changes in the whole loaded application. Though we would need Angular2 developer to confirm that.

In attempt to turning this case into lesson, here is the list of “debugging” and “trying to understand” notes and attempts I’ve taken:

  • check if it works on different OS/browser/environment
  • google
  • record memory profile
  • rise logging levels on all possible layers (webpack, karma, application itself)
  • upgrade whatever you can
  • try to change your test runner
  • change all config you can find; reduce it to minimum or switch everything to defaults
  • remove all not necesarry preprocessing of your code — for example source maps and coverage
  • try to isolate the single piece of failure, starting from the simplest and smallest possible template
  • don’t rely on error reporting in tools. The error is reported from layer to layer, and the same error message doesn’t mean the same root cause
  • remove all non-necessary logging
  • console.log may alter results of your experiments and may be the source of problem itself
  • watch out for dependencies that are not hardcoded to the last digit
  • get debugging in tests to work…
  • treat yourself as a primary suspect… If the case is simple, many people would report it so far. It must be something that you do, sometimes even without thinking!

Hopefully next time I will start with the last one as primary assumption and I will not need to spend a weekend trying to fix what I’ve broke.

--

--

Marta Tatiana

programmer. I write to learn. All opinions are private and do not reflect views of my employer, past or present.