Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The JS Instrumenting we do at CodePen
var esprima = require('esprima');
function instrument(code) {
var LOOP_CHECK = 'if (window.CP.shouldStopExecution(%d)){break;}';
var LOOP_EXIT = "\nwindow.CP.exitedLoop(%d);\n";
var loopId = 1;
var patches = [];
esprima.parse(code, {
range: true,
tolerant: false,
sourceType: "script",
jsx: true
}, function (node) {
switch (node.type) {
case 'DoWhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WhileStatement':
var start = 1 + node.body.range[0];
var end = node.body.range[1];
var prolog = LOOP_CHECK.replace('%d', loopId);
var epilog = '';
if (node.body.type !== 'BlockStatement') {
// `while(1) doThat()` becomes `while(1) {doThat()}`
prolog = '{' + prolog;
epilog = '}';
--start;
}
patches.push({ pos: start, str: prolog });
patches.push({ pos: end, str: epilog });
patches.push({ pos: node.range[1], str: LOOP_EXIT.replace('%d', loopId) });
++loopId;
break;
default:
break;
}
});
patches.sort(function (a, b) {
return b.pos - a.pos;
}).forEach(function (patch) {
code = code.slice(0, patch.pos) + patch.str + code.slice(patch.pos);
});
return code;
}
exports.handler = function(event, context) {
try {
var js = event.markup || "";
if (js === "") {
context.succeed({
"markup": ""
});
} else {
context.succeed({
"markup": instrument(event.markup)
});
}
} catch(e) {
var line = 1;
try {
line = e.lineNumber;
} catch(err) {
// go on with line number 1
}
context.succeed({
"error": e.description,
"line": line
});
}
};
"use strict";
if (typeof (window.CP) !== "object") {
window.CP = {};
}
window.CP.PenTimer = {
// If we successfully run for X seconds no need to continue
// to monitor because we know the program isn't locked
programNoLongerBeingMonitored: false,
timeOfFirstCallToShouldStopLoop: 0,
_loopExits: {},
// Keep track of how long program spends in single loop w/o an exit
_loopTimers: {},
// Give the program time to get started
START_MONITORING_AFTER: 2000,
// takes into account START_MONITORING_AFTER val
STOP_ALL_MONITORING_TIMEOUT: 5000,
// tested against pen: xbwYNm, it loops over 200k real loop
MAX_TIME_IN_LOOP_WO_EXIT: 2200,
exitedLoop: function(loopID) {
this._loopExits[loopID] = true;
},
shouldStopLoop: function(loopID) {
// Once we kill a loop, kill them all, we have an infinite loop and
// it must be fixed prior to running again.
if (this.programKilledSoStopMonitoring) {
return true;
}
// Program exceeded monitor time, we're in the clear
if (this.programNoLongerBeingMonitored) {
return false;
}
// If the loopExit already called return
// It's possible for the program to break out
if (this._loopExits[loopID]) {
return false;
}
var now = this._getTime();
if (this.timeOfFirstCallToShouldStopLoop === 0) {
this.timeOfFirstCallToShouldStopLoop = now;
// first call to shouldStopLoop so just exit already
return false;
}
var programRunningTime = now - this.timeOfFirstCallToShouldStopLoop;
// Allow program to run unmolested (yup that's the right word)
// while it starts up
if (programRunningTime < this.START_MONITORING_AFTER) {
return false;
}
// Once the program's run for a satisfactory amount of time
// we assume it won't lock up and we can simply continue w/o
// checking for infinite loops
if (programRunningTime > this.STOP_ALL_MONITORING_TIMEOUT) {
this.programNoLongerBeingMonitored = true;
return false;
}
// Second level shit around new hotness logic
try {
this._checkOnInfiniteLoop(loopID, now);
} catch(e) {
this._sendErrorMessageToEditor();
this.programKilledSoStopMonitoring = true;
return true;
}
return false;
},
_sendErrorMessageToEditor: function() {
try {
if (this._shouldPostMessage()) {
var data = {
action: "infinite-loop",
line: this._findAroundLineNumber()
};
parent.postMessage(JSON.stringify(data), "*");
} else {
this._throwAnErrorToStopPen();
}
} catch(error) {
this._throwAnErrorToStopPen();
}
},
_shouldPostMessage: function() {
return document.location.href.match(/boomerang/);
},
_throwAnErrorToStopPen: function() {
throw "We found an infinite loop in your Pen. We've stopped the Pen from running. Please correct it or contact\ support@codepen.io.";
},
_findAroundLineNumber: function() {
var err = new Error();
var lineNumber = 0;
if (err.stack) {
// match only against JS in boomerang
var m = err.stack.match(/boomerang\S+:(\d+):\d+/);
if (m) {
lineNumber = m[1];
}
}
return lineNumber;
},
_checkOnInfiniteLoop: function(loopID, now) {
if (!this._loopTimers[loopID]) {
this._loopTimers[loopID] = now;
// We just started the timer for this loop. exit early
return false;
}
var loopRunningTime = now - this._loopTimers[loopID];
if (loopRunningTime > this.MAX_TIME_IN_LOOP_WO_EXIT) {
throw "Infinite Loop found on loop: " + loopID;
}
},
_getTime: function() {
return +new Date();
}
};
window.CP.shouldStopExecution = function(loopID) {
var shouldStop = window.CP.PenTimer.shouldStopLoop(loopID);
if( shouldStop === true ) {
console.warn("[CodePen]: An infinite loop (or a loop taking too long) was detected, so we stopped its execution. Sorry!");
}
return shouldStop;
};
window.CP.exitedLoop = function(loopID) {
window.CP.PenTimer.exitedLoop(loopID);
};
@ariya

