Why setInterval() is Bad
Is setInterval() really that bad? Yes. Yes it is.
“… but not everything is bad about JavaScript”
This is a sentence I heard a lot in the past, but not so much recently. Could JavaScript have gotten better? Or is it just the frameworks that iron out the bad stuff? Or the different flavors that came out over the years? In any case there certainly is some bad stuff left and for me - as a backend developer - the garbage collection seems to be pretty odd and while TypeScript is a fun language you still have to deal with it a lot.
TypeScript for IoT - A Good Choice?
JavaScript is infamous for its memory consumption - is it a good choice for IoT? Probably every programmer likes efficiency, but efficient memory management is even more important if you don’t have a lot. Consequently I vouched to do a better job and went ahead with TypeScript.
My collection of Raspberry Pis includes an idle Pi Zero with a camera, so it should be my “lab rat”. The topic should - of course - be IoT related: a timelapse camera that sends images to an MQTT broker. This undertaking requires three major components to work:
- A ‘scheduler’ that makes sure something happens every X seconds
- A camera that takes pictures
- An MQTT client that pushes the result to its destination
After getting familiar with the various tools/lingo/… (with this tutorial), I created a simple n-tiered application, that calls a takePicture()
function in a setInterval()
callback - just as I would have done years ago. It was all good, until … Could not allocate memory
was crashing my progam (the Pi Zero has 512MB RAM). I certainly knew not to rely on timeliness or putting CPU-intensive tasks into these callbacks, so I was bummed. Hmm.
How Could This Happen?
Disappointed by my programming skills, I added tracing, replaced the photo shooting with a 10 MB test file, and read up on the JavaScript CG (Garbage Collector). Quickly I saw what I actually did:
This should not be pointing upwards
As it turns out - my highly sophisticated SchedulerService
was the issue:
export class SchedulerService {
action: () => void;
handle: NodeJS.Timer;
interval: number;
constructor(action: () => void, ms: number) {
super();
this.action = action;
this.handle = undefined;
this.interval = ms;
}
public start() {
if(!this.handle) {
this.handle = setInterval(this.action, this.interval);
}
}
public stop() {
if(this.handle) {
clearInterval(this.handle);
this.handle = undefined;
}
}
}
Some searches later it was identified: Intervals/Timeouts are bad and a common mistake. It took me a while to realize: timer callbacks are never freed as long as the timer lives! Therefore none of the serialized images are ever garbage collected and the application runs out of heap space. To proof it’s the method as a timer callback and not the method itself, I called the same method 1000 times in a loop. With the expected result:
The sawtooth we know and love.
Consequently the interval callback is kept in memory for too long which eventually crashes the application…
<--- Last few GCs --->
[57871:0x105000000] 331291 ms: Mark-sweep 1332.0 (1410.3) -> 1332.0 (1410.3) MB, 89.0 / 0.0 ms allocation failure GC in old space requested
[57871:0x105000000] 331390 ms: Mark-sweep 1332.0 (1410.3) -> 1332.0 (1375.8) MB, 99.1 / 0.0 ms last resort
[57871:0x105000000] 331485 ms: Mark-sweep 1332.0 (1375.8) -> 1332.0 (1375.8) MB, 94.8 / 0.0 ms last resort
<--- JS stacktrace --->
==== JS stack trace =========================================
Security context: 0x1a929321cca1 <JSObject>
1: map(this=0x545d243ae09 <JSArray[10240000]>)
3: /* anonymous */(aka /* anonymous */) [/Users/cm/Projects/Mine/pi-camera-api/dist/services/CamService.js:16] [bytecode=0xa1b5a87ee19 offset=35](this=0x1a9293202241 <undefined>,resolve=0x545d243ad79 <JSFunction (sfi = 0x1a9293228299)>,reject=0x545d243adc1 <JSFunction (sfi = 0x1a9293228349)>)
4: new Promise(aka Promise)(this=0x1a92932022...
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
1: node::Abort() [/usr/local/bin/node]
2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/usr/local/bin/node]
3: v8::Utils::ReportOOMFailure(char const*, bool) [/usr/local/bin/node]
4: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/local/bin/node]
5: v8::internal::Factory::NewUninitializedFixedArray(int) [/usr/local/bin/node]
6: v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::ConvertElementsWithCapacity(v8::internal::Handle<v8::internal::JSObject>, v8::internal::Handle<v8::internal::FixedArrayBase>, v8::internal::ElementsKind, unsigned int, unsigned int, unsigned int, int) [/usr/local/bin/node]
7: v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::GrowCapacityAndConvertImpl(v8::internal::Handle<v8::internal::JSObject>, unsigned int) [/usr/local/bin/node]
8: v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::SetLengthImpl(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSArray>, unsigned int, v8::internal::Handle<v8::internal::FixedArrayBase>) [/usr/local/bin/node]
9: v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments*) [/usr/local/bin/node]
10: v8::internal::Runtime_NewArray(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/local/bin/node]
11: 0x1cda9b6040dd
12: 0x1cda9b64b4fb
13: 0x1cda9b6f373c
Command terminated abnormally.
334.83 real 321.95 user 26.78 sys
How am I supposed to do this then, I thought.
This Is Great!
Then it dawned on me: The Observer pattern. Luckily Node.js provides you with a base class called EventEmitter for this! It perfectly de-couples raising the event from handling it (which is the intense part). The results were not very exciting and this is a good thing:
import { EventEmitter } from 'events';
export class SchedulerService extends EventEmitter {
action: () => void;
handle: NodeJS.Timer;
interval: number;
constructor(action: () => void, ms: number) {
super();
this.action = action;
this.handle = undefined;
this.interval = ms;
this.addListener('timeout', this.action);
}
public start() {
if (!this.handle) {
this.handle = setInterval(() => this.emit('timeout'), this.interval);
}
}
public stop() {
if (this.handle) {
clearInterval(this.handle);
this.handle = undefined;
}
}
}
By simply deriving from EventEmitter
, the class now has an addListener()
method and an emit()
method to raise events, it’s as simple as it can be. The results however very quite exciting:
Memory is now properly collected and I can focus on building features, which in the JavaScript universe works like a breeze! I also got my confidence back and I can now happily say “not everything is bad about JavaScript” and add “… but setInterval() callbacks are” 😉
TL;DR
If you like a good story and have 5 minutes or so, I recommend reading the whole thing. If not, here’s what it’s about:
- Don’t do memory intensive operations in a
setInterval()
callback - Use an EventEmitter instead
Thank you for reading! Please reach out to me on Twitter (@0x5ff) for comments and feedback. You can also find the project on Github or on the Docker Hub. Also, go and read future/past posts of mine.