Adrian B
Adrian B

Reputation: 1631

Why extension's popup is blocked when background js is processing?

I want to build a chrome extension which crawl (partial) an website. From popup I want to call an async method from background where I do some blocking requests. After this call I want to show a status in popup by requesting some information on setInterval... After I start the process, the popup is locked until background process ends. I tried many solutions like return true after background received a message, launch an async process in background but I have no solution for this... Here is my code:

Manifest

{
  "manifest_version": 2,
  "name": "...",
  "description": "...",
  "version": "1.0",
  "browser_action": {
   "default_icon": "logo_76.png",
   "default_popup": "main.htm"
  },
  "background": {
    "scripts": [
                "lib/regex_utils.js",
                "lib/uri.js",
                "lib/uri_utils.js",
                "lib/bloomfilter.js",
                "background.js"
                ],
    "persistent": true
  },
  "content_scripts":[{
        "matches": ["*://*/*"],
        "js":      ["content.js"]
    }],
  "permissions": [
    "activeTab",
    "http://*/*",
    "https://*/*"
    ]
}

Popup

var t = null;
var port = null;

function showStatus(response){
    // document.getElementById('start').innerHTML = 'parsed ' + response['parsed']
    console.log(response);
}

function checkStatus(){
    console.log('check')
    if(port){
        var data = {};
        data['command'] = 'status';
        port.postMessage(data); 
    }
}

clearInterval(t); t = setInterval(checkStatus, 2000);

document.addEventListener('DOMContentLoaded', function() {
    var checkPageButton = document.getElementById('start'); 
    checkPageButton.addEventListener('click', function() {
        chrome.tabs.getSelected(null, function(tab) {
            port = chrome.extension.connect({name: "communication"});
            var data = {}
            data['command'] = 'start_crawl'
            data['url'] = tab['url']
            port.postMessage(data);
            port.onMessage.addListener(function(msg) {
                if(msg['parsed'] != undefined){
                    showStatus(msg);                    
                }
            });
        });
  }, false);
}, false);

Bakground

function getLinksFromUrl(url){
    var xhr = new XMLHttpRequest();
    var data = null;
    var base = null;
    xhr.open("GET", url, false);

    function handleStateChange() {
      if (xhr.readyState == 4 && xhr.status==200) {
        data = xhr.responseText;
      }
    }

    xhr.onreadystatechange = handleStateChange;
    try {
        xhr.send();
    }catch(err){
        console.log('request exception');
    }

    if(data){
        linkRegex = new RegExp('a[^>]+?href=["\']{0,1}([^"\'\\s>]+)','igm');
        baseRegex = new RegExp('base[^>]+?href=["\']{0,1}([^"\'\\s>]+)','igm');
        var matches = linkRegex.execAll(data);  

        var base = baseRegex.exec(data);
        if (base && base[1] != undefined){
            base = base[1]
        }

        var rawLinks = matches.map(function(e){
            return e[1];
        });

        // Check to be on the same
        var finalLinks = relative2absolute(rawLinks, url, base).filter(function(e){
            var uri = new URI(e);
            if(ROOT_DOMAIN == uri.domain()){
                return true;
            }
            return false;
        });

        return finalLinks;
    }
    return [];
}

var TO_CRAWL = [];
var ALL_UNIQUE_URLS = []
var PASRSED_URLS = new BloomFilter();
var PAGE_LIMIT = 150;
var PARSED = 0;
var ROOT_DOMAIN = '';

function async(fn, callback) {
    setTimeout(function() {
        fn();
        callback();
    }, 0);
}

function getStatus(){
    var data = {}
    data['parsed'] = PARSED
    data['unique'] = ALL_UNIQUE_URLS.length
    data['queue'] = TO_CRAWL.length
    return data;
}

function crawl(initialUrl){
    var uri = new URI(initialUrl)
    ROOT_DOMAIN = uri.domain()
    TO_CRAWL = []
    ALL_UNIQUE_URLS = []
    PASRSED_URLS = new BloomFilter()
    PARSED = 0
    TO_CRAWL = getLinksFromUrl(initialUrl);
    while(ALL_UNIQUE_URLS.length < PAGE_LIMIT && TO_CRAWL.length > 0){
        url = TO_CRAWL.pop();
        var links = getLinksFromUrl(url)
        // TODO Add extra check for 404 and other error codes

        // Mark current URL as crawled
        PARSED++;
        PASRSED_URLS.add(url);
        ALL_UNIQUE_URLS.push(url);

        // Add new URls in queue
        links = links.filter(function(e){
            // TODO check domain here
            if(!PASRSED_URLS.check(e)){
                return true;
            }
            return false;
        });
        TO_CRAWL.push.apply(TO_CRAWL, links);
    }
}


chrome.extension.onConnect.addListener(function(port) {
  port.onMessage.addListener(function(data) {
    if (data['command'] != undefined && data['command'] == 'start_crawl'){
            async(
                    function(){
                        crawl(data['url']);
                    },
                    function(){
                        console.log(ALL_UNIQUE_URLS);
                    }
                );
            // SECOND METHOD TO SEND DATA AT POPUP
            var interval = setInterval(function(){
                port.postMessage(JSON.stringify(getStatus()));
            }, 1000);

        console.log('started')
        port.postMessage('crawling started');   
    }
    // FIRST METHOD TO SEND DATA AT POPUP
    if (data['command'] != undefined && data['command'] == 'status'){
        port.postMessage(JSON.stringify(getStatus()));
    }        
  });
});

Upvotes: 0

Views: 559

Answers (1)

woxxom
woxxom

Reputation: 73616

As the console warning message on Chrome 46 says:

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/.

Which means the UI locking you experience is the exact "detrimental effect" mentioned. In the future synchronous XHR won't even work when used outside of a web workers:

Synchronous XMLHttpRequest outside of workers is in the process of being removed from the web platform as it has detrimental effects to the end user's experience. (This is a long process that takes many years.) Developers must not pass false for the async argument when the JavaScript global environment is a document environment. User agents are strongly encouraged to warn about such usage in developer tools and may experiment with throwing an InvalidAccessError exception when it occurs. (source)

The solution is to use asynchronous XHR or put it in a web worker.

Upvotes: 2

Related Questions