This comment has been minimized.

Copy link

ariya commented Feb 2, 2017

I'm not entirely sure about the role and placement of window.CP.exitedLoop, but here is a possible alternative implementation (much much shorter, no dependency on escodegen). Error handling is left out for clarity.

For more technical details on the approach, refer also to Esprima documentation on Syntax Delegate:

var esprima = require('esprima');

exports.instrument = function(code) {
  var LOOP_CHECK = 'if (window.CP.shouldStopExecution(%d)) break; ';
  var LOOP_EXIT = '\nwindow.CP.exitedLoop(%d);\n'

  var loopId = 1;
  var patches = [];

  esprima.parse(code, { range: true }, function (node) {
    switch (node.type) {
      case 'DoWhileStatement':
      case 'ForStatement':
      case 'ForInStatement':
      case 'ForOfStatement':
      case 'WhileStatement':
        var start = 1 + node.body.range[0];
        var end = node.body.range[1];
        var prolog = LOOP_CHECK.replace('%d', loopId);
        var epilog = '';

        if (node.body.type !== 'BlockStatement') {
          // `while(1) doThat()` becomes `while(1) {doThat()}`
          prolog = '{' + prolog;
          epilog = '}';
          --start;
        }

        patches.push({ pos: start, str: prolog });
        patches.push({ pos: end, str: epilog });
        patches.push({ pos: node.range[1] + 1, str: LOOP_EXIT.replace('%d', loopId) });
        ++loopId;
        break;

      default:
        break;
    }
  });

  patches.sort(function (a, b) { return b.pos - a.pos }).forEach(function (patch) {
    code = code.slice(0, patch.pos) + patch.str + code.slice(patch.pos);
  });

  return code;
}

Edited: Apparently I did not paste the proper version, it is corrected now.

@quezo

This comment has been minimized.

Copy link

quezo commented Feb 2, 2017

@ariya This is excellent. This change makes the loop detection compatible with any version of JavaScript. Thank you for sharing this solution.

@ariya

This comment has been minimized.

Copy link

ariya commented Feb 3, 2017

@quezo Good to know it could be useful! Let me know if you encounter any problems.

@chriscoyier

This comment has been minimized.

Copy link
Owner Author

chriscoyier commented Feb 4, 2017

@ariya - I'm working on implementing it right now!

I've found a little weirdness. Check this original JavaScript:

for (var x = 0; i < 10; x++) {
  for (var y = 0; y < 10; y++) {
    if (x === y) {

    } else if (x === 3) {

    }
  }
}

It comes back fairly malformed:

for (var x = 0; i < 10; x++) {if (window.CP.shouldStopExecutionif (window.CP.shouldStopExecution(1)) break;(2)) break;
  for (var y = 0; y < 10; y++) {
    if (x ===
window.CP.exitedLoop(1);
 y) {

    } else if (x === 3) {

    }
  }
}
window.CP.exitedLoop(2);

Is it the nesting of the for loops tripping it up?

@ariya

This comment has been minimized.

Copy link

ariya commented Feb 4, 2017

@chriscoyier seems that I did not paste the correct version, sorry for that. Try it again, it should be good now.

P.S.: Time to turn it into a simple git repo?

@noseratio

This comment has been minimized.

Copy link

noseratio commented May 3, 2019

@chriscoyier, @ariya FYI, this currently doesn't seem to work well for asynchronous loops, a simple example.: https://codepen.io/noseratio/pen/jRgYMQ.

for (var i = 0; i < 100 && !cancelled; i++) {
    contentDiv.innerText = `Page: ${i}`;
    await sleep(100);
};

It gets terminated after about ~40 iterations with this error: Uncaught (in promise) We found an infinite loop in your Pen. We've stopped the Pen from running. Please correct it or contact support@codepen.io.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.