Skip to content

Instantly share code, notes, and snippets.

@chriscoyier
Last active February 15, 2023 00:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chriscoyier/b805fe91cc832fc97a3af671051e501f to your computer and use it in GitHub Desktop.
Save chriscoyier/b805fe91cc832fc97a3af671051e501f to your computer and use it in GitHub Desktop.
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
Copy link

ariya commented Feb 3, 2017

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

@chriscoyier
Copy link
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
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
Copy link

@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