meet.js Krk, 31.01.2019
Doing frontend stuff @Codewise
Event organizer @AngularDragons
@RafalRumanek
We need our (potential) users to get to use our app
image src & read more at:
https://developers.google.com/web/fundamentals/performance/rail
High-level languages embed a piece of software called "garbage collector" whose job is to track memory allocation and use in order to find when a piece of allocated memory is not needed any longer in which case, it will automatically free it.
Browser Javascript engines use Garbage Collectors use Mark and Sweep algorithm with generational approach (as most of objects 'die' young)
Memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
*circular references aren't the case in Mark and Sweep algorithm,
but there are edge cases for JS + DOM
From YUI, through Polymer*, Angular JS,
Angular Dart to Angular starting from fall of 2012
// window.performance.memory
{
totalJSHeapSize: 29400000,
usedJSHeapSize: 15200000,
jsHeapSizeLimit: 1530000000
}
you can even ask your clients to send you a snapshot and analyze it ❤️
przyklad, omowic retained vs shallow size
you can even ask your clients to send you their snapshots
Causes of memory leaks are quite often trivial. Once you find them and understand them, they might be really easy to fix.
YUI Widget class:
// meanwhile in our code
this._toolbarWidget.destroy();
// ...
this._quickSearchWidget.destroy();
// ...
this._quickSearchWidget.destroy();
Really?
Really?
Well, it's getting better
There shouldn't be more than 50 at that time, wtf
It turned out there's whole report model cached
// wrong - existed for 4 years <3
Y.on('reload-report-after-first-payment', this.load, this);
// our wrapper for auto detaching upon destruction
this.addEventHandler(Y.on('reload-report-after-first-payment', this.load, this));
_bindResizeEvent: function () {
const resizeCallback = Y.CommonHelpers.debounce(200, this.onWindowResize, this);
this._windowResizeHandle = Y.on('resize', resizeCallback, null, this);
},
// fixed
_bindResizeEvent: function () {
const resizeCallback = Y.CommonHelpers.debounce(200, this.onWindowResize, this);
this._windowResizeHandle = this.addEventHandler(Y.on('resize', resizeCallback, null, this));
},
manuallyCleanCachedView: function () {
if (this._potentiallyCachedView) {
this._potentiallyCachedView.detachEvents();
for (let member in this._potentiallyCachedView) {
delete this._potentiallyCachedView[member];
}
// for further cache clean within YUI internals
this._potentiallyCachedView.cwDestroyed = true;
this._potentiallyCachedView = null;
}
}
In general, mostly silly and too obvious mistakes (not only tho), but...
@DestroyAware()
private readonly destroy$: Subject<void> = new Subject<void>();
// ...
this.flowSave$.pipe(
takeWhileIdle(flowsRepo),
tap(() => flowValidationManager.initialize()),
filter(() => this.flowValidator.isValid()),
mergeMap(flow => flowManager.save(flow)),
retry(),
takeUntil(this.destroy$) // <--- this was doing the trick
).subscribe(savedFlow => this.onSaveSucceeded(savedFlow));
faulty auto-unsubscribe
@DestroyAware relied on ngOnDestroy hook, which worked for us only when interface OnDestroy is explicitly implmenented
export function DestroyAware(destroyHookName: string = "ngOnDestroy"): Function {
return (target: Object, key: string): void => {
let originalDestroyHook: Function;
if (destroyHookName in target && target[destroyHookName] instanceof Function) {
originalDestroyHook = target[destroyHookName];
}
Object.defineProperty(target, destroyHookName, {
value: function value(...args: any[]) {
if (originalDestroyHook instanceof Function) {
originalDestroyHook.apply(this, args);
}
this[key].next(true);
this[key].complete();
}
});
};
}
(one of scenarios)
import {Selector} from 'testcafe';
import {ITERATIONS_COUNT} from "../utils/consts";
import {testOnDemoAndProd, testOnProd} from "../utils/testWrappers";
fixture `Tracker memory leaks`;
testOnDemoAndProd('reports tabs', async t => {
for (let i = 0; i < ITERATIONS_COUNT; i++) {
await Selector('[data-button-id="cost-update"]');
await t.click('[data-filterby="offer"]');
await Selector('[title="Creates a new offer"]');
t.ctx.memoryManager.snapshot();
await t.click('[data-filterby="campaign"]');
}
});
TestCafe + headless Chrome + CircleCI
import {ClientFunction} from "testcafe";
export default async function initMemoryManager(t) {
t.ctx.memoryManager = new MemoryManager();
}
class MemoryManager {
memorySnapshots = [];
// here it goes
getUsedJSHeapSize = ClientFunction(() => performance.memory.usedJSHeapSize);
async snapshot() {
const usedJSHeapSize = await this.getUsedJSHeapSize();
this.memorySnapshots.push([this.memorySnapshots.length + 1, usedJSHeapSize]);
}
}
export function testOnDemoAndProd() {
testOnDemo.apply(null, arguments);
testOnProd.apply(null, arguments);
}
export function testOnDemo(testName, testFunction) {
initChart();
test
.page `${URL_DEMO}`
.before(async t => {
await initMemoryManager(t);
await handleDemoIframe(t);
})
.after(makeReport)
(`${testName} demo`, async function(t) {
await testFunction(t, "DEMO");
});
}
Manually calculate memory allocated on heap but:
read more at:
https://w3c.github.io/longtasks
const observer = new PerformanceObserver(function(list) {
const perfEntries = list.getEntries();
// queue reporting for long tasks existance
});
observer.observe({entryTypes: ["longtask"]});
// long task refers to occurrences of event loop tasks
// or pauses caused by UI thread work
// whose duration exceeds 50ms
- would you be interested?
- would you be interested?
@RafalRumanek
or rumanek.r@gmail.com