2

I've got a bunch of integration tests using headless chrome. Because restarting the browser on an entirely new profile is so expensive the harness tries to "clean up" the browser state (flush caches, clear cookies and storage, ...) on teardown.

However there's a recurring issue that during the cleanup phase some async operations resolve and try to do whatever they do in a now nonsensical state.

There are two issues here:

  1. async stack traces support in CDT are listed as experimental and don't appear at all in the response (possibly because they have to be enabled via a hidden flag somehow)
  2. I have no idea what's still running at that point, and can't really even debug what breaks due to (1)

Is there any way to improve the situation expect by trawling through heisenbugs as they occur, trying to slowly make my way up the async callstacks throuth ever more logging until the root cause is found?

Masklinn
  • 34,759
  • 3
  • 38
  • 57
  • 1
    No. You would need to kill the asynchronous operations themselves (such as timeouts, network requests, filesystem I/O etc). You cannot kill a "pending promise". Killing the operation will cause it to never resolve the promise though, and the promise (chain) can get garbage-collected. – Bergi Jan 16 '20 at 11:43

1 Answers1

0

First we make a hook to be able to capture all xhr packets. You'll have to execute this before any of your other scripts load. Probaly put this in your boot/prepare script before running tests.

I have implemented below a start and stop button. start makes 300 xhr requests, just the "normal" way. If you press stop, you can cancel them all. Ideally you'd put the stop event handler code in an beforeunload event.

If you don't want to stop them, you can analyze their state, requested urls, etc... from one neat array where you keep track of everything within code.

This example works because only "so" many requests can be made at the same time by the browser. The rest in the queue waits as pending until a slot comes free. I used a 300 requests because I don't know a large/slow source to request from that isn't CORS protected, and this gives us humans enough time to press the stop button(I hope).

function addXMLRequestCallback(callback){
  var oldSend, i;
  if( XMLHttpRequest.callbacks ) {
      // we've already overridden send() so just add the callback
      XMLHttpRequest.callbacks.push( callback );
  } else {
      // create a callback queue
      XMLHttpRequest.callbacks = [callback];
      // store the native send()
      oldSend = XMLHttpRequest.prototype.send;
      // override the native send()
      XMLHttpRequest.prototype.send = function(){
          // process the callback queue
          // the xhr instance is passed into each callback but seems pretty useless
          // you can't tell what its destination is or call abort() without an error
          // so only really good for logging that a request has happened
          // I could be wrong, I hope so...
          // EDIT: I suppose you could override the onreadystatechange handler though
          for( i = 0; i < XMLHttpRequest.callbacks.length; i++ ) {
              XMLHttpRequest.callbacks[i]( this );
          }
          // call the native send()
          oldSend.apply(this, arguments);
      }
  }
}
/**
 * adding some debug data to the XHR objects. Note, don't depend on this, 
 * this is against good practises, ideally you'll have your own wrapper 
 * to deal with xhr objects and meta data.
 * The same way you can extend the XHR object to catch post data etc...
 */
var xhrProto = XMLHttpRequest.prototype,
    origOpen = xhrProto.open;
    origSend = xhrProto.send;
xhrProto.open = function (method, url) {
    this._url = url;
    return origOpen.apply(this, arguments);
};
xhrProto.send = function (data) {
    this._data = data;
    return origSend.apply(this, arguments);
};

+function() {
 var xhrs = [],
     i, 
     statuscount = 0, 
     status = document.getElementById('status'),
     DONE = 4;;
 addXMLRequestCallback((xhr) => {
    xhrs.push(xhr);
 });

 document.getElementById('start').addEventListener('click',(e) => {
    statuscount = 0;
    var data = JSON.stringify({
      'user': 'person',
      'pwd': 'password',
      'organization': 'place',
      'requiredkey': 'key'
    });
    for(var i = 0;i < 300; i++) {
       var oReq = new XMLHttpRequest();
       oReq.addEventListener("load", (e) => {
          statuscount++;
          status.value=statuscount;
       });
       oReq.open("GET", 'https://code.jquery.com/jquery-3.4.1.js');
       
       oReq.send(data);
    }
 });
 
 document.getElementById('cancel').addEventListener('click', (event) => {
     for(i = 0; i < xhrs.length; i++) {
         if(xhrs[i].readyState !== DONE) {
            console.log(xhrs[i]._url, xhrs[i]._data , 'is not done');
         }
         
     }
     /** Cancel everything */
     for(i = 0; i < xhrs.length; i++) {
        if(xhrs[i]) { 
           xhrs[i].abort();
        }
     }
     
 });
}();
<button id="start">start requests</button>
<button id="cancel">cancel requests</button>
<progress id="status" value="0" max="300"></progress>

Code of addXMLRequestCallback courtesy of meouw from this answer
Code of xhrProto keeping debug variables courtesy Joel Richard of from this answer

Tschallacka
  • 27,901
  • 14
  • 88
  • 133