Skip to content

Instantly share code, notes, and snippets.

@v0lkan
Created January 19, 2019 06:13
Show Gist options
  • Save v0lkan/cde36a9d9c56b4b96a52d1f2303b06f2 to your computer and use it in GitHub Desktop.
Save v0lkan/cde36a9d9c56b4b96a52d1f2303b06f2 to your computer and use it in GitHub Desktop.
Promise Loop
/*
* \
* \\,
* \\\,^,.,,. JavaScript: from Zero to Hero
* ,;7~((\))`;;,, <zerotoherojs.com>
* ,(@') ;)`))\;;', an extraordinary course to learn JavaScript
* ) . ),(( ))\;, and related technologies
* /;`,,/7),)) )) )\,,
* (& )` (,((,((;( ))\,
*
*/
// Creating a promise chain is not strictly recursive because each
// `then(fn)`’s `fn` is executed in its own stack.
//
// HOWEVER; the resulting promise will need to maintain a chain of
// resolutions, so that once (if) the promise chain eventually resolves,
// it will pass the resolution value all the way up to the first promise.
//
// That’s why, if you don’t limit the depth of your promise chains you
// may leak memory and even crash the entire Node.js process if there’s
// not enough heap space left for Node.js to consume.
//
// Here is a one-liner to exhaust your Node.js app and crash it.
const loop = async () => Promise.resolve(true).then(loop);
loop();
// Terminal output will be similar to this:
//
//
// <--- Last few GCs --->
//
// [31436:0x103800c00] 17448 ms: Scavenge 1388.2 (1423.2) -> 1387.8 (1423.7) MB, 1.9 / 0.0 ms (average mu = 0.162, current mu = 0.129) allocation failure
// [31436:0x103800c00] 17454 ms: Scavenge 1388.5 (1423.7) -> 1388.1 (1424.2) MB, 5.2 / 0.0 ms (average mu = 0.162, current mu = 0.129) allocation failure
// [31436:0x103800c00] 17456 ms: Scavenge 1388.8 (1424.2) -> 1388.4 (1425.2) MB, 2.3 / 0.0 ms (average mu = 0.162, current mu = 0.129) allocation failure
//
//
// <--- JS stacktrace --->
//
// ==== JS stack trace =========================================
//
// 0: ExitFrame [pc: 0x3113334fb7d]
// 1: StubFrame [pc: 0x3113333e7f8]
// 2: StubFrame [pc: 0x3113331c6d2]
// 3: EntryFrame [pc: 0x31133305c9e]
// 4: ExitFrame [pc: 0x3113334fb7d]
// Security context: 0x2e871991d969 <JSObject>
// 5: _tickCallback [0x2e87f5993b49] [internal/process/next_tick.js:43] [bytecode=0x2e875eb3c269 offset=49](this=0x2e87827823d9 <process map = 0x2e875fccc941>)
// 6: /* anonymous */ [0x2e87bb559791] [internal/m...
//
// FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
// 1: 0x10003a9d9 node::Abort() [/usr/local/bin/node]
// 2: 0x10003abe4 node::FatalTryCatch::~FatalTryCatch() [/usr/local/bin/node]
// 3: 0x10019ed17 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
// 4: 0x10019ecb4 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
// 5: 0x1005a5882 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/local/bin/node]
// 6: 0x1005a4838 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/local/bin/node]
// 7: 0x1005a2443 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node]
// 8: 0x1005aecbc v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/usr/local/bin/node]
// 9: 0x1005aed3f v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/usr/local/bin/node]
// 10: 0x10057dfc4 v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/usr/local/bin/node]
// 11: 0x100832070 v8::internal::Runtime_AllocateInNewSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/local/bin/node]
// 12: 0x3113334fb7d
// 13: 0x3113333e7f8
// 14: 0x3113331c6d2
@joeyhub
Copy link

joeyhub commented Oct 21, 2019

If I get the time later I'll see if I can make a minimal promise implementation that can truncate using weakrefs or see if it would need destructors.

If I can get it to collect that will answer the question less ambiguously.

@joeyhub
Copy link

joeyhub commented Oct 21, 2019

"use strict";
var queue = [];

let l = 0;

function LeakproofPromise() {
    var tooSoon, result;

    if(++l % 1000000 === 0)
        console.log(`L: ${l} ${d} ${i}`);

    this.resolve = (...arg) => {
        if(arg.length > 1 || result)
            throw new Error();

        result = arg;

        if(tooSoon) {
            queue.push(...tooSoon.map(tooSoon => [tooSoon, arg]));
            tooSoon = undefined;
        }
    };

    this.then = (callback) => {
        const child = new LeakproofPromise();

        if(result)
            queue.push([[callback, child], result]);
        else {
            if(!tooSoon)
                tooSoon = [];

            tooSoon.push([callback, child]);
        }

        return child;
    };
}

let d = 0;

const f = () => {
    if(++d % 1000000 === 0)
        console.log(`D: ${l} ${d} ${i}`);

    const a = new LeakproofPromise(), b = new LeakproofPromise();
    b.resolve();
    const c = b.then(f);
    a.resolve(c);
    return a;
};

(() => {
    const p = new LeakproofPromise();
    p.resolve();
    p.then(f);
})();

const stack = [];

const toStack = () => {
    if(!queue.length)
        return;

    //stack.push(...queue.reverse());
    stack.push(...queue);
    queue = [];
};

toStack();

let i = 0;

while(stack.length) {
    if(++i % 1000000 === 0)
        console.log(`I: ${l} ${d} ${i}`);

    //const [callback, arg] = stack.pop();
    const [[callback, child], arg] = stack.shift(), result = callback(...arg);

    if(result)
        result.then(child.resolve);
    else
        child.resolve(result);

    toStack();        
}

Just a straight up basic promise implementation is smacking up the CPU core that drew the short straw something good for me but with constant memory. I am sure the heads (new Promise) can be collected.

Need to confirm this promise implementation doesn't have some other loop bug though or some logic fail.

I wonder about catch though. I'm finding it not so easy surprisingly to get it to leak.

Confirmed as another design flaw in Promise/A+: nodejs/node#6673

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment